import Foundation

/// The board-embedded agent guide: a `CLAUDE.md` the app silently maintains at every board root,
/// teaching any file-capable agent how to read and write the board directly (08-agent-integration.md
/// ▸ The agent guide). The folder is the API, and this is the file an agent working inside the
/// folder will actually find.
///
/// **App-owned and version-gated.** The first line carries `lanework-agent-guide vN`. The guide is
/// rewritten when it is **missing or older**, and left **byte-for-byte untouched** when it is
/// current or newer — "never downgraded" (08): a newer app version may have written it, and an
/// older copy of the app opening the board must not walk it back. Untouched means untouched: a
/// current guide is never reopened for writing, so its mtime — and, on git boards, the tree — is
/// undisturbed by an open.
///
/// **The marker is read from the first line and nowhere else**, which is 08's own wording and a
/// deliberate divergence from the pathfinder's anywhere-in-the-file match: a user's own `CLAUDE.md`
/// that merely *quotes* the marker (a note about this feature, a pasted excerpt) would otherwise
/// read as an app-owned guide and be silently overwritten.
///
/// **A markerless `CLAUDE.md` is displaced, never clobbered** (08 ▸ Ownership). It is user content
/// sitting on a name the app claims, so it moves — byte-preserving, `FileManager.moveItem` — to
/// `CLAUDE.user.md` when that name is free, and the guide is written in its place. When the name is
/// taken the guide write is **skipped entirely**: user content is never destroyed, and a board
/// without a guide is a board that merely lacks a courtesy. `CLAUDE.user.md` is otherwise never
/// written, read, upgraded or validated by this app — that one rescue is its only creation
/// (08 ▸ `CLAUDE.user.md`).
///
/// **A folder or symlink squatting `CLAUDE.md` is displaced too** (ruled 2026-07-29 — the
/// claimed-names rule, 01-storage-format.md § Fractal layout ▸ Rules): Lanework owns the board, so
/// an invalid artifact on a name the app claims is a defect rather than a resident. It moves aside
/// by the Finder-style rename ladder (`CLAUDE.md` → `CLAUDE.md 2`) — preserved verbatim, a symlink
/// moved as a link and never followed — and the guide is written on the freed name, with a
/// warning-tone notice naming old and new. This replaced an untouchable-skip; what survives from it
/// is displacement-never-destruction.
///
/// **Nothing here is a user-facing event.** Every refusal below is a log line and nothing more; the
/// only thing that can reach a banner is a genuine I/O failure of the write itself, because
/// `BoardStore.performWrite` posts every `BoardWriteError` it sees. The scheduling — when this is
/// consulted, and why it is safe to consult on every reload — lives at
/// `BoardStore.refreshAgentGuide()`.
enum AgentGuide {

    // MARK: - The two claimed names

    /// The app-owned guide, and one of the board-root names the loader already claims
    /// (`BoardLoader.reservedRootNames`) so that neither file is ever read as a stray.
    static let filename = "CLAUDE.md"

    /// The user's extension point (08 ▸ `CLAUDE.user.md`) — and the rescue destination for a
    /// markerless `CLAUDE.md`. The app writes this name exactly once per board, if ever.
    static let userFilename = "CLAUDE.user.md"

    /// The guide the app ships. **v4 was the pathfinder's**, and real boards carry it; v5 is the
    /// rewrite's guide (lanes, `.trash/`, `attachments/`, `modified-by`, the `CLAUDE.user.md`
    /// pointer) and supersedes it on the next open. v6 adds the one-folder-at-a-time move warning:
    /// a real agent incident (2026-07-29) showed `mv <lane>/*` sweeping the lane's own `index.md`
    /// along with the cards and destroying the destination lane's identity — the guide now says
    /// *why* the named-folder form is load-bearing, not just what to type. v7 teaches two more
    /// things settled after v6 shipped: the refined stamp-discipline predicate
    /// (01-storage-format.md ▸ `modified`'s scope, ruled 2026-07-29, refined 2026-07-30) — a
    /// within-container reorder rewrites only `order`, while a move that changes an item's
    /// container (another lane, another board, into or out of `.trash/`) stamps `modified` and
    /// `modified-by` like any content edit, the trash move included, since it's the same rule and
    /// not a special case — and the card-level `attachments` claimed name
    /// (01-storage-format.md § Fractal layout ▸ Rules, "level-uniform"): that name belongs to the
    /// app's own folder, so a *file* by that name is a defect the app displaces on sight.
    static let version = 7

