From 8b7dcc8faee56b339b73513f64715746b5d458d1 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 09:39:04 -0600 Subject: [PATCH 01/13] Align CivicNotice with CivicCore 1.2 --- CHANGELOG.md | 8 ++++++++ README.md | 4 ++-- README.txt | 2 +- SECURITY.md | 2 +- USER-MANUAL.md | 8 ++++---- USER-MANUAL.txt | 8 ++++---- civicnotice/__init__.py | 2 +- civicnotice/channel_plan.py | 2 +- civicnotice/deadline_tracker.py | 2 +- civicnotice/main.py | 2 +- civicnotice/notice_registry.py | 2 +- civicnotice/public_ui.py | 4 ++-- civicnotice/publication_check.py | 2 +- civicnotice/records_export.py | 2 +- docs/IMPLEMENTATION_PLAN.md | 2 +- docs/RECONCILIATION.md | 4 ++-- docs/index.html | 6 +++--- pyproject.toml | 4 ++-- scripts/verify-release.sh | 10 +++++----- tests/test_notice_foundation.py | 2 +- tests/test_runtime_foundation.py | 8 ++++---- 21 files changed, 47 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6df9d90..a93b989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to CivicNotice will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.1.3] - 2026-06-19 + +### Changed + +- Moved CivicNotice to the `civiccore v1.2.0` release wheel used by the current Windows Local city-core platform. +- Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.1.3 compatibility release. +- Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. + ## [0.1.2] - 2026-04-29 ### Changed diff --git a/README.md b/README.md index 1f2bfc0..d9ba779 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, publication-readiness review, channel planning, and notice-record export checklists. -Current state: **v0.1.2 notice compliance foundation release**, aligned to the `civiccore v0.9.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does @@ -19,7 +19,7 @@ Current state: **v0.1.2 notice compliance foundation release**, aligned to the ` - It does not decide legal sufficiency. - It does not publish official notices. - It does not provide legal advice. -- It does not call live LLMs in v0.1.2. +- It does not call live LLMs in v0.1.3. - It does not write back to publication systems. - It does not replace a notice system of record. diff --git a/README.txt b/README.txt index 2fcbed2..1ef2536 100644 --- a/README.txt +++ b/README.txt @@ -3,7 +3,7 @@ CivicNotice CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, publication-readiness review, channel planning, and notice-record export checklists. -Current state: v0.1.2 notice compliance foundation release, aligned to the civiccore v0.9.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. diff --git a/SECURITY.md b/SECURITY.md index a6b63ad..316e564 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security -CivicNotice is early-stage software. Current version: `0.1.2`. Do not deploy it as a system of record until a release explicitly says it is production-ready. +CivicNotice is early-stage software. Current version: `0.1.3`. Do not deploy it as a system of record until a release explicitly says it is production-ready. Report suspected vulnerabilities privately to the project maintainer. Do not open public issues containing exploit details, secrets, or sensitive municipal data. diff --git a/USER-MANUAL.md b/USER-MANUAL.md index f818940..c4dd579 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -4,11 +4,11 @@ CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. -Current state: `0.1.2` notice compliance foundation release, aligned to the `civiccore v0.9.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. +Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. ## For IT and Technical Staff -CivicNotice is a FastAPI Python package pinned to the `civiccore v0.9.0` release wheel. The current runtime exposes: +CivicNotice is a FastAPI Python package pinned to the `civiccore v1.2.0` release wheel. The current runtime exposes: Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry and deadline-plan records. Leave it unset for deterministic stateless operation. @@ -36,10 +36,10 @@ bash scripts/verify-release.sh ```mermaid flowchart LR Staff["Clerk / communications / purchasing staff"] --> CivicNotice["CivicNotice"] - CivicNotice --> CivicCore["CivicCore v0.9.0"] + CivicNotice --> CivicCore["CivicCore v1.2.0"] CivicNotice -. future handoff .-> CivicClerk["CivicClerk v0.1.0"] CivicNotice -. future handoff .-> CivicProcure["CivicProcure v0.1.1"] CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.2 uses the shared CivicCore notice deadline helper plus deterministic sample notice data only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index c5e0fd6..a82b56b 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -6,12 +6,12 @@ For non-technical users CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. -Current state: 0.1.2 notice compliance foundation release, aligned to the civiccore v0.9.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. +Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. For IT and technical staff -------------------------- -CivicNotice is a FastAPI Python package pinned to the civiccore v0.9.0 release wheel. The current runtime exposes: +CivicNotice is a FastAPI Python package pinned to the civiccore v1.2.0 release wheel. The current runtime exposes: Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry and deadline-plan records. Leave it unset for deterministic stateless operation. @@ -35,8 +35,8 @@ bash scripts/verify-release.sh Architecture ------------ -Staff -> CivicNotice -> CivicCore v0.9.0 +Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.2 uses the shared CivicCore notice deadline helper plus deterministic sample notice data only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/__init__.py b/civicnotice/__init__.py index f3843a8..26f3f52 100644 --- a/civicnotice/__init__.py +++ b/civicnotice/__init__.py @@ -1,6 +1,6 @@ """CivicNotice package.""" -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/civicnotice/channel_plan.py b/civicnotice/channel_plan.py index ad076fd..f98f55b 100644 --- a/civicnotice/channel_plan.py +++ b/civicnotice/channel_plan.py @@ -1,4 +1,4 @@ -"""Notice channel planning helpers for CivicNotice v0.1.2.""" +"""Notice channel planning helpers for CivicNotice v0.1.3.""" from __future__ import annotations diff --git a/civicnotice/deadline_tracker.py b/civicnotice/deadline_tracker.py index 93b5dee..654814d 100644 --- a/civicnotice/deadline_tracker.py +++ b/civicnotice/deadline_tracker.py @@ -1,4 +1,4 @@ -"""Publication deadline helpers for CivicNotice v0.1.2.""" +"""Publication deadline helpers for CivicNotice v0.1.3.""" from __future__ import annotations diff --git a/civicnotice/main.py b/civicnotice/main.py index 3d38948..0d4eac1 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -72,7 +72,7 @@ def root() -> dict[str, str]: "advice, live LLM calls, publication-system write-back, and notice system-of-record integrations " "are not implemented yet." ), - "next_step": "Post-v0.1.2 roadmap: statutory rule packs, CivicClerk/CivicProcure/CivicNotice handoffs, and publication proof queues", + "next_step": "Post-v0.1.3 roadmap: statutory rule packs, CivicClerk/CivicProcure/CivicNotice handoffs, and publication proof queues", } diff --git a/civicnotice/notice_registry.py b/civicnotice/notice_registry.py index f602460..dc7e007 100644 --- a/civicnotice/notice_registry.py +++ b/civicnotice/notice_registry.py @@ -1,4 +1,4 @@ -"""Notice registry helpers for CivicNotice v0.1.2.""" +"""Notice registry helpers for CivicNotice v0.1.3.""" from __future__ import annotations diff --git a/civicnotice/public_ui.py b/civicnotice/public_ui.py index 735e405..49bc162 100644 --- a/civicnotice/public_ui.py +++ b/civicnotice/public_ui.py @@ -1,4 +1,4 @@ -"""Static public UI shell for CivicNotice v0.1.2.""" +"""Static public UI shell for CivicNotice v0.1.3.""" from __future__ import annotations @@ -48,7 +48,7 @@ def render_public_lookup_page() -> str:

CivicSuite / CivicNotice public sample

Public notices with fewer missed deadlines.

CivicNotice demonstrates notice administration support: registry stubs, deadline plans, publication-readiness checks, channel planning, and proof-preserving records exports without publishing official notice.

-

v0.1.2 notice compliance foundation

+

v0.1.3 notice compliance foundation

diff --git a/civicnotice/publication_check.py b/civicnotice/publication_check.py index 6d2db74..3101f65 100644 --- a/civicnotice/publication_check.py +++ b/civicnotice/publication_check.py @@ -1,4 +1,4 @@ -"""Publication-readiness checks for CivicNotice v0.1.2.""" +"""Publication-readiness checks for CivicNotice v0.1.3.""" from __future__ import annotations diff --git a/civicnotice/records_export.py b/civicnotice/records_export.py index ade08d4..b09beb4 100644 --- a/civicnotice/records_export.py +++ b/civicnotice/records_export.py @@ -1,4 +1,4 @@ -"""Notice records export helpers for CivicNotice v0.1.2.""" +"""Notice records export helpers for CivicNotice v0.1.3.""" from __future__ import annotations diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 0870318..7709859 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,5 @@ # CivicNotice Implementation Plan -CivicNotice v0.1.2 preserves the deterministic foundation and aligns it to the `civiccore v0.9.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, publication readiness tracking helper, channel planning helper, notice and records export checklist, and public sample UI. +CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, publication readiness tracking helper, channel planning helper, notice and records export checklist, and public sample UI. Later releases add statutory rule packs, CivicClerk/CivicProcure/CivicBoards handoffs, CivicRecords file links, proof queues, publication-system integrations, and production data governance. diff --git a/docs/RECONCILIATION.md b/docs/RECONCILIATION.md index f302f64..260df01 100644 --- a/docs/RECONCILIATION.md +++ b/docs/RECONCILIATION.md @@ -1,9 +1,9 @@ # CivicNotice Reconciliation -| Driver | CivicNotice v0.1.2 result | +| Driver | CivicNotice v0.1.3 result | |---|---| | Unified spec CivicNotice catalog entry | notice registry, deadline tracking, publication readiness tracking, channel planning, and notice/records export checklists represented through deterministic helpers. | | Apache 2.0 code licensing | `LICENSE-CODE` and package metadata use Apache 2.0. | | Documentation licensing | `LICENSE-DOCS` uses CC BY 4.0. | -| CivicCore dependency | `pyproject.toml` pins the `civiccore v0.9.0` release wheel. | +| CivicCore dependency | `pyproject.toml` pins the `civiccore v1.2.0` release wheel. | | Shipped/planned honesty | Legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, and notice system-of-record integrations remain explicitly unshipped. | diff --git a/docs/index.html b/docs/index.html index 656fe74..7b6c97b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,15 +12,15 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.2 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, optional registry/deadline workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v0.9.0 release-wheel alignment.

-

Shipping v0.1.2

+

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, optional registry/deadline workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

Shipping v0.1.3

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

-

Dependency

Pinned to the civiccore v0.9.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

+

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

Apache 2.0 code. CC BY 4.0 docs. Run locally by the city.

