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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.6] - 2026-08-23

Correctness release: `check --checks {validate,semantic,grade,pii,safety,policy}` and the `altimate-core-*` tools (`migration`, `compare`, `track-lineage`, `query-pii`, `classify-pii`) now tell you the truth. Several previously returned false-clean or wrong output — teams gating CI on `check --fail-on error|warning` may see new failures on unchanged SQL. **These are real findings the tool previously missed, not regressions in your code.** Also fixes a data-hygiene bug where truncated tool-output files were being deleted the moment they were written.

### Changed

- **`check --checks *` now surfaces the findings it was missing — expect newly-failing CI runs.** `altimate-core` upgrade `0.5.1 → 0.7.0` plus a consumer-contract sync catches a class of latent shape-mismatch bugs where the CLI was reading fields the engine no longer emits and rendering false-clean output as a result. Concretely: (a) `check --checks validate` gated on the wrong field and passed every file, now maps `ValidationError.location` and fails closed on engine failure; (b) `check --checks semantic` treated `valid:true` as clean, but `valid` means "plannable" — cartesian products came back "OK" for months, now reads `findings` and never gates on `valid`; (c) `check --checks grade` read fields `evaluate()` doesn't return, so no grade or finding ever surfaced, now reads `overall_grade`/`scores.overall`/`lint.findings` (with nested findings from `validation.errors` + `safety.threats`); (d) `check --checks pii` mapped the column name to the numeric column-position field and printed `[object Object]` for `{ Custom: string }` classifications, now reports the exposing alias and stringifies properly; (e) `check --checks policy` titled on `pass` (engine returns `allowed`), so clean SQL always rendered "VIOLATIONS FOUND", now inverted; (f) `check --checks safety` maps engine `high → error` and `medium → warning` (previously both degraded to `info`), so `--fail-on error|warning` no longer silently passes high-risk SQL injection threats; a new `unbalanced_quote` safety rule ships from the engine. Findings' `rule` field (when set — `lint` findings may omit it if the engine didn't attach one) names the rule in `--format json` output — that's how the safety and policy catalog is discovered in practice. (#1090)
- **`altimate-core-migration` tool no longer renders "SAFE" for destructive migrations.** Previously read a non-existent `risks` field; every migration including `DROP COLUMN` came back safe. Now reads `findings`/`safe`/`overall_risk` (engine `MigrationResult`); never renders SAFE on engine error; counts only non-"safe" findings as risks. (#1090)
- **`altimate-core-compare` tool no longer says "IDENTICAL" for different queries.** Read a non-existent `differences` field; different SQL always compared equal. Now reads the engine's `identical`/`diff_count`/`diffs` shape with an error-gated title. (#1090)
- **`altimate-core-track-lineage` no longer returns "0 edges" for everything.** The tool read `edges` off the top-level result; the engine has always emitted them at `queries[].edges`, so lineage tracking silently produced nothing. Now collects correctly, renders `impact_map`, formats `{table, column}` refs, and renders ERROR instead of "0 edges" when the engine call itself fails. (#1090)
- **`schema.detect_pii` returned zero findings for every scan.** Same shape-mismatch class: the detector read `piiData.findings`, but the engine's `PiiReport` is `{ columns, pii_count, … }`. Both cache and live paths now filter `classification !== "None"` through the shared `piiColumnsFromReport` helper. Malformed `PiiReport` shapes now throw rather than silently yield zero findings. Failed per-column classifications flip `success` to `false` — the schema-detect-pii tool renders "PII Scan: ERROR" with any partial findings attached, instead of a clean "no findings" verdict. (#1090)
- **`altimate-core-{compare,column-lineage,extract-metadata,import-ddl}` no longer crash on the default invocation.** Empty-string dialects now coerce to `undefined` before hitting `Schema.fromDdl`, centralized as `dialectHint()` in `native/engine-coerce.ts` and applied across all 7 dialect-forwarding handlers. (#1090)

### Fixed

- **Truncated tool-output files were being deleted the moment they were written (since 2026-08-14).** `Identifier.create` packs `timestamp * 4096 + counter` into 6 bytes and wraps every ~795 days; the 26th wrap landed 2026-08-14T11:19:55Z. Both truncation cleanups (`tool/truncate.ts` Effect service and `tool/truncation.ts` legacy module — used by bootstrap, bash, prompt) computed a pre-wrap cutoff astronomically larger than every post-wrap file's decoded timestamp, so every truncated tool output written after Aug 14 was garbage-collected on the next cleanup pass. Both cleanups now age files by `mtime` (which doesn't wrap); `stat` failures keep the file (deletion fails safe, so a dangling symlink or transient FS error can't wipe real cached truncations). The Effect-service cleanup now `stat`s through the injected `FSUtil.Service` for consistency with every other operation. Tagged `upstream_fix` — the wrap-prone encoding is upstream OpenCode code. (#1113, closes #1112)
- **Native bridge no longer poisons itself for the process lifetime on a transient NAPI load failure.** The lazy registration hook was nulled *before* the `await fn()` that loads all bridge handler modules; if the altimate-core NAPI binding failed to load mid-sequence (older glibc, unusual arch, permissions), every subsequent `Dispatcher.call` — not just the failing one — threw `No native handler for X` for the rest of the process, and the CLI had to be restarted. Registration is now cached as an in-flight promise: concurrent callers share one attempt, and on failure the cached promise is cleared so subsequent calls can retry. (v0.9.6 review)

## [0.9.5] - 2026-08-10

Windows `grep` back for the ~16% of Windows users it silently broke since v0.9.2, plus a mid-session YOLO toggle and a welcome panel that stops eating half of narrow terminals.
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/usage/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ altimate-code check --fail-on warning
| `semantic` | Semantic validation (cartesian products, wrong JOINs) | Optional | No |
| `grade` | SQL quality grading with recommendations | Optional | No |

Findings usually carry a machine-readable `rule` field (e.g. `unbalanced_quote`, `sql_injection`, `L003`) visible in `--format json` output — see the [Finding Object](#finding-object) schema below. `safety` and `policy` findings always include one; `lint` findings may omit both `rule` and `code` if the engine didn't attach them, so switch on presence rather than assuming.

By default, `lint` and `safety` are enabled. Override with `--checks`:

```bash
Expand Down
65 changes: 61 additions & 4 deletions packages/opencode/src/altimate/native/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,84 @@ export function register(method: BridgeMethod, handler: NativeHandler): void {
/** Lazy registration hook — set by native/index.ts */
let _ensureRegistered: (() => Promise<void>) | null = null

/** In-flight registration promise (deduped across concurrent callers). */
let _registrationPromise: Promise<void> | null = null

/** Generation counter — bumped whenever the hook or in-flight promise is
* replaced. An in-flight attempt captures its generation at start; if the
* counter advanced by the time its settle handler fires, another caller
* (reset / setRegistrationHook / a distinct new attempt after failure)
* has already installed replacement state, and the stale attempt must NOT
* mutate it. Prevents a stale success from clobbering a replacement hook,
* and a stale failure from clobbering a newer in-flight promise. */
let _registrationGeneration = 0

/** Clear all registered handlers and lazy registration hook (for test isolation). */
export function reset(): void {
nativeHandlers.clear()
_ensureRegistered = null
_registrationPromise = null
_registrationGeneration++
}

/** Called by native/index.ts to set the lazy registration function. */
export function setRegistrationHook(fn: () => Promise<void>): void {
_ensureRegistered = fn
_registrationPromise = null
_registrationGeneration++
}

/** Dispatch a method call to the registered native handler. */
export async function call<M extends BridgeMethod>(
method: M,
params: (typeof BridgeMethods)[M] extends { params: infer P } ? P : never,
): Promise<(typeof BridgeMethods)[M] extends { result: infer R } ? R : never> {
// Lazy registration: load all handler modules on first call
// Lazy registration: load all handler modules on first call. Cache the
// in-flight promise so concurrent callers share one attempt; on failure
// clear the cached promise so a subsequent call can retry. Previously
// ``_ensureRegistered`` was nulled BEFORE the await, so a transient NAPI
// load failure poisoned the bridge for the process lifetime — every
// subsequent ``call`` threw ``No native handler for X`` with no way to
// recover without restarting the CLI. Generation guard prevents a stale
// attempt from mutating state a concurrent ``reset()``/``setRegistrationHook()``
// has since replaced. (coderabbit round 1 — release/v0.9.6 review.)
// Concurrency contract:
// • ``reset()`` / ``setRegistrationHook()`` MAY be called while an
// older ``Dispatcher.call`` is in flight — the generation guard below
// blocks the stale attempt's ``.then`` handler from mutating shared
// state (``_ensureRegistered`` / ``_registrationPromise``) that the
// replacement installed. Adversarial tests below exercise both races.
// • What we DO NOT guarantee: if the stale hook body itself resumes
// after replacement and calls ``Dispatcher.register(...)`` late,
// that late write overwrites whatever the newer hook wrote — and
// no in-band signal lets us self-heal it without recreating the
// shared-state race the round-1 guard is meant to prevent (see the
// coderabbit + cubic round-2 exchange on release/v0.9.6). Callers
// that need late-write safety must serialise hook mutations
// against outstanding calls themselves.
// • Production never triggers late-write clobber: ``setRegistrationHook``
// is called exactly once at startup by ``native/index.ts``, and
// ``reset()`` is test-only.
if (_ensureRegistered) {
const fn = _ensureRegistered
_ensureRegistered = null
await fn()
if (!_registrationPromise) {
const fn = _ensureRegistered
const generation = ++_registrationGeneration
_registrationPromise = fn().then(
() => {
// Only clear _ensureRegistered if our generation is still current
// — otherwise a concurrent reset()/setRegistrationHook() already
// installed a replacement, and clearing would clobber it.
if (generation === _registrationGeneration) _ensureRegistered = null
},
(err) => {
// Same guard on the failure path: don't null a newer in-flight
// promise from another attempt.
if (generation === _registrationGeneration) _registrationPromise = null
throw err
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
)
}
await _registrationPromise
}

const native = nativeHandlers.get(method as string)
Expand Down
Loading
Loading