    // MARK: - The version marker

    private static let markerPrefix = "lanework-agent-guide v"

    /// The version stamped into `text`'s **first line**, or `nil` when that line carries no marker —
    /// which is how a foreign, user-authored `CLAUDE.md` is recognized (there is no "version 0": a
    /// markerless file is not an old guide, it is somebody else's file).
    ///
    /// Parsed rather than matched with a `Regex`: `Regex` is not `Sendable`, so a stored pattern
    /// would have to be rebuilt on every call (the pathfinder's trick), and the grammar here — a
    /// literal prefix and the ASCII digits after it — is smaller than the machinery to match it.
    /// Both line endings work by construction, since the first line ends at the first newline
    /// scalar of either kind.
    static func installedVersion(of text: String) -> Int? {
        let firstLine = text.prefix { !$0.isNewline }
        guard let marker = firstLine.range(of: markerPrefix) else { return nil }
        return Int(firstLine[marker.upperBound...].prefix { $0.isASCII && $0.isNumber })
    }

    // MARK: - The decision

    /// What is sitting on `CLAUDE.md`, as the filesystem answers it — no policy, so the rule below
    /// can be a pure function of it.
    enum Existing: Equatable {
        /// Nothing at that path (or nothing this process can read there — the same non-event).
        case missing

        /// A regular file. `text` is its strict UTF-8 decoding, `nil` when it does not decode:
        /// reads are strict everywhere in this app (01-storage-format.md § Encoding), and a file
        /// the app cannot read is a file whose marker it cannot honestly claim to have checked.
        case file(text: String?)

        /// A symlink, a directory, or any other node that is not a regular file — **a squatter on a
        /// claimed name**, and since 2026-07-29 not a resident (01-storage-format.md § Fractal
        /// layout ▸ Rules: "Lanework owns the board, so an invalid artifact on a claimed name is a
        /// defect, not a resident").
        ///
        /// It is moved aside by the Finder-style rename ladder (`CLAUDE.md` → `CLAUDE.md 2`),
        /// **preserved verbatim, never destroyed** — a symlink moved as a link, never followed —
        /// and the guide is then written on the freed name. This case used to mean "skipped"; the
        /// ruling upgraded the skip to a displacement, and the invariant that survives is
        /// displacement-never-destruction.
        case squatted
    }

    /// The board root's two claimed names, read once — the input to `decide(_:)`.
    struct State: Equatable {
        var existing: Existing

        /// Whether `CLAUDE.user.md` is free — **no file, no folder, no symlink** of that name. The
        /// rescue move re-checks this atomically anyway (`FileManager.moveItem` fails rather than
        /// overwrite), so this is the decision's input, not its safety.
        var userFilenameIsFree: Bool

        /// This picture as the heal engine's comparable value (`HealScheduler`'s memo unit).
        ///
        /// The file's *text* is hashed rather than carried: two states are the same picture exactly
        /// when the same bytes are on the same name, and a memo holding a whole guide's prose for the
        /// life of a session would be the one place in this store that grows with a file's size.
        var signature: String {
            let existing = switch existing {
            case .missing: "missing"
            case .squatted: "squatted"
            case let .file(text): "file:\(text.map(EchoLedger.hash(of:)) ?? "undecodable")"
            }
            return "guide:\(existing):\(userFilenameIsFree)"
        }
    }

    /// The four outcomes, and the only four.
    enum Decision: Equatable {
        /// The guide on disk is current or newer. Nothing is opened for writing.
        case leaveAlone

        /// Missing, or an older marker: write the guide.
        case write

        /// A markerless `CLAUDE.md`: rescue it to `CLAUDE.user.md`, then write the guide.
        case displaceThenWrite

        /// A markerless `CLAUDE.md` with `CLAUDE.user.md` already taken — the ruling's
        /// skipped-with-a-log case. Two files the user owns, both left alone.
        ///
        /// **The one standing exception to the squatter displacement, and it stands** (ruled
        /// 2026-07-29): this displacement has a designated *destination*, and freeing a destination
        /// by a second displacement would cascade renames.
        case skipUserFilenameTaken