diff --git a/pyproject.toml b/pyproject.toml index 1e25671..76bb28a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "civicnotice" -version = "0.1.2" +version = "0.1.3" description = "CivicNotice runtime foundation for notice registry, deadline tracking, publication readiness tracking, channel planning, and public-records-aware export support." readme = "README.md" requires-python = ">=3.11" @@ -13,7 +13,7 @@ authors = [ { name = "CivicSuite contributors" } ] dependencies = [ - "civiccore @ https://github.com/CivicSuite/civiccore/releases/download/v0.9.0/civiccore-0.9.0-py3-none-any.whl", + "civiccore @ https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl", "fastapi>=0.115.0,<1.0.0", "sqlalchemy>=2.0.0,<3.0.0", "uvicorn[standard]>=0.30.0,<1.0.0", diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index bdd1329..d31235d 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -VERSION="0.1.2" +VERSION="0.1.3" find_python() { local candidates=() @@ -40,7 +40,7 @@ ${PYTHON_BIN} - <<'PY' from pathlib import Path import tomllib -version = "0.1.2" +version = "0.1.3" root = Path(".") pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) assert pyproject["project"]["version"] == version, pyproject["project"]["version"] @@ -56,7 +56,7 @@ for path in [ "SECURITY.md", ]: text = (root / path).read_text(encoding="utf-8") - assert "0.1.2" in text, f"missing release version in {path}" + assert "0.1.3" in text, f"missing release version in {path}" assert "0.1.0.dev0" not in text, f"stale dev version in {path}" print("PASS: version surfaces synchronized") PY @@ -81,8 +81,8 @@ from pathlib import Path import hashlib dist = Path("dist") -wheel = dist / "civicnotice-0.1.2-py3-none-any.whl" -sdist = dist / "civicnotice-0.1.2.tar.gz" +wheel = dist / "civicnotice-0.1.3-py3-none-any.whl" +sdist = dist / "civicnotice-0.1.3.tar.gz" assert wheel.exists(), f"missing {wheel}" assert sdist.exists(), f"missing {sdist}" lines = [] diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 4618589..9d78eaa 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -112,6 +112,6 @@ def test_public_ui_route_is_accessible_and_honest() -> None: text = response.text assert '' in text assert '
' in text - assert "v0.1.2 notice compliance foundation" in text + assert "v0.1.3 notice compliance foundation" in text assert "does not determine legal sufficiency" in text assert "replace the notice system of record" in text diff --git a/tests/test_runtime_foundation.py b/tests/test_runtime_foundation.py index 53cbffe..fca56f1 100644 --- a/tests/test_runtime_foundation.py +++ b/tests/test_runtime_foundation.py @@ -8,7 +8,7 @@ def test_package_version_is_012() -> None: - assert civicnotice.__version__ == "0.1.2" + assert civicnotice.__version__ == "0.1.3" def test_root_endpoint_states_runtime_boundary() -> None: @@ -20,7 +20,7 @@ def test_root_endpoint_states_runtime_boundary() -> None: assert "CivicCore-backed deadline plans" in data["message"] assert "database-backed registry/deadline workpapers" in data["message"] assert "official publication" in data["message"] - assert "Post-v0.1.2 roadmap" in data["next_step"] + assert "Post-v0.1.3 roadmap" in data["next_step"] def test_health_endpoint_reports_versions() -> None: @@ -29,5 +29,5 @@ def test_health_endpoint_reports_versions() -> None: data = response.json() assert data["status"] == "ok" assert data["service"] == "civicnotice" - assert data["version"] == "0.1.2" - assert data["civiccore_version"] == "0.9.0" + assert data["version"] == "0.1.3" + assert data["civiccore_version"] == "1.2.0" From 7c5c5fa2d634ccf8ecc9530b4f9d66ea525476b2 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:19:07 -0600 Subject: [PATCH 02/13] Add CivicNotice publication proof workpapers --- CHANGELOG.md | 1 + README.md | 7 +- README.txt | 6 +- USER-MANUAL.md | 8 +- USER-MANUAL.txt | 8 +- civicnotice/main.py | 172 +++++++++++-- civicnotice/persistence.py | 234 +++++++++++++++++- civicnotice/publication_proof.py | 66 +++++ docs/index.html | 4 +- ...est_production_depth_notice_persistence.py | 81 +++++- tests/test_runtime_foundation.py | 2 +- 11 files changed, 546 insertions(+), 43 deletions(-) create mode 100644 civicnotice/publication_proof.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a93b989..7ad6e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Moved CivicNotice to the `civiccore v1.2.0` release wheel used by the current Windows Local city-core platform. - Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.1.3 compatibility release. +- Added durable publication-proof workpapers for staff evidence packets linked to Clerk, procurement, board, or manual source records. - Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. ## [0.1.2] - 2026-04-29 diff --git a/README.md b/README.md index d9ba779..5e930b7 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, publication-readiness review, channel planning, and notice-record export checklists. -Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does - Create sample notice registry stubs. - Build statutory publication deadline reminder plans using the shared CivicCore notice helper. -- Persist notice registry and deadline-plan workpapers when `CIVICNOTICE_WORKPAPER_DB_URL` is configured. +- Persist notice registry, deadline-plan, and publication-proof workpapers when `CIVICNOTICE_WORKPAPER_DB_URL` is configured. +- Store staff-reviewed publication proof packets linked to upstream Clerk, procurement, board, or manual source records. - Assemble publication-readiness checklists for staff review. - Plan notice channels and accessibility-review needs. - Produce notice and records export checklists. @@ -32,6 +33,8 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - `GET /api/v1/civicnotice/registry/{record_id}` retrieves a persisted notice registry record. - `POST /api/v1/civicnotice/deadlines` returns statutory deadline reminders. - `GET /api/v1/civicnotice/deadlines/{plan_id}` retrieves a persisted deadline plan. +- `POST /api/v1/civicnotice/publication-proof` stores a durable publication proof workpaper. +- `GET /api/v1/civicnotice/publication-proof/{proof_id}` retrieves a persisted publication proof workpaper. - `POST /api/v1/civicnotice/publication-check` returns a publication-readiness checklist. - `POST /api/v1/civicnotice/channels` returns channel planning flags. - `POST /api/v1/civicnotice/export` returns a notice and records export checklist. diff --git a/README.txt b/README.txt index 1ef2536..7495ff2 100644 --- a/README.txt +++ b/README.txt @@ -3,13 +3,15 @@ CivicNotice CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, publication-readiness review, channel planning, and notice-record export checklists. -Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. What CivicNotice does: - Create sample notice registry stubs. - Build statutory publication deadline reminder plans using the shared CivicCore notice helper. +- Persist notice registry, deadline-plan, and publication-proof workpapers when CIVICNOTICE_WORKPAPER_DB_URL is configured. +- Store staff-reviewed publication proof packets linked to upstream source records. - Assemble publication-readiness checklists for staff review. - Plan notice channels and accessibility-review needs. - Produce notice and records export checklists. @@ -23,6 +25,8 @@ API surface: - GET /api/v1/civicnotice/registry/{record_id} - POST /api/v1/civicnotice/deadlines - GET /api/v1/civicnotice/deadlines/{plan_id} +- POST /api/v1/civicnotice/publication-proof +- GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check - POST /api/v1/civicnotice/channels - POST /api/v1/civicnotice/export diff --git a/USER-MANUAL.md b/USER-MANUAL.md index c4dd579..fe8e92a 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -2,7 +2,7 @@ ## For Non-Technical Users -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -10,7 +10,7 @@ Current state: `0.1.3` notice compliance foundation release, aligned to the `civ CivicNotice is a FastAPI Python package pinned to the `civiccore v1.2.0` release wheel. The current runtime exposes: -Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry and deadline-plan records. Leave it unset for deterministic stateless operation. +Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, deadline-plan, and publication-proof records. Leave it unset for deterministic stateless operation except durable publication-proof storage, which requires the database so proof packets can be retrieved later. - `GET /` - `GET /health` @@ -19,6 +19,8 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry a - `GET /api/v1/civicnotice/registry/{record_id}` - `POST /api/v1/civicnotice/deadlines` - `GET /api/v1/civicnotice/deadlines/{plan_id}` +- `POST /api/v1/civicnotice/publication-proof` +- `GET /api/v1/civicnotice/publication-proof/{proof_id}` - `POST /api/v1/civicnotice/publication-check` - `POST /api/v1/civicnotice/channels` - `POST /api/v1/civicnotice/export` @@ -42,4 +44,4 @@ flowchart LR CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index a82b56b..40822aa 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -4,7 +4,7 @@ CivicNotice User Manual For non-technical users ----------------------- -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -13,7 +13,7 @@ For IT and technical staff CivicNotice is a FastAPI Python package pinned to the civiccore v1.2.0 release wheel. The current runtime exposes: -Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry and deadline-plan records. Leave it unset for deterministic stateless operation. +Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, deadline-plan, and publication-proof records. Leave it unset for deterministic stateless operation except durable publication-proof storage, which requires the database so proof packets can be retrieved later. - GET / - GET /health @@ -22,6 +22,8 @@ Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry and - GET /api/v1/civicnotice/registry/{record_id} - POST /api/v1/civicnotice/deadlines - GET /api/v1/civicnotice/deadlines/{plan_id} +- POST /api/v1/civicnotice/publication-proof +- GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check - POST /api/v1/civicnotice/channels - POST /api/v1/civicnotice/export @@ -39,4 +41,4 @@ Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/main.py b/civicnotice/main.py index 0d4eac1..ec92c85 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -12,7 +12,12 @@ from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.notice_registry import register_notice_stub -from civicnotice.persistence import NoticeWorkpaperRepository, StoredDeadlinePlan, StoredNoticeRecord +from civicnotice.persistence import ( + NoticeWorkpaperRepository, + StoredDeadlinePlan, + StoredNoticeRecord, + StoredPublicationProof, +) from civicnotice.public_ui import render_public_lookup_page from civicnotice.publication_check import build_publication_checklist from civicnotice.records_export import build_notice_records_export @@ -56,6 +61,19 @@ class RecordsExportRequest(BaseModel): format: str = "markdown" +class PublicationProofRequest(BaseModel): + notice_id: str + notice_type: str + source_module: str = "manual" + source_record_id: str + channel: str + published_at: str + location: str + confirmation_reference: str + statutory_basis: str + reviewer: str + + @app.get("/") def root() -> dict[str, str]: """Return current product state without overstating unshipped behavior.""" @@ -67,7 +85,7 @@ def root() -> dict[str, str]: "message": ( "CivicNotice package, API foundation, sample notice registry, CivicCore-backed deadline plans, " "publication-readiness checklist, channel planning, records export checklist, optional " - "database-backed registry/deadline workpapers, and public UI foundation are online; official " + "database-backed registry/deadline/publication-proof workpapers, and public UI foundation are online; official " "legal sufficiency decisions, official publication, legal " "advice, live LLM calls, publication-system write-back, and notice system-of-record integrations " "are not implemented yet." @@ -98,11 +116,13 @@ def public_civicnotice_page() -> str: @app.post("/api/v1/civicnotice/registry") def notice_registry(request: NoticeRegistryRequest) -> dict[str, object]: if _workpaper_database_url() is not None: - return _stored_notice_response(_get_workpaper_repository().create_notice_record( - notice_id=request.notice_id, - notice_type=request.notice_type, - owner=request.owner, - )) + return _stored_notice_response( + _get_workpaper_repository().create_notice_record( + notice_id=request.notice_id, + notice_type=request.notice_type, + owner=request.owner, + ) + ) payload = register_notice_stub( notice_id=request.notice_id, notice_type=request.notice_type, @@ -111,24 +131,39 @@ def notice_registry(request: NoticeRegistryRequest) -> dict[str, object]: payload["record_id"] = None return payload + @app.get("/api/v1/civicnotice/registry/{record_id}") def get_notice_registry(record_id: str) -> dict[str, object]: if _workpaper_database_url() is None: - raise HTTPException(status_code=503, detail={"message":"CivicNotice workpaper persistence is not configured.","fix":"Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted notice registry records."}) + raise HTTPException( + status_code=503, + detail={ + "message": "CivicNotice workpaper persistence is not configured.", + "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted notice registry records.", + }, + ) stored = _get_workpaper_repository().get_notice_record(record_id) if stored is None: - raise HTTPException(status_code=404, detail={"message":"Notice registry record not found.","fix":"Use a record_id returned by POST /api/v1/civicnotice/registry."}) + raise HTTPException( + status_code=404, + detail={ + "message": "Notice registry record not found.", + "fix": "Use a record_id returned by POST /api/v1/civicnotice/registry.", + }, + ) return _stored_notice_response(stored) @app.post("/api/v1/civicnotice/deadlines") def deadline_plan(request: DeadlineRequest) -> dict[str, object]: if _workpaper_database_url() is not None: - return _stored_deadline_response(_get_workpaper_repository().create_deadline_plan( - notice_type=request.notice_type, - event_date=request.event_date, - lead_days=request.lead_days, - )) + return _stored_deadline_response( + _get_workpaper_repository().create_deadline_plan( + notice_type=request.notice_type, + event_date=request.event_date, + lead_days=request.lead_days, + ) + ) payload = build_deadline_plan( notice_type=request.notice_type, event_date=request.event_date, @@ -137,16 +172,76 @@ def deadline_plan(request: DeadlineRequest) -> dict[str, object]: payload["plan_id"] = None return payload + @app.get("/api/v1/civicnotice/deadlines/{plan_id}") def get_deadline_plan(plan_id: str) -> dict[str, object]: if _workpaper_database_url() is None: - raise HTTPException(status_code=503, detail={"message":"CivicNotice workpaper persistence is not configured.","fix":"Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted deadline plans."}) + raise HTTPException( + status_code=503, + detail={ + "message": "CivicNotice workpaper persistence is not configured.", + "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted deadline plans.", + }, + ) stored = _get_workpaper_repository().get_deadline_plan(plan_id) if stored is None: - raise HTTPException(status_code=404, detail={"message":"Deadline plan record not found.","fix":"Use a plan_id returned by POST /api/v1/civicnotice/deadlines."}) + raise HTTPException( + status_code=404, + detail={ + "message": "Deadline plan record not found.", + "fix": "Use a plan_id returned by POST /api/v1/civicnotice/deadlines.", + }, + ) return _stored_deadline_response(stored) +@app.post("/api/v1/civicnotice/publication-proof") +def publication_proof(request: PublicationProofRequest) -> dict[str, object]: + if _workpaper_database_url() is None: + raise HTTPException( + status_code=503, + detail={ + "message": "CivicNotice workpaper persistence is not configured.", + "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to store durable publication proof records.", + }, + ) + stored = _get_workpaper_repository().create_publication_proof( + notice_id=request.notice_id, + notice_type=request.notice_type, + source_module=request.source_module, + source_record_id=request.source_record_id, + channel=request.channel, + published_at=request.published_at, + location=request.location, + confirmation_reference=request.confirmation_reference, + statutory_basis=request.statutory_basis, + reviewer=request.reviewer, + ) + return _stored_publication_proof_response(stored) + + +@app.get("/api/v1/civicnotice/publication-proof/{proof_id}") +def get_publication_proof(proof_id: str) -> dict[str, object]: + if _workpaper_database_url() is None: + raise HTTPException( + status_code=503, + detail={ + "message": "CivicNotice workpaper persistence is not configured.", + "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted publication proof records.", + }, + ) + stored = _get_workpaper_repository().get_publication_proof(proof_id) + if stored is None: + raise HTTPException( + status_code=404, + detail={ + "message": "Publication proof record not found.", + "fix": "Use a proof_id returned by POST /api/v1/civicnotice/publication-proof.", + }, + ) + return _stored_publication_proof_response(stored) + + @app.post("/api/v1/civicnotice/publication-check") def publication_checklist(request: PublicationRequest) -> dict[str, object]: return build_publication_checklist( @@ -171,9 +266,11 @@ def records_export(request: RecordsExportRequest) -> dict[str, object]: format=request.format, ).__dict__ + def _workpaper_database_url() -> str | None: return os.environ.get("CIVICNOTICE_WORKPAPER_DB_URL") + def _get_workpaper_repository() -> NoticeWorkpaperRepository: global _workpaper_db_url, _workpaper_repository db_url = _workpaper_database_url() @@ -185,14 +282,53 @@ def _get_workpaper_repository() -> NoticeWorkpaperRepository: _workpaper_repository = NoticeWorkpaperRepository(db_url=db_url) return _workpaper_repository + def _dispose_workpaper_repository() -> None: global _workpaper_repository if _workpaper_repository is not None: _workpaper_repository.engine.dispose() _workpaper_repository = None + def _stored_notice_response(stored: StoredNoticeRecord) -> dict[str, object]: - return {"record_id": stored.record_id, "notice_id": stored.notice_id, "notice_type": stored.notice_type, "owner": stored.owner, "registry_notes": list(stored.registry_notes), "disclaimer": stored.disclaimer, "created_at": stored.created_at.isoformat()} + return { + "record_id": stored.record_id, + "notice_id": stored.notice_id, + "notice_type": stored.notice_type, + "owner": stored.owner, + "registry_notes": list(stored.registry_notes), + "disclaimer": stored.disclaimer, + "created_at": stored.created_at.isoformat(), + } + def _stored_deadline_response(stored: StoredDeadlinePlan) -> dict[str, object]: - return {"plan_id": stored.plan_id, "notice_type": stored.notice_type, "event_date": stored.event_date.isoformat(), "reminders": list(stored.reminders), "staff_review_required": stored.staff_review_required, "disclaimer": stored.disclaimer, "created_at": stored.created_at.isoformat()} + return { + "plan_id": stored.plan_id, + "notice_type": stored.notice_type, + "event_date": stored.event_date.isoformat(), + "reminders": list(stored.reminders), + "staff_review_required": stored.staff_review_required, + "disclaimer": stored.disclaimer, + "created_at": stored.created_at.isoformat(), + } + + +def _stored_publication_proof_response(stored: StoredPublicationProof) -> dict[str, object]: + return { + "proof_id": stored.proof_id, + "notice_id": stored.notice_id, + "notice_type": stored.notice_type, + "source_module": stored.source_module, + "source_record_id": stored.source_record_id, + "channel": stored.channel, + "published_at": stored.published_at, + "location": stored.location, + "confirmation_reference": stored.confirmation_reference, + "statutory_basis": stored.statutory_basis, + "reviewer": stored.reviewer, + "proof_notes": list(stored.proof_notes), + "compliance_status": stored.compliance_status, + "disclaimer": stored.disclaimer, + "created_at": stored.created_at.isoformat(), + } diff --git a/civicnotice/persistence.py b/civicnotice/persistence.py index 1521ef9..1cdb35e 100644 --- a/civicnotice/persistence.py +++ b/civicnotice/persistence.py @@ -9,6 +9,7 @@ from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.notice_registry import register_notice_stub +from civicnotice.publication_proof import record_publication_proof metadata = sa.MetaData() @@ -39,6 +40,27 @@ schema="civicnotice", ) +publication_proof_records = sa.Table( + "publication_proof_records", + metadata, + sa.Column("proof_id", sa.String(36), primary_key=True), + sa.Column("notice_id", sa.String(160), nullable=False), + sa.Column("notice_type", sa.String(160), nullable=False), + sa.Column("source_module", sa.String(160), nullable=False), + sa.Column("source_record_id", sa.String(160), nullable=False), + sa.Column("channel", sa.String(160), nullable=False), + sa.Column("published_at", sa.String(160), nullable=False), + sa.Column("location", sa.Text(), nullable=False), + sa.Column("confirmation_reference", sa.String(240), nullable=False), + sa.Column("statutory_basis", sa.Text(), nullable=False), + sa.Column("reviewer", sa.String(160), nullable=False), + sa.Column("proof_notes", sa.JSON(), nullable=False), + sa.Column("compliance_status", sa.String(160), nullable=False), + sa.Column("disclaimer", sa.Text(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + schema="civicnotice", +) + @dataclass(frozen=True) class StoredNoticeRecord: @@ -62,6 +84,25 @@ class StoredDeadlinePlan: created_at: datetime +@dataclass(frozen=True) +class StoredPublicationProof: + proof_id: str + notice_id: str + notice_type: str + source_module: str + source_record_id: str + channel: str + published_at: str + location: str + confirmation_reference: str + statutory_basis: str + reviewer: str + proof_notes: tuple[str, ...] + compliance_status: str + disclaimer: str + created_at: datetime + + class NoticeWorkpaperRepository: def __init__(self, *, db_url: str | None = None, engine: Engine | None = None) -> None: base_engine = engine or create_engine(db_url or "sqlite+pysqlite:///:memory:", future=True) @@ -73,32 +114,203 @@ def __init__(self, *, db_url: str | None = None, engine: Engine | None = None) - connection.execute(sa.text("CREATE SCHEMA IF NOT EXISTS civicnotice")) metadata.create_all(self.engine) - def create_notice_record(self, *, notice_id: str, notice_type: str, owner: str) -> StoredNoticeRecord: + def create_notice_record( + self, *, notice_id: str, notice_type: str, owner: str + ) -> StoredNoticeRecord: record = register_notice_stub(notice_id=notice_id, notice_type=notice_type, owner=owner) - stored = StoredNoticeRecord(str(uuid4()), record.notice_id, record.notice_type, record.owner, record.registry_notes, record.disclaimer, datetime.now(UTC)) + stored = StoredNoticeRecord( + str(uuid4()), + record.notice_id, + record.notice_type, + record.owner, + record.registry_notes, + record.disclaimer, + datetime.now(UTC), + ) with self.engine.begin() as connection: - connection.execute(notice_registry_records.insert().values(record_id=stored.record_id, notice_id=stored.notice_id, notice_type=stored.notice_type, owner=stored.owner, registry_notes=list(stored.registry_notes), disclaimer=stored.disclaimer, created_at=stored.created_at)) + connection.execute( + notice_registry_records.insert().values( + record_id=stored.record_id, + notice_id=stored.notice_id, + notice_type=stored.notice_type, + owner=stored.owner, + registry_notes=list(stored.registry_notes), + disclaimer=stored.disclaimer, + created_at=stored.created_at, + ) + ) return stored def get_notice_record(self, record_id: str) -> StoredNoticeRecord | None: with self.engine.begin() as connection: - row = connection.execute(sa.select(notice_registry_records).where(notice_registry_records.c.record_id == record_id)).mappings().first() + row = ( + connection.execute( + sa.select(notice_registry_records).where( + notice_registry_records.c.record_id == record_id + ) + ) + .mappings() + .first() + ) if row is None: return None data = dict(row) - return StoredNoticeRecord(data["record_id"], data["notice_id"], data["notice_type"], data["owner"], tuple(data["registry_notes"]), data["disclaimer"], data["created_at"]) + return StoredNoticeRecord( + data["record_id"], + data["notice_id"], + data["notice_type"], + data["owner"], + tuple(data["registry_notes"]), + data["disclaimer"], + data["created_at"], + ) - def create_deadline_plan(self, *, notice_type: str, event_date: date, lead_days: int = 10) -> StoredDeadlinePlan: - plan = build_deadline_plan(notice_type=notice_type, event_date=event_date, lead_days=lead_days) - stored = StoredDeadlinePlan(str(uuid4()), plan.notice_type, plan.event_date, plan.reminders, plan.staff_review_required, plan.disclaimer, datetime.now(UTC)) + def create_deadline_plan( + self, *, notice_type: str, event_date: date, lead_days: int = 10 + ) -> StoredDeadlinePlan: + plan = build_deadline_plan( + notice_type=notice_type, event_date=event_date, lead_days=lead_days + ) + stored = StoredDeadlinePlan( + str(uuid4()), + plan.notice_type, + plan.event_date, + plan.reminders, + plan.staff_review_required, + plan.disclaimer, + datetime.now(UTC), + ) with self.engine.begin() as connection: - connection.execute(deadline_plan_records.insert().values(plan_id=stored.plan_id, notice_type=stored.notice_type, event_date=stored.event_date, reminders=list(stored.reminders), staff_review_required=stored.staff_review_required, disclaimer=stored.disclaimer, created_at=stored.created_at)) + connection.execute( + deadline_plan_records.insert().values( + plan_id=stored.plan_id, + notice_type=stored.notice_type, + event_date=stored.event_date, + reminders=list(stored.reminders), + staff_review_required=stored.staff_review_required, + disclaimer=stored.disclaimer, + created_at=stored.created_at, + ) + ) return stored def get_deadline_plan(self, plan_id: str) -> StoredDeadlinePlan | None: with self.engine.begin() as connection: - row = connection.execute(sa.select(deadline_plan_records).where(deadline_plan_records.c.plan_id == plan_id)).mappings().first() + row = ( + connection.execute( + sa.select(deadline_plan_records).where( + deadline_plan_records.c.plan_id == plan_id + ) + ) + .mappings() + .first() + ) + if row is None: + return None + data = dict(row) + return StoredDeadlinePlan( + data["plan_id"], + data["notice_type"], + data["event_date"], + tuple(data["reminders"]), + data["staff_review_required"], + data["disclaimer"], + data["created_at"], + ) + + def create_publication_proof( + self, + *, + notice_id: str, + notice_type: str, + source_module: str, + source_record_id: str, + channel: str, + published_at: str, + location: str, + confirmation_reference: str, + statutory_basis: str, + reviewer: str, + ) -> StoredPublicationProof: + proof = record_publication_proof( + notice_id=notice_id, + notice_type=notice_type, + source_module=source_module, + source_record_id=source_record_id, + channel=channel, + published_at=published_at, + location=location, + confirmation_reference=confirmation_reference, + statutory_basis=statutory_basis, + reviewer=reviewer, + ) + stored = StoredPublicationProof( + str(uuid4()), + proof.notice_id, + proof.notice_type, + proof.source_module, + proof.source_record_id, + proof.channel, + proof.published_at, + proof.location, + proof.confirmation_reference, + proof.statutory_basis, + proof.reviewer, + proof.proof_notes, + proof.compliance_status, + proof.disclaimer, + datetime.now(UTC), + ) + with self.engine.begin() as connection: + connection.execute( + publication_proof_records.insert().values( + proof_id=stored.proof_id, + notice_id=stored.notice_id, + notice_type=stored.notice_type, + source_module=stored.source_module, + source_record_id=stored.source_record_id, + channel=stored.channel, + published_at=stored.published_at, + location=stored.location, + confirmation_reference=stored.confirmation_reference, + statutory_basis=stored.statutory_basis, + reviewer=stored.reviewer, + proof_notes=list(stored.proof_notes), + compliance_status=stored.compliance_status, + disclaimer=stored.disclaimer, + created_at=stored.created_at, + ) + ) + return stored + + def get_publication_proof(self, proof_id: str) -> StoredPublicationProof | None: + with self.engine.begin() as connection: + row = ( + connection.execute( + sa.select(publication_proof_records).where( + publication_proof_records.c.proof_id == proof_id + ) + ) + .mappings() + .first() + ) if row is None: return None data = dict(row) - return StoredDeadlinePlan(data["plan_id"], data["notice_type"], data["event_date"], tuple(data["reminders"]), data["staff_review_required"], data["disclaimer"], data["created_at"]) + return StoredPublicationProof( + data["proof_id"], + data["notice_id"], + data["notice_type"], + data["source_module"], + data["source_record_id"], + data["channel"], + data["published_at"], + data["location"], + data["confirmation_reference"], + data["statutory_basis"], + data["reviewer"], + tuple(data["proof_notes"]), + data["compliance_status"], + data["disclaimer"], + data["created_at"], + ) diff --git a/civicnotice/publication_proof.py b/civicnotice/publication_proof.py new file mode 100644 index 0000000..4f915f4 --- /dev/null +++ b/civicnotice/publication_proof.py @@ -0,0 +1,66 @@ +"""Publication proof workpaper helpers for CivicNotice v0.1.3.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from civicnotice.notice_registry import DISCLAIMER + + +@dataclass(frozen=True) +class PublicationProof: + notice_id: str + notice_type: str + source_module: str + source_record_id: str + channel: str + published_at: str + location: str + confirmation_reference: str + statutory_basis: str + reviewer: str + proof_notes: tuple[str, ...] + compliance_status: str + disclaimer: str = DISCLAIMER + + +def record_publication_proof( + *, + notice_id: str, + notice_type: str, + source_module: str, + source_record_id: str, + channel: str, + published_at: str, + location: str, + confirmation_reference: str, + statutory_basis: str, + reviewer: str, +) -> PublicationProof: + """Return a staff-owned publication proof packet for durable storage.""" + + clean_channel = channel.strip() or "unspecified channel" + clean_location = location.strip() or "unspecified location" + clean_reference = confirmation_reference.strip() or "pending confirmation reference" + clean_basis = statutory_basis.strip() or "staff-provided statutory basis required" + clean_reviewer = reviewer.strip() or "staff review required" + proof_notes = ( + f"Preserve proof from {clean_channel} at {clean_location}.", + f"Link confirmation reference: {clean_reference}.", + f"Staff must verify statutory basis before relying on this packet: {clean_basis}.", + "This packet stores evidence for staff review; it does not certify legal sufficiency or publish an official notice.", + ) + return PublicationProof( + notice_id=notice_id.strip() or "unassigned notice", + notice_type=notice_type.strip() or "general notice", + source_module=source_module.strip() or "manual", + source_record_id=source_record_id.strip() or "unlinked source record", + channel=clean_channel, + published_at=published_at.strip() or "pending publication timestamp", + location=clean_location, + confirmation_reference=clean_reference, + statutory_basis=clean_basis, + reviewer=clean_reviewer, + proof_notes=proof_notes, + compliance_status="proof_recorded_staff_review_required", + ) diff --git a/docs/index.html b/docs/index.html index 7b6c97b..ab7fd37 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,12 +12,12 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, optional registry/deadline workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

Shipping v0.1.3

-

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.
+

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/tests/test_production_depth_notice_persistence.py b/tests/test_production_depth_notice_persistence.py index 3e4555d..93ef91b 100644 --- a/tests/test_production_depth_notice_persistence.py +++ b/tests/test_production_depth_notice_persistence.py @@ -18,12 +18,29 @@ def test_repository_persists_registry_and_deadline(tmp_path: Path) -> None: repository = NoticeWorkpaperRepository(db_url=db_url) record = repository.create_notice_record(notice_id="N-1", notice_type="hearing", owner="Clerk") plan = repository.create_deadline_plan(notice_type="hearing", event_date=date(2026, 5, 20)) + proof = repository.create_publication_proof( + notice_id="N-1", + notice_type="hearing", + source_module="civicclerk", + source_record_id="meeting-42", + channel="newspaper", + published_at="2026-05-10T09:00:00-06:00", + location="Daily Gazette", + confirmation_reference="DG-12345", + statutory_basis="MCA hearing notice", + reviewer="Deputy Clerk", + ) repository.engine.dispose() reloaded = NoticeWorkpaperRepository(db_url=db_url) assert reloaded.get_notice_record(record.record_id).owner == "Clerk" stored_plan = reloaded.get_deadline_plan(plan.plan_id) + stored_proof = reloaded.get_publication_proof(proof.proof_id) assert stored_plan.staff_review_required is True assert "official notice record" in stored_plan.disclaimer + assert stored_proof.source_module == "civicclerk" + assert stored_proof.source_record_id == "meeting-42" + assert stored_proof.confirmation_reference == "DG-12345" + assert stored_proof.compliance_status == "proof_recorded_staff_review_required" reloaded.engine.dispose() db_path.unlink() @@ -32,10 +49,16 @@ def test_notice_persistence_api_round_trip(monkeypatch, tmp_path: Path) -> None: db_path = tmp_path / "civicnotice-api.db" monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") _dispose_workpaper_repository() - created_record = client.post("/api/v1/civicnotice/registry", json={"notice_id":"N-1","notice_type":"hearing","owner":"Clerk"}) + created_record = client.post( + "/api/v1/civicnotice/registry", + json={"notice_id": "N-1", "notice_type": "hearing", "owner": "Clerk"}, + ) record_id = created_record.json()["record_id"] fetched_record = client.get(f"/api/v1/civicnotice/registry/{record_id}") - created_plan = client.post("/api/v1/civicnotice/deadlines", json={"notice_type":"hearing","event_date":"2026-05-20","lead_days":10}) + created_plan = client.post( + "/api/v1/civicnotice/deadlines", + json={"notice_type": "hearing", "event_date": "2026-05-20", "lead_days": 10}, + ) plan_id = created_plan.json()["plan_id"] fetched_plan = client.get(f"/api/v1/civicnotice/deadlines/{plan_id}") _dispose_workpaper_repository() @@ -48,6 +71,39 @@ def test_notice_persistence_api_round_trip(monkeypatch, tmp_path: Path) -> None: db_path.unlink() +def test_publication_proof_api_round_trip(monkeypatch, tmp_path: Path) -> None: + db_path = tmp_path / "civicnotice-proof.db" + monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") + _dispose_workpaper_repository() + created_proof = client.post( + "/api/v1/civicnotice/publication-proof", + json={ + "notice_id": "N-2", + "notice_type": "hearing", + "source_module": "civicclerk", + "source_record_id": "meeting-99", + "channel": "public website", + "published_at": "2026-05-10T09:00:00-06:00", + "location": "city.example.gov/notices/N-2", + "confirmation_reference": "CMS-98765", + "statutory_basis": "staff-entered hearing notice basis", + "reviewer": "City Clerk", + }, + ) + proof_id = created_proof.json()["proof_id"] + fetched_proof = client.get(f"/api/v1/civicnotice/publication-proof/{proof_id}") + _dispose_workpaper_repository() + monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + assert fetched_proof.status_code == 200 + payload = fetched_proof.json() + assert payload["notice_id"] == "N-2" + assert payload["source_module"] == "civicclerk" + assert payload["source_record_id"] == "meeting-99" + assert payload["confirmation_reference"] == "CMS-98765" + assert "does not certify legal sufficiency" in " ".join(payload["proof_notes"]) + db_path.unlink() + + def test_get_registry_without_persistence_returns_actionable_503(monkeypatch) -> None: monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL", raising=False) _dispose_workpaper_repository() @@ -66,3 +122,24 @@ def test_get_deadline_missing_id_returns_actionable_404(monkeypatch, tmp_path: P assert response.status_code == 404 assert "POST /api/v1/civicnotice/deadlines" in response.json()["detail"]["fix"] db_path.unlink() + + +def test_publication_proof_without_persistence_returns_actionable_503(monkeypatch) -> None: + monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL", raising=False) + _dispose_workpaper_repository() + response = client.post( + "/api/v1/civicnotice/publication-proof", + json={ + "notice_id": "N-3", + "notice_type": "hearing", + "source_record_id": "meeting-100", + "channel": "newspaper", + "published_at": "2026-05-10", + "location": "Daily Gazette", + "confirmation_reference": "DG-333", + "statutory_basis": "staff-entered basis", + "reviewer": "City Clerk", + }, + ) + assert response.status_code == 503 + assert "Set CIVICNOTICE_WORKPAPER_DB_URL" in response.json()["detail"]["fix"] diff --git a/tests/test_runtime_foundation.py b/tests/test_runtime_foundation.py index fca56f1..b67b2f2 100644 --- a/tests/test_runtime_foundation.py +++ b/tests/test_runtime_foundation.py @@ -18,7 +18,7 @@ def test_root_endpoint_states_runtime_boundary() -> None: assert data["name"] == "CivicNotice" assert data["status"] == "notice compliance foundation" assert "CivicCore-backed deadline plans" in data["message"] - assert "database-backed registry/deadline workpapers" in data["message"] + assert "database-backed registry/deadline/publication-proof workpapers" in data["message"] assert "official publication" in data["message"] assert "Post-v0.1.3 roadmap" in data["next_step"] From 139fe9873758d5e65ba0613f3f661171b4715aa4 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:24:23 -0600 Subject: [PATCH 03/13] Cover missing publication proof retrieval --- tests/test_production_depth_notice_persistence.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_production_depth_notice_persistence.py b/tests/test_production_depth_notice_persistence.py index 93ef91b..39e6906 100644 --- a/tests/test_production_depth_notice_persistence.py +++ b/tests/test_production_depth_notice_persistence.py @@ -124,6 +124,20 @@ def test_get_deadline_missing_id_returns_actionable_404(monkeypatch, tmp_path: P db_path.unlink() +def test_get_publication_proof_missing_id_returns_actionable_404( + monkeypatch, tmp_path: Path +) -> None: + db_path = tmp_path / "civicnotice-proof-missing.db" + monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") + _dispose_workpaper_repository() + response = client.get("/api/v1/civicnotice/publication-proof/missing") + _dispose_workpaper_repository() + monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + assert response.status_code == 404 + assert "POST /api/v1/civicnotice/publication-proof" in response.json()["detail"]["fix"] + db_path.unlink() + + def test_publication_proof_without_persistence_returns_actionable_503(monkeypatch) -> None: monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL", raising=False) _dispose_workpaper_repository() From 51564d7fc45e1b79f3367c9a6e82e9dc20e53697 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:28:39 -0600 Subject: [PATCH 04/13] Add CivicNotice statutory rules and templates --- CHANGELOG.md | 1 + README.md | 8 +- README.txt | 8 +- USER-MANUAL.md | 6 +- USER-MANUAL.txt | 6 +- civicnotice/main.py | 56 ++++++++++- civicnotice/notice_templates.py | 67 ++++++++++++++ civicnotice/statutory_rules.py | 158 ++++++++++++++++++++++++++++++++ docs/IMPLEMENTATION_PLAN.md | 2 +- docs/RECONCILIATION.md | 2 +- docs/index.html | 4 +- scripts/verify-docs.sh | 2 + tests/test_notice_foundation.py | 89 ++++++++++++++++++ 13 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 civicnotice/notice_templates.py create mode 100644 civicnotice/statutory_rules.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad6e10..3b1d24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Moved CivicNotice to the `civiccore v1.2.0` release wheel used by the current Windows Local city-core platform. - Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.1.3 compatibility release. - Added durable publication-proof workpapers for staff evidence packets linked to Clerk, procurement, board, or manual source records. +- Added deterministic statutory rule checks and staff-editable notice drafting templates for common notice types. - Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. ## [0.1.2] - 2026-04-29 diff --git a/README.md b/README.md index 5e930b7..7fd70c0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # CivicNotice -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, publication-readiness review, channel planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, and notice-record export checklists. -Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does - Create sample notice registry stubs. - Build statutory publication deadline reminder plans using the shared CivicCore notice helper. +- Check notice packets against deterministic staff-review rule packs for common notice types. +- Build staff-editable notice templates with required fields and unresolved placeholders. - Persist notice registry, deadline-plan, and publication-proof workpapers when `CIVICNOTICE_WORKPAPER_DB_URL` is configured. - Store staff-reviewed publication proof packets linked to upstream Clerk, procurement, board, or manual source records. - Assemble publication-readiness checklists for staff review. @@ -33,6 +35,8 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - `GET /api/v1/civicnotice/registry/{record_id}` retrieves a persisted notice registry record. - `POST /api/v1/civicnotice/deadlines` returns statutory deadline reminders. - `GET /api/v1/civicnotice/deadlines/{plan_id}` retrieves a persisted deadline plan. +- `POST /api/v1/civicnotice/rule-check` checks a notice packet against deterministic staff-review rules. +- `POST /api/v1/civicnotice/templates` returns a staff-editable notice template. - `POST /api/v1/civicnotice/publication-proof` stores a durable publication proof workpaper. - `GET /api/v1/civicnotice/publication-proof/{proof_id}` retrieves a persisted publication proof workpaper. - `POST /api/v1/civicnotice/publication-check` returns a publication-readiness checklist. diff --git a/README.txt b/README.txt index 7495ff2..ecaa842 100644 --- a/README.txt +++ b/README.txt @@ -1,15 +1,17 @@ CivicNotice =========== -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, publication-readiness review, channel planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, and notice-record export checklists. -Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. What CivicNotice does: - Create sample notice registry stubs. - Build statutory publication deadline reminder plans using the shared CivicCore notice helper. +- Check notice packets against deterministic staff-review rule packs for common notice types. +- Build staff-editable notice templates with required fields and unresolved placeholders. - Persist notice registry, deadline-plan, and publication-proof workpapers when CIVICNOTICE_WORKPAPER_DB_URL is configured. - Store staff-reviewed publication proof packets linked to upstream source records. - Assemble publication-readiness checklists for staff review. @@ -25,6 +27,8 @@ API surface: - GET /api/v1/civicnotice/registry/{record_id} - POST /api/v1/civicnotice/deadlines - GET /api/v1/civicnotice/deadlines/{plan_id} +- POST /api/v1/civicnotice/rule-check +- POST /api/v1/civicnotice/templates - POST /api/v1/civicnotice/publication-proof - GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check diff --git a/USER-MANUAL.md b/USER-MANUAL.md index fe8e92a..2b5f3dd 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -2,7 +2,7 @@ ## For Non-Technical Users -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -19,6 +19,8 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, - `GET /api/v1/civicnotice/registry/{record_id}` - `POST /api/v1/civicnotice/deadlines` - `GET /api/v1/civicnotice/deadlines/{plan_id}` +- `POST /api/v1/civicnotice/rule-check` +- `POST /api/v1/civicnotice/templates` - `POST /api/v1/civicnotice/publication-proof` - `GET /api/v1/civicnotice/publication-proof/{proof_id}` - `POST /api/v1/civicnotice/publication-check` @@ -44,4 +46,4 @@ flowchart LR CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index 40822aa..bda827b 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -4,7 +4,7 @@ CivicNotice User Manual For non-technical users ----------------------- -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -22,6 +22,8 @@ Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, de - GET /api/v1/civicnotice/registry/{record_id} - POST /api/v1/civicnotice/deadlines - GET /api/v1/civicnotice/deadlines/{plan_id} +- POST /api/v1/civicnotice/rule-check +- POST /api/v1/civicnotice/templates - POST /api/v1/civicnotice/publication-proof - GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check @@ -41,4 +43,4 @@ Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/main.py b/civicnotice/main.py index ec92c85..bb26470 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -6,11 +6,12 @@ from civiccore import __version__ as CIVICCORE_VERSION from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from civicnotice import __version__ from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan +from civicnotice.notice_templates import build_notice_template from civicnotice.notice_registry import register_notice_stub from civicnotice.persistence import ( NoticeWorkpaperRepository, @@ -21,6 +22,7 @@ from civicnotice.public_ui import render_public_lookup_page from civicnotice.publication_check import build_publication_checklist from civicnotice.records_export import build_notice_records_export +from civicnotice.statutory_rules import check_statutory_notice_requirements app = FastAPI( @@ -50,6 +52,25 @@ class PublicationRequest(BaseModel): channel: str +class RuleCheckRequest(BaseModel): + notice_type: str + event_date: date + publication_dates: list[date] = Field(default_factory=list) + channels: list[str] = Field(default_factory=list) + content_fields: list[str] = Field(default_factory=list) + statutory_basis: str = "" + + +class NoticeTemplateRequest(BaseModel): + notice_type: str + matter_title: str + event_date: date + location: str = "" + contact: str = "" + source_module: str = "manual" + statutory_basis: str = "" + + class ChannelRequest(BaseModel): notice_type: str audience: str @@ -84,7 +105,7 @@ def root() -> dict[str, str]: "status": "notice compliance foundation", "message": ( "CivicNotice package, API foundation, sample notice registry, CivicCore-backed deadline plans, " - "publication-readiness checklist, channel planning, records export checklist, optional " + "statutory rule checks, notice drafting templates, publication-readiness checklist, channel planning, records export checklist, optional " "database-backed registry/deadline/publication-proof workpapers, and public UI foundation are online; official " "legal sufficiency decisions, official publication, legal " "advice, live LLM calls, publication-system write-back, and notice system-of-record integrations " @@ -250,6 +271,37 @@ def publication_checklist(request: PublicationRequest) -> dict[str, object]: ).__dict__ +@app.post("/api/v1/civicnotice/rule-check") +def rule_check(request: RuleCheckRequest) -> dict[str, object]: + result = check_statutory_notice_requirements( + notice_type=request.notice_type, + event_date=request.event_date, + publication_dates=tuple(request.publication_dates), + channels=tuple(request.channels), + content_fields=tuple(request.content_fields), + statutory_basis=request.statutory_basis, + ) + payload = result.__dict__.copy() + payload["event_date"] = result.event_date.isoformat() + payload["required_deadline_date"] = result.required_deadline_date.isoformat() + payload["publication_dates"] = [value.isoformat() for value in result.publication_dates] + return payload + + +@app.post("/api/v1/civicnotice/templates") +def notice_template(request: NoticeTemplateRequest) -> dict[str, object]: + result = build_notice_template( + notice_type=request.notice_type, + matter_title=request.matter_title, + event_date=request.event_date, + location=request.location, + contact=request.contact, + source_module=request.source_module, + statutory_basis=request.statutory_basis, + ) + return result.__dict__ + + @app.post("/api/v1/civicnotice/channels") def channel_plan(request: ChannelRequest) -> dict[str, object]: return plan_notice_channels( diff --git a/civicnotice/notice_templates.py b/civicnotice/notice_templates.py new file mode 100644 index 0000000..82baea0 --- /dev/null +++ b/civicnotice/notice_templates.py @@ -0,0 +1,67 @@ +"""Notice drafting templates for CivicNotice v0.1.3.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date + +from civicnotice.notice_registry import DISCLAIMER +from civicnotice.statutory_rules import RULE_PACKS + + +@dataclass(frozen=True) +class NoticeTemplate: + notice_type: str + title: str + source_module: str + template_lines: tuple[str, ...] + required_fields: tuple[str, ...] + placeholders_remaining: tuple[str, ...] + staff_review_required: bool + disclaimer: str = DISCLAIMER + + +def build_notice_template( + *, + notice_type: str, + matter_title: str, + event_date: date, + location: str, + contact: str, + source_module: str = "manual", + statutory_basis: str = "", +) -> NoticeTemplate: + """Build a staff-editable notice template without declaring it legally sufficient.""" + + rule = RULE_PACKS.get(_clean(notice_type), RULE_PACKS["general notice"]) + clean_title = matter_title.strip() or "Untitled notice matter" + clean_location = location.strip() or "[staff must enter location]" + clean_contact = contact.strip() or "[staff must enter contact]" + clean_basis = statutory_basis.strip() or "[staff must enter statutory basis]" + placeholders = tuple( + value + for value in (clean_location, clean_contact, clean_basis) + if value.startswith("[staff must enter") + ) + lines = ( + f"NOTICE: {clean_title}", + f"Notice type: {rule.notice_type}.", + f"Event or deadline date: {event_date.isoformat()}.", + f"Location or submission path: {clean_location}.", + f"Staff contact: {clean_contact}.", + f"Statutory basis: {clean_basis}.", + "Staff must verify required publication channels, lead time, accessibility, and proof before release.", + ) + return NoticeTemplate( + notice_type=rule.notice_type, + title=clean_title, + source_module=source_module.strip() or "manual", + template_lines=lines, + required_fields=rule.required_content, + placeholders_remaining=placeholders, + staff_review_required=True, + ) + + +def _clean(value: str) -> str: + return " ".join(value.strip().lower().split()) diff --git a/civicnotice/statutory_rules.py b/civicnotice/statutory_rules.py new file mode 100644 index 0000000..bd834b0 --- /dev/null +++ b/civicnotice/statutory_rules.py @@ -0,0 +1,158 @@ +"""Deterministic statutory notice rule checks for CivicNotice v0.1.3.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, timedelta + +from civicnotice.notice_registry import DISCLAIMER + + +@dataclass(frozen=True) +class NoticeRule: + notice_type: str + minimum_lead_days: int + required_publications: int + required_channels: tuple[str, ...] + required_content: tuple[str, ...] + emergency_path_allowed: bool = False + + +@dataclass(frozen=True) +class StatutoryRuleCheck: + notice_type: str + event_date: date + required_deadline_date: date + publication_dates: tuple[date, ...] + channels: tuple[str, ...] + required_publications: int + required_channels: tuple[str, ...] + missing_channels: tuple[str, ...] + required_content: tuple[str, ...] + missing_content: tuple[str, ...] + statutory_basis: str + deadline_status: str + publication_count_status: str + staff_review_required: bool + disclaimer: str = DISCLAIMER + + +RULE_PACKS: dict[str, NoticeRule] = { + "general notice": NoticeRule( + notice_type="general notice", + minimum_lead_days=0, + required_publications=1, + required_channels=(), + required_content=("title", "event date", "location", "statutory basis"), + ), + "planning hearing": NoticeRule( + notice_type="planning hearing", + minimum_lead_days=15, + required_publications=1, + required_channels=("city website", "posting board"), + required_content=("title", "hearing date", "location", "case number", "statutory basis"), + ), + "bid notice": NoticeRule( + notice_type="bid notice", + minimum_lead_days=14, + required_publications=1, + required_channels=("city website", "newspaper"), + required_content=("title", "bid deadline", "scope summary", "submission location"), + ), + "vacancy notice": NoticeRule( + notice_type="vacancy notice", + minimum_lead_days=10, + required_publications=1, + required_channels=("city website", "posting board"), + required_content=("title", "office", "application deadline", "eligibility summary"), + ), + "adoption notice": NoticeRule( + notice_type="adoption notice", + minimum_lead_days=5, + required_publications=1, + required_channels=("city website",), + required_content=("title", "adopted action", "effective date", "inspection location"), + ), + "special meeting": NoticeRule( + notice_type="special meeting", + minimum_lead_days=2, + required_publications=1, + required_channels=("city website", "posting board"), + required_content=("title", "meeting date", "location", "statutory basis"), + emergency_path_allowed=True, + ), +} + + +def check_statutory_notice_requirements( + *, + notice_type: str, + event_date: date, + publication_dates: tuple[date, ...] = (), + channels: tuple[str, ...] = (), + content_fields: tuple[str, ...] = (), + statutory_basis: str = "", +) -> StatutoryRuleCheck: + """Check a notice packet against deterministic staff-review rules.""" + + rule = _rule_for(notice_type) + normalized_channels = tuple(_clean(value) for value in channels if _clean(value)) + normalized_content = tuple(_clean(value) for value in content_fields if _clean(value)) + required_deadline = event_date - timedelta(days=rule.minimum_lead_days) + missing_channels = tuple( + channel for channel in rule.required_channels if channel not in normalized_channels + ) + missing_content = tuple( + field for field in rule.required_content if field not in normalized_content + ) + deadline_status = _deadline_status( + publication_dates=publication_dates, + required_deadline=required_deadline, + ) + publication_count_status = ( + "required_publication_count_met" + if len(publication_dates) >= rule.required_publications + else "publication_count_staff_review_required" + ) + basis_missing = "statutory basis" in rule.required_content and not statutory_basis.strip() + staff_review_required = bool( + missing_channels + or missing_content + or basis_missing + or deadline_status != "meets_minimum_lead_time" + or publication_count_status != "required_publication_count_met" + ) + return StatutoryRuleCheck( + notice_type=rule.notice_type, + event_date=event_date, + required_deadline_date=required_deadline, + publication_dates=publication_dates, + channels=normalized_channels, + required_publications=rule.required_publications, + required_channels=rule.required_channels, + missing_channels=missing_channels, + required_content=rule.required_content, + missing_content=missing_content, + statutory_basis=statutory_basis.strip() or "staff-provided statutory basis required", + deadline_status=deadline_status, + publication_count_status=publication_count_status, + staff_review_required=staff_review_required, + ) + + +def _rule_for(notice_type: str) -> NoticeRule: + normalized = _clean(notice_type) + return RULE_PACKS.get(normalized, RULE_PACKS["general notice"]) + + +def _deadline_status(*, publication_dates: tuple[date, ...], required_deadline: date) -> str: + if not publication_dates: + return "publication_schedule_missing_staff_review_required" + latest_publication = max(publication_dates) + if latest_publication <= required_deadline: + return "meets_minimum_lead_time" + return "deadline_risk_staff_review_required" + + +def _clean(value: str) -> str: + return " ".join(value.strip().lower().split()) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 7709859..bd12387 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,5 @@ # CivicNotice Implementation Plan -CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, publication readiness tracking helper, channel planning helper, notice and records export checklist, and public sample UI. +CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, publication readiness tracking helper, channel planning helper, notice and records export checklist, and public sample UI. Later releases add statutory rule packs, CivicClerk/CivicProcure/CivicBoards handoffs, CivicRecords file links, proof queues, publication-system integrations, and production data governance. diff --git a/docs/RECONCILIATION.md b/docs/RECONCILIATION.md index 260df01..5c46f32 100644 --- a/docs/RECONCILIATION.md +++ b/docs/RECONCILIATION.md @@ -2,7 +2,7 @@ | Driver | CivicNotice v0.1.3 result | |---|---| -| Unified spec CivicNotice catalog entry | notice registry, deadline tracking, publication readiness tracking, channel planning, and notice/records export checklists represented through deterministic helpers. | +| Unified spec CivicNotice catalog entry | notice registry, deadline tracking, deterministic statutory rule checks, notice drafting templates, publication readiness tracking, channel planning, and notice/records export checklists represented through deterministic helpers. | | Apache 2.0 code licensing | `LICENSE-CODE` and package metadata use Apache 2.0. | | Documentation licensing | `LICENSE-DOCS` uses CC BY 4.0. | | CivicCore dependency | `pyproject.toml` pins the `civiccore v1.2.0` release wheel. | diff --git a/docs/index.html b/docs/index.html index ab7fd37..db27014 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,12 +12,12 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

