diff --git a/CHANGELOG.md b/CHANGELOG.md index b519bc0..452622a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,40 @@ version `1.0.0` maps to release tag `v1.0.0`. ## [Unreleased] -No changes yet. +### Added + +- The five diagram pairs the release build used to hold back — apply semantics, destructive + utilities, mode lifecycle, pipeline, pipeline branching — and `scripts/gen-olaf-diagrams.py`, + which generates them. They were withheld because no published document referenced them, never + because a check objected: the release scanner reported no finding against any of them. +- `README.md` regained **What is OneLake security?**, **Why OLAF?** and a **Roadmap** checklist, + along with the Arctic Owl mascot. A second badge row states what CI proves and what OLAF runs on. +- A roadmap entry for a local authoring tool that would validate and build a config against a real + workspace from a developer machine, with the conditions that block it. +- The example notebooks are complete again: `olaf_cookbook` carries 33 code cells and + `olaf_master_workflow` the full stage-by-stage pipeline, instead of 3 each. + +### Changed + +- **This repository is now the only one.** Development happened in a private repository whose + history was withheld from an orphan-root release commit. Everything worth keeping is here, so the + subtraction machinery — an exclude list, a mirror-back guard against silently reverting anything + merged here first — has no remaining purpose and does not travel with it. +- The masthead art is renamed `olaf-banner-light.png` / `olaf-banner-dark.png`. It was + `olaf-lockup-*`, and overwriting those files left every cache serving the previous image from an + unchanged URL. A new name is the only thing that reliably invalidates it. +- The social preview carries the wordmark, the project name and the plan → review → apply sequence, + where it was previously the logo on a plain ground. + +### Fixed + +- `olaf_master_workflow` read `deleted` from the apply envelope, a key the runtime renamed to + `omitted_role_candidates`. It printed `deleted 0` on every run regardless of what the payload + actually left out. +- The same notebook sorted per-role verdicts on `{"delete": 0, ...}`. The runtime emits `omit` and + never `delete`, so omitted roles fell to the default rank and sorted last under a heading that + promised them first. +- `README.md` no longer pins a version number in prose; the release badge reads the tag. ## [1.0.0] - 2026-08-26 diff --git a/README.md b/README.md index 84a6656..d2d6c72 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- - OLAF — OneLake Access Framework. Arctic owl shield mark and wordmark. + + OLAF — OneLake Access Framework. Arctic owl shield mark and wordmark.

