Skip to content
Draft
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@
WBS-estimation readiness analysis.
- `wbs.json`: seed data in the user-specified JSON array format.

## SaaS bounded contexts and persistence

- **Tenant and Access** owns workspaces, memberships, authentication, and RBAC.
- **Project Planning** owns projects, revisions, baselines, comments, sprints,
attachments, shares, and schedule-control state.
- **Audit Trail** owns append-only enterprise compliance evidence. Its durable
SQLite relation is `audit_events`, with semantic persistence vocabulary such
as `audit_event_id`, `audit_action`, and `audit_metadata_json`; the principal
tenant query index is `audit_events_org_event_idx` on
`(org_id, audit_event_id)`.
- **Integration** owns webhooks, Clearfolio attachment conversion, and
contextual-orchestrator briefing boundaries.

The Audit Trail HTTP/CSV/export compatibility surface predates the persistence
rename and continues to expose wire fields such as `id`, `action`, and `meta`.
Those generic external names are isolated in explicit SQL aliases at the web
adapter boundary. Production writes and durable reads use the semantic
`audit_events` vocabulary directly.

Startup migration treats a historical durable `audit_log` table as a legacy
compatibility source only. The migration creates the semantic authority first,
validates that legacy and semantic column sets are not ambiguous, refuses to
merge two populated authorities, copies legacy rows under `BEGIN IMMEDIATE`,
drops the old table/indexes in the same transaction, and rolls back on failure.
The relation remains append-only and normalized as one audit event per row;
foreign-key semantics, the org-scoped hot read path, UPSERT behavior (none),
and runtime read/write topology are otherwise unchanged.

## CI and security structure

- `.github/workflows/pages.yml`: GitHub Pages deployment workflow for the
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Migrated Audit Trail persistence from the generic durable
`audit_log.id/action/meta` vocabulary to
`audit_events.audit_event_id/audit_action/audit_metadata_json` and the
`audit_events_org_event_idx` tenant-read index. Existing audit HTTP, CSV, and
workspace-export wire fields remain compatible through explicit web-adapter
aliases, while startup migration copies and removes legacy persistence in one
fail-closed transaction.
- Switched the repository-local OpenCode development configuration from GitHub
Models to an NVIDIA NIM-only candidate set while preserving organization-level
review-workflow ownership in `ContextualWisdomLab/.github`.
Expand Down
69 changes: 69 additions & 0 deletions docs/doctoring/audit-persistence-semantic-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Audit Trail persistence semantic naming

## Decision

ScopeWeave owns the Audit Trail persistence vocabulary. Durable SQLite names
therefore describe the bounded-context meaning rather than relying on generic
single-word identifiers.

| Legacy durable name | Semantic durable name |
| --- | --- |
| `audit_log` | `audit_events` |
| `id` | `audit_event_id` |
| `action` | `audit_action` |
| `meta` | `audit_metadata_json` |
| `idx_audit_org` | `audit_events_org_event_idx` |

The existing `/api/orgs/:id/audit` JSON contract, audit CSV columns, and
workspace-export audit records predate this persistence repair. Their historical
`id`, `action`, and `meta` fields remain compatibility surface names and are
produced only by explicit SQL aliases in `server/app.mjs`. Internal writes and
reads use `audit_events`, `audit_event_id`, `audit_action`, and
`audit_metadata_json` directly. No persistent compatibility view is retained.

## Bounded-context rationale

The Audit Trail is append-only compliance evidence scoped to an organization.
`audit_event_id` identifies one recorded audit event, `audit_action` records the
business/security action, and `audit_metadata_json` stores action-specific
structured metadata. `audit_events_org_event_idx(org_id, audit_event_id)` names
and serves the tenant-scoped reverse-event query used by the audit endpoint.
These terms are part of the Audit Trail ubiquitous language rather than generic
storage vocabulary.

## Migration and rollback safety

Startup creates the semantic `audit_events` authority, then inspects a historical
`main.audit_log` only as a legacy migration source. Migration:

1. verifies `audit_log` is a table and that its column set is either the original
`id/action/meta` shape or the short-lived semantic-column intermediate shape;
2. fails closed on mixed/ambiguous columns or when both old and new authorities
already contain data;
3. starts `BEGIN IMMEDIATE`, copies rows to `audit_events`, removes legacy audit
indexes and the legacy table, and commits as one transaction; and
4. rolls back the transaction and propagates the causal failure if any copy or
DDL operation fails.

The new relation preserves the existing `org_id` and nullable `user_id` foreign
keys, one-row-per-audit-event 3NF shape, and append-only behavior. There is no
Audit Trail UPSERT path. The migration adds no partitioning and no read/write
split; its stronger write lock exists only during startup migration. The hot
runtime read remains tenant-scoped by `(org_id, audit_event_id)`.

## Executable evidence

The repair branch was cut from protected
`develop@2c328875e00e86537df3e965170be80532571cad`. TDD introduced
`tests/unit/audit-log-database-naming.test.mjs` before the production repair.
The current test contract covers fresh semantic storage, absence of durable or
temporary `audit_log`, data-preserving migration from a realistic legacy store,
semantic index creation, and idempotent reopen without duplicate audit events.
`tests/api/smoke.mjs` additionally proves that the established HTTP
`id/action/meta` fields remain present while internal semantic persistence field
names do not leak to the wire contract. Source and compatibility repairs were
present together by `59e1e19eb1ba363efc789368b2d2217b6794b61d`; documentation
commits follow on the same ordinary, non-force PR history.

