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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Added
- **Connection state now has one explicit, epoch-fenced lifecycle** (#512
phase 1). OAuth/Basic credentials, single-flight token refresh, transport
success/failure, auth loss, reauthentication, and explicit sign-out flow
through `ConnectionSession`; stale refreshes cannot publish tokens or
lifecycle state into a newer login. The accessible header chip reacts to
that lifecycle (green connected, amber offline, red auth-required) instead
of inferring connectivity from the best-effort server-version probe. HTTP
query failures remain query errors and do not falsely mark the transport
offline.
- **A Dashboard row can create a blank Panel directly from the Dashboards
tree** (#515). Its new plus action opens the shared name/description dialog,
then atomically commits one empty panel-role query and one canonically laid
Expand Down
43 changes: 42 additions & 1 deletion build/check-boundaries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,49 @@ for (const rule of RULES) {
}
}

// Issue #512 Phase 1: connection readiness has one authority. These are the
// modules that own or project the lifecycle, and none may regain the retired
// server-version shortcut. `serverVersion` remains legitimate catalog/query
// capability metadata and user-menu display elsewhere.
const connectionAuthorityFiles = [
'src/core/connection-lifecycle.ts',
'src/application/connection-session.ts',
'src/net/ch-client.ts',
'src/ui/app-header.ts',
'src/ui/app-shell.ts',
];
for (const relFile of connectionAuthorityFiles) {
const file = path.join(repoRoot, relFile);
if (!fs.existsSync(file)) continue;
checkedFiles += 1;
if (/\bserverVersion\b/.test(fs.readFileSync(file, 'utf8'))) {
violations.push(`${relFile} → serverVersion (issue #512: lifecycle/readiness must not be inferred from version metadata)`);
}
}

// The DOM projection is exclusive too. The retired bridge lived in app.ts:
// catalog version metadata called back into the composition root, which then
// mutated the chip. Keep the handle declaration in app.types.ts, but reject
// every chip selector/projector reference outside its pure projector and
// header renderer. This catches that exact regression without banning
// legitimate server-version capability data or the user-menu version label.
const connectionProjectionOwners = new Set([
'src/core/connection-lifecycle.ts',
'src/ui/app-header.ts',
'src/ui/app.types.ts',
]);
const connectionProjectionPattern =
/\bconnStatus\b|conn-status|connection-(?:state|chip)|data-connection-state|\bconnectionLifecyclePresentation\b/;
for (const file of collectFiles(path.join(repoRoot, 'src'))) {
const relFile = path.relative(repoRoot, file).split(path.sep).join('/');
if (connectionProjectionOwners.has(relFile)) continue;
if (connectionProjectionPattern.test(fs.readFileSync(file, 'utf8'))) {
violations.push(`${relFile} → connection-chip projection (issue #512: only app-header may render lifecycle state)`);
}
}

if (violations.length) {
console.error('check-boundaries: layer-boundary violations (issue #276):');
console.error('check-boundaries: architecture violations:');
for (const line of violations) console.error(` ${line}`);
process.exit(1);
}
Expand Down
17 changes: 15 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ module is tested with plain stubs at the per-file coverage gate.
| Module | Owns |
|---|---|
| `query-execution-service` (`app.exec`) | the shared request/stream/normalize read core + the script transport loop (retry classification, stop-on-first-failure, per-attempt `query_id`); stateless `kill(queryId)` — cancellation is caller-owned (`AbortController`s live with the owning session) |
| `connection-session` (`app.conn`) | auth + connection lifecycle: OAuth PKCE login/refresh, Basic probing, IdP config, identity, token storage, sign-out, and **the single live `chCtx` object** (mutated in place — `authConfirmed` by `net/ch-client`, `origin` by sign-in — never reconstructed) |
| `connection-session` (`app.conn`) | authoritative auth + connection lifecycle (`starting` / `connected` / `refreshing` / `offline` / `auth-required` / `reauthenticating` / `signed-out`), OAuth PKCE login/refresh, Basic probing, IdP config, identity, token storage, sign-out, and **the single live `chCtx` object** (mutated in place — `authConfirmed` by `net/ch-client`, `origin` by sign-in — never reconstructed) |
| `schema-catalog-service` (`app.catalog`) | server version, schema tree, lazy columns, SQL reference/completions, entity-doc cache, `invalidate()` |
| `workbench-parameter-session` (`app.params`) | `{name:Type}` analysis/prepare/gate policy, input-vs-execute hardening, enum inference, recent values; reads the live shared `AppState` slices through accessors |
| `export-service` (`app.exports`) | direct + script export behind an injectable `ExportSink` (`pickFile`/`pickDirectory`); hold-back exception inspection, `.partial` semantics, its own cancellation state |
Expand Down Expand Up @@ -92,6 +92,17 @@ invalidates the catalog before the login screen renders. There is no
route-remount mechanism — the dashboard is its own browser tab whose closure
JS never observes — so no teardown theater beyond that.

Connection lifecycle ownership follows the same rule. `ConnectionSession`
publishes one read-only signal and assigns a monotonically increasing
credential epoch whenever credentials are installed or invalidated. Refresh is
single-flight within an epoch; a late refresh or transport response cannot
write tokens, report auth loss, or repaint the replacement epoch.
`net/ch-client` reports only successful 2xx transport settlement as connected
and rejected, non-aborted `fetch` as offline. HTTP query failures — including a
post-confirmation 401/403 — remain query outcomes, not connection state. The
header chip is a pure projection of this lifecycle; `serverVersion` remains
display metadata and is never connection authority.

## The `App` object

`createApp(env)` still returns one `app` object, but it is now a composition
Expand All @@ -112,7 +123,9 @@ test seam the parameter session reads live).
Unchanged from the beginning and now applied uniformly: every side effect is
passed in, never imported — `createApp(env)` injects
`document/window/location/fetch/crypto/sessionStorage`, `ch-client` functions
take a `ctx = {fetch, origin, getToken, refresh, authHeader, onSignedOut}`,
take a `ctx = {fetch, origin, getToken, refresh, authHeader,
onSignedOut(detail?, expectedEpoch?), currentEpoch?, onTransportConnected?,
onTransportOffline?}`,
and every `create*Service(deps)` receives its transport/clock/uid/storage
explicitly. The suite needs no network/DOM mocking libraries — plain stubs
suffice, and coverage is genuine.
Expand Down
Loading