diff --git a/assets/brand/PROVENANCE.md b/assets/brand/PROVENANCE.md index 4c0feda..f8dd239 100644 --- a/assets/brand/PROVENANCE.md +++ b/assets/brand/PROVENANCE.md @@ -9,8 +9,8 @@ personal attribution or private working files. |---|---|---| | `olaf-logo.png` | `68f46d4054cce80b23f5c8255f5f0c2c306da116adbbe3c094ca0ec0d78efa28` | 512 × 512 px | | `olaf-mascot.png` | `3078c87bd9d9a74030fb73df034f17380d01387a8dde29edf6dca5aa19740f9c` | 512 × 512 px | -| `olaf-lockup-light.png` | `f005a1b558ae309a05aacf59097a57022d91f86c45f146469f68db213dc29325` | 1280 × 640 px | -| `olaf-lockup-dark.png` | `7ad09f4cf95564e35a5270050ae8dda75553dae1c8f8aa6b892679527cf6b94e` | 1280 × 640 px | +| `olaf-banner-light.png` | `f005a1b558ae309a05aacf59097a57022d91f86c45f146469f68db213dc29325` | 1280 × 640 px | +| `olaf-banner-dark.png` | `7ad09f4cf95564e35a5270050ae8dda75553dae1c8f8aa6b892679527cf6b94e` | 1280 × 640 px | | `olaf-social-preview.png` | `c5d1fec2a4925a5babad59920c0fd5b475778d288c6285efee45a6ac12ce0e0e` | 1280 × 640 px | ## Origin and transformations diff --git a/assets/brand/README.md b/assets/brand/README.md index 06bce3f..0a4b16e 100644 --- a/assets/brand/README.md +++ b/assets/brand/README.md @@ -6,7 +6,7 @@ Use only the assets in this directory for public OLAF surfaces. |---|---| | `olaf-logo.png` | Primary mark (arctic owl in an ice shield) — repository avatar, docs header, social avatar, badges ≥ 64 px | | `olaf-mascot.png` | Arctic Owl Sentinel mascot — README hero, launch and community artwork | -| `olaf-lockup-light.png` / `olaf-lockup-dark.png` | Horizontal lockup (mark + wordmark) — README masthead and page headers, per color scheme | +| `olaf-banner-light.png` / `olaf-banner-dark.png` | Horizontal banner (mark + wordmark) — README masthead and page headers, per color scheme | | `olaf-social-preview.png` | Privacy-safe 1200 × 630 repository social preview — OLAF mark only, no Microsoft logo, product icon, or personal avatar | The source of truth for usage, messaging, spacing, and legal separation is diff --git a/assets/brand/olaf-lockup-dark.png b/assets/brand/olaf-banner-dark.png similarity index 100% rename from assets/brand/olaf-lockup-dark.png rename to assets/brand/olaf-banner-dark.png diff --git a/assets/brand/olaf-lockup-light.png b/assets/brand/olaf-banner-light.png similarity index 100% rename from assets/brand/olaf-lockup-light.png rename to assets/brand/olaf-banner-light.png diff --git a/docs/assets/apply-semantics-dark.svg b/docs/assets/apply-semantics-dark.svg new file mode 100644 index 0000000..9aaeadb --- /dev/null +++ b/docs/assets/apply-semantics-dark.svg @@ -0,0 +1,47 @@ +What apply does to the live role set. Config declares two roles, Sales and Finance, while the live item also holds DefaultReader and a hand-made LegacyRole that config does not declare. A bare apply is the default and replaces the whole set: the two declared roles remain and both undeclared roles, including DefaultReader, are deleted. Passing keep_unmanaged equals true upserts instead: the two declared roles are created or updated and both undeclared roles stay. Fabric does not recreate a deleted DefaultReader, so a default apply against a lakehouse people already read from is an access cutover, not a deploy. + + + + + + + config declares + Sales · Finance + the complete intent + + live roles on the item + Sales · Finance · DefaultReader · LegacyRole + two of these config never declared + + + + apply reads both, every run + apply() + the default + apply(keep_unmanaged=true) + the deliberate override + + REPLACE + kept: Sales · Finance + deleted: LegacyRole + deleted: DefaultReader + live now mirrors config exactly + + INCREMENTAL UPSERT + created / updated: Sales · Finance + untouched: DefaultReader · LegacyRole + nothing is ever deleted + + Fabric does not recreate a deleted DefaultReader + so a default apply on a lakehouse people already read from is an access cutover, not a deploy + diff --git a/docs/assets/apply-semantics-light.svg b/docs/assets/apply-semantics-light.svg new file mode 100644 index 0000000..55a2c56 --- /dev/null +++ b/docs/assets/apply-semantics-light.svg @@ -0,0 +1,47 @@ +What apply does to the live role set. Config declares two roles, Sales and Finance, while the live item also holds DefaultReader and a hand-made LegacyRole that config does not declare. A bare apply is the default and replaces the whole set: the two declared roles remain and both undeclared roles, including DefaultReader, are deleted. Passing keep_unmanaged equals true upserts instead: the two declared roles are created or updated and both undeclared roles stay. Fabric does not recreate a deleted DefaultReader, so a default apply against a lakehouse people already read from is an access cutover, not a deploy. + + + + + + + config declares + Sales · Finance + the complete intent + + live roles on the item + Sales · Finance · DefaultReader · LegacyRole + two of these config never declared + + + + apply reads both, every run + apply() + the default + apply(keep_unmanaged=true) + the deliberate override + + REPLACE + kept: Sales · Finance + deleted: LegacyRole + deleted: DefaultReader + live now mirrors config exactly + + INCREMENTAL UPSERT + created / updated: Sales · Finance + untouched: DefaultReader · LegacyRole + nothing is ever deleted + + Fabric does not recreate a deleted DefaultReader + so a default apply on a lakehouse people already read from is an access cutover, not a deploy + diff --git a/docs/assets/destructive-utilities-dark.svg b/docs/assets/destructive-utilities-dark.svg new file mode 100644 index 0000000..668215e --- /dev/null +++ b/docs/assets/destructive-utilities-dark.svg @@ -0,0 +1,41 @@ +The blast radius of the two irreversible utilities. reset deletes every data access role on the item, including roles config never declared and DefaultReader, so nobody reads the data through OneLake security afterwards; it writes a role backup first and aborts without deleting if that write fails, and it leaves the control tables alone. cleanup drops all four control tables, including the entire audit history, and deletes every file under the security folder, including every pre-apply role backup; it leaves the live roles alone. The two interlock: cleanup destroys the backups reset depends on, so running cleanup first makes reset unrecoverable. Neither is a deployment mode, both are refused from the pipeline entrypoints and are available only interactively, by name. + + + + + + + reset() + destroys ACCESS + ✕ every data access role on the item + config's own, anyone else's, Default* + ✓ control tables untouched + ✓ writes a role backup first + backup write fails → deletes nothing + + cleanup() + destroys EVIDENCE + ✕ all four control tables + config, mapping, member, the audit history + ✕ every file under the security folder + including every pre-apply role backup + ✓ live roles untouched + cleanup() deletes the backups reset() depends on + + + run cleanup() first and reset() becomes unrecoverable + there is no third copy — the backup folder was the recovery path for both + + neither is a deployment mode + both are refused from the pipeline entrypoints — interactive only, by name + diff --git a/docs/assets/destructive-utilities-light.svg b/docs/assets/destructive-utilities-light.svg new file mode 100644 index 0000000..da0f29f --- /dev/null +++ b/docs/assets/destructive-utilities-light.svg @@ -0,0 +1,41 @@ +The blast radius of the two irreversible utilities. reset deletes every data access role on the item, including roles config never declared and DefaultReader, so nobody reads the data through OneLake security afterwards; it writes a role backup first and aborts without deleting if that write fails, and it leaves the control tables alone. cleanup drops all four control tables, including the entire audit history, and deletes every file under the security folder, including every pre-apply role backup; it leaves the live roles alone. The two interlock: cleanup destroys the backups reset depends on, so running cleanup first makes reset unrecoverable. Neither is a deployment mode, both are refused from the pipeline entrypoints and are available only interactively, by name. + + + + + + + reset() + destroys ACCESS + ✕ every data access role on the item + config's own, anyone else's, Default* + ✓ control tables untouched + ✓ writes a role backup first + backup write fails → deletes nothing + + cleanup() + destroys EVIDENCE + ✕ all four control tables + config, mapping, member, the audit history + ✕ every file under the security folder + including every pre-apply role backup + ✓ live roles untouched + cleanup() deletes the backups reset() depends on + + + run cleanup() first and reset() becomes unrecoverable + there is no third copy — the backup folder was the recovery path for both + + neither is a deployment mode + both are refused from the pipeline entrypoints — interactive only, by name + diff --git a/docs/assets/mode-lifecycle-dark.svg b/docs/assets/mode-lifecycle-dark.svg new file mode 100644 index 0000000..425303c --- /dev/null +++ b/docs/assets/mode-lifecycle-dark.svg @@ -0,0 +1,61 @@ +Mode lifecycle for the single self-contained olaf runtime notebook: setup once, then the routine loop configure, generate, plan, a manual review gate, then apply (full replace by default; keep_unmanaged=true keeps unmanaged roles) — looping back on every config edit. rollback restores a prior config version and re-runs generate, plan and apply. validate, show and trace are read-only at any time (no log; validate dry-runs generate's checks with zero writes). One notebook dispatches every mode via the mode parameter — there are no separate persona notebooks. + + + + + + + + + + + every config edit — the routine loop + + + + + + setup + + configure + + generate + + plan + + Review Gate + human approval + + apply + full replace + keep_unmanaged → upsert + + + + + + + + + → re-runs generate · plan · apply + + rollback + restore prior config + + + + + validate · show · trace + read-only — no writes, no log + + + One self-contained notebook — olaf — dispatches every mode via the `mode` parameter. + diff --git a/docs/assets/mode-lifecycle-light.svg b/docs/assets/mode-lifecycle-light.svg new file mode 100644 index 0000000..14fd43a --- /dev/null +++ b/docs/assets/mode-lifecycle-light.svg @@ -0,0 +1,61 @@ +Mode lifecycle for the single self-contained olaf runtime notebook: setup once, then the routine loop configure, generate, plan, a manual review gate, then apply (full replace by default; keep_unmanaged=true keeps unmanaged roles) — looping back on every config edit. rollback restores a prior config version and re-runs generate, plan and apply. validate, show and trace are read-only at any time (no log; validate dry-runs generate's checks with zero writes). One notebook dispatches every mode via the mode parameter — there are no separate persona notebooks. + + + + + + + + + + + every config edit — the routine loop + + + + + + setup + + configure + + generate + + plan + + Review Gate + human approval + + apply + full replace + keep_unmanaged → upsert + + + + + + + + + → re-runs generate · plan · apply + + rollback + restore prior config + + + + + validate · show · trace + read-only — no writes, no log + + + One self-contained notebook — olaf — dispatches every mode via the `mode` parameter. + diff --git a/docs/assets/pipeline-branching-dark.svg b/docs/assets/pipeline-branching-dark.svg new file mode 100644 index 0000000..682b83d --- /dev/null +++ b/docs/assets/pipeline-branching-dark.svg @@ -0,0 +1,46 @@ +Pipeline branching: generate exits with the envelope into an If on exitValue.changed; true continues to plan, whose own If on changed continues to apply. Any of the three raising instead of exiting leaves by the pipeline's native Failure arrow, which all three share, into a single notify step. One If per gate, no per-activity error If. + + + + + + + generate + + plan + + apply + + If + exitValue.changed + + If + changed + + exit + + true + + + true + + + raise + + raise + + raise + + native Failure arrow + + notify + one branch for all three + diff --git a/docs/assets/pipeline-branching-light.svg b/docs/assets/pipeline-branching-light.svg new file mode 100644 index 0000000..dd3ba06 --- /dev/null +++ b/docs/assets/pipeline-branching-light.svg @@ -0,0 +1,46 @@ +Pipeline branching: generate exits with the envelope into an If on exitValue.changed; true continues to plan, whose own If on changed continues to apply. Any of the three raising instead of exiting leaves by the pipeline's native Failure arrow, which all three share, into a single notify step. One If per gate, no per-activity error If. + + + + + + + generate + + plan + + apply + + If + exitValue.changed + + If + changed + + exit + + true + + + true + + + raise + + raise + + raise + + native Failure arrow + + notify + one branch for all three + diff --git a/docs/assets/pipeline-dark.svg b/docs/assets/pipeline-dark.svg new file mode 100644 index 0000000..95e4fe4 --- /dev/null +++ b/docs/assets/pipeline-dark.svg @@ -0,0 +1,63 @@ +Pipeline: onelake_security_config plus onelake_security_member both feed generate, which resolves wildcards and member names to objectIds into the onelake_security_mapping lock-file; plan compares the mapping with the current live state; on no change it skips, on a change apply writes to live OneLake security. + + + + + + + + + + onelake_security_config + your logic · roles · scopes · RLS/CLS + + + + + + onelake_security_member + preloaded · name → objectId + + + + + generate + resolve wildcards + names → objectIds + + + + onelake_security_mapping + machine lock-file · resolved + frozen + + + + plan + compare mapping vs live state + + + + change? + + + + No + + Skip + no change · exit 0 + + + + Yes → apply + + + + Live OneLake security + roles enforced on the lakehouse · every run audited + diff --git a/docs/assets/pipeline-light.svg b/docs/assets/pipeline-light.svg new file mode 100644 index 0000000..cde0a2b --- /dev/null +++ b/docs/assets/pipeline-light.svg @@ -0,0 +1,63 @@ +Pipeline: onelake_security_config plus onelake_security_member both feed generate, which resolves wildcards and member names to objectIds into the onelake_security_mapping lock-file; plan compares the mapping with the current live state; on no change it skips, on a change apply writes to live OneLake security. + + + + + + + + + + onelake_security_config + your logic · roles · scopes · RLS/CLS + + + + + + onelake_security_member + preloaded · name → objectId + + + + + generate + resolve wildcards + names → objectIds + + + + onelake_security_mapping + machine lock-file · resolved + frozen + + + + plan + compare mapping vs live state + + + + change? + + + + No + + Skip + no change · exit 0 + + + + Yes → apply + + + + Live OneLake security + roles enforced on the lakehouse · every run audited + diff --git a/docs/brand-guidelines.md b/docs/brand-guidelines.md index 175401a..b9b7808 100644 --- a/docs/brand-guidelines.md +++ b/docs/brand-guidelines.md @@ -108,7 +108,7 @@ through color alone; pair status colors with text or an icon. |---|---| | `olaf-logo.png` | Primary mark — avatars, docs headers, badges ≥ 64px | | `olaf-mascot.png` | Mascot artwork — README hero, launch and community material | -| `olaf-lockup-light.png` / `olaf-lockup-dark.png` | Masthead lockups per color scheme | +| `olaf-banner-light.png` / `olaf-banner-dark.png` | Masthead banners per color scheme | | `olaf-social-preview.png` | Repository social preview — OLAF mark only, without third-party logos or product icons | ### Clear space @@ -149,8 +149,8 @@ See [Legal separation](#legal-separation) below for the identity/affiliation rul ## README usage -1. Lead with the masthead lockup (`assets/brand/olaf-lockup-light.png` / - `olaf-lockup-dark.png` via a `` element). +1. Lead with the masthead banner (`assets/brand/olaf-banner-light.png` / + `olaf-banner-dark.png` via a `` element). 2. Follow with one concrete product sentence. 3. Show the plan → review → apply promise before implementation detail. 4. Keep the quick start within the first two screenfuls. diff --git a/scripts/gen-olaf-diagrams.py b/scripts/gen-olaf-diagrams.py new file mode 100755 index 0000000..ab48bba --- /dev/null +++ b/scripts/gen-olaf-diagrams.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Emit the light/dark SVG pair for two OLAF diagrams, in the repo's house style. + + scripts/gen-olaf-diagrams.py docs/assets + +Regenerates apply-semantics-{light,dark}.svg and destructive-utilities-{light,dark}.svg. +One generator emits both themes of both diagrams so a pair cannot drift apart — the light +and dark files differ only by the colour map below, and editing one by hand is how they +stop matching. Running it against docs/assets reproduces the committed files byte for byte. + +House style, copied from docs/assets/pipeline-branching-*.svg: + viewBox only (no width/height) · system-ui font stack · role="img" + a full aria-label + classes .box .t .s .ln .lbl · rx=11 boxes · stroke-width 2 · markers get a `d` suffix in dark +""" + +import sys + +FONT = "system-ui,-apple-system,Segoe UI,Roboto,sans-serif" + +# light -> dark, taken from the existing pairs +DARK = { + "#eef1fc": "#1e2540", + "#3a4a8c": "#aeb9f0", + "#7c8fd6": "#5b68b0", + "#e6f7ee": "#16281f", + "#237a5c": "#7fe0a8", + "#2a9d6c": "#3fbf7a", + "#eeeef2": "#262b36", + "#4a5262": "#d0d6e0", + "#9aa4b2": "#8a94a6", + "#fdeaea": "#3a1f22", + "#9c3535": "#f0a8a8", + "#d97070": "#d97070", + "#fcefd6": "#3a2c10", + "#9c6a2a": "#efb35e", + "#d99a45": "#e0a052", + "#f0ebfc": "#2a1f40", + "#4a3f8c": "#c3b8f5", + "#8e44ad": "#a56fd6", + "#5b6472": "#9aa4b2", + "#f6f7f9": "#1a1d24", + "#e2e5ec": "#2b303c", +} + + +def head(vb, aria, dark, extra_css=""): + sub = DARK["#5b6472"] if dark else "#5b6472" + sfx = "d" if dark else "" + ln = "#8a94a6" + return f'''{aria} + + + + + +''' + + +def c(col, dark): + return DARK.get(col, col) if dark else col + + +def box(x, y, w, h, fill, stroke, dark, rx=11): + return f' \n' + + +def txt(x, y, s, cls="t", fill=None, anchor="middle", dark=False, size=None): + f = f' fill="{c(fill, dark)}"' if fill else "" + z = f' font-size="{size}"' if size else "" + return f' {s}\n' + + +# ══════════════════════════════════════════════════════════════════════════════ +# A · apply-semantics +# ══════════════════════════════════════════════════════════════════════════════ +ARIA_A = ( + "What apply does to the live role set. Config declares two roles, Sales and Finance, while the " + "live item also holds DefaultReader and a hand-made LegacyRole that config does not declare. " + "A bare apply is the default and replaces the whole set: the two declared roles remain and both " + "undeclared roles, including DefaultReader, are deleted. Passing keep_unmanaged equals true " + "upserts instead: the two declared roles are created or updated and both undeclared roles stay. " + "Fabric does not recreate a deleted DefaultReader, so a default apply against a lakehouse people " + "already read from is an access cutover, not a deploy." +) + + +def diagram_a(dark): + W, H = 748, 410 + m = "d" if dark else "" + o = head(f"0 0 {W} {H}", ARIA_A, dark) + + # inputs — both feed both outcomes, so they join before the split + o += box(24, 20, 320, 76, "#eef1fc", "#7c8fd6", dark) + o += txt(184, 44, "config declares", "t", "#3a4a8c", dark=dark, size=13) + o += txt(184, 65, "Sales · Finance", "chip", "#3a4a8c", dark=dark) + o += txt(184, 84, "the complete intent", "s", dark=dark) + + o += box(404, 20, 320, 76, "#eeeef2", "#9aa4b2", dark) + o += txt(564, 44, "live roles on the item", "t", "#4a5262", dark=dark, size=13) + o += txt(564, 65, "Sales · Finance · DefaultReader · LegacyRole", "chip", "#4a5262", dark=dark) + o += txt(564, 84, "two of these config never declared", "s", dark=dark) + + # join, then split — the call is what differs, not the inputs + o += ' \n' + o += f' \n' + o += f' \n' + o += txt(374, 120, "apply reads both, every run", "s", dark=dark) + o += txt(192, 150, "apply()", "lbl", anchor="start", dark=dark) + o += txt(192, 165, "the default", "s", anchor="start", dark=dark) + o += txt(572, 150, "apply(keep_unmanaged=true)", "lbl", anchor="start", dark=dark) + o += txt(572, 165, "the deliberate override", "s", anchor="start", dark=dark) + + # outcomes + o += box(24, 178, 320, 128, "#fdeaea", "#d97070", dark) + o += txt(184, 202, "REPLACE", "t", "#9c3535", dark=dark, size=13) + o += txt(184, 226, "kept: Sales · Finance", "kept", dark=dark) + o += txt(184, 250, "deleted: LegacyRole", "del", dark=dark) + o += txt(184, 268, "deleted: DefaultReader", "del", dark=dark) + o += txt(184, 292, "live now mirrors config exactly", "s", dark=dark) + + o += box(404, 178, 320, 128, "#e6f7ee", "#2a9d6c", dark) + o += txt(564, 202, "INCREMENTAL UPSERT", "t", "#237a5c", dark=dark, size=13) + o += txt(564, 226, "created / updated: Sales · Finance", "kept", dark=dark) + o += txt(564, 254, "untouched: DefaultReader · LegacyRole", "kept", dark=dark) + o += txt(564, 284, "nothing is ever deleted", "s", dark=dark) + + # the consequence band + o += box(24, 322, 700, 66, "#fcefd6", "#d99a45", dark) + o += txt( + 374, + 346, + "Fabric does not recreate a deleted DefaultReader", + "t", + "#9c6a2a", + dark=dark, + size=13, + ) + o += txt( + 374, + 368, + "so a default apply on a lakehouse people already read from is an access cutover, not a deploy", + "s", + dark=dark, + ) + return o + "\n" + + +# ══════════════════════════════════════════════════════════════════════════════ +# B · destructive-utilities +# ══════════════════════════════════════════════════════════════════════════════ +ARIA_B = ( + "The blast radius of the two irreversible utilities. reset deletes every data access role on the " + "item, including roles config never declared and DefaultReader, so nobody reads the data through " + "OneLake security afterwards; it writes a role backup first and aborts without deleting if that " + "write fails, and it leaves the control tables alone. cleanup drops all four control tables, " + "including the entire audit history, and deletes every file under the security folder, including " + "every pre-apply role backup; it leaves the live roles alone. The two interlock: cleanup destroys " + "the backups reset depends on, so running cleanup first makes reset unrecoverable. Neither is a " + "deployment mode, both are refused from the pipeline entrypoints and are available only " + "interactively, by name." +) + + +def diagram_b(dark): + W, H = 748, 404 + o = head(f"0 0 {W} {H}", ARIA_B, dark) + + # reset() + o += box(24, 22, 330, 176, "#fdeaea", "#d97070", dark) + o += txt(189, 46, "reset()", "t", "#9c3535", dark=dark) + o += txt(189, 66, "destroys ACCESS", "s", dark=dark) + o += txt(40, 92, "✕ every data access role on the item", "del", anchor="start", dark=dark) + o += txt(52, 110, "config's own, anyone else's, Default*", "s", anchor="start", dark=dark) + o += txt(40, 134, "✓ control tables untouched", "kept", anchor="start", dark=dark) + o += txt(40, 158, "✓ writes a role backup first", "kept", anchor="start", dark=dark) + o += txt(52, 176, "backup write fails → deletes nothing", "s", anchor="start", dark=dark) + + # cleanup() + o += box(394, 22, 330, 176, "#f0ebfc", "#8e44ad", dark) + o += txt(559, 46, "cleanup()", "t", "#4a3f8c", dark=dark) + o += txt(559, 66, "destroys EVIDENCE", "s", dark=dark) + o += txt(410, 92, "✕ all four control tables", "del", anchor="start", dark=dark) + o += txt(422, 110, "config, mapping, member, the audit history", "s", anchor="start", dark=dark) + o += txt(410, 134, "✕ every file under the security folder", "del", anchor="start", dark=dark) + o += txt(422, 152, "including every pre-apply role backup", "s", anchor="start", dark=dark) + o += txt(410, 176, "✓ live roles untouched", "kept", anchor="start", dark=dark) + + # the interlock — the label sits ABOVE the dashed run, never on it + m = "d" if dark else "" + o += txt(374, 220, "cleanup() deletes the backups reset() depends on", "del", dark=dark) + o += f' \n' + + o += box(24, 266, 700, 54, "#fdeaea", "#d97070", dark) + o += txt( + 374, + 290, + "run cleanup() first and reset() becomes unrecoverable", + "t", + "#9c3535", + dark=dark, + size=13, + ) + o += txt( + 374, + 310, + "there is no third copy — the backup folder was the recovery path for both", + "s", + dark=dark, + ) + + # not a mode + o += box(24, 336, 700, 52, "#eeeef2", "#9aa4b2", dark) + o += txt(374, 360, "neither is a deployment mode", "t", "#4a5262", dark=dark, size=13) + o += txt( + 374, + 378, + "both are refused from the pipeline entrypoints — interactive only, by name", + "s", + dark=dark, + ) + return o + "\n" + + +if __name__ == "__main__": + out = sys.argv[1].rstrip("/") + for name, fn in (("apply-semantics", diagram_a), ("destructive-utilities", diagram_b)): + for dark in (False, True): + p = f"{out}/{name}-{'dark' if dark else 'light'}.svg" + open(p, "w", encoding="utf-8").write(fn(dark)) + print("wrote", p)