Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: Bug report
about: Report a defect in civiccore
about: Report a defect in townlight_core
title: "[bug] "
labels: ["bug"]
---
Expand All @@ -19,7 +19,7 @@ labels: ["bug"]
<!-- Include error messages, stack traces, log output. -->

**Environment**
- civiccore version (`python -c "import civiccore; print(civiccore.__version__)"`):
- townlight_core version (`python -c "import townlight_core; print(townlight_core.__version__)"`):
- Python version:
- Consuming application (records-ai, custom, evaluation):
- Postgres version (if migration-related):
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ labels: ["documentation"]
**Audience this affects**
- [ ] Module developer / IT integrator
- [ ] Consuming application (records-ai, future modules)
- [ ] Evaluator (deciding whether to use civiccore)
- [ ] Evaluator (deciding whether to use townlight_core)
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: Feature request
about: Suggest an enhancement to civiccore
about: Suggest an enhancement to townlight_core
title: "[feat] "
labels: ["enhancement"]
---
Expand All @@ -14,6 +14,6 @@ labels: ["enhancement"]
**Alternatives considered**

**Scope check**
<!-- Does this fit civiccore's role as a shared platform library, or does it belong in a consumer module? -->
<!-- Does this fit townlight_core's role as a shared platform library, or does it belong in a consumer module? -->

**Additional context**
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
## Test plan

- [ ] `python -m pytest -q` passes
- [ ] `python -m ruff check civiccore tests` passes
- [ ] `python -m ruff check townlight_core tests` passes
- [ ] If touching migrations: idempotency tests pass and Gate 1/2/3 logic still holds for downstream consumers
- [ ] If touching public API in `civiccore.llm`: smoke test verifies symbols still importable
- [ ] If touching public API in `townlight_core.llm`: smoke test verifies symbols still importable

## Checklist

- [ ] CHANGELOG.md updated under `[Unreleased]`
- [ ] If version bumped: pyproject.toml, civiccore/__init__.py, scripts/verify-release.sh fresh-venv asserts all aligned
- [ ] If version bumped: pyproject.toml, townlight_core/__init__.py, scripts/verify-release.sh fresh-venv asserts all aligned
- [ ] No secrets committed
- [ ] Linked issue: <!-- #N -->
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: civiccore CI
name: townlight_core CI

on:
pull_request:
Expand Down Expand Up @@ -65,7 +65,7 @@ jobs:
- name: Pre-pull pgvector image
run: docker pull pgvector/pgvector:pg17

- name: Install civiccore (editable, dev extras)
- name: Install townlight_core (editable, dev extras)
run: |
pip install --upgrade pip
pip install -e .[dev]
Expand Down Expand Up @@ -146,5 +146,5 @@ jobs:

- name: Accessibility gate (no browser surface)
run: |
echo "CivicCore exposes shared backend contracts and has no operator-facing browser surface in this repo."
echo "Per-PR axe gates run in CivicRecords AI, CivicClerk, CivicCode, and the CivicSuite launcher."
echo "TownlightCore exposes shared backend contracts and has no operator-facing browser surface in this repo."
echo "Per-PR axe gates run in TownlightRecords, TownlightMeetings, TownlightCode, and the Townlight launcher."
12 changes: 6 additions & 6 deletions .github/workflows/cleanroom.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: civiccore cleanroom
name: townlight_core cleanroom

on:
pull_request:
Expand All @@ -7,8 +7,8 @@ on:
- ".github/workflows/cleanroom.yml"
- "cleanroom/**"
- "scripts/cleanroom/**"
- "scripts/run-civiccore-cleanroom.sh"
- "civiccore/release_provenance.py"
- "scripts/run-townlight_core-cleanroom.sh"
- "townlight_core/release_provenance.py"
- "tests/fixtures/release_provenance/**"
- "tests/test_cleanroom_harness.py"
- "docs/ops/cleanroom-harness.md"
Expand Down Expand Up @@ -36,10 +36,10 @@ jobs:
fetch-depth: 0
- name: Verify Docker is available
run: docker version
- name: Run CivicCore cleanroom harness
run: bash scripts/run-civiccore-cleanroom.sh "$TARGET_COMMIT" "docs/evidence/co6-ci-${TARGET_COMMIT}"
- name: Run TownlightCore cleanroom harness
run: bash scripts/run-townlight_core-cleanroom.sh "$TARGET_COMMIT" "docs/evidence/co6-ci-${TARGET_COMMIT}"
- name: Upload cleanroom evidence
uses: actions/upload-artifact@v7
with:
name: civiccore-co6-cleanroom-${{ env.TARGET_COMMIT }}
name: townlight_core-co6-cleanroom-${{ env.TARGET_COMMIT }}
path: docs/evidence/co6-ci-${{ env.TARGET_COMMIT }}/
82 changes: 41 additions & 41 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ on:
push:
tags:
- "v*"
- "civiccore-*-freeze"
- "townlight_core-*-freeze"
workflow_dispatch:
inputs:
release_tag:
description: "Published release tag to rehearse from cold caches"
required: true
default: "v1.2.1"
default: "v1.3.0.dev2"