        /// `CLAUDE.md` is a symlink, a folder, or some other non-file: move it aside by the
        /// Finder-style rename ladder, then write the guide on the freed name (ruled 2026-07-29 —
        /// the claimed-name squatter rule; it replaced a skip).
        case displaceSquatterThenWrite
    }

    /// The whole rule, as a pure function of `state` — so "never downgrade", "never clobber" and
    /// "never touch a symlink" are pinned by the suite without a filesystem in the way.
    ///
    /// One edge worth naming rather than special-casing: a **zero-byte** `CLAUDE.md` is markerless,
    /// so it takes the displacement path like any other foreign file. Uniformity is the point —
    /// every rule that decides whether to destroy something answers "no" the same way.
    static func decide(_ state: State) -> Decision {
        switch state.existing {
        case .missing:
            .write
        case .squatted:
            .displaceSquatterThenWrite
        case let .file(text):
            if let text, let installed = installedVersion(of: text) {
                installed >= version ? .leaveAlone : .write
            } else {
                state.userFilenameIsFree ? .displaceThenWrite : .skipUserFilenameTaken
            }
        }
    }

    // MARK: - Reading the board root

    /// Reads the state of the two claimed names. Purely a read — it creates nothing, and it is
    /// cheap enough (one `lstat`, plus a small file read only when there is a file to read) to run
    /// on every reload.
    ///
    /// **`lstat` semantics throughout, never `fileExists`** (`IntegrityRules.node(at:)`): a
    /// **dangling** symlink is a node that is *there* — it holds the name, and it is displaced as a
    /// link rather than followed — while `fileExists` follows the link, finds nothing, and would
    /// call the name free.
    static func inspect(atBoardRoot root: URL) -> State {
        State(
            existing: existingNode(at: root.appendingPathComponent(filename)),
            userFilenameIsFree: IntegrityRules.node(at: root.appendingPathComponent(userFilename)) == nil
        )
    }

    private static func existingNode(at url: URL) -> Existing {
        guard let node = IntegrityRules.node(at: url) else { return .missing }
        guard node == .file else { return .squatted }
        // A regular file whose *contents* cannot be read reads as undecodable rather than as
        // missing, and the difference is the whole promise: `.missing` would overwrite it, while
        // undecodable displaces it — and the rescue move needs no read permission on the file to
        // preserve it byte for byte.
        guard let data = try? Data(contentsOf: url) else { return .file(text: nil) }
        return .file(text: String(data: data, encoding: .utf8))
    }

    // MARK: - Writing it

    /// Puts the current guide at the board root, first moving a displaced `CLAUDE.md` out of the way
    /// when the decision called for it.
    ///
    /// **Called inside `BoardStore.performWrite`**, so both halves ride one watcher bracket: the
    /// rescue and the guide land as a single app-mediated reload, and (under Pro) as a
    /// single honestly-attributed commit rather than a foreign-looking rename followed by an
    /// app write (06-history-undo.md ▸ Commit messages, "Update agent guide (vN)").
    ///
    /// The move is `FileManager.moveItem` and nothing else: it preserves the bytes exactly — the
    /// displaced file may not even be UTF-8 — and it **fails rather than overwrite** if
    /// `CLAUDE.user.md` appeared between the decision and this call, which is what makes the
    /// "user content is never destroyed" promise hold against a race rather than merely against a
    /// stale read.
    ///
    /// **It re-verifies against disk** (01-storage-format.md § Validation and healing: "every
    /// scheduled heal re-verifies its defect against disk at write time and no-ops when it is
    /// gone"): the board root is re-inspected here, and a guide that has become current since the
    /// decision — an agent wrote it, another window healed it first — returns `nil` rather than
    /// rewriting a file that no longer needs it. Losing the race to a foreign fix is success.
    ///
    /// - Returns: what this call displaced, or `nil` when it wrote nothing at all.
    @discardableResult
    static func install(atBoardRoot root: URL) throws(BoardWriteError) -> Displacement? {
        let guideURL = root.appendingPathComponent(filename)
        let decision = decide(inspect(atBoardRoot: root))
        var displaced: Displacement?

        switch decision {
        case .leaveAlone, .skipUserFilenameTaken:
            // Nothing to write: either the defect healed itself under us, or the standing exception
            // applies and both of the user's files stay exactly where they are.
            return nil
        case .write:
            break
        case .displaceThenWrite:
            // The rescue, not a squatter: a markerless `CLAUDE.md` is user *content*, and it has a
            // designated destination (08-agent-integration.md ▸ Ownership).
            do {
                try FileManager.default.moveItem(at: guideURL, to: root.appendingPathComponent(userFilename))
            } catch {
                throw BoardWriteError(
                    operation: .agentGuide,
                    path: guideURL.path,
                    reason: .io(message: "could not move the existing \(filename) aside to \(userFilename): \(error.localizedDescription)")
                )
            }
            EchoLedger.current?.recordMove(from: guideURL, to: root.appendingPathComponent(userFilename))
            displaced = Displacement(name: filename, movedTo: userFilename, wasUserContent: true)
        case .displaceSquatterThenWrite:
            // The claimed-name displacement (ruled 2026-07-29): a folder or symlink on the app's own
            // name, moved aside by the Finder ladder and never destroyed.
            guard let freed = try BoardWriter.displaceClaimedName(
                ClaimedNameSquatter(
                    name: filename,
                    found: IntegrityRules.node(at: guideURL) ?? .directory,
                    expected: .file
                ),
                atBoardRoot: root
            ) else {
                // Gone under us — re-decide rather than write blind, which the next reload does
                // anyway. Nothing displaced, nothing written.
                return nil
            }
            displaced = Displacement(name: filename, movedTo: freed, wasUserContent: false)
        }

        try BoardWriter.atomicReplace(text: content, at: guideURL, operation: .agentGuide)
        // Heal-marked: the guide's refresh is app-initiated work, and its commit is its own
        // ("Update agent guide (vN)" already commits alone — 06-history-undo.md ▸ Commit messages).
        EchoLedger.current?.markHeal(at: guideURL)
        return displaced
    }