Fresh required GitHub verification must attach to the final unchanged PR head;
predecessor, base, or model-only evidence is not a merge signal.
113 changes: 113 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# ScopeWeave product and technical gap baseline

## Buyer outcome and product responsibility

ScopeWeave is the project-planning and WBS product boundary: teams create and
maintain project plans, collaborate under tenant-scoped access control, measure
schedule/cost progress, exchange planning artifacts, and retain enterprise audit
evidence. The repository owns the browser planner plus its SaaS API/persistence
implementation. Shared LLM policy remains delegated to
`ContextualWisdomLab/contextual-orchestrator`; Clearfolio document conversion is
an integration boundary rather than ScopeWeave-owned document rendering.

The current naming-repair priority is the **Audit Trail** persistence contract,
because compliance evidence is durable, tenant-scoped, security-sensitive, and
buyer-visible through audit/export APIs. A persistence rename therefore has
higher migration and compatibility risk than a local implementation-variable
rename.

## DDD context map and ubiquitous language

| Bounded context | Owned concepts | Important invariants |
| --- | --- | --- |
| Tenant and Access | workspace, membership, authenticated principal, access role | project/audit reads remain tenant scoped; owner/admin manage audit access |
| Project Planning | project plan, task hierarchy, revision, baseline, sprint | optimistic project versions remain linear; planning records stay within the owning workspace |
| Audit Trail | audit event, audit action, audit target, audit metadata | append-only evidence; every durable audit identifier is semantically specific; organization filtering is indexed |
| Integration | webhook delivery, attachment conversion, AI briefing | external/vendor schemas are translated at adapters and do not define internal ubiquitous language |

Audit Trail aggregate/repository language is `audit_event`, not generic
`record`/`item`/`event` in persistence code. The durable relation is
`audit_events`; its key is `audit_event_id`, business action is `audit_action`,
and structured detail is `audit_metadata_json`.

## Persistence model and migration boundary

Relevant ERD slice:

```text
users(user_id compatibility column today: id)
1 ─────< audit_events.user_id (nullable, ON DELETE SET NULL)

orgs(org_id compatibility column today: id)
1 ─────< audit_events.org_id (required, ON DELETE CASCADE)

audit_events
├─ audit_event_id PK
├─ org_id FK
├─ user_id FK nullable
├─ audit_action
├─ target_type
├─ target_id
├─ audit_metadata_json
└─ created_at

audit_events_org_event_idx(org_id, audit_event_id)
```

This PR intentionally repairs only the verified Audit Trail ownership slice; it
does not mechanically rename every legacy table in the repository. Historical
`audit_log(id, action, meta)` is accepted only as a startup migration source.
Migration uses `BEGIN IMMEDIATE`, refuses ambiguous column sets and dual
populated authorities, copies rows into `audit_events`, removes legacy audit
indexes/table atomically, and rolls back on failure. Foreign-key semantics and
3NF remain unchanged. Audit writes are append-only, so no UPSERT path changes.
The existing tenant hot path remains supported by
`audit_events_org_event_idx(org_id, audit_event_id)`; no partitioning or runtime
read/write split is introduced.

## Compatibility and naming-contract status

The established `/api/orgs/:id/audit` JSON payload, audit CSV, and workspace
export retain historical wire names such as `id`, `action`, and `meta`. Those
fields are compatibility surface names and are isolated at explicit SQL aliases
inside the HTTP/export adapter. Durable storage and production audit writes use
semantic multiword names directly. Tests reject leakage of
`audit_event_id`/`audit_action`/`audit_metadata_json` into the established HTTP
payload while also rejecting generic audit persistence columns.

Organization-wide naming is not declared complete for this repository. The
current repair is deliberately bounded to the audited high-leverage persistence
contract; other legacy persistence/API names require separate ownership and
migration evidence before change. Idiomatic multiword camelCase/PascalCase names
are not naming defects.

## Security, test, and operability baseline

- Protected development base inspected before the repair:
`develop@2c328875e00e86537df3e965170be80532571cad`.
- Required repository contexts observed at that base included `unit-and-api`,
`cloud-e2e`, JavaScript/TypeScript CodeQL, Python CodeQL, and `property fuzz`.
- The naming regression was introduced before production repair. Current unit
coverage verifies fresh semantic storage, absence of persistent/runtime
`audit_log`, realistic legacy data migration, row preservation, semantic index
creation, and idempotent reopen.
- API smoke coverage verifies the historical audit wire fields remain present,
semantic persistence names do not leak, and the CSV formula-injection fixture
writes through the semantic durable relation.
- The source + consumer compatibility repair was present together by
`59e1e19eb1ba363efc789368b2d2217b6794b61d`; subsequent commits update
architecture/change/doctoring evidence on the same non-force history.
- Final merge evidence must come from the unchanged current PR head with all live
required checks terminal-success and independent current approval. Base or
predecessor checks are not transferable.

## Current gaps and next leverage order

1. Complete fresh exact-head verification and independent review for the Audit
Trail migration before merge.
2. Continue persistence-first naming audit in ScopeWeave, prioritizing durable
public/domain contracts over local variables. Each subsequent table or API
slice requires its own bounded-context rationale, migration/compatibility
plan, and executable consumer coverage.
3. After repository-local high-risk contracts are exhausted, return to shared
cross-repository schemas/libraries before lower-scope implementation names.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"coverage": "npm run test:coverage",
"server": "node server/server.mjs",
"test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs",
"test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs",
"test:unit": "node tests/unit/audit-log-database-naming.test.mjs && node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/toast-accessibility.test.mjs",
"test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases",
"test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api",
"test:e2e": "playwright test",
Expand Down
Loading
Loading