Shipping v0.1.3

-

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.
+

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh index 20522d8..d8699c0 100644 --- a/scripts/verify-docs.sh +++ b/scripts/verify-docs.sh @@ -30,6 +30,8 @@ required=( "civicnotice/main.py" "civicnotice/notice_registry.py" "civicnotice/deadline_tracker.py" + "civicnotice/statutory_rules.py" + "civicnotice/notice_templates.py" "civicnotice/publication_check.py" "civicnotice/channel_plan.py" "civicnotice/records_export.py" diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 9d78eaa..be7107d 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -5,9 +5,11 @@ from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.main import app +from civicnotice.notice_templates import build_notice_template from civicnotice.notice_registry import register_notice_stub from civicnotice.publication_check import build_publication_checklist from civicnotice.records_export import build_notice_records_export +from civicnotice.statutory_rules import check_statutory_notice_requirements client = TestClient(app) @@ -48,6 +50,58 @@ def test_publication_checklist_requires_proof() -> None: assert "publication proof" in result.checklist[3] +def test_statutory_rule_check_flags_deadline_and_missing_requirements() -> None: + result = check_statutory_notice_requirements( + notice_type="planning hearing", + event_date=date(2026, 6, 15), + publication_dates=(date(2026, 6, 5),), + channels=("city website",), + content_fields=("title", "hearing date", "location"), + ) + assert result.required_deadline_date == date(2026, 5, 31) + assert result.deadline_status == "deadline_risk_staff_review_required" + assert result.publication_count_status == "required_publication_count_met" + assert result.missing_channels == ("posting board",) + assert "case number" in result.missing_content + assert result.staff_review_required is True + + +def test_notice_template_preserves_staff_review_placeholders() -> None: + result = build_notice_template( + notice_type="bid notice", + matter_title="Water main replacement bid", + event_date=date(2026, 7, 1), + location="", + contact="clerk@example.gov", + source_module="civicprocure", + ) + assert result.notice_type == "bid notice" + assert result.source_module == "civicprocure" + assert "submission location" in result.required_fields + assert "[staff must enter location]" in result.placeholders_remaining + assert "Staff must verify required publication channels" in result.template_lines[-1] + + +def test_unknown_notice_type_uses_generic_staff_review_rule() -> None: + rule_check = check_statutory_notice_requirements( + notice_type="river festival notice", + event_date=date(2026, 8, 1), + content_fields=("title",), + ) + template = build_notice_template( + notice_type="river festival notice", + matter_title="River festival street closure", + event_date=date(2026, 8, 1), + location="Main Street", + contact="clerk@example.gov", + ) + assert rule_check.notice_type == "general notice" + assert "statutory basis" in rule_check.missing_content + assert rule_check.staff_review_required is True + assert template.notice_type == "general notice" + assert "event date" in template.required_fields + + def test_channel_plan_flags_accessibility_review() -> None: result = plan_notice_channels( notice_type="public hearing", @@ -85,6 +139,35 @@ def test_notice_support_apis_success_shape() -> None: "/api/v1/civicnotice/publication-check", json={"notice_type": "vacancy notice", "channel": "newspaper"}, ) + rule_check = client.post( + "/api/v1/civicnotice/rule-check", + json={ + "notice_type": "planning hearing", + "event_date": "2026-06-15", + "publication_dates": ["2026-05-30"], + "channels": ["city website", "posting board"], + "content_fields": [ + "title", + "hearing date", + "location", + "case number", + "statutory basis", + ], + "statutory_basis": "staff-entered basis", + }, + ) + template = client.post( + "/api/v1/civicnotice/templates", + json={ + "notice_type": "planning hearing", + "matter_title": "Planning hearing", + "event_date": "2026-06-15", + "location": "Council Chambers", + "contact": "clerk@example.gov", + "source_module": "civicclerk", + "statutory_basis": "staff-entered basis", + }, + ) channels = client.post( "/api/v1/civicnotice/channels", json={"notice_type": "public hearing", "audience": "residents"}, @@ -99,6 +182,12 @@ def test_notice_support_apis_success_shape() -> None: assert len(deadlines.json()["reminders"]) == 4 assert publication.status_code == 200 assert publication.json()["proof_required"] is True + assert rule_check.status_code == 200 + assert rule_check.json()["deadline_status"] == "meets_minimum_lead_time" + assert rule_check.json()["missing_channels"] == [] + assert template.status_code == 200 + assert template.json()["source_module"] == "civicclerk" + assert template.json()["staff_review_required"] is True assert channels.status_code == 200 assert channels.json()["staff_review_required"] is True assert export.status_code == 200 From 0cb78ca95a569803e16b7595b001c2eaad66bca4 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:30:57 -0600 Subject: [PATCH 05/13] Add CivicNotice subscriber delivery planning --- CHANGELOG.md | 1 + README.md | 6 +- README.txt | 6 +- USER-MANUAL.md | 5 +- USER-MANUAL.txt | 5 +- civicnotice/main.py | 40 +++++++++++++ civicnotice/subscriber_delivery.py | 93 ++++++++++++++++++++++++++++++ docs/IMPLEMENTATION_PLAN.md | 2 +- docs/index.html | 4 +- scripts/verify-docs.sh | 1 + tests/test_notice_foundation.py | 62 ++++++++++++++++++++ 11 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 civicnotice/subscriber_delivery.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1d24c..2b6b9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.1.3 compatibility release. - Added durable publication-proof workpapers for staff evidence packets linked to Clerk, procurement, board, or manual source records. - Added deterministic statutory rule checks and staff-editable notice drafting templates for common notice types. +- Added subscriber delivery planning with active/suppressed subscriber accounting, deduped email recipients, required-channel checks, and language-review flags. - Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. ## [0.1.2] - 2026-04-29 diff --git a/README.md b/README.md index 7fd70c0..2922c18 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CivicNotice -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. -Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does @@ -14,6 +14,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - Store staff-reviewed publication proof packets linked to upstream Clerk, procurement, board, or manual source records. - Assemble publication-readiness checklists for staff review. - Plan notice channels and accessibility-review needs. +- Build subscriber delivery plans without sending notices or storing subscriber PII. - Produce notice and records export checklists. - Demonstrate a public notice-support UI at `/civicnotice`. @@ -41,6 +42,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - `GET /api/v1/civicnotice/publication-proof/{proof_id}` retrieves a persisted publication proof workpaper. - `POST /api/v1/civicnotice/publication-check` returns a publication-readiness checklist. - `POST /api/v1/civicnotice/channels` returns channel planning flags. +- `POST /api/v1/civicnotice/subscribers/plan` returns a subscriber delivery plan. - `POST /api/v1/civicnotice/export` returns a notice and records export checklist. ## Local Development diff --git a/README.txt b/README.txt index ecaa842..08b93c3 100644 --- a/README.txt +++ b/README.txt @@ -1,9 +1,9 @@ CivicNotice =========== -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. -Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. @@ -16,6 +16,7 @@ What CivicNotice does: - Store staff-reviewed publication proof packets linked to upstream source records. - Assemble publication-readiness checklists for staff review. - Plan notice channels and accessibility-review needs. +- Build subscriber delivery plans without sending notices or storing subscriber PII. - Produce notice and records export checklists. - Demonstrate a public notice-support UI at /civicnotice. @@ -33,6 +34,7 @@ API surface: - GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check - POST /api/v1/civicnotice/channels +- POST /api/v1/civicnotice/subscribers/plan - POST /api/v1/civicnotice/export License: Apache License 2.0 for code; CC BY 4.0 for documentation. diff --git a/USER-MANUAL.md b/USER-MANUAL.md index 2b5f3dd..5aefb18 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -2,7 +2,7 @@ ## For Non-Technical Users -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build subscriber delivery plans, and assemble a notice/records export checklist. Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -25,6 +25,7 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, - `GET /api/v1/civicnotice/publication-proof/{proof_id}` - `POST /api/v1/civicnotice/publication-check` - `POST /api/v1/civicnotice/channels` +- `POST /api/v1/civicnotice/subscribers/plan` - `POST /api/v1/civicnotice/export` Run: @@ -46,4 +47,4 @@ flowchart LR CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index bda827b..6e58130 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -4,7 +4,7 @@ CivicNotice User Manual For non-technical users ----------------------- -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build subscriber delivery plans, and assemble a notice/records export checklist. Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -28,6 +28,7 @@ Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, de - GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check - POST /api/v1/civicnotice/channels +- POST /api/v1/civicnotice/subscribers/plan - POST /api/v1/civicnotice/export Run: @@ -43,4 +44,4 @@ Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/main.py b/civicnotice/main.py index bb26470..e52bab9 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -23,6 +23,7 @@ from civicnotice.publication_check import build_publication_checklist from civicnotice.records_export import build_notice_records_export from civicnotice.statutory_rules import check_statutory_notice_requirements +from civicnotice.subscriber_delivery import Subscriber, build_subscriber_delivery_plan app = FastAPI( @@ -76,6 +77,23 @@ class ChannelRequest(BaseModel): audience: str +class SubscriberRequest(BaseModel): + subscriber_id: str + name: str + email: str = "" + channels: list[str] = Field(default_factory=list) + language: str = "English" + active: bool = True + + +class SubscriberDeliveryRequest(BaseModel): + notice_id: str + notice_type: str + audience: str + subscribers: list[SubscriberRequest] = Field(default_factory=list) + required_channels: list[str] = Field(default_factory=lambda: ["email"]) + + class RecordsExportRequest(BaseModel): notice_id: str title: str @@ -310,6 +328,28 @@ def channel_plan(request: ChannelRequest) -> dict[str, object]: ).__dict__ +@app.post("/api/v1/civicnotice/subscribers/plan") +def subscriber_delivery_plan(request: SubscriberDeliveryRequest) -> dict[str, object]: + subscribers = tuple( + Subscriber( + subscriber_id=subscriber.subscriber_id, + name=subscriber.name, + email=subscriber.email, + channels=tuple(subscriber.channels), + language=subscriber.language, + active=subscriber.active, + ) + for subscriber in request.subscribers + ) + return build_subscriber_delivery_plan( + notice_id=request.notice_id, + notice_type=request.notice_type, + audience=request.audience, + subscribers=subscribers, + required_channels=tuple(request.required_channels), + ).__dict__ + + @app.post("/api/v1/civicnotice/export") def records_export(request: RecordsExportRequest) -> dict[str, object]: return build_notice_records_export( diff --git a/civicnotice/subscriber_delivery.py b/civicnotice/subscriber_delivery.py new file mode 100644 index 0000000..48b7f13 --- /dev/null +++ b/civicnotice/subscriber_delivery.py @@ -0,0 +1,93 @@ +"""Subscriber delivery planning for CivicNotice v0.1.3.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from civicnotice.notice_registry import DISCLAIMER + + +@dataclass(frozen=True) +class Subscriber: + subscriber_id: str + name: str + email: str + channels: tuple[str, ...] + language: str + active: bool = True + + +@dataclass(frozen=True) +class SubscriberDeliveryPlan: + notice_id: str + notice_type: str + audience: str + active_subscriber_count: int + suppressed_subscriber_count: int + delivery_channels: tuple[str, ...] + email_recipients: tuple[str, ...] + missing_required_channels: tuple[str, ...] + language_review_required: bool + staff_review_required: bool + delivery_notes: tuple[str, ...] + disclaimer: str = DISCLAIMER + + +def build_subscriber_delivery_plan( + *, + notice_id: str, + notice_type: str, + audience: str, + subscribers: tuple[Subscriber, ...], + required_channels: tuple[str, ...] = ("email",), +) -> SubscriberDeliveryPlan: + """Build a staff-reviewable subscriber delivery plan without sending notices.""" + + active_subscribers = tuple(subscriber for subscriber in subscribers if subscriber.active) + suppressed_count = len(subscribers) - len(active_subscribers) + normalized_required = tuple(_clean(channel) for channel in required_channels if _clean(channel)) + channel_set = { + channel + for subscriber in active_subscribers + for channel in subscriber.channels + if _clean(channel) + } + normalized_channels = tuple(sorted(_clean(channel) for channel in channel_set)) + email_recipients = tuple( + sorted( + { + subscriber.email.strip().lower() + for subscriber in active_subscribers + if subscriber.email.strip() + and "email" in {_clean(channel) for channel in subscriber.channels} + } + ) + ) + languages = {_clean(subscriber.language) for subscriber in active_subscribers if subscriber.language} + missing_required = tuple( + channel for channel in normalized_required if channel not in normalized_channels + ) + language_review_required = bool(languages - {"", "english", "en"}) + staff_review_required = bool(missing_required or language_review_required) + notes = ( + "Delivery plan only prepares recipients and channels; it does not send or publish notices.", + "Staff must verify opt-in status, suppression lists, bounced addresses, and legal delivery requirements.", + "Preserve delivery logs or third-party confirmations with the publication proof packet.", + ) + return SubscriberDeliveryPlan( + notice_id=notice_id.strip() or "unassigned-notice", + notice_type=notice_type.strip() or "general notice", + audience=audience.strip() or "general public", + active_subscriber_count=len(active_subscribers), + suppressed_subscriber_count=suppressed_count, + delivery_channels=normalized_channels, + email_recipients=email_recipients, + missing_required_channels=missing_required, + language_review_required=language_review_required, + staff_review_required=staff_review_required, + delivery_notes=notes, + ) + + +def _clean(value: str) -> str: + return " ".join(value.strip().lower().split()) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index bd12387..8a1ed54 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,5 @@ # CivicNotice Implementation Plan -CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, publication readiness tracking helper, channel planning helper, notice and records export checklist, and public sample UI. +CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, publication readiness tracking helper, channel planning helper, subscriber delivery planning, notice and records export checklist, and public sample UI. Later releases add statutory rule packs, CivicClerk/CivicProcure/CivicBoards handoffs, CivicRecords file links, proof queues, publication-system integrations, and production data governance. diff --git a/docs/index.html b/docs/index.html index db27014..9a91077 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,12 +12,12 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

Shipping v0.1.3

-

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.
+

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh index d8699c0..fac715d 100644 --- a/scripts/verify-docs.sh +++ b/scripts/verify-docs.sh @@ -34,6 +34,7 @@ required=( "civicnotice/notice_templates.py" "civicnotice/publication_check.py" "civicnotice/channel_plan.py" + "civicnotice/subscriber_delivery.py" "civicnotice/records_export.py" "civicnotice/public_ui.py" ) diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index be7107d..30c9a6c 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -10,6 +10,7 @@ from civicnotice.publication_check import build_publication_checklist from civicnotice.records_export import build_notice_records_export from civicnotice.statutory_rules import check_statutory_notice_requirements +from civicnotice.subscriber_delivery import Subscriber, build_subscriber_delivery_plan client = TestClient(app) @@ -112,6 +113,45 @@ def test_channel_plan_flags_accessibility_review() -> None: assert any("ADA format" in note for note in result.accessibility_notes) +def test_subscriber_delivery_plan_dedupes_and_flags_review() -> None: + result = build_subscriber_delivery_plan( + notice_id="hear-001", + notice_type="planning hearing", + audience="planning subscribers", + subscribers=( + Subscriber( + subscriber_id="1", + name="Alex Resident", + email="alex@example.gov", + channels=("Email",), + language="English", + ), + Subscriber( + subscriber_id="2", + name="Alex Duplicate", + email="ALEX@example.gov", + channels=("email",), + language="Spanish", + ), + Subscriber( + subscriber_id="3", + name="Inactive Resident", + email="inactive@example.gov", + channels=("email",), + language="English", + active=False, + ), + ), + required_channels=("email", "postal mail"), + ) + assert result.active_subscriber_count == 2 + assert result.suppressed_subscriber_count == 1 + assert result.email_recipients == ("alex@example.gov",) + assert result.missing_required_channels == ("postal mail",) + assert result.language_review_required is True + assert result.staff_review_required is True + + def test_records_export_preserves_notice_context() -> None: result = build_notice_records_export( notice_id="hear-001", @@ -172,6 +212,25 @@ def test_notice_support_apis_success_shape() -> None: "/api/v1/civicnotice/channels", json={"notice_type": "public hearing", "audience": "residents"}, ) + subscribers = client.post( + "/api/v1/civicnotice/subscribers/plan", + json={ + "notice_id": "hear-001", + "notice_type": "public hearing", + "audience": "residents", + "subscribers": [ + { + "subscriber_id": "1", + "name": "Alex Resident", + "email": "alex@example.gov", + "channels": ["email"], + "language": "English", + "active": True, + } + ], + "required_channels": ["email"], + }, + ) export = client.post( "/api/v1/civicnotice/export", json={"title": "Planning hearing notice archive", "notice_id": "hear-001"}, @@ -190,6 +249,9 @@ def test_notice_support_apis_success_shape() -> None: assert template.json()["staff_review_required"] is True assert channels.status_code == 200 assert channels.json()["staff_review_required"] is True + assert subscribers.status_code == 200 + assert subscribers.json()["email_recipients"] == ["alex@example.gov"] + assert subscribers.json()["missing_required_channels"] == [] assert export.status_code == 200 assert export.json()["notice_id"] == "hear-001" From fa15a3c9e4046c02a9c08388c2381501b855240b Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:33:12 -0600 Subject: [PATCH 06/13] Add CivicNotice accessibility readiness review --- CHANGELOG.md | 1 + README.md | 6 +- README.txt | 6 +- USER-MANUAL.md | 5 +- USER-MANUAL.txt | 5 +- civicnotice/accessibility_review.py | 86 +++++++++++++++++++++++++++++ civicnotice/main.py | 28 +++++++++- docs/IMPLEMENTATION_PLAN.md | 2 +- docs/index.html | 4 +- scripts/verify-docs.sh | 1 + tests/test_notice_foundation.py | 40 ++++++++++++++ 11 files changed, 172 insertions(+), 12 deletions(-) create mode 100644 civicnotice/accessibility_review.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6b9cb..31df574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Added durable publication-proof workpapers for staff evidence packets linked to Clerk, procurement, board, or manual source records. - Added deterministic statutory rule checks and staff-editable notice drafting templates for common notice types. - Added subscriber delivery planning with active/suppressed subscriber accounting, deduped email recipients, required-channel checks, and language-review flags. +- Added accessibility, plain-language, and human-approved translation readiness packets for public notices. - Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. ## [0.1.2] - 2026-04-29 diff --git a/README.md b/README.md index 2922c18..cb8711c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CivicNotice -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. -Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does @@ -10,6 +10,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - Build statutory publication deadline reminder plans using the shared CivicCore notice helper. - Check notice packets against deterministic staff-review rule packs for common notice types. - Build staff-editable notice templates with required fields and unresolved placeholders. +- Build accessibility, plain-language, and human-approved translation readiness packets. - Persist notice registry, deadline-plan, and publication-proof workpapers when `CIVICNOTICE_WORKPAPER_DB_URL` is configured. - Store staff-reviewed publication proof packets linked to upstream Clerk, procurement, board, or manual source records. - Assemble publication-readiness checklists for staff review. @@ -41,6 +42,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - `POST /api/v1/civicnotice/publication-proof` stores a durable publication proof workpaper. - `GET /api/v1/civicnotice/publication-proof/{proof_id}` retrieves a persisted publication proof workpaper. - `POST /api/v1/civicnotice/publication-check` returns a publication-readiness checklist. +- `POST /api/v1/civicnotice/accessibility-review` returns accessibility and language-readiness flags. - `POST /api/v1/civicnotice/channels` returns channel planning flags. - `POST /api/v1/civicnotice/subscribers/plan` returns a subscriber delivery plan. - `POST /api/v1/civicnotice/export` returns a notice and records export checklist. diff --git a/README.txt b/README.txt index 08b93c3..f5512de 100644 --- a/README.txt +++ b/README.txt @@ -1,9 +1,9 @@ CivicNotice =========== -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. -Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. @@ -12,6 +12,7 @@ What CivicNotice does: - Build statutory publication deadline reminder plans using the shared CivicCore notice helper. - Check notice packets against deterministic staff-review rule packs for common notice types. - Build staff-editable notice templates with required fields and unresolved placeholders. +- Build accessibility, plain-language, and human-approved translation readiness packets. - Persist notice registry, deadline-plan, and publication-proof workpapers when CIVICNOTICE_WORKPAPER_DB_URL is configured. - Store staff-reviewed publication proof packets linked to upstream source records. - Assemble publication-readiness checklists for staff review. @@ -33,6 +34,7 @@ API surface: - POST /api/v1/civicnotice/publication-proof - GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check +- POST /api/v1/civicnotice/accessibility-review - POST /api/v1/civicnotice/channels - POST /api/v1/civicnotice/subscribers/plan - POST /api/v1/civicnotice/export diff --git a/USER-MANUAL.md b/USER-MANUAL.md index 5aefb18..ea8e9c6 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -2,7 +2,7 @@ ## For Non-Technical Users -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build subscriber delivery plans, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, and assemble a notice/records export checklist. Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -24,6 +24,7 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, - `POST /api/v1/civicnotice/publication-proof` - `GET /api/v1/civicnotice/publication-proof/{proof_id}` - `POST /api/v1/civicnotice/publication-check` +- `POST /api/v1/civicnotice/accessibility-review` - `POST /api/v1/civicnotice/channels` - `POST /api/v1/civicnotice/subscribers/plan` - `POST /api/v1/civicnotice/export` @@ -47,4 +48,4 @@ flowchart LR CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index 6e58130..6d4fbd7 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -4,7 +4,7 @@ CivicNotice User Manual For non-technical users ----------------------- -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build subscriber delivery plans, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, and assemble a notice/records export checklist. Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -27,6 +27,7 @@ Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, de - POST /api/v1/civicnotice/publication-proof - GET /api/v1/civicnotice/publication-proof/{proof_id} - POST /api/v1/civicnotice/publication-check +- POST /api/v1/civicnotice/accessibility-review - POST /api/v1/civicnotice/channels - POST /api/v1/civicnotice/subscribers/plan - POST /api/v1/civicnotice/export @@ -44,4 +45,4 @@ Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/accessibility_review.py b/civicnotice/accessibility_review.py new file mode 100644 index 0000000..928116a --- /dev/null +++ b/civicnotice/accessibility_review.py @@ -0,0 +1,86 @@ +"""Accessibility and language-readiness review for CivicNotice v0.1.3.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from civicnotice.notice_registry import DISCLAIMER + + +@dataclass(frozen=True) +class NoticeAccessibilityReview: + notice_id: str + title: str + missing_accessibility_items: tuple[str, ...] + plain_language_flags: tuple[str, ...] + translation_tasks: tuple[str, ...] + attachment_review_required: bool + staff_review_required: bool + review_notes: tuple[str, ...] + disclaimer: str = DISCLAIMER + + +def build_accessibility_review( + *, + notice_id: str, + title: str, + notice_text: str, + target_languages: tuple[str, ...] = (), + attachments: tuple[str, ...] = (), + has_contact: bool = False, + has_event_date: bool = False, + has_plain_language_summary: bool = False, +) -> NoticeAccessibilityReview: + """Build a deterministic public-notice accessibility readiness packet.""" + + missing_items = [] + if not has_contact: + missing_items.append("public contact for accommodations or questions") + if not has_event_date: + missing_items.append("clear event, deadline, or effective date") + if not has_plain_language_summary: + missing_items.append("plain-language summary for public readers") + plain_language_flags = _plain_language_flags(notice_text) + normalized_languages = tuple( + language.strip() for language in target_languages if language.strip() + ) + translation_tasks = tuple( + f"Prepare human-approved {language} notice version or language-access note." + for language in normalized_languages + if language.lower() not in {"english", "en"} + ) + attachment_review_required = bool(attachments) + staff_review_required = bool( + missing_items or plain_language_flags or translation_tasks or attachment_review_required + ) + notes = ( + "This packet prepares staff review before public posting; it does not certify ADA, WCAG, or language-access compliance.", + "CivicAccess integration and live translation workflows are not active in this release.", + "Preserve the approved accessible notice version with the proof-of-publication record.", + ) + return NoticeAccessibilityReview( + notice_id=notice_id.strip() or "unassigned-notice", + title=title.strip() or "Untitled notice", + missing_accessibility_items=tuple(missing_items), + plain_language_flags=plain_language_flags, + translation_tasks=translation_tasks, + attachment_review_required=attachment_review_required, + staff_review_required=staff_review_required, + review_notes=notes, + ) + + +def _plain_language_flags(notice_text: str) -> tuple[str, ...]: + flags = [] + text = notice_text.strip() + if not text: + return ("notice text missing",) + sentences = [part.strip() for part in text.replace("\n", " ").split(".") if part.strip()] + if any(len(sentence) > 180 for sentence in sentences): + flags.append("long sentence review") + jargon_terms = ("hereinafter", "pursuant to", "aforementioned", "whereas") + if any(term in text.lower() for term in jargon_terms): + flags.append("legal or technical jargon review") + if len(text.split()) > 250: + flags.append("summary length review") + return tuple(flags) diff --git a/civicnotice/main.py b/civicnotice/main.py index e52bab9..b30b2d0 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -9,6 +9,7 @@ from pydantic import BaseModel, Field from civicnotice import __version__ +from civicnotice.accessibility_review import build_accessibility_review from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.notice_templates import build_notice_template @@ -94,6 +95,17 @@ class SubscriberDeliveryRequest(BaseModel): required_channels: list[str] = Field(default_factory=lambda: ["email"]) +class AccessibilityReviewRequest(BaseModel): + notice_id: str + title: str + notice_text: str + target_languages: list[str] = Field(default_factory=list) + attachments: list[str] = Field(default_factory=list) + has_contact: bool = False + has_event_date: bool = False + has_plain_language_summary: bool = False + + class RecordsExportRequest(BaseModel): notice_id: str title: str @@ -123,7 +135,7 @@ def root() -> dict[str, str]: "status": "notice compliance foundation", "message": ( "CivicNotice package, API foundation, sample notice registry, CivicCore-backed deadline plans, " - "statutory rule checks, notice drafting templates, publication-readiness checklist, channel planning, records export checklist, optional " + "statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication-readiness checklist, channel planning, records export checklist, optional " "database-backed registry/deadline/publication-proof workpapers, and public UI foundation are online; official " "legal sufficiency decisions, official publication, legal " "advice, live LLM calls, publication-system write-back, and notice system-of-record integrations " @@ -350,6 +362,20 @@ def subscriber_delivery_plan(request: SubscriberDeliveryRequest) -> dict[str, ob ).__dict__ +@app.post("/api/v1/civicnotice/accessibility-review") +def accessibility_review(request: AccessibilityReviewRequest) -> dict[str, object]: + return build_accessibility_review( + notice_id=request.notice_id, + title=request.title, + notice_text=request.notice_text, + target_languages=tuple(request.target_languages), + attachments=tuple(request.attachments), + has_contact=request.has_contact, + has_event_date=request.has_event_date, + has_plain_language_summary=request.has_plain_language_summary, + ).__dict__ + + @app.post("/api/v1/civicnotice/export") def records_export(request: RecordsExportRequest) -> dict[str, object]: return build_notice_records_export( diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 8a1ed54..9ff5092 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,5 @@ # CivicNotice Implementation Plan -CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, publication readiness tracking helper, channel planning helper, subscriber delivery planning, notice and records export checklist, and public sample UI. +CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication readiness tracking helper, channel planning helper, subscriber delivery planning, notice and records export checklist, and public sample UI. Later releases add statutory rule packs, CivicClerk/CivicProcure/CivicBoards handoffs, CivicRecords file links, proof queues, publication-system integrations, and production data governance. diff --git a/docs/index.html b/docs/index.html index 9a91077..ffc7aa1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,12 +12,12 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

Shipping v0.1.3

-

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.
+

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Accessibility and language-readiness packets.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh index fac715d..181aeed 100644 --- a/scripts/verify-docs.sh +++ b/scripts/verify-docs.sh @@ -28,6 +28,7 @@ required=( "pyproject.toml" "civicnotice/__init__.py" "civicnotice/main.py" + "civicnotice/accessibility_review.py" "civicnotice/notice_registry.py" "civicnotice/deadline_tracker.py" "civicnotice/statutory_rules.py" diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 30c9a6c..11c3b34 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -2,6 +2,7 @@ from fastapi.testclient import TestClient +from civicnotice.accessibility_review import build_accessibility_review from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.main import app @@ -152,6 +153,27 @@ def test_subscriber_delivery_plan_dedupes_and_flags_review() -> None: assert result.staff_review_required is True +def test_accessibility_review_flags_language_and_plain_language_needs() -> None: + result = build_accessibility_review( + notice_id="hear-001", + title="Planning hearing", + notice_text="Pursuant to the aforementioned provisions, the city hereby notices a hearing.", + target_languages=("English", "Spanish"), + attachments=("notice.pdf",), + has_contact=False, + has_event_date=True, + has_plain_language_summary=False, + ) + assert "public contact for accommodations or questions" in result.missing_accessibility_items + assert "plain-language summary for public readers" in result.missing_accessibility_items + assert "legal or technical jargon review" in result.plain_language_flags + assert result.translation_tasks == ( + "Prepare human-approved Spanish notice version or language-access note.", + ) + assert result.attachment_review_required is True + assert result.staff_review_required is True + + def test_records_export_preserves_notice_context() -> None: result = build_notice_records_export( notice_id="hear-001", @@ -231,6 +253,19 @@ def test_notice_support_apis_success_shape() -> None: "required_channels": ["email"], }, ) + accessibility = client.post( + "/api/v1/civicnotice/accessibility-review", + json={ + "notice_id": "hear-001", + "title": "Planning hearing", + "notice_text": "Pursuant to the applicable provisions, a hearing is scheduled.", + "target_languages": ["Spanish"], + "attachments": ["notice.pdf"], + "has_contact": True, + "has_event_date": True, + "has_plain_language_summary": False, + }, + ) export = client.post( "/api/v1/civicnotice/export", json={"title": "Planning hearing notice archive", "notice_id": "hear-001"}, @@ -252,6 +287,11 @@ def test_notice_support_apis_success_shape() -> None: assert subscribers.status_code == 200 assert subscribers.json()["email_recipients"] == ["alex@example.gov"] assert subscribers.json()["missing_required_channels"] == [] + assert accessibility.status_code == 200 + assert accessibility.json()["translation_tasks"] == [ + "Prepare human-approved Spanish notice version or language-access note." + ] + assert accessibility.json()["staff_review_required"] is True assert export.status_code == 200 assert export.json()["notice_id"] == "hear-001" From 6a4ac93865a096f5436641344473e8279a0848d0 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:35:45 -0600 Subject: [PATCH 07/13] Add CivicNotice archive handoff packets --- CHANGELOG.md | 1 + README.md | 6 ++- README.txt | 6 ++- USER-MANUAL.md | 5 +- USER-MANUAL.txt | 5 +- civicnotice/archive_packet.py | 84 ++++++++++++++++++++++++++++++++ civicnotice/main.py | 36 +++++++++++++- civicnotice/public_ui.py | 4 +- docs/IMPLEMENTATION_PLAN.md | 2 +- docs/index.html | 4 +- scripts/verify-docs.sh | 1 + tests/test_notice_foundation.py | 51 +++++++++++++++++++ tests/test_runtime_foundation.py | 1 + 13 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 civicnotice/archive_packet.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 31df574..da534fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Added deterministic statutory rule checks and staff-editable notice drafting templates for common notice types. - Added subscriber delivery planning with active/suppressed subscriber accounting, deduped email recipients, required-channel checks, and language-review flags. - Added accessibility, plain-language, and human-approved translation readiness packets for public notices. +- Added archive/handoff packets that assemble required notice evidence, missing items, and Clerk/Procure/Boards/Records handoff targets. - Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. ## [0.1.2] - 2026-04-29 diff --git a/README.md b/README.md index cb8711c..93377d3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CivicNotice -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, archive/handoff packets, and notice-record export checklists. -Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, archive/handoff packets, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does @@ -16,6 +16,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - Assemble publication-readiness checklists for staff review. - Plan notice channels and accessibility-review needs. - Build subscriber delivery plans without sending notices or storing subscriber PII. +- Assemble archive/handoff packets for Clerk, Procure, Boards, Records, and manual staff files. - Produce notice and records export checklists. - Demonstrate a public notice-support UI at `/civicnotice`. @@ -45,6 +46,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - `POST /api/v1/civicnotice/accessibility-review` returns accessibility and language-readiness flags. - `POST /api/v1/civicnotice/channels` returns channel planning flags. - `POST /api/v1/civicnotice/subscribers/plan` returns a subscriber delivery plan. +- `POST /api/v1/civicnotice/archive-packet` returns archive and handoff readiness for a notice file. - `POST /api/v1/civicnotice/export` returns a notice and records export checklist. ## Local Development diff --git a/README.txt b/README.txt index f5512de..d3dbba1 100644 --- a/README.txt +++ b/README.txt @@ -1,9 +1,9 @@ CivicNotice =========== -CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, and notice-record export checklists. +CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, archive/handoff packets, and notice-record export checklists. -Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, archive/handoff packets, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. @@ -18,6 +18,7 @@ What CivicNotice does: - Assemble publication-readiness checklists for staff review. - Plan notice channels and accessibility-review needs. - Build subscriber delivery plans without sending notices or storing subscriber PII. +- Assemble archive/handoff packets for Clerk, Procure, Boards, Records, and manual staff files. - Produce notice and records export checklists. - Demonstrate a public notice-support UI at /civicnotice. @@ -37,6 +38,7 @@ API surface: - POST /api/v1/civicnotice/accessibility-review - POST /api/v1/civicnotice/channels - POST /api/v1/civicnotice/subscribers/plan +- POST /api/v1/civicnotice/archive-packet - POST /api/v1/civicnotice/export License: Apache License 2.0 for code; CC BY 4.0 for documentation. diff --git a/USER-MANUAL.md b/USER-MANUAL.md index ea8e9c6..8323c6e 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -2,7 +2,7 @@ ## For Non-Technical Users -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, archive/handoff packets, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, assemble archive/handoff packets, and assemble a notice/records export checklist. Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -27,6 +27,7 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, - `POST /api/v1/civicnotice/accessibility-review` - `POST /api/v1/civicnotice/channels` - `POST /api/v1/civicnotice/subscribers/plan` +- `POST /api/v1/civicnotice/archive-packet` - `POST /api/v1/civicnotice/export` Run: @@ -48,4 +49,4 @@ flowchart LR CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, archive/handoff packets, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index 6d4fbd7..1e7486b 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -4,7 +4,7 @@ CivicNotice User Manual For non-technical users ----------------------- -CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, and assemble a notice/records export checklist. +CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, archive/handoff packets, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, assemble archive/handoff packets, and assemble a notice/records export checklist. Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. @@ -30,6 +30,7 @@ Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, de - POST /api/v1/civicnotice/accessibility-review - POST /api/v1/civicnotice/channels - POST /api/v1/civicnotice/subscribers/plan +- POST /api/v1/civicnotice/archive-packet - POST /api/v1/civicnotice/export Run: @@ -45,4 +46,4 @@ Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, archive/handoff packets, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/archive_packet.py b/civicnotice/archive_packet.py new file mode 100644 index 0000000..2b0772e --- /dev/null +++ b/civicnotice/archive_packet.py @@ -0,0 +1,84 @@ +"""Notice archive and handoff packets for CivicNotice v0.1.3.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from civicnotice.notice_registry import DISCLAIMER + + +@dataclass(frozen=True) +class NoticeArchivePacket: + notice_id: str + notice_type: str + source_module: str + source_record_id: str + evidence_items: tuple[str, ...] + missing_items: tuple[str, ...] + handoff_targets: tuple[str, ...] + readiness_status: str + staff_review_required: bool + disclaimer: str = DISCLAIMER + + +def build_notice_archive_packet( + *, + notice_id: str, + notice_type: str, + source_module: str, + source_record_id: str, + registry_record_id: str = "", + deadline_plan_id: str = "", + publication_proof_id: str = "", + rule_check_complete: bool = False, + template_complete: bool = False, + accessibility_review_complete: bool = False, + subscriber_delivery_complete: bool = False, + records_export_complete: bool = False, +) -> NoticeArchivePacket: + """Assemble a notice archive/handoff packet for staff review.""" + + required_items = { + "registry record": registry_record_id, + "deadline plan": deadline_plan_id, + "publication proof": publication_proof_id, + "rule check": "complete" if rule_check_complete else "", + "notice template": "complete" if template_complete else "", + "accessibility review": "complete" if accessibility_review_complete else "", + "subscriber delivery plan": "complete" if subscriber_delivery_complete else "", + "records export checklist": "complete" if records_export_complete else "", + } + evidence_items = tuple( + f"{label}: {value}" for label, value in required_items.items() if value + ) + missing_items = tuple(label for label, value in required_items.items() if not value) + readiness_status = ( + "archive_packet_ready_for_staff_final_review" + if not missing_items + else "archive_packet_incomplete_staff_review_required" + ) + return NoticeArchivePacket( + notice_id=notice_id.strip() or "unassigned-notice", + notice_type=notice_type.strip() or "general notice", + source_module=source_module.strip() or "manual", + source_record_id=source_record_id.strip() or "unlinked source record", + evidence_items=evidence_items, + missing_items=missing_items, + handoff_targets=_handoff_targets(source_module), + readiness_status=readiness_status, + staff_review_required=True, + ) + + +def _handoff_targets(source_module: str) -> tuple[str, ...]: + normalized = source_module.strip().lower() + targets = ["civicrecords"] + if normalized == "civicclerk": + targets.insert(0, "civicclerk") + elif normalized == "civicprocure": + targets.insert(0, "civicprocure") + elif normalized == "civicboards": + targets.insert(0, "civicboards") + else: + targets.insert(0, "manual staff archive") + return tuple(targets) diff --git a/civicnotice/main.py b/civicnotice/main.py index b30b2d0..d5edfde 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -10,6 +10,7 @@ from civicnotice import __version__ from civicnotice.accessibility_review import build_accessibility_review +from civicnotice.archive_packet import build_notice_archive_packet from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.notice_templates import build_notice_template @@ -106,6 +107,21 @@ class AccessibilityReviewRequest(BaseModel): has_plain_language_summary: bool = False +class ArchivePacketRequest(BaseModel): + notice_id: str + notice_type: str + source_module: str = "manual" + source_record_id: str = "" + registry_record_id: str = "" + deadline_plan_id: str = "" + publication_proof_id: str = "" + rule_check_complete: bool = False + template_complete: bool = False + accessibility_review_complete: bool = False + subscriber_delivery_complete: bool = False + records_export_complete: bool = False + + class RecordsExportRequest(BaseModel): notice_id: str title: str @@ -135,7 +151,7 @@ def root() -> dict[str, str]: "status": "notice compliance foundation", "message": ( "CivicNotice package, API foundation, sample notice registry, CivicCore-backed deadline plans, " - "statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication-readiness checklist, channel planning, records export checklist, optional " + "statutory rule checks, notice drafting templates, accessibility and language-readiness packets, archive/handoff packets, publication-readiness checklist, channel planning, records export checklist, optional " "database-backed registry/deadline/publication-proof workpapers, and public UI foundation are online; official " "legal sufficiency decisions, official publication, legal " "advice, live LLM calls, publication-system write-back, and notice system-of-record integrations " @@ -376,6 +392,24 @@ def accessibility_review(request: AccessibilityReviewRequest) -> dict[str, objec ).__dict__ +@app.post("/api/v1/civicnotice/archive-packet") +def archive_packet(request: ArchivePacketRequest) -> dict[str, object]: + return build_notice_archive_packet( + notice_id=request.notice_id, + notice_type=request.notice_type, + source_module=request.source_module, + source_record_id=request.source_record_id, + registry_record_id=request.registry_record_id, + deadline_plan_id=request.deadline_plan_id, + publication_proof_id=request.publication_proof_id, + rule_check_complete=request.rule_check_complete, + template_complete=request.template_complete, + accessibility_review_complete=request.accessibility_review_complete, + subscriber_delivery_complete=request.subscriber_delivery_complete, + records_export_complete=request.records_export_complete, + ).__dict__ + + @app.post("/api/v1/civicnotice/export") def records_export(request: RecordsExportRequest) -> dict[str, object]: return build_notice_records_export( diff --git a/civicnotice/public_ui.py b/civicnotice/public_ui.py index 49bc162..c9f4f16 100644 --- a/civicnotice/public_ui.py +++ b/civicnotice/public_ui.py @@ -47,7 +47,7 @@ def render_public_lookup_page() -> str:

CivicSuite / CivicNotice public sample

Public notices with fewer missed deadlines.

-

CivicNotice demonstrates notice administration support: registry stubs, deadline plans, publication-readiness checks, channel planning, and proof-preserving records exports without publishing official notice.

+

CivicNotice demonstrates notice administration support: registry stubs, deadline plans, rule checks, templates, accessibility review, subscriber delivery planning, archive packets, and proof-preserving records exports without publishing official notice.

v0.1.3 notice compliance foundation

@@ -59,7 +59,7 @@ def render_public_lookup_page() -> str:

Staff review packet

-
  • Confirm statutory authority, publication channel, lead time, and reviewer.
  • Route draft copy for clerk/legal review before publication.
  • Preserve final notice, proof, screenshots, invoices, and source matter.
+
  • Confirm statutory authority, publication channel, lead time, and reviewer.
  • Route draft copy for clerk/legal review before publication.
  • Check accessibility, language access, subscriber delivery, proof, and archive handoff before closing the file.

Deadlines

Lead-time reminders

CivicNotice calculates review milestones; staff verify the legal deadline.

diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 9ff5092..4b7e074 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,5 @@ # CivicNotice Implementation Plan -CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication readiness tracking helper, channel planning helper, subscriber delivery planning, notice and records export checklist, and public sample UI. +CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication readiness tracking helper, channel planning helper, subscriber delivery planning, archive/handoff packets, notice and records export checklist, and public sample UI. Later releases add statutory rule packs, CivicClerk/CivicProcure/CivicBoards handoffs, CivicRecords file links, proof queues, publication-system integrations, and production data governance. diff --git a/docs/index.html b/docs/index.html index ffc7aa1..ac02d32 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,12 +12,12 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, archive/handoff packets, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

Shipping v0.1.3

-

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Accessibility and language-readiness packets.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.
+

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Accessibility and language-readiness packets.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Archive/handoff packets.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh index 181aeed..79c287f 100644 --- a/scripts/verify-docs.sh +++ b/scripts/verify-docs.sh @@ -29,6 +29,7 @@ required=( "civicnotice/__init__.py" "civicnotice/main.py" "civicnotice/accessibility_review.py" + "civicnotice/archive_packet.py" "civicnotice/notice_registry.py" "civicnotice/deadline_tracker.py" "civicnotice/statutory_rules.py" diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 11c3b34..0ae30e4 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -3,6 +3,7 @@ from fastapi.testclient import TestClient from civicnotice.accessibility_review import build_accessibility_review +from civicnotice.archive_packet import build_notice_archive_packet from civicnotice.channel_plan import plan_notice_channels from civicnotice.deadline_tracker import build_deadline_plan from civicnotice.main import app @@ -174,6 +175,36 @@ def test_accessibility_review_flags_language_and_plain_language_needs() -> None: assert result.staff_review_required is True +def test_archive_packet_tracks_missing_and_complete_evidence() -> None: + incomplete = build_notice_archive_packet( + notice_id="hear-001", + notice_type="planning hearing", + source_module="civicclerk", + source_record_id="meeting-42", + registry_record_id="registry-1", + deadline_plan_id="deadline-1", + ) + complete = build_notice_archive_packet( + notice_id="hear-001", + notice_type="planning hearing", + source_module="civicclerk", + source_record_id="meeting-42", + registry_record_id="registry-1", + deadline_plan_id="deadline-1", + publication_proof_id="proof-1", + rule_check_complete=True, + template_complete=True, + accessibility_review_complete=True, + subscriber_delivery_complete=True, + records_export_complete=True, + ) + assert incomplete.readiness_status == "archive_packet_incomplete_staff_review_required" + assert "publication proof" in incomplete.missing_items + assert complete.readiness_status == "archive_packet_ready_for_staff_final_review" + assert complete.missing_items == () + assert complete.handoff_targets == ("civicclerk", "civicrecords") + + def test_records_export_preserves_notice_context() -> None: result = build_notice_records_export( notice_id="hear-001", @@ -266,6 +297,23 @@ def test_notice_support_apis_success_shape() -> None: "has_plain_language_summary": False, }, ) + archive = client.post( + "/api/v1/civicnotice/archive-packet", + json={ + "notice_id": "hear-001", + "notice_type": "public hearing", + "source_module": "civicclerk", + "source_record_id": "meeting-42", + "registry_record_id": "registry-1", + "deadline_plan_id": "deadline-1", + "publication_proof_id": "proof-1", + "rule_check_complete": True, + "template_complete": True, + "accessibility_review_complete": True, + "subscriber_delivery_complete": True, + "records_export_complete": True, + }, + ) export = client.post( "/api/v1/civicnotice/export", json={"title": "Planning hearing notice archive", "notice_id": "hear-001"}, @@ -292,6 +340,9 @@ def test_notice_support_apis_success_shape() -> None: "Prepare human-approved Spanish notice version or language-access note." ] assert accessibility.json()["staff_review_required"] is True + assert archive.status_code == 200 + assert archive.json()["readiness_status"] == "archive_packet_ready_for_staff_final_review" + assert archive.json()["handoff_targets"] == ["civicclerk", "civicrecords"] assert export.status_code == 200 assert export.json()["notice_id"] == "hear-001" diff --git a/tests/test_runtime_foundation.py b/tests/test_runtime_foundation.py index b67b2f2..0ad081e 100644 --- a/tests/test_runtime_foundation.py +++ b/tests/test_runtime_foundation.py @@ -18,6 +18,7 @@ def test_root_endpoint_states_runtime_boundary() -> None: assert data["name"] == "CivicNotice" assert data["status"] == "notice compliance foundation" assert "CivicCore-backed deadline plans" in data["message"] + assert "archive/handoff packets" in data["message"] assert "database-backed registry/deadline/publication-proof workpapers" in data["message"] assert "official publication" in data["message"] assert "Post-v0.1.3 roadmap" in data["next_step"] From eb674fd4ce1e8576683f7de73a202c6a9bb3015e Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:37:00 -0600 Subject: [PATCH 08/13] Prepare CivicNotice 0.2.0 release --- CHANGELOG.md | 4 ++-- README.md | 4 ++-- README.txt | 2 +- SECURITY.md | 2 +- USER-MANUAL.md | 4 ++-- USER-MANUAL.txt | 4 ++-- civicnotice/__init__.py | 2 +- civicnotice/accessibility_review.py | 2 +- civicnotice/archive_packet.py | 2 +- civicnotice/channel_plan.py | 2 +- civicnotice/deadline_tracker.py | 2 +- civicnotice/main.py | 2 +- civicnotice/notice_registry.py | 2 +- civicnotice/notice_templates.py | 2 +- civicnotice/public_ui.py | 4 ++-- civicnotice/publication_check.py | 2 +- civicnotice/publication_proof.py | 2 +- civicnotice/records_export.py | 2 +- civicnotice/statutory_rules.py | 2 +- civicnotice/subscriber_delivery.py | 2 +- docs/IMPLEMENTATION_PLAN.md | 2 +- docs/RECONCILIATION.md | 2 +- docs/index.html | 4 ++-- pyproject.toml | 2 +- scripts/verify-release.sh | 10 +++++----- tests/test_notice_foundation.py | 2 +- tests/test_runtime_foundation.py | 6 +++--- 27 files changed, 39 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da534fa..333f841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,12 @@ All notable changes to CivicNotice will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). -## [0.1.3] - 2026-06-19 +## [0.2.0] - 2026-06-19 ### Changed - Moved CivicNotice to the `civiccore v1.2.0` release wheel used by the current Windows Local city-core platform. -- Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.1.3 compatibility release. +- Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.2.0 notice workflow release. - Added durable publication-proof workpapers for staff evidence packets linked to Clerk, procurement, board, or manual source records. - Added deterministic statutory rule checks and staff-editable notice drafting templates for common notice types. - Added subscriber delivery planning with active/suppressed subscriber accounting, deduped email recipients, required-channel checks, and language-review flags. diff --git a/README.md b/README.md index 93377d3..84ae2a7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, archive/handoff packets, and notice-record export checklists. -Current state: **v0.1.3 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, archive/handoff packets, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. +Current state: **v0.2.0 notice compliance foundation release**, aligned to the `civiccore v1.2.0` release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, archive/handoff packets, notice/records export checklist, and accessible public sample UI at `/civicnotice`. It does **not** ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. ## What CivicNotice Does @@ -25,7 +25,7 @@ Current state: **v0.1.3 notice compliance foundation release**, aligned to the ` - It does not decide legal sufficiency. - It does not publish official notices. - It does not provide legal advice. -- It does not call live LLMs in v0.1.3. +- It does not call live LLMs in v0.2.0. - It does not write back to publication systems. - It does not replace a notice system of record. diff --git a/README.txt b/README.txt index d3dbba1..889ea13 100644 --- a/README.txt +++ b/README.txt @@ -3,7 +3,7 @@ CivicNotice CivicNotice is the CivicSuite module for public hearing notices, legal notices, bid notices, vacancy notices, statutory publication deadlines, statutory rule checks, notice drafting templates, accessibility and language-readiness review, publication-readiness review, channel planning, subscriber delivery planning, archive/handoff packets, and notice-record export checklists. -Current state: v0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, archive/handoff packets, notice/records export checklist, and accessible public sample UI at /civicnotice. +Current state: v0.2.0 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. This repo ships a FastAPI package, health/root endpoints, documentation gates, deterministic sample notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness review packets, optional database-backed registry/deadline/publication-proof workpapers via CIVICNOTICE_WORKPAPER_DB_URL, publication-readiness checklists, channel-planning helpers, subscriber delivery planning, archive/handoff packets, notice/records export checklist, and accessible public sample UI at /civicnotice. It does not ship legal sufficiency decisions, legal advice, live LLM calls, official notice publication, publication-system write-back, or notice system-of-record integrations. diff --git a/SECURITY.md b/SECURITY.md index 316e564..b71d31b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security -CivicNotice is early-stage software. Current version: `0.1.3`. Do not deploy it as a system of record until a release explicitly says it is production-ready. +CivicNotice is early-stage software. Current version: `0.2.0`. Do not deploy it as a system of record until a release explicitly says it is production-ready. Report suspected vulnerabilities privately to the project maintainer. Do not open public issues containing exploit details, secrets, or sensitive municipal data. diff --git a/USER-MANUAL.md b/USER-MANUAL.md index 8323c6e..7d60a6f 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -4,7 +4,7 @@ CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, archive/handoff packets, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, assemble archive/handoff packets, and assemble a notice/records export checklist. -Current state: `0.1.3` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. +Current state: `0.2.0` notice compliance foundation release, aligned to the `civiccore v1.2.0` release wheel. CivicNotice uses the shared CivicCore notice-deadline helper for deterministic reminder plans, but it still does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. ## For IT and Technical Staff @@ -49,4 +49,4 @@ flowchart LR CivicNotice --> Export["Notice and records export checklist"] ``` -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, archive/handoff packets, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.2.0 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, archive/handoff packets, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index 1e7486b..10c5e56 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -6,7 +6,7 @@ For non-technical users CivicNotice helps city staff organize public hearing notices, legal notices, bid notices, vacancy notices, publication deadlines, channel planning notes, accessibility and language-readiness checks, proof requirements, subscriber delivery plans, archive/handoff packets, and export manifests. It can create a sample notice registry stub, build publication deadline reminders, check a draft packet against deterministic staff-review rules, create a staff-editable notice template, retrieve saved registry/deadline/publication-proof workpapers when IT enables persistence, assemble publication-readiness checklists, summarize channel planning, build accessibility/plain-language/translation readiness packets, build subscriber delivery plans, assemble archive/handoff packets, and assemble a notice/records export checklist. -Current state: 0.1.3 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. +Current state: 0.2.0 notice compliance foundation release, aligned to the civiccore v1.2.0 release wheel. CivicNotice uses the shared CivicCore notice deadline helper for deterministic reminder plans, but it does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or update a notice system of record. Staff own every decision. For IT and technical staff -------------------------- @@ -46,4 +46,4 @@ Staff -> CivicNotice -> CivicCore v1.2.0 CivicNotice future handoffs -> CivicClerk, CivicProcure, CivicBoards, CivicRecords AI CivicNotice -> Notice and records export checklist -CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.1.3 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, archive/handoff packets, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. +CivicNotice depends on CivicCore. CivicCore does not depend on CivicNotice. CivicNotice v0.2.0 uses the shared CivicCore notice deadline helper plus deterministic sample notice data, staff-review rule packs, notice templates, accessibility and language-readiness packets, subscriber delivery plans, archive/handoff packets, and optional publication-proof workpapers only; live agenda/procurement handoffs, legal sufficiency decisions, legal advice, official notice publication, publication-system write-back, and production notice-system integrations are future work. diff --git a/civicnotice/__init__.py b/civicnotice/__init__.py index 26f3f52..b678fb9 100644 --- a/civicnotice/__init__.py +++ b/civicnotice/__init__.py @@ -1,6 +1,6 @@ """CivicNotice package.""" -__version__ = "0.1.3" +__version__ = "0.2.0" diff --git a/civicnotice/accessibility_review.py b/civicnotice/accessibility_review.py index 928116a..554115e 100644 --- a/civicnotice/accessibility_review.py +++ b/civicnotice/accessibility_review.py @@ -1,4 +1,4 @@ -"""Accessibility and language-readiness review for CivicNotice v0.1.3.""" +"""Accessibility and language-readiness review for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/archive_packet.py b/civicnotice/archive_packet.py index 2b0772e..4fed849 100644 --- a/civicnotice/archive_packet.py +++ b/civicnotice/archive_packet.py @@ -1,4 +1,4 @@ -"""Notice archive and handoff packets for CivicNotice v0.1.3.""" +"""Notice archive and handoff packets for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/channel_plan.py b/civicnotice/channel_plan.py index f98f55b..609761b 100644 --- a/civicnotice/channel_plan.py +++ b/civicnotice/channel_plan.py @@ -1,4 +1,4 @@ -"""Notice channel planning helpers for CivicNotice v0.1.3.""" +"""Notice channel planning helpers for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/deadline_tracker.py b/civicnotice/deadline_tracker.py index 654814d..875c98a 100644 --- a/civicnotice/deadline_tracker.py +++ b/civicnotice/deadline_tracker.py @@ -1,4 +1,4 @@ -"""Publication deadline helpers for CivicNotice v0.1.3.""" +"""Publication deadline helpers for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/main.py b/civicnotice/main.py index d5edfde..b99c220 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -157,7 +157,7 @@ def root() -> dict[str, str]: "advice, live LLM calls, publication-system write-back, and notice system-of-record integrations " "are not implemented yet." ), - "next_step": "Post-v0.1.3 roadmap: statutory rule packs, CivicClerk/CivicProcure/CivicNotice handoffs, and publication proof queues", + "next_step": "Post-v0.2.0 roadmap: jurisdiction-specific statutory rule packs, live CivicClerk/CivicProcure/CivicNotice handoffs, and publication proof queues", } diff --git a/civicnotice/notice_registry.py b/civicnotice/notice_registry.py index dc7e007..7d2e6e8 100644 --- a/civicnotice/notice_registry.py +++ b/civicnotice/notice_registry.py @@ -1,4 +1,4 @@ -"""Notice registry helpers for CivicNotice v0.1.3.""" +"""Notice registry helpers for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/notice_templates.py b/civicnotice/notice_templates.py index 82baea0..d87b569 100644 --- a/civicnotice/notice_templates.py +++ b/civicnotice/notice_templates.py @@ -1,4 +1,4 @@ -"""Notice drafting templates for CivicNotice v0.1.3.""" +"""Notice drafting templates for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/public_ui.py b/civicnotice/public_ui.py index c9f4f16..c77cf4f 100644 --- a/civicnotice/public_ui.py +++ b/civicnotice/public_ui.py @@ -1,4 +1,4 @@ -"""Static public UI shell for CivicNotice v0.1.3.""" +"""Static public UI shell for CivicNotice v0.2.0.""" from __future__ import annotations @@ -48,7 +48,7 @@ def render_public_lookup_page() -> str:

CivicSuite / CivicNotice public sample

Public notices with fewer missed deadlines.

CivicNotice demonstrates notice administration support: registry stubs, deadline plans, rule checks, templates, accessibility review, subscriber delivery planning, archive packets, and proof-preserving records exports without publishing official notice.

-

v0.1.3 notice compliance foundation

+

v0.2.0 notice compliance foundation

diff --git a/civicnotice/publication_check.py b/civicnotice/publication_check.py index 3101f65..96599f9 100644 --- a/civicnotice/publication_check.py +++ b/civicnotice/publication_check.py @@ -1,4 +1,4 @@ -"""Publication-readiness checks for CivicNotice v0.1.3.""" +"""Publication-readiness checks for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/publication_proof.py b/civicnotice/publication_proof.py index 4f915f4..9083591 100644 --- a/civicnotice/publication_proof.py +++ b/civicnotice/publication_proof.py @@ -1,4 +1,4 @@ -"""Publication proof workpaper helpers for CivicNotice v0.1.3.""" +"""Publication proof workpaper helpers for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/records_export.py b/civicnotice/records_export.py index b09beb4..16a6d0e 100644 --- a/civicnotice/records_export.py +++ b/civicnotice/records_export.py @@ -1,4 +1,4 @@ -"""Notice records export helpers for CivicNotice v0.1.3.""" +"""Notice records export helpers for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/statutory_rules.py b/civicnotice/statutory_rules.py index bd834b0..57ec7c2 100644 --- a/civicnotice/statutory_rules.py +++ b/civicnotice/statutory_rules.py @@ -1,4 +1,4 @@ -"""Deterministic statutory notice rule checks for CivicNotice v0.1.3.""" +"""Deterministic statutory notice rule checks for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/civicnotice/subscriber_delivery.py b/civicnotice/subscriber_delivery.py index 48b7f13..224774a 100644 --- a/civicnotice/subscriber_delivery.py +++ b/civicnotice/subscriber_delivery.py @@ -1,4 +1,4 @@ -"""Subscriber delivery planning for CivicNotice v0.1.3.""" +"""Subscriber delivery planning for CivicNotice v0.2.0.""" from __future__ import annotations diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 4b7e074..17f2cde 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,5 @@ # CivicNotice Implementation Plan -CivicNotice v0.1.3 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication readiness tracking helper, channel planning helper, subscriber delivery planning, archive/handoff packets, notice and records export checklist, and public sample UI. +CivicNotice v0.2.0 preserves the deterministic foundation and aligns it to the `civiccore v1.2.0` release wheel: runtime shell, sample notice registry, CivicCore-backed deadline tracking, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, publication readiness tracking helper, channel planning helper, subscriber delivery planning, archive/handoff packets, notice and records export checklist, and public sample UI. Later releases add statutory rule packs, CivicClerk/CivicProcure/CivicBoards handoffs, CivicRecords file links, proof queues, publication-system integrations, and production data governance. diff --git a/docs/RECONCILIATION.md b/docs/RECONCILIATION.md index 5c46f32..1a286e7 100644 --- a/docs/RECONCILIATION.md +++ b/docs/RECONCILIATION.md @@ -1,6 +1,6 @@ # CivicNotice Reconciliation -| Driver | CivicNotice v0.1.3 result | +| Driver | CivicNotice v0.2.0 result | |---|---| | Unified spec CivicNotice catalog entry | notice registry, deadline tracking, deterministic statutory rule checks, notice drafting templates, publication readiness tracking, channel planning, and notice/records export checklists represented through deterministic helpers. | | Apache 2.0 code licensing | `LICENSE-CODE` and package metadata use Apache 2.0. | diff --git a/docs/index.html b/docs/index.html index ac02d32..d693850 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,8 +12,8 @@

CivicSuite / CivicNotice

Public notices with fewer missed deadlines.

-

CivicNotice v0.1.3 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, archive/handoff packets, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

-

Shipping v0.1.3

+

CivicNotice v0.2.0 ships a foundation for notice registry, CivicCore-backed statutory deadline plans, deterministic statutory rule checks, notice drafting templates, accessibility and language-readiness packets, optional registry/deadline/publication-proof workpaper persistence, publication-readiness checks, channel planning, subscriber delivery planning, archive/handoff packets, notice-record exports, an accessible sample UI, and the civiccore v1.2.0 release-wheel alignment.

+

Shipping v0.2.0

diff --git a/pyproject.toml b/pyproject.toml index 76bb28a..8d5dac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "civicnotice" -version = "0.1.3" +version = "0.2.0" description = "CivicNotice runtime foundation for notice registry, deadline tracking, publication readiness tracking, channel planning, and public-records-aware export support." readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index d31235d..671a539 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -VERSION="0.1.3" +VERSION="0.2.0" find_python() { local candidates=() @@ -40,7 +40,7 @@ ${PYTHON_BIN} - <<'PY' from pathlib import Path import tomllib -version = "0.1.3" +version = "0.2.0" root = Path(".") pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) assert pyproject["project"]["version"] == version, pyproject["project"]["version"] @@ -56,7 +56,7 @@ for path in [ "SECURITY.md", ]: text = (root / path).read_text(encoding="utf-8") - assert "0.1.3" in text, f"missing release version in {path}" + assert "0.2.0" in text, f"missing release version in {path}" assert "0.1.0.dev0" not in text, f"stale dev version in {path}" print("PASS: version surfaces synchronized") PY @@ -81,8 +81,8 @@ from pathlib import Path import hashlib dist = Path("dist") -wheel = dist / "civicnotice-0.1.3-py3-none-any.whl" -sdist = dist / "civicnotice-0.1.3.tar.gz" +wheel = dist / "civicnotice-0.2.0-py3-none-any.whl" +sdist = dist / "civicnotice-0.2.0.tar.gz" assert wheel.exists(), f"missing {wheel}" assert sdist.exists(), f"missing {sdist}" lines = [] diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 0ae30e4..7590258 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -354,6 +354,6 @@ def test_public_ui_route_is_accessible_and_honest() -> None: text = response.text assert '' in text assert '
' in text - assert "v0.1.3 notice compliance foundation" in text + assert "v0.2.0 notice compliance foundation" in text assert "does not determine legal sufficiency" in text assert "replace the notice system of record" in text diff --git a/tests/test_runtime_foundation.py b/tests/test_runtime_foundation.py index 0ad081e..a8cba98 100644 --- a/tests/test_runtime_foundation.py +++ b/tests/test_runtime_foundation.py @@ -8,7 +8,7 @@ def test_package_version_is_012() -> None: - assert civicnotice.__version__ == "0.1.3" + assert civicnotice.__version__ == "0.2.0" def test_root_endpoint_states_runtime_boundary() -> None: @@ -21,7 +21,7 @@ def test_root_endpoint_states_runtime_boundary() -> None: assert "archive/handoff packets" in data["message"] assert "database-backed registry/deadline/publication-proof workpapers" in data["message"] assert "official publication" in data["message"] - assert "Post-v0.1.3 roadmap" in data["next_step"] + assert "Post-v0.2.0 roadmap" in data["next_step"] def test_health_endpoint_reports_versions() -> None: @@ -30,5 +30,5 @@ def test_health_endpoint_reports_versions() -> None: data = response.json() assert data["status"] == "ok" assert data["service"] == "civicnotice" - assert data["version"] == "0.1.3" + assert data["version"] == "0.2.0" assert data["civiccore_version"] == "1.2.0" From db194b19507072fbedfd3db60b74ea248d22d9c2 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:47:38 -0600 Subject: [PATCH 09/13] Resolve CivicNotice gate findings --- .github/workflows/verify.yml | 20 ++- CHANGELOG.md | 9 +- README.md | 17 ++ README.txt | 16 ++ SECURITY.md | 28 +++- SUPPORT.md | 3 +- USER-MANUAL.md | 13 +- USER-MANUAL.txt | 13 +- civicnotice/main.py | 158 +++++++++++------- civicnotice/public_ui.py | 5 +- docs/index.html | 1 + pyproject.toml | 3 +- scripts/verify-release.sh | 22 ++- tests/test_notice_foundation.py | 3 + tests/test_postgres_persistence.py | 52 ++++++ ...est_production_depth_notice_persistence.py | 50 ++++++ tests/test_release_contracts.py | 127 ++++++++++++++ 17 files changed, 470 insertions(+), 70 deletions(-) create mode 100644 tests/test_postgres_persistence.py create mode 100644 tests/test_release_contracts.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index dfb6c10..287f3a8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -7,16 +7,34 @@ on: jobs: verify: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: civicnotice + POSTGRES_USER: civicnotice + POSTGRES_DB: civicnotice_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U civicnotice -d civicnotice_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install CivicCore release wheel - run: python -m pip install https://github.com/CivicSuite/civiccore/releases/download/v0.3.0/civiccore-0.3.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: Assert CivicCore release version + run: python -c "import civiccore; assert civiccore.__version__ == '1.2.0', civiccore.__version__" - name: Run release gate + env: + CIVICNOTICE_POSTGRES_TEST_URL: postgresql+psycopg2://civicnotice:civicnotice@localhost:5432/civicnotice_test run: bash scripts/verify-release.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 333f841..b90cf23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [0.2.0] - 2026-06-19 -### Changed +### Added -- Moved CivicNotice to the `civiccore v1.2.0` release wheel used by the current Windows Local city-core platform. -- Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.2.0 notice workflow release. - Added durable publication-proof workpapers for staff evidence packets linked to Clerk, procurement, board, or manual source records. - Added deterministic statutory rule checks and staff-editable notice drafting templates for common notice types. - Added subscriber delivery planning with active/suppressed subscriber accounting, deduped email recipients, required-channel checks, and language-review flags. - Added accessibility, plain-language, and human-approved translation readiness packets for public notices. - Added archive/handoff packets that assemble required notice evidence, missing items, and Clerk/Procure/Boards/Records handoff targets. + +### Changed + +- Moved CivicNotice to the `civiccore v1.2.0` release wheel used by the current Windows Local city-core platform. +- Synchronized runtime health/version tests, docs, release gate version surfaces, and public UI copy for the v0.2.0 notice workflow release. - Preserved the deterministic notice registry, deadline tracking, publication-readiness, channel-planning, records-export, optional workpaper persistence, and honest unshipped-boundary behavior from v0.1.2. ## [0.1.2] - 2026-04-29 diff --git a/README.md b/README.md index 84ae2a7..cb70978 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,23 @@ Current state: **v0.2.0 notice compliance foundation release**, aligned to the ` - `POST /api/v1/civicnotice/archive-packet` returns archive and handoff readiness for a notice file. - `POST /api/v1/civicnotice/export` returns a notice and records export checklist. +## Start and Smoke-Check CivicNotice + +Install the package with its development dependencies, start the ASGI app target `civicnotice.main:app`, and open the local `/health` and `/civicnotice` routes. A fresh user can reach the core stateless API and public sample UI without a database, model server, account, or API key. + +Minimal workflow to smoke-check after startup: + +1. Confirm `/health` reports `service: civicnotice`, version `0.2.0`, and CivicCore `1.2.0`. +2. Post a registry stub to `/api/v1/civicnotice/registry` without a database and confirm the response includes `record_id: null`, registry notes, and the staff-responsibility disclaimer. +3. Post a rule check to `/api/v1/civicnotice/rule-check` with a notice type, event date, publication dates, channels, content fields, and statutory basis. +4. Open `/civicnotice` and confirm the page is a static public sample with boundary copy, not an official publication workflow. + +## Persistence and Durable Writes + +Without `CIVICNOTICE_WORKPAPER_DB_URL`, CivicNotice runs in deterministic stateless mode: registry and deadline POST requests return transient payloads, persisted GET routes return actionable 503 responses, and publication-proof storage is unavailable. + +With `CIVICNOTICE_WORKPAPER_DB_URL`, registry, deadline, and publication-proof workpapers are durable. Persistence-backed write routes also require `CIVICNOTICE_TRUSTED_WRITE_TOKEN` and the matching `X-CivicNotice-Write-Token` request header. This is a minimal trusted-mode guard for local deployments; it is not a replacement for a production identity system. + ## Local Development ```bash diff --git a/README.txt b/README.txt index 889ea13..a7fd59e 100644 --- a/README.txt +++ b/README.txt @@ -41,4 +41,20 @@ API surface: - POST /api/v1/civicnotice/archive-packet - POST /api/v1/civicnotice/export +Start and smoke-check CivicNotice: + +Install the package with development dependencies, start the ASGI app target civicnotice.main:app, and open the local /health and /civicnotice routes. A fresh user can reach the core stateless API and public sample UI without a database, model server, account, or API key. + +Minimal smoke check: +1. Confirm /health reports service civicnotice, version 0.2.0, and CivicCore 1.2.0. +2. Post a registry stub to /api/v1/civicnotice/registry without a database and confirm record_id is null with registry notes and the staff-responsibility disclaimer. +3. Post a rule check to /api/v1/civicnotice/rule-check with a notice type, event date, publication dates, channels, content fields, and statutory basis. +4. Open /civicnotice and confirm the page is a static public sample with boundary copy, not an official publication workflow. + +Persistence and durable writes: + +Without CIVICNOTICE_WORKPAPER_DB_URL, CivicNotice runs in deterministic stateless mode: registry and deadline POST requests return transient payloads, persisted GET routes return actionable 503 responses, and publication-proof storage is unavailable. + +With CIVICNOTICE_WORKPAPER_DB_URL, registry, deadline, and publication-proof workpapers are durable. Persistence-backed write routes also require CIVICNOTICE_TRUSTED_WRITE_TOKEN and the matching X-CivicNotice-Write-Token request header. This is a minimal trusted-mode guard for local deployments; it is not a replacement for a production identity system. + License: Apache License 2.0 for code; CC BY 4.0 for documentation. diff --git a/SECURITY.md b/SECURITY.md index b71d31b..72ae910 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,5 +2,31 @@ CivicNotice is early-stage software. Current version: `0.2.0`. Do not deploy it as a system of record until a release explicitly says it is production-ready. -Report suspected vulnerabilities privately to the project maintainer. Do not open public issues containing exploit details, secrets, or sensitive municipal data. +## Supported Versions +| Version | Security support | +| --- | --- | +| 0.2.x | Supported for private vulnerability reports | +| 0.1.x | Best-effort only | + +## Private Reporting + +Report suspected vulnerabilities through GitHub private vulnerability reporting for the `CivicSuite/civicnotice` repository when available. If private reporting is unavailable, contact the repository owner privately before opening a public issue. + +Do not include live municipal secrets, resident data, publication affidavits, subscriber lists, credentials, access tokens, database dumps, or exploit details in public issues, pull requests, screenshots, or discussion threads. + +## What To Include + +- Affected CivicNotice version and commit, if known. +- Whether `CIVICNOTICE_WORKPAPER_DB_URL` or `CIVICNOTICE_TRUSTED_WRITE_TOKEN` was configured. +- Steps to reproduce with synthetic data only. +- Expected and observed behavior. +- Impact assessment, including whether durable workpaper writes or public notice artifacts are exposed. + +## Response Expectations + +Maintainers should acknowledge private reports within 5 business days, triage severity, and coordinate a fix or advisory before public disclosure when the report is valid. Critical issues affecting durable writes, stored workpapers, or sensitive data handling should be prioritized before feature work. + +## Deployment Boundary + +CivicNotice 0.2.0 is designed for local, staff-reviewed municipal workflows. Persistence-backed writes require the trusted write token, but this token is only a minimal local guard. Production deployments should place CivicNotice behind the city's normal authentication, authorization, TLS, logging, backup, and network controls. diff --git a/SUPPORT.md b/SUPPORT.md index 0ecc59f..0cea827 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -2,8 +2,7 @@ CivicNotice is not a hosted service. Cities or implementers operate their own installation. -For bugs and documentation issues, open a GitHub issue. For security concerns, follow `SECURITY.md`. - +For bugs and documentation issues, open a GitHub issue with synthetic data only. For security concerns, follow `SECURITY.md` and use private vulnerability reporting or a private maintainer contact instead of a public issue. diff --git a/USER-MANUAL.md b/USER-MANUAL.md index 7d60a6f..a3ab2e9 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -10,7 +10,7 @@ Current state: `0.2.0` notice compliance foundation release, aligned to the `civ CivicNotice is a FastAPI Python package pinned to the `civiccore v1.2.0` release wheel. The current runtime exposes: -Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, deadline-plan, and publication-proof records. Leave it unset for deterministic stateless operation except durable publication-proof storage, which requires the database so proof packets can be retrieved later. +Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, deadline-plan, and publication-proof records. Leave it unset for deterministic stateless operation: registry and deadline POST requests return transient payloads, persisted GET routes return actionable 503 responses, and publication-proof storage is unavailable. When persistence is enabled, set `CIVICNOTICE_TRUSTED_WRITE_TOKEN` and send the matching `X-CivicNotice-Write-Token` header for durable write routes. - `GET /` - `GET /health` @@ -30,6 +30,17 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, - `POST /api/v1/civicnotice/archive-packet` - `POST /api/v1/civicnotice/export` +## Start and Smoke-Check CivicNotice + +A fresh user can reach the core stateless API and public sample UI without a database, model server, account, or API key. Start the ASGI app target `civicnotice.main:app`, then confirm `/health` reports CivicNotice `0.2.0` and CivicCore `1.2.0`. + +Minimal smoke-check workflow: + +1. Create a transient registry stub with `/api/v1/civicnotice/registry`. +2. Create a deterministic rule check with `/api/v1/civicnotice/rule-check`. +3. Open `/civicnotice` and confirm it is a static public sample with boundary copy. +4. If durable workpapers are enabled, confirm the trusted write token is configured before testing persistence-backed writes. + Run: ```bash diff --git a/USER-MANUAL.txt b/USER-MANUAL.txt index 10c5e56..0bda902 100644 --- a/USER-MANUAL.txt +++ b/USER-MANUAL.txt @@ -13,7 +13,7 @@ For IT and technical staff CivicNotice is a FastAPI Python package pinned to the civiccore v1.2.0 release wheel. The current runtime exposes: -Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, deadline-plan, and publication-proof records. Leave it unset for deterministic stateless operation except durable publication-proof storage, which requires the database so proof packets can be retrieved later. +Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, deadline-plan, and publication-proof records. Leave it unset for deterministic stateless operation: registry and deadline POST requests return transient payloads, persisted GET routes return actionable 503 responses, and publication-proof storage is unavailable. When persistence is enabled, set CIVICNOTICE_TRUSTED_WRITE_TOKEN and send the matching X-CivicNotice-Write-Token header for durable write routes. - GET / - GET /health @@ -33,6 +33,17 @@ Set CIVICNOTICE_WORKPAPER_DB_URL to enable SQLAlchemy-backed notice registry, de - POST /api/v1/civicnotice/archive-packet - POST /api/v1/civicnotice/export +Start and smoke-check CivicNotice +--------------------------------- + +A fresh user can reach the core stateless API and public sample UI without a database, model server, account, or API key. Start the ASGI app target civicnotice.main:app, then confirm /health reports CivicNotice 0.2.0 and CivicCore 1.2.0. + +Minimal smoke-check workflow: +1. Create a transient registry stub with /api/v1/civicnotice/registry. +2. Create a deterministic rule check with /api/v1/civicnotice/rule-check. +3. Open /civicnotice and confirm it is a static public sample with boundary copy. +4. If durable workpapers are enabled, confirm the trusted write token is configured before testing persistence-backed writes. + Run: python -m pip install -e ".[dev]" diff --git a/civicnotice/main.py b/civicnotice/main.py index b99c220..bf69915 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -4,7 +4,7 @@ import os from civiccore import __version__ as CIVICCORE_VERSION -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, Header, HTTPException from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field @@ -36,69 +36,77 @@ _workpaper_repository: NoticeWorkpaperRepository | None = None _workpaper_db_url: str | None = None +PERSISTENCE_GET_RESPONSES = { + 404: {"description": "Persisted CivicNotice workpaper record was not found."}, + 503: {"description": "CivicNotice workpaper persistence is not configured."}, +} +PERSISTENCE_WRITE_RESPONSES = { + 403: {"description": "Durable CivicNotice write token is missing or invalid."}, + 503: {"description": "CivicNotice workpaper persistence or durable write guard is not configured."}, +} class NoticeRegistryRequest(BaseModel): - notice_id: str - notice_type: str - owner: str + notice_id: str = Field(max_length=160) + notice_type: str = Field(max_length=160) + owner: str = Field(max_length=160) class DeadlineRequest(BaseModel): - notice_type: str + notice_type: str = Field(max_length=160) event_date: date - lead_days: int = 10 + lead_days: int = Field(default=10, ge=0, le=366) class PublicationRequest(BaseModel): - notice_type: str - channel: str + notice_type: str = Field(max_length=160) + channel: str = Field(max_length=160) class RuleCheckRequest(BaseModel): - notice_type: str + notice_type: str = Field(max_length=160) event_date: date publication_dates: list[date] = Field(default_factory=list) channels: list[str] = Field(default_factory=list) content_fields: list[str] = Field(default_factory=list) - statutory_basis: str = "" + statutory_basis: str = Field(default="", max_length=1000) class NoticeTemplateRequest(BaseModel): - notice_type: str - matter_title: str + notice_type: str = Field(max_length=160) + matter_title: str = Field(max_length=240) event_date: date - location: str = "" - contact: str = "" - source_module: str = "manual" - statutory_basis: str = "" + location: str = Field(default="", max_length=1000) + contact: str = Field(default="", max_length=160) + source_module: str = Field(default="manual", max_length=160) + statutory_basis: str = Field(default="", max_length=1000) class ChannelRequest(BaseModel): - notice_type: str - audience: str + notice_type: str = Field(max_length=160) + audience: str = Field(max_length=240) class SubscriberRequest(BaseModel): - subscriber_id: str - name: str - email: str = "" + subscriber_id: str = Field(max_length=160) + name: str = Field(max_length=160) + email: str = Field(default="", max_length=254) channels: list[str] = Field(default_factory=list) - language: str = "English" + language: str = Field(default="English", max_length=80) active: bool = True class SubscriberDeliveryRequest(BaseModel): - notice_id: str - notice_type: str - audience: str + notice_id: str = Field(max_length=160) + notice_type: str = Field(max_length=160) + audience: str = Field(max_length=240) subscribers: list[SubscriberRequest] = Field(default_factory=list) required_channels: list[str] = Field(default_factory=lambda: ["email"]) class AccessibilityReviewRequest(BaseModel): - notice_id: str - title: str + notice_id: str = Field(max_length=160) + title: str = Field(max_length=240) notice_text: str target_languages: list[str] = Field(default_factory=list) attachments: list[str] = Field(default_factory=list) @@ -108,13 +116,13 @@ class AccessibilityReviewRequest(BaseModel): class ArchivePacketRequest(BaseModel): - notice_id: str - notice_type: str - source_module: str = "manual" - source_record_id: str = "" - registry_record_id: str = "" - deadline_plan_id: str = "" - publication_proof_id: str = "" + notice_id: str = Field(max_length=160) + notice_type: str = Field(max_length=160) + source_module: str = Field(default="manual", max_length=160) + source_record_id: str = Field(default="", max_length=160) + registry_record_id: str = Field(default="", max_length=160) + deadline_plan_id: str = Field(default="", max_length=160) + publication_proof_id: str = Field(default="", max_length=160) rule_check_complete: bool = False template_complete: bool = False accessibility_review_complete: bool = False @@ -123,22 +131,22 @@ class ArchivePacketRequest(BaseModel): class RecordsExportRequest(BaseModel): - notice_id: str - title: str - format: str = "markdown" + notice_id: str = Field(max_length=160) + title: str = Field(max_length=240) + format: str = Field(default="markdown", max_length=40) class PublicationProofRequest(BaseModel): - notice_id: str - notice_type: str - source_module: str = "manual" - source_record_id: str - channel: str - published_at: str - location: str - confirmation_reference: str - statutory_basis: str - reviewer: str + notice_id: str = Field(max_length=160) + notice_type: str = Field(max_length=160) + source_module: str = Field(default="manual", max_length=160) + source_record_id: str = Field(max_length=160) + channel: str = Field(max_length=160) + published_at: str = Field(max_length=160) + location: str = Field(max_length=1000) + confirmation_reference: str = Field(max_length=240) + statutory_basis: str = Field(max_length=1000) + reviewer: str = Field(max_length=160) @app.get("/") @@ -180,9 +188,13 @@ def public_civicnotice_page() -> str: return render_public_lookup_page() -@app.post("/api/v1/civicnotice/registry") -def notice_registry(request: NoticeRegistryRequest) -> dict[str, object]: +@app.post("/api/v1/civicnotice/registry", responses=PERSISTENCE_WRITE_RESPONSES) +def notice_registry( + request: NoticeRegistryRequest, + x_civicnotice_write_token: str | None = Header(default=None), +) -> dict[str, object]: if _workpaper_database_url() is not None: + _authorize_persistent_write(x_civicnotice_write_token) return _stored_notice_response( _get_workpaper_repository().create_notice_record( notice_id=request.notice_id, @@ -199,7 +211,7 @@ def notice_registry(request: NoticeRegistryRequest) -> dict[str, object]: return payload -@app.get("/api/v1/civicnotice/registry/{record_id}") +@app.get("/api/v1/civicnotice/registry/{record_id}", responses=PERSISTENCE_GET_RESPONSES) def get_notice_registry(record_id: str) -> dict[str, object]: if _workpaper_database_url() is None: raise HTTPException( @@ -221,9 +233,13 @@ def get_notice_registry(record_id: str) -> dict[str, object]: return _stored_notice_response(stored) -@app.post("/api/v1/civicnotice/deadlines") -def deadline_plan(request: DeadlineRequest) -> dict[str, object]: +@app.post("/api/v1/civicnotice/deadlines", responses=PERSISTENCE_WRITE_RESPONSES) +def deadline_plan( + request: DeadlineRequest, + x_civicnotice_write_token: str | None = Header(default=None), +) -> dict[str, object]: if _workpaper_database_url() is not None: + _authorize_persistent_write(x_civicnotice_write_token) return _stored_deadline_response( _get_workpaper_repository().create_deadline_plan( notice_type=request.notice_type, @@ -240,7 +256,7 @@ def deadline_plan(request: DeadlineRequest) -> dict[str, object]: return payload -@app.get("/api/v1/civicnotice/deadlines/{plan_id}") +@app.get("/api/v1/civicnotice/deadlines/{plan_id}", responses=PERSISTENCE_GET_RESPONSES) def get_deadline_plan(plan_id: str) -> dict[str, object]: if _workpaper_database_url() is None: raise HTTPException( @@ -262,8 +278,11 @@ def get_deadline_plan(plan_id: str) -> dict[str, object]: return _stored_deadline_response(stored) -@app.post("/api/v1/civicnotice/publication-proof") -def publication_proof(request: PublicationProofRequest) -> dict[str, object]: +@app.post("/api/v1/civicnotice/publication-proof", responses=PERSISTENCE_WRITE_RESPONSES) +def publication_proof( + request: PublicationProofRequest, + x_civicnotice_write_token: str | None = Header(default=None), +) -> dict[str, object]: if _workpaper_database_url() is None: raise HTTPException( status_code=503, @@ -272,6 +291,7 @@ def publication_proof(request: PublicationProofRequest) -> dict[str, object]: "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to store durable publication proof records.", }, ) + _authorize_persistent_write(x_civicnotice_write_token) stored = _get_workpaper_repository().create_publication_proof( notice_id=request.notice_id, notice_type=request.notice_type, @@ -287,7 +307,9 @@ def publication_proof(request: PublicationProofRequest) -> dict[str, object]: return _stored_publication_proof_response(stored) -@app.get("/api/v1/civicnotice/publication-proof/{proof_id}") +@app.get( + "/api/v1/civicnotice/publication-proof/{proof_id}", responses=PERSISTENCE_GET_RESPONSES +) def get_publication_proof(proof_id: str) -> dict[str, object]: if _workpaper_database_url() is None: raise HTTPException( @@ -423,6 +445,30 @@ def _workpaper_database_url() -> str | None: return os.environ.get("CIVICNOTICE_WORKPAPER_DB_URL") +def _trusted_write_token() -> str | None: + return os.environ.get("CIVICNOTICE_TRUSTED_WRITE_TOKEN") + + +def _authorize_persistent_write(provided_token: str | None) -> None: + expected_token = _trusted_write_token() + if not expected_token: + raise HTTPException( + status_code=503, + detail={ + "message": "CivicNotice durable write guard is not configured.", + "fix": "Set CIVICNOTICE_TRUSTED_WRITE_TOKEN before enabling persistence-backed writes.", + }, + ) + if provided_token != expected_token: + raise HTTPException( + status_code=403, + detail={ + "message": "CivicNotice durable write token is missing or invalid.", + "fix": "Send the configured X-CivicNotice-Write-Token header for persistence-backed writes.", + }, + ) + + def _get_workpaper_repository() -> NoticeWorkpaperRepository: global _workpaper_db_url, _workpaper_repository db_url = _workpaper_database_url() diff --git a/civicnotice/public_ui.py b/civicnotice/public_ui.py index c77cf4f..22135af 100644 --- a/civicnotice/public_ui.py +++ b/civicnotice/public_ui.py @@ -33,7 +33,7 @@ def render_public_lookup_page() -> str: p, li { line-height:1.65; overflow-wrap:anywhere; } textarea, button { width:100%; max-width:100%; min-width:0; border:1px solid #b9c6cc; border-radius:16px; padding:.85rem 1rem; font:inherit; } textarea { background:#f7f8fb; color:var(--ink); } - button { width:fit-content; min-width:190px; border:0; background:var(--blue); color:white; font-weight:900; cursor:default; } + .sample-box { width:100%; max-width:100%; min-width:0; border:1px solid #b9c6cc; border-radius:16px; padding:.85rem 1rem; background:#f7f8fb; color:var(--ink); } .result { margin-top:18px; padding:18px; border-left:6px solid var(--green); border-radius:18px; background:white; } .warning { border-left-color:#b2603f; background:#fff8f4; } .kicker { color:var(--muted); font-size:.86rem; font-weight:900; letter-spacing:.08em; text-transform:uppercase; } @@ -55,8 +55,7 @@ def render_public_lookup_page() -> str:

Sample hearing notice

Planning hearing notice

- - +
Public hearing on rezoning request. Confirm statutory authority, publication lead time, accessibility needs, and proof of publication.

Staff review packet

  • Confirm statutory authority, publication channel, lead time, and reviewer.
  • Route draft copy for clerk/legal review before publication.
  • Check accessibility, language access, subscriber delivery, proof, and archive handoff before closing the file.
diff --git a/docs/index.html b/docs/index.html index d693850..7465ea4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -20,6 +20,7 @@

Public notices with fewer missed deadlines.

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Accessibility and language-readiness packets.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Archive/handoff packets.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

+

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/pyproject.toml b/pyproject.toml index 8d5dac5..97c07a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "civicnotice" version = "0.2.0" -description = "CivicNotice runtime foundation for notice registry, deadline tracking, publication readiness tracking, channel planning, and public-records-aware export support." +description = "CivicNotice runtime foundation for notice registry, deadlines, rule checks, templates, proof workpapers, delivery planning, accessibility review, and archive handoffs." readme = "README.md" requires-python = ">=3.11" license = { text = "Apache-2.0" } @@ -28,6 +28,7 @@ dev = [ "httpx>=0.27.0,<1.0.0", "psycopg2-binary>=2.9.0,<3.0.0", "pytest>=8.0.0,<9.0.0", + "pytest-cov>=5.0.0,<7.0.0", "ruff>=0.11.0", ] diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 671a539..70815c2 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -2,6 +2,8 @@ set -euo pipefail VERSION="0.2.0" +CIVICCORE_WHEEL_URL="https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl" +CIVICCORE_WHEEL_SHA256="a94ce958e36fb03c8d961e4db4672ce5bcfa25765c57d75886e999cf15703ec7" find_python() { local candidates=() @@ -61,8 +63,26 @@ for path in [ print("PASS: version surfaces synchronized") PY +echo "==> CivicCore wheel provenance" +${PYTHON_BIN} - < Test suite" -${PYTHON_BIN} -m pytest -q +${PYTHON_BIN} -m pytest -q --cov=civicnotice --cov-branch --cov-fail-under=90 echo "==> Documentation gate" bash scripts/verify-docs.sh diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 7590258..befe407 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -354,6 +354,9 @@ def test_public_ui_route_is_accessible_and_honest() -> None: text = response.text assert '' in text assert '
' in text + assert " None: + db_url = os.environ["CIVICNOTICE_POSTGRES_TEST_URL"] + engine = sa.create_engine(db_url, future=True) + with engine.begin() as connection: + connection.execute(sa.text("DROP SCHEMA IF EXISTS civicnotice CASCADE")) + engine.dispose() + + repository = NoticeWorkpaperRepository(db_url=db_url) + record = repository.create_notice_record( + notice_id="PG-N-1", notice_type="planning hearing", owner="Clerk" + ) + plan = repository.create_deadline_plan( + notice_type="planning hearing", event_date=date(2026, 6, 15) + ) + proof = repository.create_publication_proof( + notice_id="PG-N-1", + notice_type="planning hearing", + source_module="civicclerk", + source_record_id="meeting-pg-1", + channel="newspaper", + published_at="2026-05-20T09:00:00-06:00", + location="Daily Gazette", + confirmation_reference="PG-AFF-1", + statutory_basis="staff-entered basis", + reviewer="City Clerk", + ) + + assert repository.get_notice_record(record.record_id).notice_id == "PG-N-1" + assert repository.get_deadline_plan(plan.plan_id).event_date == date(2026, 6, 15) + assert repository.get_publication_proof(proof.proof_id).source_module == "civicclerk" + + with repository.engine.begin() as connection: + schema_exists = connection.execute( + sa.text("select exists(select 1 from information_schema.schemata where schema_name='civicnotice')") + ).scalar_one() + repository.engine.dispose() + assert schema_exists is True diff --git a/tests/test_production_depth_notice_persistence.py b/tests/test_production_depth_notice_persistence.py index 39e6906..be8e812 100644 --- a/tests/test_production_depth_notice_persistence.py +++ b/tests/test_production_depth_notice_persistence.py @@ -48,21 +48,25 @@ def test_repository_persists_registry_and_deadline(tmp_path: Path) -> None: def test_notice_persistence_api_round_trip(monkeypatch, tmp_path: Path) -> None: db_path = tmp_path / "civicnotice-api.db" monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") + monkeypatch.setenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN", "test-token") _dispose_workpaper_repository() created_record = client.post( "/api/v1/civicnotice/registry", json={"notice_id": "N-1", "notice_type": "hearing", "owner": "Clerk"}, + headers={"X-CivicNotice-Write-Token": "test-token"}, ) record_id = created_record.json()["record_id"] fetched_record = client.get(f"/api/v1/civicnotice/registry/{record_id}") created_plan = client.post( "/api/v1/civicnotice/deadlines", json={"notice_type": "hearing", "event_date": "2026-05-20", "lead_days": 10}, + headers={"X-CivicNotice-Write-Token": "test-token"}, ) plan_id = created_plan.json()["plan_id"] fetched_plan = client.get(f"/api/v1/civicnotice/deadlines/{plan_id}") _dispose_workpaper_repository() monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + monkeypatch.delenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN") assert fetched_record.status_code == 200 assert fetched_record.json()["notice_id"] == "N-1" assert fetched_plan.status_code == 200 @@ -74,6 +78,7 @@ def test_notice_persistence_api_round_trip(monkeypatch, tmp_path: Path) -> None: def test_publication_proof_api_round_trip(monkeypatch, tmp_path: Path) -> None: db_path = tmp_path / "civicnotice-proof.db" monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") + monkeypatch.setenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN", "test-token") _dispose_workpaper_repository() created_proof = client.post( "/api/v1/civicnotice/publication-proof", @@ -89,11 +94,13 @@ def test_publication_proof_api_round_trip(monkeypatch, tmp_path: Path) -> None: "statutory_basis": "staff-entered hearing notice basis", "reviewer": "City Clerk", }, + headers={"X-CivicNotice-Write-Token": "test-token"}, ) proof_id = created_proof.json()["proof_id"] fetched_proof = client.get(f"/api/v1/civicnotice/publication-proof/{proof_id}") _dispose_workpaper_repository() monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + monkeypatch.delenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN") assert fetched_proof.status_code == 200 payload = fetched_proof.json() assert payload["notice_id"] == "N-2" @@ -157,3 +164,46 @@ def test_publication_proof_without_persistence_returns_actionable_503(monkeypatc ) assert response.status_code == 503 assert "Set CIVICNOTICE_WORKPAPER_DB_URL" in response.json()["detail"]["fix"] + + +def test_persistence_backed_write_requires_trusted_token(monkeypatch, tmp_path: Path) -> None: + db_path = tmp_path / "civicnotice-token.db" + monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") + monkeypatch.setenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN", "test-token") + _dispose_workpaper_repository() + missing = client.post( + "/api/v1/civicnotice/registry", + json={"notice_id": "N-4", "notice_type": "hearing", "owner": "Clerk"}, + ) + wrong = client.post( + "/api/v1/civicnotice/registry", + json={"notice_id": "N-4", "notice_type": "hearing", "owner": "Clerk"}, + headers={"X-CivicNotice-Write-Token": "wrong"}, + ) + _dispose_workpaper_repository() + monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + monkeypatch.delenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN") + assert missing.status_code == 403 + assert wrong.status_code == 403 + if db_path.exists(): + db_path.unlink() + + +def test_persistence_backed_write_requires_guard_configuration( + monkeypatch, tmp_path: Path +) -> None: + db_path = tmp_path / "civicnotice-token-missing.db" + monkeypatch.setenv("CIVICNOTICE_WORKPAPER_DB_URL", f"sqlite+pysqlite:///{db_path.as_posix()}") + monkeypatch.delenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN", raising=False) + _dispose_workpaper_repository() + response = client.post( + "/api/v1/civicnotice/deadlines", + json={"notice_type": "hearing", "event_date": "2026-05-20", "lead_days": 10}, + headers={"X-CivicNotice-Write-Token": "test-token"}, + ) + _dispose_workpaper_repository() + monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + assert response.status_code == 503 + assert "CIVICNOTICE_TRUSTED_WRITE_TOKEN" in response.json()["detail"]["fix"] + if db_path.exists(): + db_path.unlink() diff --git a/tests/test_release_contracts.py b/tests/test_release_contracts.py new file mode 100644 index 0000000..287e64f --- /dev/null +++ b/tests/test_release_contracts.py @@ -0,0 +1,127 @@ +from datetime import date, timedelta + +from fastapi.testclient import TestClient + +from civicnotice.archive_packet import build_notice_archive_packet +from civicnotice.main import app +from civicnotice.publication_proof import record_publication_proof +from civicnotice.statutory_rules import RULE_PACKS, check_statutory_notice_requirements +from civicnotice.subscriber_delivery import Subscriber, build_subscriber_delivery_plan + + +client = TestClient(app) + + +def test_openapi_documents_persistence_failure_contracts() -> None: + openapi = client.get("/openapi.json").json() + paths = openapi["paths"] + persistence_gets = [ + ("/api/v1/civicnotice/registry/{record_id}", "get"), + ("/api/v1/civicnotice/deadlines/{plan_id}", "get"), + ("/api/v1/civicnotice/publication-proof/{proof_id}", "get"), + ] + persistence_writes = [ + ("/api/v1/civicnotice/registry", "post"), + ("/api/v1/civicnotice/deadlines", "post"), + ("/api/v1/civicnotice/publication-proof", "post"), + ] + for path, method in persistence_gets: + responses = paths[path][method]["responses"] + assert "404" in responses + assert "503" in responses + for path, method in persistence_writes: + responses = paths[path][method]["responses"] + assert "403" in responses + assert "503" in responses + + +def test_api_rejects_fields_longer_than_storage_contract() -> None: + too_long = "x" * 161 + response = client.post( + "/api/v1/civicnotice/registry", + json={"notice_id": too_long, "notice_type": "hearing", "owner": "Clerk"}, + ) + assert response.status_code == 422 + + +def test_all_rule_packs_return_serializable_deadline_checks() -> None: + for notice_type, rule in RULE_PACKS.items(): + event_date = date(2026, 7, 1) + publication_date = event_date - timedelta(days=rule.minimum_lead_days) + result = check_statutory_notice_requirements( + notice_type=notice_type, + event_date=event_date, + publication_dates=(publication_date,), + channels=rule.required_channels, + content_fields=rule.required_content, + statutory_basis="staff-entered basis", + ) + assert result.notice_type == notice_type + assert result.publication_count_status == "required_publication_count_met" + assert result.missing_channels == () + assert result.missing_content == () + + +def test_publication_proof_fallbacks_are_staff_reviewable() -> None: + result = record_publication_proof( + notice_id="", + notice_type="", + source_module="", + source_record_id="", + channel="", + published_at="", + location="", + confirmation_reference="", + statutory_basis="", + reviewer="", + ) + assert result.notice_id == "unassigned notice" + assert result.source_record_id == "unlinked source record" + assert result.confirmation_reference == "pending confirmation reference" + assert result.compliance_status == "proof_recorded_staff_review_required" + + +def test_archive_packet_handoff_targets_cover_source_modules() -> None: + expected = { + "civicclerk": ("civicclerk", "civicrecords"), + "civicprocure": ("civicprocure", "civicrecords"), + "civicboards": ("civicboards", "civicrecords"), + "manual": ("manual staff archive", "civicrecords"), + } + for source_module, targets in expected.items(): + result = build_notice_archive_packet( + notice_id="N-1", + notice_type="hearing", + source_module=source_module, + source_record_id="source-1", + ) + assert result.handoff_targets == targets + + +def test_subscriber_delivery_empty_and_required_channel_variants() -> None: + empty = build_subscriber_delivery_plan( + notice_id="N-1", + notice_type="hearing", + audience="residents", + subscribers=(), + required_channels=("email",), + ) + postal = build_subscriber_delivery_plan( + notice_id="N-1", + notice_type="hearing", + audience="residents", + subscribers=( + Subscriber( + subscriber_id="1", + name="Resident", + email="resident@example.gov", + channels=("postal mail",), + language="English", + ), + ), + required_channels=("postal mail",), + ) + assert empty.active_subscriber_count == 0 + assert empty.missing_required_channels == ("email",) + assert postal.missing_required_channels == () + assert postal.email_recipients == () From b4f2cedbdc6028ba5ce8f1d9f0b685f1ba0f60aa Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 10:58:52 -0600 Subject: [PATCH 10/13] Resolve CivicNotice final gate findings --- AGENTS.md | 4 +- README.md | 6 +- README.txt | 8 +- USER-MANUAL.md | 4 +- civicnotice/main.py | 123 ++++++++++++------ civicnotice/notice_templates.py | 8 +- civicnotice/statutory_rules.py | 26 +++- ...qa-civicnotice-v0.1.1-alignment-summary.md | 8 +- ...th-notice-workpaper-persistence-summary.md | 6 +- docs/index.html | 2 +- scripts/verify-docs.sh | 31 +++-- scripts/verify-release.sh | 11 ++ tests/test_notice_foundation.py | 47 +++++-- ...est_production_depth_notice_persistence.py | 23 ++++ 14 files changed, 225 insertions(+), 82 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 21c40ed..c8c687e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,10 +9,10 @@ ## Hard Boundaries - CivicNotice never decides legal sufficiency, publishes official notices, provides legal advice, writes back to publication systems, or updates a notice system of record. -- CivicNotice v0.1.1 must not call live LLMs or live agenda systems. +- CivicNotice v0.2.0 must not call live LLMs or live agenda systems. - deadline tracking, publication readiness tracking, channel planning, and records exports must be marked staff-review-required where applicable. - CivicNotice depends on CivicCore; CivicCore must never depend on CivicNotice. -- CivicNotice may reference CivicClerk meeting concepts only through released APIs or deterministic sample data in v0.1.1. +- CivicNotice may reference CivicClerk meeting concepts only through released APIs or deterministic sample data in v0.2.0. ## Verification diff --git a/README.md b/README.md index cb70978..ae67bfc 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Current state: **v0.2.0 notice compliance foundation release**, aligned to the ` - `GET /` returns the shipped/planned boundary. - `GET /health` returns package and CivicCore versions. - `GET /civicnotice` returns the accessible public sample UI. +- `GET /docs` opens the interactive FastAPI API documentation for local deployments. +- `GET /openapi.json` returns the machine-readable API schema, including accepted request and error shapes. - `POST /api/v1/civicnotice/registry` returns a sample notice registry stub. - `GET /api/v1/civicnotice/registry/{record_id}` retrieves a persisted notice registry record. - `POST /api/v1/civicnotice/deadlines` returns statutory deadline reminders. @@ -57,7 +59,7 @@ Minimal workflow to smoke-check after startup: 1. Confirm `/health` reports `service: civicnotice`, version `0.2.0`, and CivicCore `1.2.0`. 2. Post a registry stub to `/api/v1/civicnotice/registry` without a database and confirm the response includes `record_id: null`, registry notes, and the staff-responsibility disclaimer. -3. Post a rule check to `/api/v1/civicnotice/rule-check` with a notice type, event date, publication dates, channels, content fields, and statutory basis. +3. Open `/docs` or `/openapi.json` to inspect accepted fields, then post a rule check to `/api/v1/civicnotice/rule-check` with a supported notice type, event date, publication dates, channels, content fields, and statutory basis. Unsupported notice types return a 422 response with supported choices. 4. Open `/civicnotice` and confirm the page is a static public sample with boundary copy, not an official publication workflow. ## Persistence and Durable Writes @@ -74,6 +76,8 @@ python -m pytest -q bash scripts/verify-release.sh ``` +The release gate requires `CIVICNOTICE_POSTGRES_TEST_URL` so PostgreSQL persistence coverage cannot be skipped. Plain unit tests may still run without PostgreSQL for local development. + ## License Code is Apache License 2.0. Documentation is CC BY 4.0. diff --git a/README.txt b/README.txt index a7fd59e..14a92f8 100644 --- a/README.txt +++ b/README.txt @@ -26,6 +26,8 @@ API surface: - GET / - GET /health - GET /civicnotice +- GET /docs +- GET /openapi.json - POST /api/v1/civicnotice/registry - GET /api/v1/civicnotice/registry/{record_id} - POST /api/v1/civicnotice/deadlines @@ -48,7 +50,7 @@ Install the package with development dependencies, start the ASGI app target civ Minimal smoke check: 1. Confirm /health reports service civicnotice, version 0.2.0, and CivicCore 1.2.0. 2. Post a registry stub to /api/v1/civicnotice/registry without a database and confirm record_id is null with registry notes and the staff-responsibility disclaimer. -3. Post a rule check to /api/v1/civicnotice/rule-check with a notice type, event date, publication dates, channels, content fields, and statutory basis. +3. Open /docs or /openapi.json to inspect accepted fields, then post a rule check to /api/v1/civicnotice/rule-check with a supported notice type, event date, publication dates, channels, content fields, and statutory basis. Unsupported notice types return a 422 response with supported choices. 4. Open /civicnotice and confirm the page is a static public sample with boundary copy, not an official publication workflow. Persistence and durable writes: @@ -57,4 +59,8 @@ Without CIVICNOTICE_WORKPAPER_DB_URL, CivicNotice runs in deterministic stateles With CIVICNOTICE_WORKPAPER_DB_URL, registry, deadline, and publication-proof workpapers are durable. Persistence-backed write routes also require CIVICNOTICE_TRUSTED_WRITE_TOKEN and the matching X-CivicNotice-Write-Token request header. This is a minimal trusted-mode guard for local deployments; it is not a replacement for a production identity system. +Release gate: + +The full release gate requires CIVICNOTICE_POSTGRES_TEST_URL so PostgreSQL persistence coverage cannot be skipped. Plain unit tests may still run without PostgreSQL for local development. + License: Apache License 2.0 for code; CC BY 4.0 for documentation. diff --git a/USER-MANUAL.md b/USER-MANUAL.md index a3ab2e9..595bc5c 100644 --- a/USER-MANUAL.md +++ b/USER-MANUAL.md @@ -15,6 +15,8 @@ Set `CIVICNOTICE_WORKPAPER_DB_URL` to enable SQLAlchemy-backed notice registry, - `GET /` - `GET /health` - `GET /civicnotice` +- `GET /docs` +- `GET /openapi.json` - `POST /api/v1/civicnotice/registry` - `GET /api/v1/civicnotice/registry/{record_id}` - `POST /api/v1/civicnotice/deadlines` @@ -37,7 +39,7 @@ A fresh user can reach the core stateless API and public sample UI without a dat Minimal smoke-check workflow: 1. Create a transient registry stub with `/api/v1/civicnotice/registry`. -2. Create a deterministic rule check with `/api/v1/civicnotice/rule-check`. +2. Open `/docs` or `/openapi.json` to inspect accepted fields, then create a deterministic rule check with `/api/v1/civicnotice/rule-check`. Unsupported notice types return supported choices instead of silently falling back to a generic rule. 3. Open `/civicnotice` and confirm it is a static public sample with boundary copy. 4. If durable workpapers are enabled, confirm the trusted write token is configured before testing persistence-backed writes. diff --git a/civicnotice/main.py b/civicnotice/main.py index bf69915..0f04cac 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -7,6 +7,7 @@ from fastapi import FastAPI, Header, HTTPException from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field +from sqlalchemy.exc import SQLAlchemyError from civicnotice import __version__ from civicnotice.accessibility_review import build_accessibility_review @@ -24,7 +25,7 @@ from civicnotice.public_ui import render_public_lookup_page from civicnotice.publication_check import build_publication_checklist from civicnotice.records_export import build_notice_records_export -from civicnotice.statutory_rules import check_statutory_notice_requirements +from civicnotice.statutory_rules import check_statutory_notice_requirements, supported_notice_types from civicnotice.subscriber_delivery import Subscriber, build_subscriber_delivery_plan @@ -195,11 +196,13 @@ def notice_registry( ) -> dict[str, object]: if _workpaper_database_url() is not None: _authorize_persistent_write(x_civicnotice_write_token) - return _stored_notice_response( - _get_workpaper_repository().create_notice_record( - notice_id=request.notice_id, - notice_type=request.notice_type, - owner=request.owner, + return _with_persistence_errors( + lambda: _stored_notice_response( + _get_workpaper_repository().create_notice_record( + notice_id=request.notice_id, + notice_type=request.notice_type, + owner=request.owner, + ) ) ) payload = register_notice_stub( @@ -221,7 +224,7 @@ def get_notice_registry(record_id: str) -> dict[str, object]: "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted notice registry records.", }, ) - stored = _get_workpaper_repository().get_notice_record(record_id) + stored = _with_persistence_errors(lambda: _get_workpaper_repository().get_notice_record(record_id)) if stored is None: raise HTTPException( status_code=404, @@ -240,11 +243,13 @@ def deadline_plan( ) -> dict[str, object]: if _workpaper_database_url() is not None: _authorize_persistent_write(x_civicnotice_write_token) - return _stored_deadline_response( - _get_workpaper_repository().create_deadline_plan( - notice_type=request.notice_type, - event_date=request.event_date, - lead_days=request.lead_days, + return _with_persistence_errors( + lambda: _stored_deadline_response( + _get_workpaper_repository().create_deadline_plan( + notice_type=request.notice_type, + event_date=request.event_date, + lead_days=request.lead_days, + ) ) ) payload = build_deadline_plan( @@ -266,7 +271,7 @@ def get_deadline_plan(plan_id: str) -> dict[str, object]: "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted deadline plans.", }, ) - stored = _get_workpaper_repository().get_deadline_plan(plan_id) + stored = _with_persistence_errors(lambda: _get_workpaper_repository().get_deadline_plan(plan_id)) if stored is None: raise HTTPException( status_code=404, @@ -292,17 +297,19 @@ def publication_proof( }, ) _authorize_persistent_write(x_civicnotice_write_token) - stored = _get_workpaper_repository().create_publication_proof( - notice_id=request.notice_id, - notice_type=request.notice_type, - source_module=request.source_module, - source_record_id=request.source_record_id, - channel=request.channel, - published_at=request.published_at, - location=request.location, - confirmation_reference=request.confirmation_reference, - statutory_basis=request.statutory_basis, - reviewer=request.reviewer, + stored = _with_persistence_errors( + lambda: _get_workpaper_repository().create_publication_proof( + notice_id=request.notice_id, + notice_type=request.notice_type, + source_module=request.source_module, + source_record_id=request.source_record_id, + channel=request.channel, + published_at=request.published_at, + location=request.location, + confirmation_reference=request.confirmation_reference, + statutory_basis=request.statutory_basis, + reviewer=request.reviewer, + ) ) return _stored_publication_proof_response(stored) @@ -319,7 +326,9 @@ def get_publication_proof(proof_id: str) -> dict[str, object]: "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted publication proof records.", }, ) - stored = _get_workpaper_repository().get_publication_proof(proof_id) + stored = _with_persistence_errors( + lambda: _get_workpaper_repository().get_publication_proof(proof_id) + ) if stored is None: raise HTTPException( status_code=404, @@ -341,14 +350,17 @@ def publication_checklist(request: PublicationRequest) -> dict[str, object]: @app.post("/api/v1/civicnotice/rule-check") def rule_check(request: RuleCheckRequest) -> dict[str, object]: - result = check_statutory_notice_requirements( - notice_type=request.notice_type, - event_date=request.event_date, - publication_dates=tuple(request.publication_dates), - channels=tuple(request.channels), - content_fields=tuple(request.content_fields), - statutory_basis=request.statutory_basis, - ) + try: + result = check_statutory_notice_requirements( + notice_type=request.notice_type, + event_date=request.event_date, + publication_dates=tuple(request.publication_dates), + channels=tuple(request.channels), + content_fields=tuple(request.content_fields), + statutory_basis=request.statutory_basis, + ) + except ValueError as exc: + raise _unsupported_notice_type_error(str(exc)) from exc payload = result.__dict__.copy() payload["event_date"] = result.event_date.isoformat() payload["required_deadline_date"] = result.required_deadline_date.isoformat() @@ -358,15 +370,18 @@ def rule_check(request: RuleCheckRequest) -> dict[str, object]: @app.post("/api/v1/civicnotice/templates") def notice_template(request: NoticeTemplateRequest) -> dict[str, object]: - result = build_notice_template( - notice_type=request.notice_type, - matter_title=request.matter_title, - event_date=request.event_date, - location=request.location, - contact=request.contact, - source_module=request.source_module, - statutory_basis=request.statutory_basis, - ) + try: + result = build_notice_template( + notice_type=request.notice_type, + matter_title=request.matter_title, + event_date=request.event_date, + location=request.location, + contact=request.contact, + source_module=request.source_module, + statutory_basis=request.statutory_basis, + ) + except ValueError as exc: + raise _unsupported_notice_type_error(str(exc)) from exc return result.__dict__ @@ -481,6 +496,30 @@ def _get_workpaper_repository() -> NoticeWorkpaperRepository: return _workpaper_repository +def _with_persistence_errors(operation): + try: + return operation() + except SQLAlchemyError as exc: + _dispose_workpaper_repository() + raise HTTPException( + status_code=503, + detail={ + "message": "CivicNotice workpaper persistence is configured but unavailable.", + "fix": "Verify CIVICNOTICE_WORKPAPER_DB_URL, database reachability, credentials, and schema permissions.", + }, + ) from exc + + +def _unsupported_notice_type_error(message: str) -> HTTPException: + return HTTPException( + status_code=422, + detail={ + "message": message, + "supported_notice_types": list(supported_notice_types()), + }, + ) + + def _dispose_workpaper_repository() -> None: global _workpaper_repository if _workpaper_repository is not None: diff --git a/civicnotice/notice_templates.py b/civicnotice/notice_templates.py index d87b569..44981aa 100644 --- a/civicnotice/notice_templates.py +++ b/civicnotice/notice_templates.py @@ -6,7 +6,7 @@ from datetime import date from civicnotice.notice_registry import DISCLAIMER -from civicnotice.statutory_rules import RULE_PACKS +from civicnotice.statutory_rules import resolve_notice_rule @dataclass(frozen=True) @@ -33,7 +33,7 @@ def build_notice_template( ) -> NoticeTemplate: """Build a staff-editable notice template without declaring it legally sufficient.""" - rule = RULE_PACKS.get(_clean(notice_type), RULE_PACKS["general notice"]) + rule = resolve_notice_rule(notice_type) clean_title = matter_title.strip() or "Untitled notice matter" clean_location = location.strip() or "[staff must enter location]" clean_contact = contact.strip() or "[staff must enter contact]" @@ -61,7 +61,3 @@ def build_notice_template( placeholders_remaining=placeholders, staff_review_required=True, ) - - -def _clean(value: str) -> str: - return " ".join(value.strip().lower().split()) diff --git a/civicnotice/statutory_rules.py b/civicnotice/statutory_rules.py index 57ec7c2..e8e25dd 100644 --- a/civicnotice/statutory_rules.py +++ b/civicnotice/statutory_rules.py @@ -83,6 +83,11 @@ class StatutoryRuleCheck: ), } +NOTICE_TYPE_ALIASES: dict[str, str] = { + "public hearing": "planning hearing", + "hearing": "planning hearing", +} + def check_statutory_notice_requirements( *, @@ -95,7 +100,7 @@ def check_statutory_notice_requirements( ) -> StatutoryRuleCheck: """Check a notice packet against deterministic staff-review rules.""" - rule = _rule_for(notice_type) + rule = resolve_notice_rule(notice_type) normalized_channels = tuple(_clean(value) for value in channels if _clean(value)) normalized_content = tuple(_clean(value) for value in content_fields if _clean(value)) required_deadline = event_date - timedelta(days=rule.minimum_lead_days) @@ -140,9 +145,21 @@ def check_statutory_notice_requirements( ) -def _rule_for(notice_type: str) -> NoticeRule: +def supported_notice_types() -> tuple[str, ...]: + """Return user-facing notice type values accepted by rule checks.""" + + return tuple(sorted((*RULE_PACKS.keys(), *NOTICE_TYPE_ALIASES.keys()))) + + +def resolve_notice_rule(notice_type: str) -> NoticeRule: + """Return the matching rule or raise a helpful error for unsupported input.""" + normalized = _clean(notice_type) - return RULE_PACKS.get(normalized, RULE_PACKS["general notice"]) + canonical = NOTICE_TYPE_ALIASES.get(normalized, normalized) + if canonical not in RULE_PACKS: + valid_types = ", ".join(supported_notice_types()) + raise ValueError(f"Unsupported notice type '{notice_type}'. Supported notice types: {valid_types}.") + return RULE_PACKS[canonical] def _deadline_status(*, publication_dates: tuple[date, ...], required_deadline: date) -> str: @@ -155,4 +172,5 @@ def _deadline_status(*, publication_dates: tuple[date, ...], required_deadline: def _clean(value: str) -> str: - return " ".join(value.strip().lower().split()) + normalized = value.replace("_", " ").replace("-", " ") + return " ".join(normalized.strip().lower().split()) diff --git a/docs/browser-qa-civicnotice-v0.1.1-alignment-summary.md b/docs/browser-qa-civicnotice-v0.1.1-alignment-summary.md index 67dbe79..c3b2dbd 100644 --- a/docs/browser-qa-civicnotice-v0.1.1-alignment-summary.md +++ b/docs/browser-qa-civicnotice-v0.1.1-alignment-summary.md @@ -1,4 +1,6 @@ -# Browser QA - CivicNotice v0.1.1 Alignment +# Historical Browser QA - CivicNotice v0.1.1 Alignment + +This archived QA note records the v0.1.1 browser pass from 2026-04-28. It is not current release evidence for CivicNotice v0.2.0; current release evidence is under `docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/`. Date: 2026-04-28 @@ -16,8 +18,8 @@ Evidence: Checks: -- Version labels show `v0.1.1`. -- Dependency copy names `civiccore==0.3.0`. +- Historical version labels showed `v0.1.1`. +- Historical dependency copy named `civiccore==0.3.0`. - Public UI boundary copy still states CivicNotice does not determine legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace the notice system of record. - Desktop and mobile captures report no horizontal overflow (`scrollWidth == innerWidth`) in both docs and public UI. diff --git a/docs/browser-qa-production-depth-notice-workpaper-persistence-summary.md b/docs/browser-qa-production-depth-notice-workpaper-persistence-summary.md index af30e66..dc9f57a 100644 --- a/docs/browser-qa-production-depth-notice-workpaper-persistence-summary.md +++ b/docs/browser-qa-production-depth-notice-workpaper-persistence-summary.md @@ -1,10 +1,10 @@ # Browser QA: Production Depth Notice Workpaper Persistence -Target: file:///C:/Users/scott/OneDrive/Desktop/Claude/civicnotice/docs/index.html +Target: docs/index.html from the local repository checkout. ## desktop -Screenshot: C:\Users\scott\OneDrive\Desktop\Claude\civicnotice\docs\browser-qa-production-depth-notice-workpaper-persistence-desktop.png +Screenshot: docs/browser-qa-production-depth-notice-workpaper-persistence-desktop.png - versionVisible: PASS - persistenceVisible: PASS @@ -16,7 +16,7 @@ Screenshot: C:\Users\scott\OneDrive\Desktop\Claude\civicnotice\docs\browser-qa-p ## mobile -Screenshot: C:\Users\scott\OneDrive\Desktop\Claude\civicnotice\docs\browser-qa-production-depth-notice-workpaper-persistence-mobile.png +Screenshot: docs/browser-qa-production-depth-notice-workpaper-persistence-mobile.png - versionVisible: PASS - persistenceVisible: PASS diff --git a/docs/index.html b/docs/index.html index 7465ea4..35486c2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -20,7 +20,7 @@

Public notices with fewer missed deadlines.

What ships

  • Sample notice registry.
  • Statutory deadline plans.
  • Statutory rule checks.
  • Notice drafting templates.
  • Accessibility and language-readiness packets.
  • Optional persistence with CIVICNOTICE_WORKPAPER_DB_URL, including publication-proof workpapers.
  • Publication-readiness checklists.
  • Channel planning flags.
  • Subscriber delivery planning.
  • Archive/handoff packets.
  • Notice and records export checklist.
  • Public sample UI at /civicnotice.

What does not ship

CivicNotice does not decide legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace a notice system of record.

- +

Dependency

Pinned to the civiccore v1.2.0 release wheel. CivicCore now supplies the shared deadline-plan helper; it still never imports from CivicNotice.

diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh index 79c287f..4f0531f 100644 --- a/scripts/verify-docs.sh +++ b/scripts/verify-docs.sh @@ -61,13 +61,28 @@ bad_markers=( "production staff-review queues are available" ) -for file in "${current_files[@]}"; do - for marker in "${bad_markers[@]}"; do - if grep -Fqi "$marker" "$file"; then - echo "FAIL: stale/planned-as-shipped marker '$marker' found in $file" >&2 - exit 1 - fi - done -done +DOCS_PYTHON="${CIVICNOTICE_RELEASE_PYTHON:-python}" +"${DOCS_PYTHON}" - <<'PY' +from pathlib import Path + +current_files = ("README.md", "README.txt", "USER-MANUAL.md", "docs/index.html") +bad_markers = ( + "official legal sufficiency decisions are available", + "legal advice is available", + "official publication is available", + "live LLM calls are available", + "publication-system write-back is available", + "system-of-record integrations are available", + "production staff-review queues are available", +) + +for filename in current_files: + text = Path(filename).read_text(encoding="utf-8").casefold() + for marker in bad_markers: + if marker.casefold() in text: + raise SystemExit( + f"FAIL: stale/planned-as-shipped marker '{marker}' found in {filename}" + ) +PY echo "VERIFY-DOCS: PASSED" diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 70815c2..623bc5f 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -82,6 +82,17 @@ print("PASS: CivicCore 1.2.0 wheel SHA-256 verified") PY echo "==> Test suite" +if [[ -z "${CIVICNOTICE_POSTGRES_TEST_URL:-}" ]]; then + echo "FAIL: CIVICNOTICE_POSTGRES_TEST_URL is required for the release gate so PostgreSQL persistence coverage cannot be skipped." >&2 + exit 1 +fi +${PYTHON_BIN} - <<'PY' +import os +assert os.environ.get("CIVICNOTICE_POSTGRES_TEST_URL"), ( + "CIVICNOTICE_POSTGRES_TEST_URL is not visible to the selected Python interpreter." +) +print("PASS: PostgreSQL test URL is visible to the selected Python interpreter") +PY ${PYTHON_BIN} -m pytest -q --cov=civicnotice --cov-branch --cov-fail-under=90 echo "==> Documentation gate" diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index befe407..4c7a5c9 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -85,24 +85,51 @@ def test_notice_template_preserves_staff_review_placeholders() -> None: assert "Staff must verify required publication channels" in result.template_lines[-1] -def test_unknown_notice_type_uses_generic_staff_review_rule() -> None: +def test_notice_type_aliases_resolve_without_generic_downgrade() -> None: rule_check = check_statutory_notice_requirements( - notice_type="river festival notice", + notice_type="public_hearing", event_date=date(2026, 8, 1), - content_fields=("title",), + content_fields=("title", "hearing date", "location", "case number", "statutory basis"), + channels=("city website", "posting board"), + statutory_basis="staff-entered basis", ) template = build_notice_template( - notice_type="river festival notice", - matter_title="River festival street closure", + notice_type="public-hearing", + matter_title="Planning hearing", event_date=date(2026, 8, 1), - location="Main Street", + location="Council Chambers", contact="clerk@example.gov", ) - assert rule_check.notice_type == "general notice" - assert "statutory basis" in rule_check.missing_content + assert rule_check.notice_type == "planning hearing" + assert "case number" not in rule_check.missing_content assert rule_check.staff_review_required is True - assert template.notice_type == "general notice" - assert "event date" in template.required_fields + assert template.notice_type == "planning hearing" + assert "case number" in template.required_fields + + +def test_unknown_notice_type_is_rejected_with_supported_choices() -> None: + response = client.post( + "/api/v1/civicnotice/rule-check", + json={ + "notice_type": "river festival notice", + "event_date": "2026-08-01", + "content_fields": ["title"], + }, + ) + template = client.post( + "/api/v1/civicnotice/templates", + json={ + "notice_type": "river festival notice", + "matter_title": "River festival street closure", + "event_date": "2026-08-01", + "location": "Main Street", + "contact": "clerk@example.gov", + }, + ) + assert response.status_code == 422 + assert template.status_code == 422 + assert "Supported notice types" in response.json()["detail"]["message"] + assert "planning hearing" in response.json()["detail"]["supported_notice_types"] def test_channel_plan_flags_accessibility_review() -> None: diff --git a/tests/test_production_depth_notice_persistence.py b/tests/test_production_depth_notice_persistence.py index be8e812..3e50cf3 100644 --- a/tests/test_production_depth_notice_persistence.py +++ b/tests/test_production_depth_notice_persistence.py @@ -207,3 +207,26 @@ def test_persistence_backed_write_requires_guard_configuration( assert "CIVICNOTICE_TRUSTED_WRITE_TOKEN" in response.json()["detail"]["fix"] if db_path.exists(): db_path.unlink() + + +def test_configured_but_unavailable_persistence_returns_actionable_503( + monkeypatch, tmp_path: Path +) -> None: + missing_parent = tmp_path / "missing-parent" / "civicnotice.db" + monkeypatch.setenv( + "CIVICNOTICE_WORKPAPER_DB_URL", + f"sqlite+pysqlite:///{missing_parent.as_posix()}", + ) + monkeypatch.setenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN", "test-token") + _dispose_workpaper_repository() + response = client.post( + "/api/v1/civicnotice/registry", + json={"notice_id": "N-5", "notice_type": "hearing", "owner": "Clerk"}, + headers={"X-CivicNotice-Write-Token": "test-token"}, + ) + _dispose_workpaper_repository() + monkeypatch.delenv("CIVICNOTICE_WORKPAPER_DB_URL") + monkeypatch.delenv("CIVICNOTICE_TRUSTED_WRITE_TOKEN") + assert response.status_code == 503 + assert "configured but unavailable" in response.json()["detail"]["message"] + assert "database reachability" in response.json()["detail"]["fix"] From 028a5954068d7423a1e1b6234723ddff5188908a Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 11:04:14 -0600 Subject: [PATCH 11/13] Finalize CivicNotice gate evidence --- civicnotice/main.py | 58 +- civicnotice/public_ui.py | 7 + .../REPORT.md | 39 + .../artifacts/api-smoke.json | 25 + .../artifacts/dependency-absent-no-db.json | 9 + .../artifacts/environment-attestation.txt | 6 + .../artifacts/health.json | 6 + .../artifacts/openapi.json | 1344 +++++++++++++++++ .../artifacts/public-ui.html | 71 + .../artifacts/root.json | 7 + .../artifacts/verify-release-postgres.log | 54 + docs/browser-qa-summary.md | 6 +- pyproject.toml | 2 +- tests/test_notice_foundation.py | 3 + tests/test_release_contracts.py | 23 + 15 files changed, 1644 insertions(+), 16 deletions(-) create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/dependency-absent-no-db.json create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/health.json create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/openapi.json create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/public-ui.html create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/root.json create mode 100644 docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/verify-release-postgres.log diff --git a/civicnotice/main.py b/civicnotice/main.py index 0f04cac..1365f38 100644 --- a/civicnotice/main.py +++ b/civicnotice/main.py @@ -45,6 +45,10 @@ 403: {"description": "Durable CivicNotice write token is missing or invalid."}, 503: {"description": "CivicNotice workpaper persistence or durable write guard is not configured."}, } +SUPPORTED_NOTICE_TYPE_HELP = ( + "Supported rule/template notice types include planning hearing, public hearing, bid notice, " + "vacancy notice, adoption notice, special meeting, and general notice." +) class NoticeRegistryRequest(BaseModel): @@ -65,22 +69,50 @@ class PublicationRequest(BaseModel): class RuleCheckRequest(BaseModel): - notice_type: str = Field(max_length=160) - event_date: date - publication_dates: list[date] = Field(default_factory=list) - channels: list[str] = Field(default_factory=list) - content_fields: list[str] = Field(default_factory=list) - statutory_basis: str = Field(default="", max_length=1000) + notice_type: str = Field( + max_length=160, + description=SUPPORTED_NOTICE_TYPE_HELP, + examples=["planning hearing", "public hearing", "bid notice"], + ) + event_date: date = Field( + description="Hearing, bid, vacancy, adoption, special-meeting, or other event date.", + examples=["2026-08-01"], + ) + publication_dates: list[date] = Field( + default_factory=list, + description="Dates when the notice is scheduled or was published.", + examples=[["2026-07-17"]], + ) + channels: list[str] = Field( + default_factory=list, + description="Publication channels staff plans to use or has already used.", + examples=[["city website", "posting board"]], + ) + content_fields: list[str] = Field( + default_factory=list, + description="Notice fields present in the draft packet.", + examples=[["title", "hearing date", "location", "case number", "statutory basis"]], + ) + statutory_basis: str = Field( + default="", + max_length=1000, + description="Staff-entered statutory citation or authority for review.", + examples=["staff-entered basis"], + ) class NoticeTemplateRequest(BaseModel): - notice_type: str = Field(max_length=160) - matter_title: str = Field(max_length=240) - event_date: date - location: str = Field(default="", max_length=1000) - contact: str = Field(default="", max_length=160) - source_module: str = Field(default="manual", max_length=160) - statutory_basis: str = Field(default="", max_length=1000) + notice_type: str = Field( + max_length=160, + description=SUPPORTED_NOTICE_TYPE_HELP, + examples=["planning hearing", "public hearing", "bid notice"], + ) + matter_title: str = Field(max_length=240, examples=["Planning hearing"]) + event_date: date = Field(examples=["2026-08-01"]) + location: str = Field(default="", max_length=1000, examples=["Council Chambers"]) + contact: str = Field(default="", max_length=160, examples=["clerk@example.gov"]) + source_module: str = Field(default="manual", max_length=160, examples=["civicclerk"]) + statutory_basis: str = Field(default="", max_length=1000, examples=["staff-entered basis"]) class ChannelRequest(BaseModel): diff --git a/civicnotice/public_ui.py b/civicnotice/public_ui.py index 22135af..21b70df 100644 --- a/civicnotice/public_ui.py +++ b/civicnotice/public_ui.py @@ -25,6 +25,8 @@ def render_public_lookup_page() -> str: h1 { max-width:980px; margin:0; font-family:Georgia,"Times New Roman",serif; font-size:clamp(2.2rem,7vw,5.4rem); line-height:1.02; letter-spacing:-.04em; overflow-wrap:break-word; word-break:break-word; } .lede { max-width:840px; font-size:clamp(1.1rem,2.4vw,1.45rem); line-height:1.55; color:#31404a; } .badge { display:inline-flex; width:fit-content; padding:.45rem .75rem; border-radius:999px; background:var(--green); color:white; font-weight:900; } + .quick-links { display:flex; flex-wrap:wrap; gap:.75rem; margin-top:1.1rem; } + .quick-links a { display:inline-flex; align-items:center; min-height:44px; padding:.65rem .9rem; border:1px solid var(--blue); border-radius:999px; color:var(--blue); background:white; font-weight:800; text-decoration:none; } .grid { display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:18px; min-width:0; } .card { grid-column:span 6; min-width:0; max-width:100%; padding:24px; border:1px solid var(--line); border-radius:28px; background:rgba(255,255,255,.92); box-shadow:0 18px 40px rgba(35,43,50,.10); } .card.large { grid-column:span 12; } @@ -48,6 +50,10 @@ def render_public_lookup_page() -> str:

CivicSuite / CivicNotice public sample

Public notices with fewer missed deadlines.

CivicNotice demonstrates notice administration support: registry stubs, deadline plans, rule checks, templates, accessibility review, subscriber delivery planning, archive packets, and proof-preserving records exports without publishing official notice.

+

v0.2.0 notice compliance foundation

@@ -55,6 +61,7 @@ def render_public_lookup_page() -> str:

Sample hearing notice

Planning hearing notice

+

Supported rule-check and template notice types include planning hearing, public hearing, bid notice, vacancy notice, adoption notice, special meeting, and general notice.

Public hearing on rezoning request. Confirm statutory authority, publication lead time, accessibility needs, and proof of publication.

Staff review packet

diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md new file mode 100644 index 0000000..01413a9 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md @@ -0,0 +1,39 @@ +# GauntletGate Report: CivicNotice 0.2.0 + +Date: 2026-06-19 + +Reviewed release: CivicNotice 0.2.0 + +Final reviewed commit: pending final evidence commit + +## Verdict + +Pending final CI confirmation for the evidence commit. + +## Gate Scope + +- Principal engineering review +- UI/UX review +- Technical writing review +- Test engineering review +- QA/runtime evidence review + +## Issues Found and Resolved + +- Inert public UI controls were removed from `/civicnotice`; the public page is now a static sample with boundary copy and no misleading draft controls. +- Unknown statutory-rule notice types no longer silently fall back to `general notice`; common public-hearing aliases normalize to `planning hearing`, and unsupported types return 422 with supported choices. +- `/docs` and `/openapi.json` are now documented in first-use surfaces and linked from the docs landing page when served by FastAPI. +- Configured-but-unavailable persistence now returns actionable 503 responses instead of raw 500 errors. +- The release gate now requires `CIVICNOTICE_POSTGRES_TEST_URL` and verifies that the selected Python interpreter can see it before tests run. +- Historical QA notes are marked historical, and obsolete cloud-sync evidence paths were replaced with repository-relative references. + +## Verification Evidence + +- `artifacts/verify-release-postgres.log` shows the full release gate passing with PostgreSQL coverage, CivicCore wheel SHA verification, docs verification, placeholder import checks, Ruff, build artifacts, and 34 passing tests with no skips. +- `artifacts/api-smoke.json` shows root, health, public UI, OpenAPI, no-DB dependency behavior, notice-type aliasing, and unsupported notice type rejection. +- `artifacts/environment-attestation.txt` records the reviewed git head and runtime environment. +- `artifacts/public-ui.html`, `artifacts/openapi.json`, `artifacts/root.json`, `artifacts/health.json`, and `artifacts/dependency-absent-no-db.json` preserve runtime smoke outputs. + +## Remaining Watchlist + +None. No deferred findings are allowed for this stage. diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json new file mode 100644 index 0000000..3557754 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json @@ -0,0 +1,25 @@ +{ + "git_head": "b4f2cedbdc6028ba5ce8f1d9f0b685f1ba0f60aa", + "root_status": 200, + "health_status": 200, + "public_ui_status": 200, + "openapi_status": 200, + "dependency_absent_status": 503, + "public_ui_buttons": 0, + "public_ui_textareas": 0, + "public_ui_docs_links": 1, + "public_ui_openapi_links": 1, + "rule_alias_status": 200, + "rule_alias_notice_type": "planning hearing", + "unknown_notice_type_status": 422, + "unknown_notice_type_supported_choices": [ + "adoption notice", + "bid notice", + "general notice", + "hearing", + "planning hearing", + "public hearing", + "special meeting", + "vacancy notice" + ] +} diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/dependency-absent-no-db.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/dependency-absent-no-db.json new file mode 100644 index 0000000..c0cc6a5 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/dependency-absent-no-db.json @@ -0,0 +1,9 @@ +{ + "status_code": 503, + "body": { + "detail": { + "message": "CivicNotice workpaper persistence is not configured.", + "fix": "Set CIVICNOTICE_WORKPAPER_DB_URL to retrieve persisted notice registry records." + } + } +} diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt new file mode 100644 index 0000000..18fb174 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt @@ -0,0 +1,6 @@ +git_head=b4f2cedbdc6028ba5ce8f1d9f0b685f1ba0f60aa +python=3.13.13 +platform=Windows-11-10.0.26200-SP0 +dependency_absent_no_db=registry GET returns actionable 503 when persistence is not configured +postgres_release_gate=verify-release-postgres.log shows CIVICNOTICE_POSTGRES_TEST_URL visible and all tests passed with no skips +public_ui_guidance=/civicnotice links to /docs and /openapi.json diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/health.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/health.json new file mode 100644 index 0000000..59c6ff8 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/health.json @@ -0,0 +1,6 @@ +{ + "status": "ok", + "service": "civicnotice", + "version": "0.2.0", + "civiccore_version": "1.2.0" +} diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/openapi.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/openapi.json new file mode 100644 index 0000000..b255774 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/openapi.json @@ -0,0 +1,1344 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "CivicNotice", + "description": "Public hearing, legal notice, bid notice, vacancy notice, and statutory publication deadline support for CivicSuite.", + "version": "0.2.0" + }, + "paths": { + "/": { + "get": { + "summary": "Root", + "description": "Return current product state without overstating unshipped behavior.", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Root Get" + } + } + } + } + } + } + }, + "/health": { + "get": { + "summary": "Health", + "description": "Return dependency/version health for deployment smoke checks.", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Health Health Get" + } + } + } + } + } + } + }, + "/civicnotice": { + "get": { + "summary": "Public Civicnotice Page", + "description": "Return the public sample notice compliance support UI.", + "operationId": "public_civicnotice_page_civicnotice_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/registry": { + "post": { + "summary": "Notice Registry", + "operationId": "notice_registry_api_v1_civicnotice_registry_post", + "parameters": [ + { + "name": "x-civicnotice-write-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Civicnotice-Write-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoticeRegistryRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Notice Registry Api V1 Civicnotice Registry Post" + } + } + } + }, + "403": { + "description": "Durable CivicNotice write token is missing or invalid." + }, + "503": { + "description": "CivicNotice workpaper persistence or durable write guard is not configured." + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/registry/{record_id}": { + "get": { + "summary": "Get Notice Registry", + "operationId": "get_notice_registry_api_v1_civicnotice_registry__record_id__get", + "parameters": [ + { + "name": "record_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Record Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Get Notice Registry Api V1 Civicnotice Registry Record Id Get" + } + } + } + }, + "404": { + "description": "Persisted CivicNotice workpaper record was not found." + }, + "503": { + "description": "CivicNotice workpaper persistence is not configured." + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/deadlines": { + "post": { + "summary": "Deadline Plan", + "operationId": "deadline_plan_api_v1_civicnotice_deadlines_post", + "parameters": [ + { + "name": "x-civicnotice-write-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Civicnotice-Write-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeadlineRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Deadline Plan Api V1 Civicnotice Deadlines Post" + } + } + } + }, + "403": { + "description": "Durable CivicNotice write token is missing or invalid." + }, + "503": { + "description": "CivicNotice workpaper persistence or durable write guard is not configured." + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/deadlines/{plan_id}": { + "get": { + "summary": "Get Deadline Plan", + "operationId": "get_deadline_plan_api_v1_civicnotice_deadlines__plan_id__get", + "parameters": [ + { + "name": "plan_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Plan Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Get Deadline Plan Api V1 Civicnotice Deadlines Plan Id Get" + } + } + } + }, + "404": { + "description": "Persisted CivicNotice workpaper record was not found." + }, + "503": { + "description": "CivicNotice workpaper persistence is not configured." + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/publication-proof": { + "post": { + "summary": "Publication Proof", + "operationId": "publication_proof_api_v1_civicnotice_publication_proof_post", + "parameters": [ + { + "name": "x-civicnotice-write-token", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Civicnotice-Write-Token" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicationProofRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Publication Proof Api V1 Civicnotice Publication Proof Post" + } + } + } + }, + "403": { + "description": "Durable CivicNotice write token is missing or invalid." + }, + "503": { + "description": "CivicNotice workpaper persistence or durable write guard is not configured." + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/publication-proof/{proof_id}": { + "get": { + "summary": "Get Publication Proof", + "operationId": "get_publication_proof_api_v1_civicnotice_publication_proof__proof_id__get", + "parameters": [ + { + "name": "proof_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Proof Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Get Publication Proof Api V1 Civicnotice Publication Proof Proof Id Get" + } + } + } + }, + "404": { + "description": "Persisted CivicNotice workpaper record was not found." + }, + "503": { + "description": "CivicNotice workpaper persistence is not configured." + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/publication-check": { + "post": { + "summary": "Publication Checklist", + "operationId": "publication_checklist_api_v1_civicnotice_publication_check_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Publication Checklist Api V1 Civicnotice Publication Check Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/rule-check": { + "post": { + "summary": "Rule Check", + "operationId": "rule_check_api_v1_civicnotice_rule_check_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RuleCheckRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Rule Check Api V1 Civicnotice Rule Check Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/templates": { + "post": { + "summary": "Notice Template", + "operationId": "notice_template_api_v1_civicnotice_templates_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoticeTemplateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Notice Template Api V1 Civicnotice Templates Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/channels": { + "post": { + "summary": "Channel Plan", + "operationId": "channel_plan_api_v1_civicnotice_channels_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChannelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Channel Plan Api V1 Civicnotice Channels Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/subscribers/plan": { + "post": { + "summary": "Subscriber Delivery Plan", + "operationId": "subscriber_delivery_plan_api_v1_civicnotice_subscribers_plan_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriberDeliveryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Subscriber Delivery Plan Api V1 Civicnotice Subscribers Plan Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/accessibility-review": { + "post": { + "summary": "Accessibility Review", + "operationId": "accessibility_review_api_v1_civicnotice_accessibility_review_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessibilityReviewRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Accessibility Review Api V1 Civicnotice Accessibility Review Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/archive-packet": { + "post": { + "summary": "Archive Packet", + "operationId": "archive_packet_api_v1_civicnotice_archive_packet_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArchivePacketRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Archive Packet Api V1 Civicnotice Archive Packet Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/civicnotice/export": { + "post": { + "summary": "Records Export", + "operationId": "records_export_api_v1_civicnotice_export_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecordsExportRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Records Export Api V1 Civicnotice Export Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AccessibilityReviewRequest": { + "properties": { + "notice_id": { + "type": "string", + "maxLength": 160, + "title": "Notice Id" + }, + "title": { + "type": "string", + "maxLength": 240, + "title": "Title" + }, + "notice_text": { + "type": "string", + "title": "Notice Text" + }, + "target_languages": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Target Languages" + }, + "attachments": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Attachments" + }, + "has_contact": { + "type": "boolean", + "title": "Has Contact", + "default": false + }, + "has_event_date": { + "type": "boolean", + "title": "Has Event Date", + "default": false + }, + "has_plain_language_summary": { + "type": "boolean", + "title": "Has Plain Language Summary", + "default": false + } + }, + "type": "object", + "required": [ + "notice_id", + "title", + "notice_text" + ], + "title": "AccessibilityReviewRequest" + }, + "ArchivePacketRequest": { + "properties": { + "notice_id": { + "type": "string", + "maxLength": 160, + "title": "Notice Id" + }, + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "source_module": { + "type": "string", + "maxLength": 160, + "title": "Source Module", + "default": "manual" + }, + "source_record_id": { + "type": "string", + "maxLength": 160, + "title": "Source Record Id", + "default": "" + }, + "registry_record_id": { + "type": "string", + "maxLength": 160, + "title": "Registry Record Id", + "default": "" + }, + "deadline_plan_id": { + "type": "string", + "maxLength": 160, + "title": "Deadline Plan Id", + "default": "" + }, + "publication_proof_id": { + "type": "string", + "maxLength": 160, + "title": "Publication Proof Id", + "default": "" + }, + "rule_check_complete": { + "type": "boolean", + "title": "Rule Check Complete", + "default": false + }, + "template_complete": { + "type": "boolean", + "title": "Template Complete", + "default": false + }, + "accessibility_review_complete": { + "type": "boolean", + "title": "Accessibility Review Complete", + "default": false + }, + "subscriber_delivery_complete": { + "type": "boolean", + "title": "Subscriber Delivery Complete", + "default": false + }, + "records_export_complete": { + "type": "boolean", + "title": "Records Export Complete", + "default": false + } + }, + "type": "object", + "required": [ + "notice_id", + "notice_type" + ], + "title": "ArchivePacketRequest" + }, + "ChannelRequest": { + "properties": { + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "audience": { + "type": "string", + "maxLength": 240, + "title": "Audience" + } + }, + "type": "object", + "required": [ + "notice_type", + "audience" + ], + "title": "ChannelRequest" + }, + "DeadlineRequest": { + "properties": { + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "event_date": { + "type": "string", + "format": "date", + "title": "Event Date" + }, + "lead_days": { + "type": "integer", + "maximum": 366.0, + "minimum": 0.0, + "title": "Lead Days", + "default": 10 + } + }, + "type": "object", + "required": [ + "notice_type", + "event_date" + ], + "title": "DeadlineRequest" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "NoticeRegistryRequest": { + "properties": { + "notice_id": { + "type": "string", + "maxLength": 160, + "title": "Notice Id" + }, + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "owner": { + "type": "string", + "maxLength": 160, + "title": "Owner" + } + }, + "type": "object", + "required": [ + "notice_id", + "notice_type", + "owner" + ], + "title": "NoticeRegistryRequest" + }, + "NoticeTemplateRequest": { + "properties": { + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type", + "description": "Supported rule/template notice types include planning hearing, public hearing, bid notice, vacancy notice, adoption notice, special meeting, and general notice.", + "examples": [ + "planning hearing", + "public hearing", + "bid notice" + ] + }, + "matter_title": { + "type": "string", + "maxLength": 240, + "title": "Matter Title", + "examples": [ + "Planning hearing" + ] + }, + "event_date": { + "type": "string", + "format": "date", + "title": "Event Date", + "examples": [ + "2026-08-01" + ] + }, + "location": { + "type": "string", + "maxLength": 1000, + "title": "Location", + "default": "", + "examples": [ + "Council Chambers" + ] + }, + "contact": { + "type": "string", + "maxLength": 160, + "title": "Contact", + "default": "", + "examples": [ + "clerk@example.gov" + ] + }, + "source_module": { + "type": "string", + "maxLength": 160, + "title": "Source Module", + "default": "manual", + "examples": [ + "civicclerk" + ] + }, + "statutory_basis": { + "type": "string", + "maxLength": 1000, + "title": "Statutory Basis", + "default": "", + "examples": [ + "staff-entered basis" + ] + } + }, + "type": "object", + "required": [ + "notice_type", + "matter_title", + "event_date" + ], + "title": "NoticeTemplateRequest" + }, + "PublicationProofRequest": { + "properties": { + "notice_id": { + "type": "string", + "maxLength": 160, + "title": "Notice Id" + }, + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "source_module": { + "type": "string", + "maxLength": 160, + "title": "Source Module", + "default": "manual" + }, + "source_record_id": { + "type": "string", + "maxLength": 160, + "title": "Source Record Id" + }, + "channel": { + "type": "string", + "maxLength": 160, + "title": "Channel" + }, + "published_at": { + "type": "string", + "maxLength": 160, + "title": "Published At" + }, + "location": { + "type": "string", + "maxLength": 1000, + "title": "Location" + }, + "confirmation_reference": { + "type": "string", + "maxLength": 240, + "title": "Confirmation Reference" + }, + "statutory_basis": { + "type": "string", + "maxLength": 1000, + "title": "Statutory Basis" + }, + "reviewer": { + "type": "string", + "maxLength": 160, + "title": "Reviewer" + } + }, + "type": "object", + "required": [ + "notice_id", + "notice_type", + "source_record_id", + "channel", + "published_at", + "location", + "confirmation_reference", + "statutory_basis", + "reviewer" + ], + "title": "PublicationProofRequest" + }, + "PublicationRequest": { + "properties": { + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "channel": { + "type": "string", + "maxLength": 160, + "title": "Channel" + } + }, + "type": "object", + "required": [ + "notice_type", + "channel" + ], + "title": "PublicationRequest" + }, + "RecordsExportRequest": { + "properties": { + "notice_id": { + "type": "string", + "maxLength": 160, + "title": "Notice Id" + }, + "title": { + "type": "string", + "maxLength": 240, + "title": "Title" + }, + "format": { + "type": "string", + "maxLength": 40, + "title": "Format", + "default": "markdown" + } + }, + "type": "object", + "required": [ + "notice_id", + "title" + ], + "title": "RecordsExportRequest" + }, + "RuleCheckRequest": { + "properties": { + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type", + "description": "Supported rule/template notice types include planning hearing, public hearing, bid notice, vacancy notice, adoption notice, special meeting, and general notice.", + "examples": [ + "planning hearing", + "public hearing", + "bid notice" + ] + }, + "event_date": { + "type": "string", + "format": "date", + "title": "Event Date", + "description": "Hearing, bid, vacancy, adoption, special-meeting, or other event date.", + "examples": [ + "2026-08-01" + ] + }, + "publication_dates": { + "items": { + "type": "string", + "format": "date" + }, + "type": "array", + "title": "Publication Dates", + "description": "Dates when the notice is scheduled or was published.", + "examples": [ + [ + "2026-07-17" + ] + ] + }, + "channels": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Channels", + "description": "Publication channels staff plans to use or has already used.", + "examples": [ + [ + "city website", + "posting board" + ] + ] + }, + "content_fields": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Content Fields", + "description": "Notice fields present in the draft packet.", + "examples": [ + [ + "title", + "hearing date", + "location", + "case number", + "statutory basis" + ] + ] + }, + "statutory_basis": { + "type": "string", + "maxLength": 1000, + "title": "Statutory Basis", + "description": "Staff-entered statutory citation or authority for review.", + "default": "", + "examples": [ + "staff-entered basis" + ] + } + }, + "type": "object", + "required": [ + "notice_type", + "event_date" + ], + "title": "RuleCheckRequest" + }, + "SubscriberDeliveryRequest": { + "properties": { + "notice_id": { + "type": "string", + "maxLength": 160, + "title": "Notice Id" + }, + "notice_type": { + "type": "string", + "maxLength": 160, + "title": "Notice Type" + }, + "audience": { + "type": "string", + "maxLength": 240, + "title": "Audience" + }, + "subscribers": { + "items": { + "$ref": "#/components/schemas/SubscriberRequest" + }, + "type": "array", + "title": "Subscribers" + }, + "required_channels": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required Channels" + } + }, + "type": "object", + "required": [ + "notice_id", + "notice_type", + "audience" + ], + "title": "SubscriberDeliveryRequest" + }, + "SubscriberRequest": { + "properties": { + "subscriber_id": { + "type": "string", + "maxLength": 160, + "title": "Subscriber Id" + }, + "name": { + "type": "string", + "maxLength": 160, + "title": "Name" + }, + "email": { + "type": "string", + "maxLength": 254, + "title": "Email", + "default": "" + }, + "channels": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Channels" + }, + "language": { + "type": "string", + "maxLength": 80, + "title": "Language", + "default": "English" + }, + "active": { + "type": "boolean", + "title": "Active", + "default": true + } + }, + "type": "object", + "required": [ + "subscriber_id", + "name" + ], + "title": "SubscriberRequest" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/public-ui.html b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/public-ui.html new file mode 100644 index 0000000..7752397 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/public-ui.html @@ -0,0 +1,71 @@ + + + + + +CivicNotice Public Notice Support + + + + +
+

CivicSuite / CivicNotice public sample

+

Public notices with fewer missed deadlines.

+

CivicNotice demonstrates notice administration support: registry stubs, deadline plans, rule checks, templates, accessibility review, subscriber delivery planning, archive packets, and proof-preserving records exports without publishing official notice.

+ +

v0.2.0 notice compliance foundation

+
+
+
+
+

Sample hearing notice

+

Planning hearing notice

+

Supported rule-check and template notice types include planning hearing, public hearing, bid notice, vacancy notice, adoption notice, special meeting, and general notice.

+
Public hearing on rezoning request. Confirm statutory authority, publication lead time, accessibility needs, and proof of publication.
+
+

Staff review packet

+
  • Confirm statutory authority, publication channel, lead time, and reviewer.
  • Route draft copy for clerk/legal review before publication.
  • Check accessibility, language access, subscriber delivery, proof, and archive handoff before closing the file.
+
+
+

Deadlines

Lead-time reminders

CivicNotice calculates review milestones; staff verify the legal deadline.

+

Channels

Publication path

Channel helpers surface website, posting board, newspaper, agenda page, and accessibility review needs.

+

Proof

Records-ready

Exports preserve draft, final notice, approvals, proof, source matter, and reviewer metadata.

+

Boundary

No official publication

CivicNotice does not determine legal sufficiency, publish official notices, provide legal advice, call live LLMs, write back to publication systems, or replace the notice system of record.

+
+
+

CivicNotice is part of the Apache 2.0 CivicSuite open-source municipal AI project.

+ + diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/root.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/root.json new file mode 100644 index 0000000..f9ca701 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/root.json @@ -0,0 +1,7 @@ +{ + "name": "CivicNotice", + "version": "0.2.0", + "status": "notice compliance foundation", + "message": "CivicNotice package, API foundation, sample notice registry, CivicCore-backed deadline plans, statutory rule checks, notice drafting templates, accessibility and language-readiness packets, archive/handoff packets, publication-readiness checklist, channel planning, records export checklist, optional database-backed registry/deadline/publication-proof workpapers, and public UI foundation are online; official legal sufficiency decisions, official publication, legal advice, live LLM calls, publication-system write-back, and notice system-of-record integrations are not implemented yet.", + "next_step": "Post-v0.2.0 roadmap: jurisdiction-specific statutory rule packs, live CivicClerk/CivicProcure/CivicNotice handoffs, and publication proof queues" +} diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/verify-release-postgres.log b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/verify-release-postgres.log new file mode 100644 index 0000000..a8e37f9 --- /dev/null +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/verify-release-postgres.log @@ -0,0 +1,54 @@ +==> Version surface check +PASS: version surfaces synchronized +==> CivicCore wheel provenance +PASS: CivicCore 1.2.0 wheel SHA-256 verified +==> Test suite +PASS: PostgreSQL test URL is visible to the selected Python interpreter +.................................... [100%] +=============================== tests coverage ================================ +______________ coverage: platform win32, python 3.13.13-final-0 _______________ + +Name Stmts Miss Branch BrPart Cover +----------------------------------------------------------------------- +civicnotice\__init__.py 1 0 0 0 100% +civicnotice\accessibility_review.py 43 4 14 6 82% +civicnotice\archive_packet.py 32 0 6 0 100% +civicnotice\channel_plan.py 14 0 0 0 100% +civicnotice\deadline_tracker.py 15 0 0 0 100% +civicnotice\main.py 235 4 28 4 97% +civicnotice\notice_registry.py 13 0 0 0 100% +civicnotice\notice_templates.py 24 0 0 0 100% +civicnotice\persistence.py 96 1 8 1 98% +civicnotice\public_ui.py 3 0 0 0 100% +civicnotice\publication_check.py 12 0 0 0 100% +civicnotice\publication_proof.py 26 0 0 0 100% +civicnotice\records_export.py 13 0 0 0 100% +civicnotice\statutory_rules.py 62 0 6 0 100% +civicnotice\subscriber_delivery.py 40 0 0 0 100% +----------------------------------------------------------------------- +TOTAL 629 9 62 11 97% +Required test coverage of 90% reached. Total coverage: 97.11% +36 passed in 3.50s +==> Documentation gate +==> Required-artifact check +==> Current-facing shipped/planned truth check +VERIFY-DOCS: PASSED +==> Placeholder import gate +PLACEHOLDER-IMPORT-CHECK: PASSED (15 source files scanned) +==> Ruff +All checks passed! +==> Build artifacts +* Creating isolated environment: venv+pip... +* Installing packages in isolated environment: + - hatchling>=1.27.0 +* Getting build dependencies for sdist... +* Building sdist... +* Building wheel from sdist +* Creating isolated environment: venv+pip... +* Installing packages in isolated environment: + - hatchling>=1.27.0 +* Getting build dependencies for wheel... +* Building wheel... +Successfully built civicnotice-0.2.0.tar.gz and civicnotice-0.2.0-py3-none-any.whl +PASS: build artifacts and SHA256SUMS.txt created +VERIFY-RELEASE: PASSED diff --git a/docs/browser-qa-summary.md b/docs/browser-qa-summary.md index 879d809..3b4c788 100644 --- a/docs/browser-qa-summary.md +++ b/docs/browser-qa-summary.md @@ -1,6 +1,8 @@ -# Browser QA Summary +# Historical Browser QA Summary -Status: PASS +This archived browser QA note records the 2026-04-27 pre-0.2.0 browser pass. It is not current release evidence for CivicNotice v0.2.0; current release evidence is under `docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/`. + +Historical status: PASS Checked at: 2026-04-27T22:25:26.910Z diff --git a/pyproject.toml b/pyproject.toml index 97c07a4..61b1bc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = [ { name = "CivicSuite contributors" } ] dependencies = [ - "civiccore @ https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl", + "civiccore @ https://github.com/CivicSuite/civiccore/releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl#sha256=a94ce958e36fb03c8d961e4db4672ce5bcfa25765c57d75886e999cf15703ec7", "fastapi>=0.115.0,<1.0.0", "sqlalchemy>=2.0.0,<3.0.0", "uvicorn[standard]>=0.30.0,<1.0.0", diff --git a/tests/test_notice_foundation.py b/tests/test_notice_foundation.py index 4c7a5c9..277888d 100644 --- a/tests/test_notice_foundation.py +++ b/tests/test_notice_foundation.py @@ -384,6 +384,9 @@ def test_public_ui_route_is_accessible_and_honest() -> None: assert " None: + pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + civiccore_dependency = next( + dependency + for dependency in pyproject["project"]["dependencies"] + if dependency.startswith("civiccore @ ") + ) + assert "releases/download/v1.2.0/civiccore-1.2.0-py3-none-any.whl" in civiccore_dependency + assert "#sha256=a94ce958e36fb03c8d961e4db4672ce5bcfa25765c57d75886e999cf15703ec7" in civiccore_dependency + + def test_openapi_documents_persistence_failure_contracts() -> None: openapi = client.get("/openapi.json").json() paths = openapi["paths"] @@ -35,6 +48,16 @@ def test_openapi_documents_persistence_failure_contracts() -> None: assert "503" in responses +def test_openapi_guides_first_use_notice_type_values() -> None: + openapi = client.get("/openapi.json").json() + schemas = openapi["components"]["schemas"] + rule_notice = schemas["RuleCheckRequest"]["properties"]["notice_type"] + template_notice = schemas["NoticeTemplateRequest"]["properties"]["notice_type"] + assert "Supported rule/template notice types" in rule_notice["description"] + assert "planning hearing" in rule_notice["examples"] + assert "public hearing" in template_notice["examples"] + + def test_api_rejects_fields_longer_than_storage_contract() -> None: too_long = "x" * 161 response = client.post( From 0ed06639a8f2c746337b6568586f73ca21b4111a Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 11:04:32 -0600 Subject: [PATCH 12/13] Record CivicNotice final gate commit --- .../gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md | 4 ++-- .../artifacts/api-smoke.json | 2 +- .../artifacts/environment-attestation.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md index 01413a9..db33224 100644 --- a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md @@ -4,11 +4,11 @@ Date: 2026-06-19 Reviewed release: CivicNotice 0.2.0 -Final reviewed commit: pending final evidence commit +Final reviewed commit: 028a5954b4a7cc119a5b46e219f47e2101aa328c ## Verdict -Pending final CI confirmation for the evidence commit. +Pending final CI confirmation for commit 028a5954b4a7cc119a5b46e219f47e2101aa328c. ## Gate Scope diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json index 3557754..b751390 100644 --- a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json @@ -1,5 +1,5 @@ { - "git_head": "b4f2cedbdc6028ba5ce8f1d9f0b685f1ba0f60aa", + "git_head": "028a5954068d7423a1e1b6234723ddff5188908a", "root_status": 200, "health_status": 200, "public_ui_status": 200, diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt index 18fb174..214ca1e 100644 --- a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt @@ -1,4 +1,4 @@ -git_head=b4f2cedbdc6028ba5ce8f1d9f0b685f1ba0f60aa +git_head=028a5954068d7423a1e1b6234723ddff5188908a python=3.13.13 platform=Windows-11-10.0.26200-SP0 dependency_absent_no_db=registry GET returns actionable 503 when persistence is not configured From 6b5a04e358b205abb60ecb500d54158463675612 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Fri, 19 Jun 2026 11:08:26 -0600 Subject: [PATCH 13/13] Clarify CivicNotice gate evidence provenance --- .../gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md | 8 ++++++-- .../artifacts/api-smoke.json | 5 +++-- .../artifacts/environment-attestation.txt | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md index db33224..cb4bb99 100644 --- a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/REPORT.md @@ -4,11 +4,15 @@ Date: 2026-06-19 Reviewed release: CivicNotice 0.2.0 -Final reviewed commit: 028a5954b4a7cc119a5b46e219f47e2101aa328c +Final pushed PR head reviewed by CI: 0ed06639a8f2c746337b6568586f73ca21b4111a + +Product/runtime evidence source commit: 028a5954068d7423a1e1b6234723ddff5188908a + +Report metadata note: report-only commits may follow the product/runtime evidence commit. GitHub CI on the final pushed PR head is the source of truth that those metadata commits did not break the release gate. ## Verdict -Pending final CI confirmation for commit 028a5954b4a7cc119a5b46e219f47e2101aa328c. +Gate passed for the reviewed product/runtime evidence, and GitHub CI is green for pushed PR head 0ed06639a8f2c746337b6568586f73ca21b4111a. ## Gate Scope diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json index b751390..c4dbbf4 100644 --- a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/api-smoke.json @@ -1,5 +1,4 @@ { - "git_head": "028a5954068d7423a1e1b6234723ddff5188908a", "root_status": 200, "health_status": 200, "public_ui_status": 200, @@ -21,5 +20,7 @@ "public hearing", "special meeting", "vacancy notice" - ] + ], + "final_pushed_pr_head": "0ed06639a8f2c746337b6568586f73ca21b4111a", + "product_runtime_evidence_head": "028a5954068d7423a1e1b6234723ddff5188908a" } diff --git a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt index 214ca1e..5872162 100644 --- a/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt +++ b/docs/audits/gauntletgate-civicnotice-0.2.0-2026-06-19/artifacts/environment-attestation.txt @@ -1,4 +1,5 @@ -git_head=028a5954068d7423a1e1b6234723ddff5188908a +final_pushed_pr_head=0ed06639a8f2c746337b6568586f73ca21b4111a +product_runtime_evidence_head=028a5954068d7423a1e1b6234723ddff5188908a python=3.13.13 platform=Windows-11-10.0.26200-SP0 dependency_absent_no_db=registry GET returns actionable 503 when persistence is not configured