diff --git a/crates/omnigraph-cli/src/main.rs b/crates/omnigraph-cli/src/main.rs index 35740cc..260cfea 100644 --- a/crates/omnigraph-cli/src/main.rs +++ b/crates/omnigraph-cli/src/main.rs @@ -1036,13 +1036,24 @@ fn render_schema_plan_step(step: &SchemaMigrationStep) -> String { render_annotations(annotations) ), SchemaMigrationStep::UnsupportedChange { - entity, - reason, - code, - } => match code { - Some(c) => format!("unsupported change on {} [{}]: {}", entity, c, reason), - None => format!("unsupported change on {}: {}", entity, reason), - }, + entity, reason, .. + } => { + // When a schema-lint code is attached, render code + tier + // so operators see at-a-glance the kind of risk (destructive + // / validated / safe) — not just the rule identifier. + // Reach the diagnostic via the `diagnostic()` helper so the + // CLI doesn't need to know how the lookup works. + match step.diagnostic() { + Some(diag) => format!( + "unsupported change on {} [{}, {}]: {}", + entity, + diag.code, + schema_lint_tier_label(diag.tier), + reason, + ), + None => format!("unsupported change on {}: {}", entity, reason), + } + } } } @@ -1054,6 +1065,14 @@ fn schema_type_kind_label(kind: omnigraph_compiler::SchemaTypeKind) -> &'static } } +fn schema_lint_tier_label(tier: omnigraph_compiler::SafetyTier) -> &'static str { + match tier { + omnigraph_compiler::SafetyTier::Safe => "safe", + omnigraph_compiler::SafetyTier::Validated => "validated", + omnigraph_compiler::SafetyTier::Destructive => "destructive", + } +} + fn render_prop_type(prop_type: &omnigraph_compiler::PropType) -> String { let base = if let Some(values) = &prop_type.enum_values { format!("Enum({})", values.join("|")) diff --git a/crates/omnigraph-compiler/src/catalog/schema_plan.rs b/crates/omnigraph-compiler/src/catalog/schema_plan.rs index 835a1be..a06a036 100644 --- a/crates/omnigraph-compiler/src/catalog/schema_plan.rs +++ b/crates/omnigraph-compiler/src/catalog/schema_plan.rs @@ -93,6 +93,24 @@ impl SchemaMigrationStep { _ => None, } } + + /// If this step carries a schema-lint code, return the full + /// catalog entry — including family, safety tier, and default + /// severity. Used by renderers that want to display richer + /// context than just the code string (e.g. `omnigraph schema + /// plan` annotating each line with its tier). + /// + /// Returns `None` for steps that carry no code (the 12 of 17 + /// `UnsupportedChange` paths still untagged in v0, plus every + /// non-`UnsupportedChange` variant). + pub fn diagnostic(&self) -> Option<&'static crate::lint::DiagnosticCode> { + match self { + Self::UnsupportedChange { + code: Some(c), .. + } => crate::lint::lookup(c), + _ => None, + } + } } pub fn plan_schema_migration( diff --git a/docs/schema-lint-v1-plan.md b/docs/schema-lint-v1-plan.md new file mode 100644 index 0000000..c224e82 --- /dev/null +++ b/docs/schema-lint-v1-plan.md @@ -0,0 +1,78 @@ +# Schema-lint chassis v1 — implementation plan + +Work-in-progress checklist for the next slice of the chassis. v0 (the code-tagged diagnostics layer, MR-694 first PR #87) shipped on `main`. v1 brings the chassis to **enforced behavior**: `--allow-data-loss` flag, the `Soft | Hard` mode dimension on drops, and the first real "destructive but supported" migration step. + +This document tracks scope so the PR can land in incremental commits without losing the thread. Delete after the work is merged. + +## Done in this branch so far + +- [x] `SchemaMigrationStep::diagnostic()` helper — returns the full `DiagnosticCode` (family + tier + severity) for `UnsupportedChange` steps with codes. +- [x] CLI `omnigraph schema plan` output displays tier alongside the code: `unsupported change on node:Person.age [OG-DS-104, destructive]: ...`. +- [x] No behavior change. All 11 existing `schema_apply` tests still pass. + +## Next commits (in order) + +### Commit 2 — `Soft` / `Hard` mode enum on drop steps + +- [ ] Add `DropMode { Soft, Hard }` enum in `schema_plan.rs`. Tier follows the mode: `Soft → safe` (catalog tombstone, no data touched), `Hard → destructive` (Lance-level removal). +- [ ] Add new `SchemaMigrationStep` variants: + - `DropProperty { type_kind, type_name, property_name, mode }` + - `DropType { type_kind, name, mode }` +- [ ] Existing `UnsupportedChange` emission paths for property/type removal keep emitting `UnsupportedChange` until the planner is wired in commit 3. New variants are dormant. +- [ ] Unit test: the variants serialize/deserialize cleanly (SchemaIR round-trip). + +### Commit 3 — Tombstone fields on catalog IR + +- [ ] Add `tombstoned: bool` (default `false`, `#[serde(default)]` for backward compat) to `NodeIR`, `EdgeIR`, `PropertyIR`. Same on the `Catalog` types if needed. +- [ ] Readers (`build_catalog`, `build_catalog_from_ir`) propagate the flag. +- [ ] Manifest read path filters tombstoned items from the query-visible surface — they exist on disk, not in `Catalog`'s public iteration. +- [ ] Unit tests: a tombstoned property is invisible to `node_type.properties.get(...)`; the underlying Arrow column survives the rewrite. + +### Commit 4 — Planner emits `DropProperty { Soft }` by default + +- [ ] In `plan_properties`'s leftover-property branch: emit `DropProperty { Soft }` instead of `UnsupportedChange` for OG-DS-104. +- [ ] Same shape for node-type removal (`plan_nodes` leftover branch → `DropType { Soft }`, OG-DS-102) and edge-type removal (`plan_edges` leftover → `DropType { Soft }`, OG-DS-103). +- [ ] CLI plan output: render the new variants with mode visible. + +### Commit 5 — Apply path implements `Soft` mode + +- [ ] `apply_schema_with_lock` handles `DropProperty { Soft }`: writes `tombstoned: true` into the catalog/IR, no data manipulation. +- [ ] Same for `DropType { Soft }`. +- [ ] Recovery sidecar discipline: register a `RecoveryKind::CatalogOnly` sidecar entry; soft drops are roll-forward-safe. +- [ ] Integration test (extends `tests/schema_apply.rs`): remove a property, assert apply succeeds, row count preserved, schema query no longer surfaces the property. + +### Commit 6 — Convert PR #62 destructive-rejection tests + +- [ ] `tests/schema_apply.rs`'s 6 PR #62 tests currently assert "removing X fails with `OG-DS-XXX`". Convert each to: + - **Without `--allow-data-loss`**: soft drop succeeds (catalog tombstone, rows preserved). + - **With `--allow-data-loss`**: hard drop succeeds (rows deleted) — covered by commit 7. +- [ ] Add new tests for the supported soft path (verify tombstone state). + +### Commit 7 — `--allow-data-loss` CLI flag + `Hard` mode + +- [ ] Add `--allow-data-loss` boolean flag to `omnigraph schema apply` and `omnigraph schema plan` (plan shows what would happen if applied with the flag). +- [ ] Thread through to `apply_schema_with_lock(.., allow_data_loss: bool)`. +- [ ] Planner: when `--allow-data-loss` is set, emit `Hard` mode instead of `Soft` for drop paths. +- [ ] Apply path for `Hard` mode: `stage_overwrite` removing the column (DropProperty) or `Dataset::restore`-style table drop (DropType). Full recovery-sidecar discipline (`RecoveryKind::FullRewrite`). +- [ ] Integration tests: hard drop deletes data; without flag, hard drop is impossible (planner only emits soft). + +### Commit 8 — Tombstone unhide / restore command (optional) + +- [ ] `omnigraph schema unhide ` reverses a soft drop within a grace period. +- [ ] Tests: tombstone → unhide → property is queryable again with rows intact. +- [ ] Defer if scope creeps; can ship in a follow-up PR. + +## Open questions + +- **Tombstone visibility in `omnigraph schema show`**: should the CLI list tombstoned items by default (with a flag indicator), or hide them by default? Recommend: hide by default; `--include-tombstoned` reveals. +- **Query-level enforcement on tombstoned types**: should a `match { $p: TombstonedType }` query fail at parse time, lint time, or runtime? Recommend: lint warning at parse time (new code in the QL family); runtime falls back to empty result. +- **Hard-drop semantics for `DropType`**: drop the Lance dataset entirely vs. retain on disk for forensics. Recommend: dataset deletion via Lance API (the `--allow-data-loss` flag justifies it; operators can take a snapshot first via `branch_create` if they want forensics). + +## Not in v1 scope (deferred) + +- Severity config in `omnigraph.yaml` (per-rule `error` / `warn` / `force`). +- `@allow(OG-XXX-NNN, "rationale")` suppression directives. +- Pre-migration checks (MR-941). +- CD / VE / LK / NM family rules (MR-942..MR-945). +- CI integration (MR-946). +- The remaining 12 of 17 `UnsupportedChange` paths still untagged with codes (interface removal, edge endpoint change, edge cardinality change, etc.). Each goes through its own MR-XXX issue.