permissions:
contents: write
Expand All @@ -33,7 +33,7 @@ jobs:
with:
python-version: "3.13"

- name: Cleanroom cache strip (scoped, CivicSuite-only)
- name: Cleanroom cache strip (scoped, Townlight-only)
shell: bash
run: |
python - <<'PY'
Expand All @@ -44,33 +44,33 @@ jobs:
for rel in (".npm", ".cache/pip"):
shutil.rmtree(os.path.join(runner_temp, rel), ignore_errors=True)
PY
docker builder prune --all --force --filter "label=civicsuite-cleanroom=1" || true
docker network ls --filter "label=civicsuite-cleanroom=1" -q \
docker builder prune --all --force --filter "label=townlight-cleanroom=1" || true
docker network ls --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker network rm || true
docker volume ls --filter "label=civicsuite-cleanroom=1" -q \
docker volume ls --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker volume rm || true

- name: Install published wheel from release URL
shell: bash
run: |
TAG="${{ inputs.release_tag }}"
VERSION="${TAG#v}"
WHEEL_URL="https://github.com/CivicSuite/civiccore/releases/download/${TAG}/civiccore-${VERSION}-py3-none-any.whl"
WHEEL_URL="https://github.com/Townlight/townlight_core/releases/download/${TAG}/townlight_core-${VERSION}-py3-none-any.whl"
echo "Cleanroom wheel URL: ${WHEEL_URL}"
python -m venv .cleanroom-venv
. .cleanroom-venv/bin/activate
python -m pip install --upgrade pip
python -m pip install --no-cache-dir --force-reinstall "${WHEEL_URL}"
TESTING=1 python - <<PY
import civiccore
from civiccore.ingest import Document, DocumentChunk, compute_file_hash, ingest_file, ingest_bytes, register_handler
import townlight_core
from townlight_core.ingest import Document, DocumentChunk, compute_file_hash, ingest_file, ingest_bytes, register_handler

expected = "${VERSION}"
print(f"civiccore.__version__={civiccore.__version__}")
if civiccore.__version__ != expected:
raise SystemExit(f"Expected CivicCore {expected}, got {civiccore.__version__}")
print(f"townlight_core.__version__={townlight_core.__version__}")
if townlight_core.__version__ != expected:
raise SystemExit(f"Expected TownlightCore {expected}, got {townlight_core.__version__}")
print(
"civiccore ingest import ok: "
"townlight_core ingest import ok: "
f"{Document.__name__}, {DocumentChunk.__name__}, {compute_file_hash.__name__}, "
f"{ingest_file.__name__}, {ingest_bytes.__name__}, {register_handler.__name__}"
)
Expand All @@ -80,10 +80,10 @@ jobs:
if: always()
shell: bash
run: |
docker compose -p civicsuite-cleanroom-${{ github.run_id }} down -v --remove-orphans || true
docker ps -a --filter "label=civicsuite-cleanroom=1" -q \
docker compose -p townlight-cleanroom-${{ github.run_id }} down -v --remove-orphans || true
docker ps -a --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker container rm --force || true
docker network ls --filter "label=civicsuite-cleanroom=1" -q \
docker network ls --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker network rm || true