    /// What an install moved out of the way, for the notice that names old and new.
    ///
    /// Two shapes ride one type because the *user-facing* fact is the same in both — a file the user
    /// owns is now under a different name — and only the tone differs: the `CLAUDE.user.md` rescue
    /// is the settled, silent ownership rule (08-agent-integration.md), while a squatter's
    /// displacement gets the relocation-style warning-tone notice (01-storage-format.md § Fractal
    /// layout ▸ Rules, ruled 2026-07-29).
    struct Displacement: Sendable, Equatable {
        /// The claimed name that was freed.
        let name: String
        /// The name the displaced node now has.
        let movedTo: String
        /// Whether this was the markerless-`CLAUDE.md` rescue (silent) rather than a squatter's
        /// displacement (announced).
        let wasUserContent: Bool
    }

    // MARK: - The guide itself

    /// The bytes written to `CLAUDE.md`: the prose below plus the closing newline a multi-line
    /// literal does not carry. Files the app creates end with LF (01-storage-format.md § Encoding
    /// and line endings), and this one is no exception for being prose.
    static let content = guideBody + "\n"

    /// **The one swappable string.** Its wording is a separate concern from this file's mechanism —
    /// what the guide must teach is 08-agent-integration.md ▸ The agent guide's list, and revising
    /// it is a `version` bump plus a new literal here, with nothing else to change.
    ///
    /// The marker interpolates `version` rather than spelling the number twice: the constant and the
    /// first line cannot drift apart, and a bump is one edit.
    private static let guideBody = """
    <!-- lanework-agent-guide v\(version) — created and kept up to date by the Lanework app. Don't edit this file: it is overwritten on upgrades. Board-specific instructions live in CLAUDE.user.md (see below), which the app never touches. -->

    # This folder is a Lanework kanban board

    Plain folders and Markdown, rendered live by the Lanework app. You can (and
    should) manipulate the board by editing files directly — while the board is
    open, the app picks up every filesystem change automatically. There is
    nothing to sync and no API to call: the files are the board.

    **If a `CLAUDE.user.md` exists next to this file, read it too** — it carries
    board-specific instructions from the board's owner.

    ## Layout

    ```
    <board>/                     this folder (the board)
    ├── index.md                 board title + settings; body = board description
    ├── CLAUDE.md                this guide (app-maintained)
    ├── .trash/                  deleted cards and lanes (app-managed — see Deleting)
    ├── <uuid>/                  a LANE
    │   ├── index.md             lane title + order; body = lane notes/policy
    │   ├── <uuid>/              a CARD
    │   │   ├── index.md         card title + order; body = the card's content
    │   │   └── attachments/     the card's files (flat, top-level)
    │   └── <uuid>/              another card
    └── <uuid>/                  another lane
    ```

    - Depth alone defines meaning: depth 1 = lane, depth 2 = card. There is no
      type field.
    - Folder names are lowercase UUIDs and are the item's permanent identity.
      **Never rename a folder.** Titles live in frontmatter only.
    - Every `index.md` is YAML frontmatter between `---` lines, then a Markdown
      body. Files are plain UTF-8, **no BOM**; keep each file's existing line
      endings, and end new files with LF.

    ## Reading the board

    - Lanes run left→right by ascending `order`; cards top→bottom by ascending
      `order` within their lane. Ties break by folder name.
    - Lane titles carry the workflow semantics (e.g. To Do → In Progress →
      Done). Read the board's and lanes' index.md bodies for descriptions and
      per-lane policy before deciding where a card belongs.
    - `.trash/` holds deleted cards and lanes; everything else at board root that isn't a
      UUID-named folder is not part of the board's content.

    ## Frontmatter

    All levels: `schema` (required, always `1`), `title` (optional — an item
    without one renders as untitled, so give cards real titles), `created` and
    `modified` (ISO-8601 with timezone, e.g. `2026-07-24T18:00:00Z`),
    `background` (color), `icon` (SF Symbol name), `iconColor` (color, tints
    `icon`). Lanes and cards additionally require `order` (a number; floats are
    fine). Lanes may set `width` (integer ≥ 1, multiplier of the standard lane
    width).

    **Quote any `title` containing a colon** — `title: Fix: the thing` is
    invalid YAML; write `title: "Fix: the thing"`. The same goes for any value
    containing `: ` or starting with `#`, `[`, `{`, or a quote — when in doubt,
    double-quote.

    Unknown keys are preserved verbatim by the app and invisible in its UI —
    custom metadata (`project:`, `tags:`, `claimed-by:` …) is safe to add and
    survives every app rewrite. Reserved for Lanework's upcoming tracker sync —
    preserved but not rendered, don't repurpose them: the card keys `labels`,
    `assignees`, `due`, the `remote` key (cards and board), `remote-state`
    (lanes), and a card-level `comments/` folder.

    ## Stamping your work: `modified-by`

    Add `modified-by: <your-name>` (e.g. `modified-by: claude`) to the
    frontmatter of every `index.md` you write — it attributes the change in the
    app and, on git boards, in the auto-commit. The app clears the stamp on its
    own writes, so **re-stamp on every write, and after every move**: a bare
    folder move rewrites no file, so the moved card arrives unstamped unless you
    touch its `index.md` again. When you need exact authorship, commit your
    changes yourself instead (see Git below).

    ## Creating a card

    1. Pick the lane folder. Compute `order`: bottom of the lane = max existing
       card `order` + 1024; top = min − 1024; between two cards = their
       midpoint. (Empty lane: any number, conventionally 1024.)
    2. Create a folder named a fresh lowercase UUID:
       `id=$(uuidgen | tr 'A-Z' 'a-z')`.
    3. Write `<lane>/$id/index.md` (timestamp: `date -u +%FT%TZ`):

    ```markdown
    ---
    schema: 1
    kind: card
    title: Short imperative card title
    order: 3072
    created: 2026-07-24T18:00:00Z
    modified: 2026-07-24T18:00:00Z
    modified-by: claude
    ---
    The card's content — any Markdown.
    ```

    Creating a lane is the same one level up (body optional; `kind: lane`;
    `order` ranks lanes left→right).

    **Always write `kind`** at creation — `kind: card`, `kind: lane`,
    `kind: board` at board root. Depth already says what an item is on the
    board, but `.trash/` is flat, and there the value is the only thing that
    tells a trashed lane from a card. Omitting it is healable, never fatal: the
    app fills a missing `kind` in the next time it rewrites that file.

    ## Moving and reordering

    - **The stamp rule**: a move that changes an item's container — another
      lane, another board, or into/out of `.trash/` — stamps `modified` and
      re-stamps `modified-by`, the same as any content edit. A reorder that
      keeps an item in the same container (a card among its lane's cards, a
      lane among the board's) rewrites only `order`; leave `modified` and
      `modified-by` alone. The trash move isn't an exception to this — it
      stamps because every container change stamps.
    - Move to another lane: `mv <laneA>/<card-uuid> <laneB>/` — the folder move
      IS the move. Then set the card's `order` to place it among the
      destination's cards, update `modified`, and re-stamp `modified-by`.
    - Move folders **one at a time, by name** — never `mv <laneA>/* <laneB>/`.
      A lane folder holds its own `index.md` beside its cards, so a glob
      sweeps the lane's identity file along with them and overwrites the
      destination lane's.
    - Reorder within a lane: rewrite only that card's `order` — don't touch
      `modified` or `modified-by`.

    ## Editing and deleting

    - Edit bodies freely; update `modified` on every write. Preserve frontmatter
      keys you don't recognize and don't reformat content you didn't change.
    - **Delete a card or a lane = move its folder into `<board>/.trash/`**:
      `mv <lane>/<card-uuid> <board>/.trash/`, or `mv <lane-uuid>
      <board>/.trash/` for a whole lane (create `.trash/` if missing). A lane
      travels with its cards inside it. Arrivals go on top: set the moved
      item's `order` to the smallest `order` already in `.trash/` minus 1024
      (empty trash: any number). It's a container change like any other move
      (Moving and reordering above): stamp `modified` and re-stamp
      `modified-by`. Restore is the same move in reverse — a card into a lane,
      a lane back to board root, with a fresh `order`, stamped the same way.
    - **Stamp `kind: lane` when you trash a lane that lacks it.** `.trash/` is
      flat, so an empty lane folder looks exactly like a card folder; the `kind`
      value is what tells them apart in there.
    - Never write a `deleted:` key — that convention is retired; the app
      migrates any it finds.
    - Remove a folder outright (`rm -r`) only when you mean permanent,
      unrecoverable deletion — the trash is the recoverable path for both
      cards and lanes.

    ## Attachments

    - A card's files live in `attachments/` inside the card folder, flat at its
      top level. **Put files there, never beside `index.md`** — the app
      relocates loose files into `attachments/` and tells the user it did.
      The name `attachments` itself belongs to that folder — never create a
      *file* called `attachments` in a card; the app treats one as a defect
      and displaces it on sight.
    - To attach a file: create `attachments/` if missing and copy the file in.
      If the name is taken, pick a free one Finder-style (`shot.png` →
      `shot 2.png`) — never overwrite.
    - Reference attachments from the card body by relative path:
      `![](attachments/sketch.png)`.
    - Subfolders under `attachments/` are tolerated but the app never creates
      or lists them — keep attachments top-level.

    ## Hard rules (the app fails loudly on violations)

    - Frontmatter must parse as YAML; `schema` (plus `order` on lanes and
      cards) is required. Keep `schema: 1`. The classic violation is an
      unquoted colon in a title (see Frontmatter above).
    - Files must be UTF-8 without BOM.
    - Never create a card folder without an `index.md`.
    - Never rename UUID folders.
    - Prefer atomic writes (write a temp file, then rename over the target) —
      the app reloads on every filesystem event and can catch half-written
      files.

    ## Colors and icons

    `background` takes a palette name (preferred) or `#RRGGBB[AA]` hex. `icon`
    is any SF Symbol name; `iconColor` takes a palette name (preferred) or hex
    to tint it.

    - Icon tint palette: `obsidian`, `aluminum`, `soapstone`, `chalk`,
      `carnation`, `rich-grapefruit`, `smokey-tangerine`, `fern`,
      `light-teal`, `deep-sky-blue`, `pale-violet`, `deep-cool-granite`.
    - Background palette: `obsidian`, `shale`, `aluminum`, `chalk`,
      `light-cayenne`, `light-mocha`, `smokey-mocha`, `smokey-fern`,
      `dark-teal`, `smokey-ocean`, `smokey-rich-eggplant`,
      `intense-cool-shale`.

    ## Git

    Some boards are git repositories — because the board lives inside a repo of
    yours, or because Lanework Pro manages its history. Two rules when one is:

    - **Stage only your own paths** — never `git add -A` or `git add .`: a
      sweep would commit the user's not-yet-committed changes under your name.
    - Committing your changes yourself is fine and gives you exact authorship;
      the app follows along. If you don't commit, Lanework Pro auto-commits
      your changes as external edits (attributed via `modified-by` when you
      stamped it).
    """
}
