diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 81134b3..a0e40a4 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -30,7 +30,7 @@ The shortest path you know. The most useful ingredients, sanitized:
**Environment**
-- OLAF version (`__version__`, printed at load): e.g. 1.0.0
+- OLAF version (`__version__`, printed at load):
- Where it ran: Fabric notebook / pipeline / the pytest suite locally
- Python version, if running the suite locally
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2a0161b..348faaa 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -103,17 +103,22 @@ jobs:
shell: bash
run: |
set -euo pipefail
- archive_one="$RUNNER_TEMP/olaf-v1.0.0-one.tar"
- archive_two="$RUNNER_TEMP/olaf-v1.0.0-two.tar"
- git archive --format=tar --prefix=olaf-1.0.0/ --output="$archive_one" HEAD
- git archive --format=tar --prefix=olaf-1.0.0/ --output="$archive_two" HEAD
+ # The prefix is release evidence, so it must not name a version this tree may no
+ # longer be. One source of truth, and no inline script: a heredoc here sits at
+ # column 0 and silently ends the YAML block, which parses to zero jobs.
+ version="$(python scripts/print_version.py)"
+ prefix="olaf-$version/"
+ archive_one="$RUNNER_TEMP/olaf-v$version-one.tar"
+ archive_two="$RUNNER_TEMP/olaf-v$version-two.tar"
+ git archive --format=tar --prefix="$prefix" --output="$archive_one" HEAD
+ git archive --format=tar --prefix="$prefix" --output="$archive_two" HEAD
cmp --silent "$archive_one" "$archive_two"
sha256sum "$archive_one"
- python scripts/check_public_release.py archive "$archive_one" --tree HEAD --prefix olaf-1.0.0/
- source_tree="$RUNNER_TEMP/olaf-v1.0.0"
+ python scripts/check_public_release.py archive "$archive_one" --tree HEAD --prefix "$prefix"
+ source_tree="$RUNNER_TEMP/olaf-v$version"
mkdir "$source_tree"
tar --extract --file="$archive_one" --directory="$source_tree"
python scripts/check_secrets.py self-test
- python scripts/check_secrets.py tree "$source_tree/olaf-1.0.0"
+ python scripts/check_secrets.py tree "$source_tree/olaf-$version"
python scripts/check_secrets.py history .
python scripts/check_secrets.py all-objects .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bbd296..b94b84f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,55 @@
# Changelog
Notable changes to OLAF — OneLake Access Framework — are recorded here using
-[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) structure. Runtime
-version `1.0.0` maps to release tag `v1.0.0`.
+[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) structure. The runtime's
+`__version__` maps to the release tag `v{__version__}`.
## [Unreleased]
No changes yet.
+## [1.1.0] - 2026-08-27
+
+### Changed
+
+- Guardrail **G3** is no longer a runtime warning. The trap it named is real and unchanged —
+ `NOT IN` against a NULL is UNKNOWN, `WHERE` keeps only TRUE, and the row disappears from a
+ deny-list's result. **The direction is over-rejection: it removes rows a role should see and
+ cannot widen access to any row.** What went was the check, because it could not tell the two
+ apart. It was a substring test on the raw condition rather than the literal-stripping lexer
+ rules C9/C11/C13 share, so it warned on `status = 'CANNOT INVOICE'` — which contains no `NOT IN`
+ operator — and stayed silent on `region NOT IN ('a') AND type IS NULL`, where an unrelated
+ column's `IS NULL` muted it. Both directions wrong, on every run, for the life of a config.
+ It also named only one of the two valid mitigations, so a config that deliberately asserts the
+ column is never NULL could never come back clean.
+- The `Key invariants` entry in `docs/architecture.md` now carries the trap, both mitigations, and
+ the direction of failure. The starter workbook's own note said `Every NOT IN needs OR
IS
+ NULL`; it now names both mitigations too.
+
+### Removed
+
+- `RLS.null_safety_warning()`, the helper behind G3. It was listed in `docs/api/functions.md` as a
+ directly-callable helper, so this is a public-API removal. Shipped as MINOR rather than MAJOR as
+ a recorded deviation, not an oversight: nothing is distributed as a package — the runtime is a
+ notebook users copy — `SECURITY.md` supports only the latest Preview, and the helper had no
+ caller inside or outside the framework. The repo states no Preview exemption from SemVer, so
+ this note is the exemption.
+
+### Upgrade note
+
+- **The first `generate` after upgrading re-stamps the mapping, even on an unchanged config.** The
+ idempotent-skip fast path requires the stored `framework_version` to equal `__version__`, so on
+ the first run it does not match: `changed` is `True` rather than `False`, the status is `success`
+ rather than `skipped`, two log rows are written instead of one, and the mapping table is rewritten
+ with `framework_version` `1.1.0`. `config_hash` does not move — it hashes config rows only — and
+ the run after that skips again. A pipeline gated on `envelope["changed"]` will therefore re-plan
+ and re-apply once per deployment on upgrade. Nothing fails; both `success` and `skipped` exit
+ normally.
+- `mapping_hash` projects to the mapping columns and excludes provenance, so it is unchanged: a
+ saved-plan row stamped `1.0.0` still opens the apply gate, and the mapping-history CSV is reused
+ verbatim. That CSV's own `framework_version` column therefore still reads `1.0.0` while the
+ mapping table reads `1.1.0` — expected, and the reason the plan gate survives an upgrade at all.
+
## [1.0.0] - 2026-08-26
First public release, positioned as an independent community Preview for
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0f31604..b86265f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -147,7 +147,20 @@ build that has nothing wrong with it. Read its report; it will not block you.
## Versioning
`__version__` in `notebooks/olaf.ipynb` is stamped into `framework_version` on audit rows. Release
-tags use `v{__version__}` (for example, runtime `1.0.0` maps to tag `v1.0.0`). Put user-visible
+tags use `v{__version__}`, and the annotated tag object must be created with the **sanitized
+maintainer identity**, never a personal one. A tag object is public and immutable, and
+`scripts/check_public_release.py` has `tree` and `archive` modes only — nothing catches a bad
+tagger afterwards. The identity is deliberately absent from every tracked file (the gate reports
+`APPROVED_IDENTITY_CONTEXT` if it appears in one), so read it off the previous release tag rather
+than typing it:
+
+```
+git -c user.name="$(git for-each-ref --format='%(taggername)' refs/tags/vPREV)" \
+ -c user.email="$(git for-each-ref --format='%(taggeremail:trim)' refs/tags/vPREV)" \
+ tag -a vX.Y.Z -m 'release: OLAF vX.Y.Z'
+```
+
+Put user-visible
changes under `Unreleased` in `CHANGELOG.md`; release maintainers move them into a dated version
section as part of the release review. Do not bump a version or create a tag in an ordinary pull
request unless the pull request is explicitly the release change.
diff --git a/SECURITY.md b/SECURITY.md
index 16bbdd8..87813b1 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,7 +4,7 @@ OLAF changes authorization state and stores principal/access metadata. A defect
misconfiguration can grant unintended access, remove required access, or expose
control data. Treat this policy as part of the operating contract.
-> **Release status:** OLAF v1.0.0 is an independent community Preview for
+> **Release status:** OLAF is an independent community Preview for
> evaluation and development, not a production-ready security product. The bulk
> DAR mutation endpoint on which it depends is officially Preview:
> [Microsoft REST reference](https://learn.microsoft.com/en-us/rest/api/fabric/core/onelake-data-access-security/create-or-update-data-access-roles).
@@ -164,7 +164,7 @@ a platform guarantee:
The same-lakehouse design is not cryptographic or transactional isolation. If the
trusted-administrator and externally controlled sharing boundary is unacceptable,
-do not import real principal data or run sensitive modes in v1.0.0.
+do not import real principal data or run sensitive modes in this release.
## Operational recommendations
diff --git a/SUPPORT.md b/SUPPORT.md
index 801c137..7dd0de7 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -3,7 +3,7 @@
Thanks for using OLAF. Here is where to take each kind of question, and what to
honestly expect back.
-OLAF v1.0.0 is an independent community Preview for evaluation and development;
+OLAF is an independent community Preview for evaluation and development;
it is not a production support offering. Its mutating DAR endpoint is officially
Preview: [Microsoft REST reference](https://learn.microsoft.com/en-us/rest/api/fabric/core/onelake-data-access-security/create-or-update-data-access-roles).
diff --git a/configs/README.md b/configs/README.md
index 7710d34..fabe3ee 100644
--- a/configs/README.md
+++ b/configs/README.md
@@ -40,6 +40,6 @@ intent from that ID. Microsoft documents NotebookUtils token audiences separatel
OLAF's decision not to call Graph must not be read as a universal Fabric limitation:
[NotebookUtils credentials](https://learn.microsoft.com/en-us/fabric/data-engineering/notebookutils/notebookutils-credentials#get-token).
-Platform guidance in the workbook is versioned for OLAF v1.0.0 and points to the
+Platform guidance in the workbook is versioned for this OLAF release and points to the
maintained [platform contract](../docs/platform-contract.md). Microsoft's service
rules and limits may change; the official links remain authoritative.
diff --git a/configs/onelake_security.xlsx b/configs/onelake_security.xlsx
index b90040b..49c31f2 100644
Binary files a/configs/onelake_security.xlsx and b/configs/onelake_security.xlsx differ
diff --git a/docs/README.md b/docs/README.md
index e2c372b..ecfcac2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,7 +3,7 @@
A one-stop index of everything under `docs/`. Start with the "Start here" row if you're new to
the framework; the rest is reference material you'll come back to.
-OLAF v1.0.0 is an independent community Preview for evaluation and development.
+OLAF is an independent community Preview for evaluation and development.
Read the platform and control-data boundaries before using real principal data.
## Start here
diff --git a/docs/api/Audit.md b/docs/api/Audit.md
index 2b4da14..cdf7b69 100644
--- a/docs/api/Audit.md
+++ b/docs/api/Audit.md
@@ -2,7 +2,7 @@
Back to [API index](../api-reference.md) - [docs](../README.md).
-> OLAF v1.0.0 is a community Preview. Audit helpers summarize OLAF control records and the DAR
+> OLAF is a community Preview. Audit helpers summarize OLAF control records and the DAR
> responses they can read; they are not universal authorization proofs across every Fabric engine
> or access mode. Use the [platform contract](../platform-contract.md) when interpreting results.
@@ -185,7 +185,7 @@ Returns: dict | None -- `config_hash`, `config_version`, `framework_version`, `g
```python
trail.current_generation()
-# {"config_hash": "284ae40f8b47a294", "config_version": 42, "framework_version": "1.0.0",
+# {"config_hash": "284ae40f8b47a294", "config_version": 42, "framework_version": "1.1.0",
# "generated_at": "2026-07-11T12:00:00+00:00", "mapping_hash": "9f8e7d6c00000000", "mapping_version": 7}
```
diff --git a/docs/api/FabricClient.md b/docs/api/FabricClient.md
index b6f54a3..d278505 100644
--- a/docs/api/FabricClient.md
+++ b/docs/api/FabricClient.md
@@ -7,7 +7,7 @@ and submits OneLake data access roles and resolves a lakehouse display name. Kee
real identifiers, request bodies, and responses out of repository artifacts and public issues.
> **Preview boundary:** Microsoft labels the bulk create/update DAR endpoint **Preview**, for
-> evaluation and development, and not recommended for production use. OLAF v1.0.0 is therefore a
+> evaluation and development, and not recommended for production use. OLAF is therefore a
> community Preview. The official contract says the supplied roles are created or updated; it does
> not promise atomic full-set replacement or deletion of roles omitted from the body.
> [Official bulk endpoint](https://learn.microsoft.com/en-us/rest/api/fabric/core/onelake-data-access-security/create-or-update-data-access-roles).
diff --git a/docs/api/functions.md b/docs/api/functions.md
index 6abcb5b..9069523 100644
--- a/docs/api/functions.md
+++ b/docs/api/functions.md
@@ -8,7 +8,7 @@ beyond what's noted.
They are **static methods on classes**, not bare module-level functions -- call them as written
here. Most of those classes are pure namespaces (`Hash`, `Parse`, `ScopePath`, `Target`, `DAR`,
-`RLS`, `Catalog`). `OLAFError` is not: it is the framework's **exception base class**
+`Catalog`). `OLAFError` is not: it is the framework's **exception base class**
(`class OLAFError(Exception)`, the parent of `ValidationError`, `DARHTTPError` and `UsageError` --
see [errors.md](errors.md)) that additionally hosts the `classify` static method below.
@@ -34,7 +34,6 @@ see [errors.md](errors.md)) that additionally hosts the `classify` static method
| `Target.tenant(tenant_id=None)` | Explicit `tenant_id` wins; else best-effort auto-resolve from the runtime context; `None` if neither is available. |
| `Target.run_by(spark=None)` | Who is running this: runtime-context `userName` (an interactive user's UPN), else runtime-context `userId` (the running principal's Entra **object id** -- the layer that makes a service-principal / workspace-identity pipeline run attributable at all), else Spark `current_user()`, else `None`. Exception-safe. `Log` then labels a GUID-shaped result via [`Log.resolve_principal`](Log.md#resolve_principalspark-member_table-value) -- the id is never replaced. |
| `OLAFError.classify(exc)` | Map an exception to the audit `error_category` vocabulary: `http` \| `validation` \| `guard` \| `unexpected`. |
-| `RLS.null_safety_warning(rls_condition)` | Warn when an `rls_condition` has `NOT IN` without `OR IS NULL` (SQL three-valued-logic trap; guardrail G3). |
The rest of `notebooks/olaf.ipynb` -- the `generate`/`plan`/`apply` pipeline steps
(`Generate.rows`, `Catalog.canonical`, `DAR.diff`, `DAR.merge_upsert`/`DAR.merge_replace`,
diff --git a/docs/architecture.md b/docs/architecture.md
index 651bb54..9ba6fe7 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,6 +1,6 @@
# Architecture — OneLake Access Framework
-OLAF v1.0.0 is an independent community Preview for evaluation and development,
+OLAF is an independent community Preview for evaluation and development,
not a production-ready security product. It turns an authored role × scope × rule
matrix into reviewed Microsoft Fabric OneLake data access role (DAR) requests and
audit evidence from a Fabric notebook. Its bulk DAR mutation dependency is officially
@@ -597,7 +597,7 @@ the release does not publish an exact-SHA live service result for constant predi
The check asks whether a bareword appears outside string literals. It deliberately
avoids interpreting keywords as columns. The contract is limited to ASCII identifiers;
-non-ASCII predicate identifiers are outside OLAF v1.0.0's supported authoring surface.
+non-ASCII predicate identifiers are outside OLAF's supported authoring surface.
**Scope of that claim — ASCII only (known limitation).** The bareword test is `[A-Za-z_]`, so
"bareword" means *ASCII* bareword. This never widens access; it can only over-reject. Any non-ASCII
@@ -762,7 +762,7 @@ Two consequences worth stating plainly:
### Limits (fail at generate/plan, not at apply)
-OLAF v1.0.0 snapshots the following compatibility ceilings from Microsoft's
+OLAF snapshots the following compatibility ceilings from Microsoft's
official limitations reviewed on 2026-08-22. They are volatile service values; check
the current source before operation:
[OneLake security limitations](https://learn.microsoft.com/en-us/fabric/onelake/security/data-access-control-model#onelake-security-limitations).
@@ -790,6 +790,6 @@ records when generate ran. Full detail: data-model.md.
1. The plan/apply role build reads **only** the mapping lock-file — never the short config (TOCTOU closed).
2. New tables are absent from the saved mapping until the next generate. The resulting platform access still depends on workspace/item permissions and engine/access mode; OLAF does not infer that nobody can read them.
3. Glob (A2): 0-match on an include OR an exclude = error; table schema part is literal-only. Case resolves through the catalog. Role and predicate limits are OLAF compatibility guards tied to the cited current platform pages, not permanent no-workaround guarantees.
-4. G3: `NOT IN` without `OR IS NULL` = warning (three-valued logic drops NULL rows silently).
+4. `NOT IN` against a nullable column drops NULL rows silently — three-valued logic makes the term UNKNOWN for a NULL and `WHERE` keeps only TRUE. **This never widens access; it can only over-reject** — a row vanishes from a deny-list's result, so the failure is missing data, not exposure. Guard it with `OR IS NULL`, or assert the column is never NULL; do one of the two deliberately. OLAF warned about this per row (guardrail G3) until the release noted in the CHANGELOG and no longer does: the check was a substring test on the raw condition, so it fired on `status = 'CANNOT INVOICE'` (no `NOT IN` operator at all) and stayed silent on `region NOT IN ('a') AND type IS NULL` (a different column's `IS NULL` muted it). It could not be made sound without the literal-stripping lexer the other RLS rules share, and it named only one of the two mitigations above.
5. Cross-row rules (see the Rule catalog above) warn/block ambiguous multi-role policy shapes. Effective access is engine-explicit because SQL endpoint CLS differs from non-SQL CLS. C5 is a conservative guard around Microsoft's documented unsupported RLS/CLS combinations; it does not extrapolate untested behavior across tables or membership paths.
6. Log rows are single-valued — one row per (role × scope × member × action) step; lists never reach the log. Every log row also carries `config_hash`/`config_version`.
diff --git a/docs/config-examples.md b/docs/config-examples.md
index aa2ac44..6c390b0 100644
--- a/docs/config-examples.md
+++ b/docs/config-examples.md
@@ -1,6 +1,6 @@
# Config cookbook — worked config → mapping examples
-All values here are synthetic. OLAF v1.0.0 is an independent community Preview;
+All values here are synthetic. OLAF is an independent community Preview;
these examples describe OLAF's authored/mapping behavior, not proof of service
enforcement. Platform rules and limits remain governed by the
[official platform contract](platform-contract.md).
@@ -32,6 +32,13 @@ platform impossibility claim. NotebookUtils documents its current audience surfa
ceiling, rule C6) — every `role_name` used below already fits; see
[architecture.md](architecture.md#c12) for the full format rules.
+**`NOT IN` and NULL — nothing checks this for you.** A deny-list predicate silently drops rows whose
+column is NULL: three-valued logic makes `col NOT IN (...)` UNKNOWN for a NULL, and `WHERE` keeps
+only TRUE. The direction is over-rejection — a role sees fewer rows than intended, never more — so
+it is a data-completeness bug, not an exposure one, and no rule catches it. Either write
+`col NOT IN (...) OR col IS NULL`, or satisfy yourself the column is never NULL. Decide which;
+do not leave it to chance. See [architecture.md](architecture.md#key-invariants).
+
**Autotrim:** every cell shown below is stripped of leading/trailing whitespace before `generate`/
`validate` reads it (`Parse.trim_row`), so a stray space from a copy-paste never trips a validation
error. Whitespace *inside* an `rls_condition` string literal (e.g. `Region = 'TH '`) is preserved —
@@ -364,7 +371,7 @@ aggregates every error above (catalog validation **plus** the No-Graph member ga
lakehouse target guard E14, the cross-role RLS×CLS guard E15, and the column-case guard E16) and
rejects once with the full list, so you fix them in a single pass.
-The numeric platform-limit example is an OLAF v1.0.0 compatibility snapshot reviewed
+The numeric platform-limit example is an OLAF compatibility snapshot reviewed
on 2026-08-22, not a permanent service guarantee. Verify Microsoft's current
[OneLake security limitations](https://learn.microsoft.com/en-us/fabric/onelake/security/data-access-control-model#onelake-security-limitations)
before operation. Folder and permission behavior likewise follows the current
diff --git a/docs/control-data-security.md b/docs/control-data-security.md
index 6c32d38..0ce85bc 100644
--- a/docs/control-data-security.md
+++ b/docs/control-data-security.md
@@ -5,7 +5,7 @@ audit log, role backups, and review artifacts contain principal identifiers and
authorization or recovery state. The reserved boundary is the four configured
control-table paths plus the complete `/Files/security` subtree.
-OLAF v1.0.0 uses a fail-closed operating model for every sensitive write,
+OLAF uses a fail-closed operating model for every sensitive write,
including first setup, workbook import, generate, plan, apply, reset, rollback,
and backup creation. These modes are disabled by default until the technical
DAR check passes. The per-run operator attestation is recorded, never required.
@@ -79,7 +79,7 @@ Fabric REST. The operator-attestation model deliberately trusts authorized
administrators and external access controls for the unobservable interval.
Organizations that cannot accept that trusted-administrator boundary should not
-import real principal data or run sensitive modes in v1.0.0. Use a separately
+import real principal data or run sensitive modes in this release. Use a separately
secured control store or wait for a design that provides the required isolation.
## Bootstrap sequence
diff --git a/docs/data-model.md b/docs/data-model.md
index aaf3019..f90ff11 100644
--- a/docs/data-model.md
+++ b/docs/data-model.md
@@ -128,7 +128,7 @@ latest generation; history lives in `onelake_security_log`. This is the **only**
| `generated_at` | timestamp | — | UTC ISO-8601 | No | `2026-07-11T12:00:00+00:00` | `generate` | audit display; sufficient proxy for "when generate ran" (replaces the dropped `catalog_snapshot_at`) |
| `config_hash` | string | — | 16-char hex — `sha256(json.dumps(rows, sort_keys=True))[:16]` over the active rows **projected to `CONFIG_AUTHOR_COLUMNS`** (foreign columns on the physical table never enter the fingerprint) | No | `284ae40f8b47a294` | `generate` | `plan`/`apply` staleness guard (content-based — a no-op config rewrite does not invalidate a pending plan); generation-trace queries |
| `config_version` | bigint | — | Delta commit version of `onelake_security_config` at generate time; `null` if the config table isn't Delta or `DESCRIBE HISTORY` is unavailable | Yes | `42` | `generate` (`SELECT max(version) FROM (DESCRIBE HISTORY onelake_security_config)`) | generation-trace queries (`VERSION AS OF`), generation timeline, `show` config_version annotation |
-| `framework_version` | string | — | semver of the library (`__version__`) | No | `1.0.0` | `generate` | provenance display, compatibility checks |
+| `framework_version` | string | — | semver of the library (`__version__`) | No | `1.1.0` | `generate` | provenance display, compatibility checks |
23 columns. The `member_*_names` carry the human-facing effective set; the `member_*_ids` carry the
objectIds resolved from `onelake_security_member` (No-Graph; aligned 1:1) that `DAR.to_role`
@@ -178,7 +178,7 @@ string deliberately (see the physical note above).
| `tenant_id` | string | — | Entra **tenant GUID** of the run (blank on `setup`, which resolves no tenant) | Yes | `00000…` | every mode that logs | tenant-scoped audit queries |
| `mapping_hash` | string | — | 16-char content fingerprint of the mapping lock-file at run time | Yes | `9f8e7d6c…` | `generate`/`plan`/`apply` | the saved-plan gate (`apply` matches the `plan` record on `config_hash` **and** `mapping_hash`, binding the plan to the exact mapping generation it reviewed); mapping-generation trace (config → mapping → run) |
| `mapping_version` | bigint | — | Delta version of `onelake_security_mapping`, or null | Yes | `7` | `generate`/`plan`/`apply` | mapping-generation timeline |
-| `framework_version` | string | — | semver of the framework (`__version__`) that wrote the row — the **run-time** code version (may differ from the mapping's generate-time version) | No | `1.0.0` | every mode that logs | which code version ran — completes the config → mapping → code → run provenance chain |
+| `framework_version` | string | — | semver of the framework (`__version__`) that wrote the row — the **run-time** code version (may differ from the mapping's generate-time version) | No | `1.1.0` | every mode that logs | which code version ran — completes the config → mapping → code → run provenance chain |
27 columns. `member_name` (display name) and `member_id` (resolved objectId) ride together on every
grant-grain row; `show`'s enrichment joins the live DAR (which exposes objectIds) on `member_id`.
diff --git a/docs/fabric-import.md b/docs/fabric-import.md
index 2bd5842..bb68b81 100644
--- a/docs/fabric-import.md
+++ b/docs/fabric-import.md
@@ -112,7 +112,7 @@ Deployment pipelines are documented separately in
[Fabric deployment pipelines](https://learn.microsoft.com/en-us/fabric/cicd/deployment-pipelines/intro-to-deployment-pipelines).
Use local CI for deterministic tests. Treat any external smoke run as separately authorized,
-redacted evidence bound to the exact commit SHA. Do not promote OLAF v1.0.0 Preview as a
+redacted evidence bound to the exact commit SHA. Do not promote the OLAF Preview as a
production-ready security control.
## Which path when
diff --git a/docs/live-smoke-test.md b/docs/live-smoke-test.md
index d453e2f..3763912 100644
--- a/docs/live-smoke-test.md
+++ b/docs/live-smoke-test.md
@@ -1,6 +1,6 @@
# Optional live Fabric validation protocol
-The public v1.0.0 release has fixture-based CI evidence only. It does **not** claim
+The public release has fixture-based CI evidence only. It does **not** claim
that the release commit was run against a live Microsoft Fabric tenant.
This page defines the minimum record for a future, separately authorized live
diff --git a/docs/modes.md b/docs/modes.md
index 1ca7e54..0c2c15e 100644
--- a/docs/modes.md
+++ b/docs/modes.md
@@ -1,6 +1,6 @@
# Mode manual
-OLAF v1.0.0 is an independent community Preview for evaluation and development.
+OLAF is an independent community Preview for evaluation and development.
The bulk DAR mutation endpoint used by `apply`, `reset`, and rollback's apply leg is
officially Preview and is not a production contract:
[Microsoft REST reference](https://learn.microsoft.com/en-us/rest/api/fabric/core/onelake-data-access-security/create-or-update-data-access-roles).
diff --git a/docs/platform-contract.md b/docs/platform-contract.md
index 064d380..3f88441 100644
--- a/docs/platform-contract.md
+++ b/docs/platform-contract.md
@@ -8,7 +8,7 @@ platform with which OLAF is intended to interoperate; see Microsoft's
## Release status
-OLAF v1.0.0 is a **community Preview for evaluation and development**, not a
+OLAF is a **community Preview for evaluation and development**, not a
production-ready security product. Its mutating path uses the bulk Data Access
Roles (DAR) `PUT`, which Microsoft labels **Preview** and says is not recommended
for production use. Review that endpoint's current status before every deployment:
@@ -115,7 +115,7 @@ or remediated:
## Evidence status
-The public v1.0.0 release has automated fixture-based CI evidence. It does **not**
+The public release has automated fixture-based CI evidence. It does **not**
claim exact-release-SHA verification against a live Fabric tenant. Any future live
result must state the release SHA, Fabric Runtime, API date, target class, test scope,
and cleanup result, and must be described as a dated observation rather than a
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 34f7770..588cc47 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap
-OLAF v1.0.0 is a community Preview. Roadmap items are options, not promises or
+OLAF is a community Preview. Roadmap items are options, not promises or
release dates. They must not weaken the control-data boundary or turn an observed
service behavior into a Microsoft platform contract.
diff --git a/docs/runbook.md b/docs/runbook.md
index d2b4425..bbf1bf8 100644
--- a/docs/runbook.md
+++ b/docs/runbook.md
@@ -1,6 +1,6 @@
# Runbook — setup, config, and operations
-OLAF v1.0.0 is an independent community Preview for evaluation and development,
+OLAF is an independent community Preview for evaluation and development,
not a production-ready security product. Its mutating path depends on Microsoft's
Preview bulk DAR endpoint:
[Create or update data access roles](https://learn.microsoft.com/en-us/rest/api/fabric/core/onelake-data-access-security/create-or-update-data-access-roles).
@@ -37,6 +37,12 @@ Every shipped data row is synthetic and must be replaced. Keep the working copy
outside Fabric until the prerequisite access review is complete, then upload it only
under `Files/security`.
+Writing an `rls_condition` with `NOT IN`: it drops rows whose column is NULL, because
+three-valued logic makes the term UNKNOWN and `WHERE` keeps only TRUE. That over-rejects — the
+role sees fewer rows, never more — and no rule catches it. Add `OR IS NULL`, or satisfy
+yourself the column is never NULL. See
+[config-examples.md](config-examples.md) and [architecture.md](architecture.md#key-invariants).
+
The authored tables are:
- `config`: role name, target lakehouse label, table/folder includes and excludes,
@@ -173,7 +179,7 @@ Before a real apply/reset, OLAF records prepared intent and a backup pointer. If
operation becomes ambiguous, preserve the incident sentinel, prepared row, and backup.
Stop later sensitive modes; do not automatically restore over a concurrent change.
-OLAF v1.0.0 has **no supported public backup-replay method**. Do not call
+OLAF has **no supported public backup-replay method**. Do not call
`FabricClient.put_roles()` directly from a public workflow and do not treat a backup
pointer as a local file path. A low-level call bypasses the mandatory sentinel and
control-data gates, and no high-level guarded recovery method exists yet.
diff --git a/notebooks/README.md b/notebooks/README.md
index 12962be..ae6521e 100644
--- a/notebooks/README.md
+++ b/notebooks/README.md
@@ -1,6 +1,6 @@
# notebooks/
-**One self-contained runtime, plus three optional notebooks.** OLAF v1.0.0 is
+**One self-contained runtime, plus three optional notebooks.** OLAF is
an independent community Preview for evaluation and development. `olaf` is the only file you *need* —
it's the whole tool a user or pipeline runs. `olaf_master_workflow` is the recommended starting
point: the runtime driven end to end, one stage per cell. `olaf_runner` is the pipeline wrapper —
diff --git a/notebooks/olaf.ipynb b/notebooks/olaf.ipynb
index e30e050..cc0096b 100644
--- a/notebooks/olaf.ipynb
+++ b/notebooks/olaf.ipynb
@@ -24,7 +24,7 @@
"\n",
"Cells execute in order, so every name is defined before use — do not reorder the code cells.\n",
"\n",
- "Naming rule for maintainers: names must say what they do — internal design codes (G3, C1, …)\n",
+ "Naming rule for maintainers: names must say what they do — internal design codes (C1, B2, …)\n",
"appear only inside docstrings/messages as traceability references to docs/architecture.md."
]
},
@@ -144,7 +144,7 @@
"source": [
"## Constants — no Spark, no side effects; the CI extract-and-exec suite runs these directly.\n",
"\n",
- "Module constants (member-column maps, rule/limit thresholds, regexes) plus the smallest pure helpers: the config `config_hash` staleness fingerprint, the G3 NULL-safety warning, and the RLS predicate/column-reference extractors. Everything below is importable and runs anywhere."
+ "Module constants (member-column maps, rule/limit thresholds, regexes) plus the smallest pure helpers: the config `config_hash` staleness fingerprint and the RLS predicate/column-reference extractors. Everything below is importable and runs anywhere."
],
"id": "0f8356efc505"
},
@@ -155,7 +155,7 @@
"execution_count": null,
"outputs": [],
"source": [
- "__version__ = \"1.0.0\"\n",
+ "__version__ = \"1.1.0\"\n",
"\n",
"import re\n",
"import json\n",
@@ -1512,18 +1512,6 @@
" referenced-column extraction, unsupported-syntax/complexity checks (B2, C7, C9, C10).\"\"\"\n",
"\n",
" @staticmethod\n",
- " def null_safety_warning(rls_condition):\n",
- " \"\"\"'NOT IN' without 'OR IS NULL' is an OLAF SQL guardrail for null handling.\n",
- " Validate the observed result in the intended target engine/access mode.\"\"\"\n",
- " if rls_condition:\n",
- " u = rls_condition.upper()\n",
- " if \"NOT IN\" in u and \"IS NULL\" not in u:\n",
- " return (\n",
- " \"'NOT IN' without 'OR IS NULL' silently drops NULL rows (guardrail G3)\"\n",
- " )\n",
- " return None\n",
- "\n",
- " @staticmethod\n",
" def to_predicate(table, condition):\n",
" \"\"\"WHERE-only condition from config -> full predicate string the DAR API expects.\n",
" Verify once on your live API that the schema-qualified FROM is accepted.\"\"\"\n",
@@ -3092,9 +3080,6 @@
" perm = raw_perm # carried for display only — the error above blocks the config\n",
" rls = row.get(\"rls_condition\") or None\n",
" if rls:\n",
- " w = RLS.null_safety_warning(rls)\n",
- " if w:\n",
- " warnings.append(f\"{rid}: {w}\")\n",
" # B2 — RLS needs a table: an rls_condition with no include_tables is rejected\n",
" if not eff_tables:\n",
" errors.append(\n",
diff --git a/notebooks/olaf_cookbook.ipynb b/notebooks/olaf_cookbook.ipynb
index 865e03e..2f4f596 100644
--- a/notebooks/olaf_cookbook.ipynb
+++ b/notebooks/olaf_cookbook.ipynb
@@ -22,29 +22,13 @@
"> control tables, so they only execute inside a Fabric workspace with `olaf` imported\n",
"> and a lakehouse attached. The CI test + coverage harness targets `olaf.ipynb` only;\n",
"> this cookbook is never executed or measured. Values below (`priya@contoso.com`, `SalesReaders`,\n",
- "> `2026-07-15`, …) are illustrative placeholders. **The first cell is a guard** — running this notebook whole (Run All / a pipeline / `notebook.run`) exits immediately without touching anything; run the cells you want **one at a time**.\n",
+ "> `2026-07-15`, …) are illustrative placeholders. **The first code cell is a guard** — running this notebook whole (Run All / a pipeline / `notebook.run`) exits immediately without touching anything; run the cells you want **one at a time**.\n",
"\n",
"Every `OLAF.*` method returns a **Spark DataFrame** for an easy `display(...)` — a query method\n",
"returns its result table; an ops method returns a compact DataFrame *view* of the outcome envelope\n",
"(the raw dict stays at `OLAF.last_result`)."
]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "9540642a",
- "metadata": {},
- "outputs": [],
- "source": [
- "# Safety guard: Run All stops here. Every example below reaches a live lakehouse, so\n",
- "# they are meant to be read first and run one at a time, deliberately.\n",
- "import notebookutils\n",
- "\n",
- "notebookutils.notebook.exit(\n",
- " \"OLAF cookbook is examples-only; review and run individual cells deliberately\"\n",
- ")"
- ]
- },
{
"cell_type": "markdown",
"id": "toc",
diff --git a/scripts/print_version.py b/scripts/print_version.py
new file mode 100644
index 0000000..db70f4c
--- /dev/null
+++ b/scripts/print_version.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+"""Print the runtime's `__version__`, read from the notebook that defines it.
+
+The release-evidence archive is named after the version, and naming it in the workflow froze it:
+the v1.1.0 evidence would have been filed under `olaf-1.0.0`. Reading it here keeps one source of
+truth and keeps the workflow free of an inline script -- an earlier attempt embedded a heredoc in
+the `run:` block, whose body sat at column 0 and silently terminated the YAML literal, so GitHub
+could not parse the workflow and ran zero jobs while reporting only "no checks reported".
+"""
+
+import json
+import pathlib
+import re
+import sys
+
+NOTEBOOK = pathlib.Path(__file__).resolve().parent.parent / "notebooks" / "olaf.ipynb"
+
+
+def main() -> int:
+ cells = json.loads(NOTEBOOK.read_text(encoding="utf-8"))["cells"]
+ source = "\n".join("".join(cell.get("source", [])) for cell in cells)
+ found = re.search(r'__version__\s*=\s*"([^"]+)"', source)
+ if not found:
+ print(f"no __version__ in {NOTEBOOK}", file=sys.stderr)
+ return 1
+ print(found.group(1))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tests/olaf_test_smoke.ipynb b/tests/olaf_test_smoke.ipynb
index 2996d14..0b105ba 100644
--- a/tests/olaf_test_smoke.ipynb
+++ b/tests/olaf_test_smoke.ipynb
@@ -7,7 +7,7 @@
"source": [
"# OLAF external smoke protocol — no embedded results\n",
"\n",
- "> **Community Preview:** this notebook is an authorization-only protocol for an isolated non-production Microsoft Fabric environment. It contains no proof that OLAF v1.0.0 or any repository commit has been executed against Fabric. OLAF is an independent community project and is not affiliated with or endorsed by Microsoft.\n",
+ "> **Community Preview:** this notebook is an authorization-only protocol for an isolated non-production Microsoft Fabric environment. It contains no proof that OLAF or any repository commit has been executed against Fabric. OLAF is an independent community project and is not affiliated with or endorsed by Microsoft.\n",
"\n",
"Do not upload or execute this notebook without explicit authorization, an external-access review, synthetic fixtures, a recovery plan, and a result-redaction plan. See [docs/live-smoke-test.md](../docs/live-smoke-test.md).\n"
]
diff --git a/tests/test_notebook_contracts.py b/tests/test_notebook_contracts.py
index e068995..78a5db5 100644
--- a/tests/test_notebook_contracts.py
+++ b/tests/test_notebook_contracts.py
@@ -701,3 +701,34 @@ def test_no_shipped_file_still_names_the_retired_since_accessor():
assert not offenders, "retired `since` accessor/column still named in:\n " + "\n ".join(
offenders
)
+
+
+def test_the_cookbook_guard_still_stands_before_every_live_example():
+ """olaf_cookbook runs live operations against the attached lakehouse, and one cell is all that
+ stops a Run All from executing them for real.
+
+ It used to carry TWO guards — the 1.0.0 release added one and a later commit added another
+ without noticing it. The duplicate was removed, which is correct, but it also took the margin:
+ what was two independent stops is now one, and nothing pinned either of them. A cell reordered
+ above the survivor would un-guard a notebook whose own text says every cell below reaches a
+ live lakehouse, and every gate would stay green while it happened.
+
+ Pin the property, not the prose: exactly one guard, it is the FIRST code cell, and it precedes
+ the `%run` that loads the runtime.
+ """
+ cookbook = REPO_ROOT / "notebooks" / "olaf_cookbook.ipynb"
+ cells = code_cells(cookbook)
+ guards = [i for i, text in cells if "notebookutils.notebook.exit(" in text]
+ assert len(guards) == 1, f"expected exactly one Run-All guard, found {len(guards)} at {guards}"
+
+ first_code_index = cells[0][0]
+ assert guards[0] == first_code_index, (
+ f"the guard is at cell {guards[0]} but the first code cell is {first_code_index} — "
+ "a live example now runs before it"
+ )
+
+ # the magic itself, not a mention of it — the guard cell names %run in its own comment,
+ # and this repo already pins a %run cell to hold nothing else
+ runs = [i for i, text in cells if text.lstrip().startswith("%run")]
+ assert runs, "the cookbook no longer loads the runtime"
+ assert guards[0] < runs[0], "the guard must exit before %run loads anything"
diff --git a/tests/test_public_release.py b/tests/test_public_release.py
index f317542..9ad324d 100644
--- a/tests/test_public_release.py
+++ b/tests/test_public_release.py
@@ -694,3 +694,77 @@ def test_all_objects_hands_the_object_stream_to_an_interactive_container(tmp_pat
"the stdin-mode container must be interactive, or docker replaces its stdin with "
f"/dev/null and the scan reads nothing: {command}"
)
+
+
+def test_workflow_block_scalars_never_dedent_below_their_own_block():
+ """The other half of the same footgun, and it has now bitten this repo twice.
+
+ A `run: |` block ends at the first non-blank line indented LESS than the block's own content
+ indent. A heredoc written inside one looks natural at column 0 --
+
+ run: |
+ version="$(python - <<'PY'
+ import json
+ PY
+ )"
+
+ -- and YAML stops reading the block at `import json`, then tries that line as a mapping key.
+ GitHub rejects the whole file: zero jobs, no annotations, and `gh pr checks` says "no checks
+ reported on the branch", which reads like CI has not started rather than like CI is broken.
+ Branch protection then waits forever for checks that can never appear.
+
+ The sibling test above guards the `": "` inline-scalar shape, and its docstring records that
+ the same class of break once meant CI "had never run a single time, on any commit". It cannot
+ see this one: nothing on those lines is an inline scalar. Same outcome, different syntax.
+
+ pyyaml is deliberately not a test dependency (see the sibling), so this measures indentation
+ rather than parsing the document.
+ """
+ import re
+
+ block_open = re.compile(r"^(\s*)-?\s*(?:run|if|shell|env|with):\s*[|>][+-]?\s*$")
+ # a mapping key, or a list item — anything else at this indent is not YAML
+ yaml_key = re.compile(r"^\s*(?:-\s+)?(?:[A-Za-z_][\w.-]*|'[^']*'|\"[^\"]*\"):(?:\s|$)|^\s*-\s")
+ offenders = []
+ for path in sorted((REPO_ROOT / ".github").rglob("*.yml")):
+ lines = path.read_text(encoding="utf-8").splitlines()
+ relative = path.relative_to(REPO_ROOT).as_posix()
+ index = 0
+ while index < len(lines):
+ opened = block_open.match(lines[index])
+ if not opened:
+ index += 1
+ continue
+ key_indent = len(opened.group(1))
+ index += 1
+ # the block's content indent is set by its first non-blank line
+ while index < len(lines) and not lines[index].strip():
+ index += 1
+ if index >= len(lines):
+ break
+ content_indent = len(lines[index]) - len(lines[index].lstrip())
+ if content_indent <= key_indent:
+ continue # an empty block; the next key follows
+ while index < len(lines):
+ line = lines[index]
+ if not line.strip():
+ index += 1
+ continue
+ indent = len(line) - len(line.lstrip())
+ if indent >= content_indent:
+ index += 1
+ continue
+ # Dedented, so YAML has ended the block here. That is only legal if this line is
+ # the next mapping key or list item. Testing the indent instead is what a first
+ # draft of this guard did, and it passed on the very break it was written for:
+ # a heredoc body at column 0 is BELOW the block's key, which read as "closing an
+ # outer mapping" rather than as the killer it is.
+ if not yaml_key.match(line):
+ offenders.append(f"{relative}:{index + 1}: {line.strip()[:60]!r}")
+ break
+
+ assert not offenders, (
+ "a line inside a `run: |` block is indented below the block, which ends the block and "
+ "makes the workflow unparseable — GitHub then runs zero jobs and reports no checks:\n "
+ + "\n ".join(offenders)
+ )
diff --git a/tests/test_unit_lib.py b/tests/test_unit_lib.py
index 8fba65c..7d9f346 100644
--- a/tests/test_unit_lib.py
+++ b/tests/test_unit_lib.py
@@ -13,7 +13,6 @@
CONFIG_AUTHOR_COLUMNS,
DAR,
MAPPING_COLUMNS,
- RLS,
Catalog,
Generate,
Member,
@@ -74,11 +73,6 @@ def test_parse_table_entry_empty_part_rejected(entry):
Parse.table_entry(entry)
-@pytest.mark.parametrize("condition", [None, ""], ids=["None", "empty"])
-def test_null_safety_warning_empty_is_none(condition):
- assert RLS.null_safety_warning(condition) is None
-
-
# ---------------------------------------------------------------------------------------------
# LibPureLogicGaps — diff 'update' verdict
# ---------------------------------------------------------------------------------------------
@@ -193,7 +187,10 @@ def test_exclude_side_parse_error_recorded():
assert any("schema part must be literal" in e for e in errors)
-def test_rls_null_safety_warning_in_generate():
+def test_a_bare_not_in_no_longer_warns():
+ """G3 was a runtime warning until 1.1.0. It fired on every deny-list a config held, forever,
+ and the only mitigation it named was one of two valid ones — so it became noise that hid real
+ warnings. The trap it described is real and now lives in docs/architecture.md instead."""
_errors, warnings = generate_warnings(
[
make_row(
@@ -204,7 +201,7 @@ def test_rls_null_safety_warning_in_generate():
)
]
)
- assert any("guardrail G3" in w for w in warnings)
+ assert warnings == [], warnings
def test_rls_without_tables_b3():
diff --git a/tests/test_unit_validation.py b/tests/test_unit_validation.py
index c5f030b..dd9c4c7 100644
--- a/tests/test_unit_validation.py
+++ b/tests/test_unit_validation.py
@@ -1,4 +1,4 @@
-"""The documented error/warning cases E1-E12 plus the retained G3 / duplicate invariants and
+"""The documented error/warning cases E1-E12 plus the retained duplicate invariants and
the C4-C13 rule family, as pytest functions.
Ported from `olaf_test_unit.ipynb` class `ValidationRules`.
@@ -946,14 +946,6 @@ def test_b3_readwrite_cannot_carry_cls():
assert any("rule B3" in e for e in errors)
-def test_g3_not_in_without_is_null_warns():
- assert RLS.null_safety_warning("CategoryId NOT IN ('a')") is not None
-
-
-def test_g3_not_in_with_or_is_null_is_clean():
- assert RLS.null_safety_warning("CategoryId NOT IN ('a') OR CategoryId IS NULL") is None
-
-
def test_exact_duplicate_row_skipped():
row = make_row(role_name="R", include_tables="sales.orders", include_group_names="sg-analysts")
grants, errors, warnings, _ = Generate.rows([row, dict(row)], CANON)