preflight:
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: civiccore-dist
name: townlight_core-dist
path: |
dist/*
release-attestation.json
Expand All @@ -207,7 +207,7 @@ jobs:
steps:
- uses: actions/download-artifact@v8
with:
name: civiccore-dist
name: townlight_core-dist
path: release-assets/

- name: Create draft GitHub release with wheel + sdist
Expand All @@ -217,26 +217,26 @@ jobs:
run: |
latest_flag=()
case "${{ github.ref_name }}" in
civiccore-*-freeze) latest_flag=(--latest=false) ;;
townlight_core-*-freeze) latest_flag=(--latest=false) ;;
esac
export VERSION="${GITHUB_REF_NAME#v}"
export REPO="${GITHUB_REPOSITORY}"
export TAG="${GITHUB_REF_NAME}"
export WORKFLOW_RUN_ID="${GITHUB_RUN_ID}"
export WHEEL_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/civiccore-${VERSION}-py3-none-any.whl"
export WHEEL_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/townlight_core-${VERSION}-py3-none-any.whl"
export CLEANROOM_TIMESTAMP="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
cat > release-notes.md <<EOF
CivicCore ${TAG} release.
TownlightCore ${TAG} release.

Cleanroom rehearsal: PASSED in workflow run ${WORKFLOW_RUN_ID}.
Verified clean install of ${WHEEL_URL} from cold caches at ${CLEANROOM_TIMESTAMP}.

Release provenance:
- The Git tag is a release pointer; the trust artifact is release-attestation.json plus release-attestation.json.bundle.
- Pre-flight provenance fixtures and live attestation verification ran before publication in workflow run ${WORKFLOW_RUN_ID}.
- Procurement evidence pack: https://github.com/${REPO}/blob/${GITHUB_SHA}/docs/evidence/co8-civiccore-procurement-evidence-pack/index.md
- Historical procurement evidence pack: https://github.com/${REPO}/blob/${GITHUB_SHA}/docs/evidence/co8-civiccore-procurement-evidence-pack/index.md
- Historical provenance policy: https://github.com/${REPO}/blob/${GITHUB_SHA}/docs/ops/historical-provenance.md
- CO-9 closeout report, when present for the release tag: https://github.com/${REPO}/blob/${GITHUB_SHA}/docs/ops/co-9-civiccore-v1-closeout.md
- Historical CO-9 closeout report: https://github.com/${REPO}/blob/${GITHUB_SHA}/docs/ops/co-9-civiccore-v1-closeout.md
- Verify with:

```bash
Expand All @@ -260,7 +260,7 @@ jobs:
release-assets/release-attestation.json \
release-assets/release-attestation.json.bundle \
--repo "$GH_REPO" \
--title "civiccore ${{ github.ref_name }}" \
--title "townlight_core ${{ github.ref_name }}" \
"${latest_flag[@]}" \
--notes-file release-notes.md

Expand All @@ -281,7 +281,7 @@ jobs:
with:
python-version: "3.13"

- name: Cleanroom cache strip (scoped, CivicSuite-only)
- name: Cleanroom cache strip (scoped, Townlight-only)
shell: bash
run: |
python - <<'PY'
Expand All @@ -292,10 +292,10 @@ jobs:
for rel in (".npm", ".cache/pip"):
shutil.rmtree(os.path.join(runner_temp, rel), ignore_errors=True)
PY
docker builder prune --all --force --filter "label=civicsuite-cleanroom=1" || true
docker network ls --filter "label=civicsuite-cleanroom=1" -q \
docker builder prune --all --force --filter "label=townlight-cleanroom=1" || true
docker network ls --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker network rm || true
docker volume ls --filter "label=civicsuite-cleanroom=1" -q \
docker volume ls --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker volume rm || true

- name: Download uploaded wheel from draft release
Expand All @@ -306,17 +306,17 @@ jobs:
mkdir -p .cleanroom-assets
gh release download "${{ github.ref_name }}" \
--repo "${{ github.repository }}" \
--pattern 'civiccore-*-py3-none-any.whl' \
--pattern 'townlight_core-*-py3-none-any.whl' \
--dir .cleanroom-assets
find .cleanroom-assets -maxdepth 1 -type f -print

- name: Install uploaded wheel in cleanroom venv
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
WHEEL="$(find .cleanroom-assets -name 'civiccore-*-py3-none-any.whl' -type f | head -1)"
WHEEL="$(find .cleanroom-assets -name 'townlight_core-*-py3-none-any.whl' -type f | head -1)"
if [ -z "$WHEEL" ]; then
echo "::error::No CivicCore wheel downloaded from draft release."
echo "::error::No TownlightCore wheel downloaded from draft release."
exit 1
fi
echo "Cleanroom wheel asset: ${WHEEL}"
Expand All @@ -325,15 +325,15 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --no-cache-dir --force-reinstall "${WHEEL}"
TESTING=1 python - <<PY
import civiccore
from civiccore.ingest import Document, DocumentChunk, compute_file_hash, ingest_file, ingest_bytes, register_handler
import townlight_core
from townlight_core.ingest import Document, DocumentChunk, compute_file_hash, ingest_file, ingest_bytes, register_handler

expected = "${VERSION}"
print(f"civiccore.__version__={civiccore.__version__}")
if civiccore.__version__ != expected:
raise SystemExit(f"Expected CivicCore {expected}, got {civiccore.__version__}")
print(f"townlight_core.__version__={townlight_core.__version__}")
if townlight_core.__version__ != expected:
raise SystemExit(f"Expected TownlightCore {expected}, got {townlight_core.__version__}")
print(
"civiccore ingest import ok: "
"townlight_core ingest import ok: "
f"{Document.__name__}, {DocumentChunk.__name__}, {compute_file_hash.__name__}, "
f"{ingest_file.__name__}, {ingest_bytes.__name__}, {register_handler.__name__}"
)
Expand All @@ -343,10 +343,10 @@ jobs:
if: always()
shell: bash
run: |
docker compose -p civicsuite-cleanroom-${{ github.run_id }} down -v --remove-orphans || true
docker ps -a --filter "label=civicsuite-cleanroom=1" -q \
docker compose -p townlight-cleanroom-${{ github.run_id }} down -v --remove-orphans || true
docker ps -a --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker container rm --force || true
docker network ls --filter "label=civicsuite-cleanroom=1" -q \
docker network ls --filter "label=townlight-cleanroom=1" -q \
| xargs -r docker network rm || true

publish-release:
Expand All @@ -362,7 +362,7 @@ jobs:
run: |
latest_flag=()
case "${{ github.ref_name }}" in
civiccore-*-freeze) latest_flag=(--latest=false) ;;
townlight_core-*-freeze) latest_flag=(--latest=false) ;;
esac
gh release edit "${{ github.ref_name }}" \
--repo "$GH_REPO" \
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ env/
ENV/
.python-version

# Node (for civiccore-ui/)
# Node (for townlight_core-ui/)
node_modules/
npm-debug.log*
yarn-debug.log*
Expand Down
6 changes: 3 additions & 3 deletions .pipelines/module-release.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# module-release pipeline - the 4-phase pattern that prevents the
# "cascading CI bug discovery" failure mode from civicrecords-ai v1.5.0.
# "cascading CI bug discovery" failure mode from townlight_records v1.5.0.
#
# Use when:
# - Bumping a module to a new version
# - Releasing a downstream-dependency change (e.g. civiccore pin sweep)
# - Releasing a downstream-dependency change (e.g. townlight_core pin sweep)
# - Any work whose end-state is a published release artifact
#
# DO NOT use for:
Expand All @@ -12,7 +12,7 @@
# - Documentation-only changes
#
# Why this exists:
# The CivicSuite recovery sweep of 2026-05-10 burned 8 hours on a single
# The Townlight recovery sweep of 2026-05-10 burned 8 hours on a single
# module migration because three latent release-workflow bugs surfaced
# one at a time during Phase 3 (remote release). Each surface required
# a PR + merge + tag-move + 4-minute CI cycle. This pipeline forces all
Expand Down
6 changes: 3 additions & 3 deletions .pipelines/roles/local-rehearsal.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ cat > .env <<EOF
EOF
```

The civicrecords-ai email-validator bug only surfaced because CI's hermetic email used `.local` (reserved per RFC 6762). Local rehearsal that uses a different email value won't catch it. **Mirror exactly.**
The townlight_records email-validator bug only surfaced because CI's hermetic email used `.local` (reserved per RFC 6762). Local rehearsal that uses a different email value won't catch it. **Mirror exactly.**

### Step 2 - Run the verification script the workflow runs

Expand Down Expand Up @@ -124,7 +124,7 @@ act push --eventpath <(echo '{"ref": "refs/tags/v0.0.0-rehearsal", "ref_type": "

`act` can simulate the entire workflow tree (preflight, verify-release-linux, build-windows-installer if Windows runner available). Doesn't replicate every CI nuance but catches structural workflow bugs in seconds.

If `act` isn't available, document that gap and rely on Steps 1-4. The civicrecords-ai sweep would have been ~2 hours instead of 8 if anyone had `act`-rehearsed before pushing.
If `act` isn't available, document that gap and rely on Steps 1-4. The townlight_records sweep would have been ~2 hours instead of 8 if anyone had `act`-rehearsed before pushing.

## Local-only failures that CAN happen (not a blocker)

Expand Down Expand Up @@ -168,7 +168,7 @@ The Phase 3 stage MUST NOT execute until this report contains a PASS line for St

## Why this role exists

Tag push is the single non-reversible step in the pipeline. The agent-pipeline-codex allows tag-moves (CivicSuite v1.5.0 moved 4 times) but each move costs ~30 minutes (PR, merge, tag move, CI cycle) and pollutes the audit trail. The civicrecords-ai sweep tag-moved 4 times because the release workflow was the discovery mechanism for the bugs. With local rehearsal, the workflow becomes the EXECUTION mechanism, not the discovery mechanism.
Tag push is the single non-reversible step in the pipeline. The agent-pipeline-codex allows tag-moves (Townlight v1.5.0 moved 4 times) but each move costs ~30 minutes (PR, merge, tag move, CI cycle) and pollutes the audit trail. The townlight_records sweep tag-moved 4 times because the release workflow was the discovery mechanism for the bugs. With local rehearsal, the workflow becomes the EXECUTION mechanism, not the discovery mechanism.

Local rehearsal failing is good news (free fix). Remote rehearsal failing after a tag push is bad news (~30 minutes of plumbing per failure).

Expand Down
Loading
Loading