Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4f8daef
fix: create workspace_entities/workspace_documents and provision Work…
claude Sep 1, 2026
31b400c
fix(data): serialize workspace provisioning
seonghobae Sep 1, 2026
37bcd6e
fix(data): bind documents to organization scope
seonghobae Sep 1, 2026
bfe347e
test: cover the workspace/organization collision in the quality surface
claude Sep 1, 2026
317c721
fix: guard 0016 against a genuinely missing workspace_documents table
claude Sep 1, 2026
e05f1b3
test(data): reproduce legacy document organization scope regression
seonghobae Sep 1, 2026
5054a8e
fix: trust legacy NULL-organization documents only for the owning org
claude Sep 1, 2026
6fe1de1
fix: reconcile emails/email_records fix with PR #1502
claude Sep 1, 2026
053c066
fix: guard 0011_email_read_state on column, not just table, existence
claude Sep 1, 2026
d3020ca
fix: make 0011_email_read_state's downgrade non-destructive
claude Sep 1, 2026
da2d7fe
fix: add forward migration repairing already-stamped is_read gap
claude Sep 1, 2026
2beaf0e
fix: preserve document scope migration data
seonghobae Sep 1, 2026
9c4e5ed
fix: create legacy index through SQLAlchemy
seonghobae Sep 1, 2026
9c18513
fix: explain intentional best-effort DB teardown swallow
claude Sep 1, 2026
b4cb934
ci: validate stacked pull request bases
seonghobae Sep 4, 2026
804b5e7
docs(governance): define stacked validation gate
seonghobae Sep 4, 2026
1ac1f82
fix(actions): activate locked pnpm before cache
seonghobae Sep 4, 2026
f209170
merge(stack): build trigger repair on governance owner
seonghobae Sep 4, 2026
9990a99
fix(test): install Starlette TestClient dependency
seonghobae Sep 4, 2026
3a4ec58
test(deps): verify Starlette client pin hashes
seonghobae Sep 4, 2026
948f1e3
test(actions): preserve Bandit rerun isolation in successor
seonghobae Sep 4, 2026
bfc87a3
fix(actions): preserve Bandit rerun isolation in stacked-base repair
seonghobae Sep 4, 2026
ac46035
test(actions): compose stacked-base and rerun concurrency contracts
seonghobae Sep 4, 2026
bc91b36
fix(actions): compose stacked-base and rerun concurrency identities
seonghobae Sep 4, 2026
52dfc86
test(deps): verify Starlette httpx2 runtime
seonghobae Sep 5, 2026
19d5860
fix(migrations): inherit locked TestClient prerequisite
seonghobae Sep 5, 2026
ef85817
fix: run migrated PostgreSQL evidence in Application CI
seonghobae Sep 6, 2026
e30aa0d
fix(deps): integrate patched js-yaml prerequisite
seonghobae Sep 6, 2026
4d2e4ab
Merge branch 'codex/js-yaml-4-3-1' of https://github.com/ContextualWi…
seonghobae Sep 6, 2026
30d8476
fix(ci): preserve isolation in migration subprocesses
seonghobae Sep 6, 2026
b2e98a5
fix(ci): separate signal probe startup observation
seonghobae Sep 6, 2026
1f538b1
fix(ci): retain cancellation during process registration
seonghobae Sep 6, 2026
b0d1bb6
merge: inherit multiline governance owner repair
seonghobae Sep 6, 2026
0648eac
fix(ci): inherit complete stale-notice governance repair
seonghobae Sep 6, 2026
938d4b1
merge: inherit complete review publisher and warning repair
seonghobae Sep 6, 2026
0ec1cf9
merge(ci): inherit clean-summary governance repair
seonghobae Sep 6, 2026
cdef603
merge(ci): inherit complete security prerequisite stack
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions .github/workflows/app-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:
contents: read

concurrency:
group: application-ci-${{ github.event.pull_request.number || github.ref }}
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -44,7 +44,8 @@ jobs:

- name: Install backend dependencies
run: |
python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt
python -m venv backend/.venv
backend/.venv/bin/python -m pip install --disable-pip-version-check --require-hashes -r backend/requirements-hashes.txt

- name: Install Noema agent optional dependency
# Installs pydantic-ai (services/noema_agent.py) so the real build-path
Expand All @@ -53,24 +54,28 @@ jobs:
# --require-hashes can validate both preinstalled shared dependencies and
# agent-only packages.
run: |
python -m pip install --disable-pip-version-check --require-hashes \
backend/.venv/bin/python -m pip install --disable-pip-version-check --require-hashes \
-r backend/requirements-hashes.txt \
-r backend/requirements-agent.txt

- name: Run backend lint
run: |
cd backend
python -m ruff check .
.venv/bin/python -m ruff check .

- name: Run backend tests
run: |
set -o pipefail
cd backend
python -m pytest -q 2>&1 | tee pytest_output.log
if grep -qiE 'timeout|fatal|warn|denied' pytest_output.log; then
echo "::error::Tests produced Timeout, Fatal, Warn, or Denied outputs"
exit 1
fi
- name: Run backend tests on fresh and repeatedly migrated PostgreSQL
id: backend_tests
run: bash scripts/ci/run_backend_postgres.sh

- name: Preserve backend database and test evidence
if: ${{ always() && steps.backend_tests.outputs.evidence_dir != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-postgres-${{ matrix.python-version }}
path: |
${{ steps.backend_tests.outputs.evidence_dir }}/*.log
${{ steps.backend_tests.outputs.evidence_dir }}/pytest.xml
if-no-files-found: error

frontend:
name: frontend
Expand All @@ -87,7 +92,9 @@ jobs:
persist-credentials: false

- name: Install pnpm
run: corepack enable pnpm
run: |
corepack enable pnpm
corepack install --global pnpm@11.5.3

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/bandit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name == 'pull_request' && github.run_attempt == 1 && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}

jobs:
security:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate non-supersedable Docker workflow runs

When a maintainer reruns this workflow from an older PR head while current-head image validation is running, both attempts share this PR-only group and the stale rerun cancels the required current-head run. Tag runs for the same ref also share one group, so cancel-in-progress: false still permits GitHub concurrency to replace an older pending publication, contrary to the documented non-cancellation contract. Mirror Bandit's first-attempt PR cancellation plus run_id isolation and cover Docker in backend/tests/test_workflow_concurrency.py.

AGENTS.md reference: AGENTS.md:L192-L192

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head 1f538b1: the finding is valid, but copying Bandit's run_id suffix is not a complete repair.

  • docker-publish.yml:13-14 admits stale reruns to the same PR group before any live-head validation.
  • cancel-in-progress:false alone still replaces older pending work. Native queue:max now retains up to100 pending entries, but over-capacity runs are cancelled; it is not unbounded durable delivery.
  • docker-publish.yml:304-307 publishes a shared latest tag for every version. A per-ref or per-run group does not serialize different version publications to that shared target; a slower old release can overwrite the newer latest tag.
  • Application CI has the same pre-admission PR group boundary; Bandit's isolated reruns do not enforce the requested workflow-repository-PR ownership contract.

Repair must separate trusted current-head admission and PR validation cancellation from a shared-target release/deployment lock, exact-revision/idempotency checks and retained/recoverable release intent. Matrix components must not cancel sibling validations. Reuse the canonical .github owner contract rather than copying a local scheduler or appending arbitrary group suffixes. Do not waive this finding based on prior backend tests.

Reference: GitHub. (n.d.). Control the concurrency of workflows and jobs. https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency (accessed2026-09-06). No workflow, release, ruleset or PR-state mutation made by this diagnosis.


env:
REGISTRY: ghcr.io
# Keep the explicit opt-in as belt-and-suspenders. The real warning removal
Expand Down
13 changes: 13 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Architecture

## Application CI database boundary

`scripts/ci/run_backend_postgres.sh` is the local/Actions test entrypoint for a
fresh task-only PostgreSQL instance. `docker-compose.test.yml` pins the
multi-platform image digest and exposes only a random loopback port; generated
test credentials never select an operator database. Alembic runs twice before
the complete backend suite. `ci_postgres_gate` fails PostgreSQL-marked skips,
while optional live API skips disclose missing deployment evidence. Cleanup
targets only the generated Compose project and preserves command failures.
This is repository test infrastructure, not a new runtime service or a copy
of central review/security workflows. See
[`application_ci_postgres.md`](docs/doctoring/application_ci_postgres.md).

## System shape

```mermaid
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## [Unreleased]
- 데이터 저장·검색 검사가 실제 DB 연결 없이 건너뛰어져도 성공으로 보이던 검증 공백을 보완했습니다. 새 DB 설치와 반복 업그레이드 후 전체 백엔드 검사를 실행하며, 아직 실제 배포 환경 검증을 뜻하지는 않습니다.
- Starlette `TestClient`의 기존 `httpx2==2.5.0` pin을 core 개발·테스트 의존성으로 승격하고, deprecated `httpx` fallback 경고 억제를 제거했습니다.
- 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다.
- EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다.

Expand Down
22 changes: 14 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,23 @@ and the merge scheduler come from central workflows in
### Backend (FastAPI, Python, in `backend/`)

```bash
uv sync --project backend --locked
uv pip install --python backend/.venv/bin/python --require-hashes \
-r backend/requirements-hashes.txt -r backend/requirements-agent.txt
bash scripts/ci/run_backend_postgres.sh # isolated DB, fresh/repeat migrations, full pytest
cd backend
python3 -m pip install -r requirements.txt # CI: --require-hashes -r requirements-hashes.txt
python3 scripts/migrate_db.py # Alembic upgrade head (managed path)
python3 -m pytest -q # full test suite
python -m pytest tests/test_tasks_api.py -q # single test file
python -m ruff check . # lint (CI-enforced)
uvicorn main:app --reload # local dev server only
.venv/bin/python -m pytest tests/test_tasks_api.py -q # focused test; not DB CI evidence
.venv/bin/python -m ruff check . # lint (CI-enforced)
.venv/bin/python scripts/start_backend.py # validates injected settings first
```

- CI runs backend tests with `PYTHONWARNINGS=error` and
`DISABLE_BACKGROUND_WORKERS=1`, then fails the job if the pytest output
contains `Timeout`, `Fatal`, `Warn`, or `Denied`. Match that locally for
merge evidence.
contains `Timeout`, `Fatal`, `Warn`, or `Denied`. The shared runner loads
`ci_postgres_gate` so PostgreSQL-marked skips fail; unconfigured live API
tests remain explicit skips, not browser or deployment proof. Its printed
evidence directory contains redacted logs/JUnit after task-scoped cleanup.
See `docs/doctoring/application_ci_postgres.md` for scope and baseline.
- Containers never run `uvicorn main:app` directly; the entrypoint is
`python scripts/start_backend.py`, which validates required settings first.
- `scripts/bootstrap_db.py` is the local/dev-only schema compatibility path;
Expand Down Expand Up @@ -81,6 +85,8 @@ source (never mount `~/.env` as a Compose `env_file`):

The other compose files are purpose-specific evaluation/evidence stacks:

- `docker-compose.test.yml` — disposable PostgreSQL only, used by the CI runner;
no operator env file, customer volume, model service or provider call.
- `docker-compose.live-e2e.yml` — live E2E evidence: pre-built
`BACKEND_IMAGE`/`FRONTEND_IMAGE`, migrate/seed marker containers, nginx at
`127.0.0.1:18080`.
Expand Down
5 changes: 2 additions & 3 deletions backend/alembic/versions/0001_initial_control_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sqlalchemy import text

from db.models import Base
from scripts.bootstrap_db import schema_backfill_sql
from scripts.bootstrap_db import execute_schema_backfill

revision = "0001_initial_control_plane"
down_revision = None
Expand All @@ -19,8 +19,7 @@ def upgrade() -> None:
connection = op.get_bind()
connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
Base.metadata.create_all(connection)
for statement in schema_backfill_sql():
connection.execute(statement)
execute_schema_backfill(connection)


def downgrade() -> None:
Expand Down
52 changes: 41 additions & 11 deletions backend/alembic/versions/0011_email_read_state.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
"""Add is_read to emails (IMAP \\Seen read state).
"""Add is_read to email_records (IMAP \\Seen read state).

Existing rows default to read so historical/file imports do not surface as unread.

Checks both "email_records" (the real, current table -- a database whose own
0001_initial_control_plane ran before ``is_read`` was added to the ``Email``
model has this table without the column, and needs it added) and "emails"
(a legacy name that, per 0011_email_model_reconciliation's docstring, no
migration in this repo's history ever actually created for a real managed
database, but is checked defensively in case one somehow exists). Guarded by
column existence, not just table existence, so it is safely idempotent.
"""

from alembic import op
Expand All @@ -12,18 +20,40 @@
branch_labels = None
depends_on = None

_CANDIDATE_TABLES = ("email_records", "emails")


def upgrade() -> None:
op.add_column(
"emails",
sa.Column(
"is_read",
sa.Boolean(),
nullable=False,
server_default=sa.text("true"),
),
)
inspector = sa.inspect(op.get_bind())
for table_name in _CANDIDATE_TABLES:
if inspector.has_table(table_name) and not _has_column(
inspector, table_name, "is_read"
):
op.add_column(
table_name,
sa.Column(
"is_read",
sa.Boolean(),
nullable=False,
server_default=sa.text("true"),
),
)


def downgrade() -> None:
op.drop_column("emails", "is_read")
# Same ownership-ambiguity problem as 0018_workspace_registry's downgrade:
# a fresh database gets email_records.is_read from 0001's live
# Base.metadata.create_all, not from this revision, so there is no way to
# tell "this revision added the column" apart from "the baseline already
# had it" -- and is_read holds real per-message read/unread state, not
# rebuildable derived data. As with 0001_initial_control_plane and
# 0018_workspace_registry: production rollbacks should restore from
# backup or a later explicit down revision rather than dropping
# customer-owned data.
return None


def _has_column(inspector, table_name: str, column_name: str) -> bool:
return any(
column["name"] == column_name for column in inspector.get_columns(table_name)
)
19 changes: 13 additions & 6 deletions backend/alembic/versions/0016_document_org_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
recognition worker can resolve the owning organization's provider without
joining through the (organization-less) workspace entity. Nullable and additive
so existing rows and personal-scope documents are unaffected.

A database that has never had ``workspace_documents`` at all (one that ran
``0001_initial_control_plane`` before ``Workspace``/``Document`` existed in
``db/models.py``, and has only applied incremental migrations since) has no
table for this revision to alter. This revision is a no-op for that case;
``0018_workspace_registry`` creates the table later in the chain, already
including this column.
"""

from alembic import op
Expand All @@ -24,6 +31,8 @@
def upgrade() -> None:
connection = op.get_bind()
inspector = sa.inspect(connection)
if not inspector.has_table(_DOCUMENTS_TABLE):
return
columns = {column["name"] for column in inspector.get_columns(_DOCUMENTS_TABLE)}
if _ORG_COLUMN not in columns:
op.add_column(
Expand All @@ -39,9 +48,7 @@ def upgrade() -> None:


def downgrade() -> None:
connection = op.get_bind()
inspector = sa.inspect(connection)
columns = {column["name"] for column in inspector.get_columns(_DOCUMENTS_TABLE)}
op.drop_index(_ORG_INDEX, table_name=_DOCUMENTS_TABLE, if_exists=True)
if _ORG_COLUMN in columns:
op.drop_column(_DOCUMENTS_TABLE, _ORG_COLUMN)
# 0018 can create this table and column after 0016 was a no-op. Alembic
# cannot distinguish that case from a table altered by this revision, so
# dropping the column here could destroy later organization assignments.
return None
81 changes: 81 additions & 0 deletions backend/alembic/versions/0018_workspace_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""create workspace registry and workspace document tables

Revision ID: 0018_workspace_registry
Revises: 0017_merge_newsdom_carddav_heads
Create Date: 2026-09-01 00:00:00.000000

``Workspace``/``Document`` (``workspace_entities``/``workspace_documents``) have
been declared in ``db/models.py`` since before this repository's incremental
migration history begins tracking them explicitly. A database that ran
``0001_initial_control_plane``'s ``Base.metadata.create_all`` after these
models existed already has both tables; a database that ran ``0001`` earlier
and has only applied incremental migrations since never got them, so
``/api/data/documents`` fails with an undefined-relation error the first time
it is hit. This revision is idempotent (``has_table`` guarded) so it is a
no-op for a database that already has the tables and a real fix for one that
does not.
"""

from alembic import op
import sqlalchemy as sa

revision = "0018_workspace_registry"
down_revision = "0017_merge_newsdom_carddav_heads"

_ENTITIES_TABLE = "workspace_entities"
_DOCUMENTS_TABLE = "workspace_documents"


def upgrade() -> None:
connection = op.get_bind()
inspector = sa.inspect(connection)

if not inspector.has_table(_ENTITIES_TABLE):
op.create_table(
_ENTITIES_TABLE,
sa.Column("workspace_id", sa.String(), nullable=False),
sa.Column("workspace_name", sa.String(), nullable=False),
sa.Column("workspace_domain", sa.String(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("workspace_id"),
)

if not inspector.has_table(_DOCUMENTS_TABLE):
op.create_table(
_DOCUMENTS_TABLE,
sa.Column("document_id", sa.String(), nullable=False),
sa.Column("workspace_id", sa.String(), nullable=False),
sa.Column("organization_id", sa.String(), nullable=True),
sa.Column("document_name", sa.String(), nullable=False),
sa.Column("document_type", sa.String(), nullable=False),
sa.Column("document_content", sa.Text(), nullable=True),
sa.Column("document_status", sa.String(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["workspace_id"], [f"{_ENTITIES_TABLE}.workspace_id"]
),
sa.PrimaryKeyConstraint("document_id"),
)

for index_name, column_names in (
("ix_workspace_documents_workspace_id", ["workspace_id"]),
("ix_workspace_documents_organization_id", ["organization_id"]),
):
op.create_index(
index_name,
_DOCUMENTS_TABLE,
column_names,
if_not_exists=True,
)


def downgrade() -> None:
# This revision's upgrade is a no-op whenever the tables already exist
# (e.g. created by 0001's create_all), so a downgrade cannot tell "this
# revision created these tables" apart from "they predate it" -- and
# workspace_documents.document_content holds real uploaded content, not
# rebuildable derived state. Unconditionally dropping it risks destroying
# data this revision never created. As with 0001_initial_control_plane:
# production rollbacks should restore from backup or a later explicit
# down revision rather than dropping customer-owned data.
return None
Loading
Loading