# consent_classes.yaml — the consent-class registry.
#
# Copy to `tracks/_meta/consent_classes.yaml` (gitignored, per-environment) to enable
# accept-side consent promotion. See `knowledge/shared/rules/operational_adaptation.md`
# §Consent promotion.
#
# WHY A REGISTRY AT ALL (cross-family review, 2026-07-29)
#   The first draft let the running session name the class it was counting approvals against.
#   That defeats the class-inflation guard before it fires: the class does not need to be WIDENED
#   later if it was born wide. Recording three approvals as `sim-conductor` instead of
#   `dispatch read-only sim on a local artifact` silently grants every future sim-conductor mode.
#   So classes are DECLARED here, reviewed by a human, and the session may only count against
#   an entry that already exists. A session may propose a new entry; it may not mint one mid-run.
#
# THE FLOOR IS READ FROM THIS FILE, NOT JUDGED AT RUNTIME
#   `sinks` is the field the irreversibility floor is decided from. A session asking itself
#   "is this reversible?" is the wrong arbiter — it is the party that wants to stop being asked.
#   Absent entry / absent file / unparseable entry => NO promotion, keep asking. Unknown is not
#   reversible.
#
# TAINT
#   A class inherits the floor if it FEEDS an irreversible sink, not only if it crosses one.
#   `local-commit` below is the worked example: committing is locally reversible, but if a
#   post-commit hook, sync daemon, or CI workflow publishes from that commit, the chain crossed
#   go-public and the class must be marked tainted. Declare `feeds:` honestly — the whole point
#   is that no single step in a bad chain looks dangerous on its own.
#
# WHAT A GRANT AGAINST ONE OF THESE LOOKS LIKE (UAP frontmatter, gitignored)
#   Consent binds to the action's SHAPE, not its name, so the grant records the fingerprint it was
#   given for and ALL FIVE scope fields are mandatory — R6 refuses a grant missing any of them,
#   because a fingerprint that omits a field cannot detect drift in that field. `sinks: []` is a
#   real recorded fingerprint, not an omission.
#
#     standing_consent:
#       dispatch-readonly-sim-local-artifact:
#         granted: 2026-07-31
#         expires: 2026-08-30            # a lease, max 365 days
#         owner:  fh-meta:sim-conductor  # must still match the registry entry below
#         mode:   blind-target-tier-sim  # ...and so must this
#         target: a single local file under the current repo
#         effects: [read, dispatch]      # must be a subset of `capabilities`
#         sinks:  []                     # the class may not later declare sinks not listed here
#
#   Re-point the class's `owner` or `mode` after the grant and the join fails: that is the point.

classes:
  # ---- REAL, ADOPTABLE class (not illustrative) -------------------------------------
  # Ships with fh_node_check.sh's consent-gated auto-pull. Copy this block verbatim into your
  # own tracks/_meta/consent_classes.yaml, then grant it in your UAP frontmatter to enable the
  # apply arm. Without both, the check surfaces "N commits behind" and never touches the repo —
  # absent is not granted.
  #
  # Read the reason before adopting: this class takes a DIFFERENT verdict from the `local-commit`
  # example further down, on a deliberately narrow ground. If your integration branch is not
  # PR-only/protected, that ground does not hold for you and you should not adopt it.
  - name: repo-freshness-autopull
    owner: scripts/fh_node_check.sh
    mode: ff-only-merge-on-default-branch
    target: this repo's own default branch, in the local clone, when it is already checked out
    capabilities: [network, repo-mutation]
    sinks: []
    feeds: []
    promotion_eligible: true
    lease_days: 92
    reason: >
      A fast-forward pull introduces nothing new: every commit it brings down is already on the
      integration branch, which is PR-only and server-side protected, so all of it has passed the
      gates. `local-commit` below is classified feeds:[go-public] because it CREATES ungated
      content that publish can then ship; this makes the tree MATCH canon instead. Publish packs
      the working tree, so a tree matching origin is strictly safer to publish from than a stale
      one. Blast radius also excludes personal state by construction — a pull touches tracked
      files only, and session/memory/companion state is gitignored or outside the repo.
      Envelope (enforced in code, not prose): applies ONLY when the default branch is already
      checked out, never switches branches, and --ff-only refuses a divergence rather than
      absorbing it. Lanes: scripts/test_node_check_lanes.sh lane10-a..e + CONTROL.

  # ---- promotion-eligible example -------------------------------------------------
  - name: dispatch-readonly-sim-local-artifact
    owner: fh-meta:sim-conductor          # the gate/skill that does the asking
    mode: blind-target-tier-sim
    target: a single local file under the current repo
    capabilities: [read, dispatch]        # read · local-write · network · dispatch · repo-mutation
    sinks: []                             # crosses nothing irreversible
    feeds: []                             # enables/queues nothing irreversible
    promotion_eligible: true
    lease_days: 30                        # standing consent is a renewable lease, never permanent

  # ---- NOT promotion-eligible: sink is irreversible --------------------------------
  - name: npm-publish
    owner: Pre-Publish Surface Gate
    mode: registry-publish
    target: the package tarball file set
    capabilities: [read, network]
    sinks: [go-public]                    # irreversible => never promotes, at any count
    feeds: []
    promotion_eligible: false
    reason: "publish is effectively irreversible — a package is live the instant it ships and may
             be cached or forked before any scrub (CLAUDE.md §Pre-Publish Surface Gate)."

  # ---- NOT promotion-eligible: TAINTED (this is the one that looks safe) -----------
  - name: local-commit
    owner: FH 4-axis commit gate
    mode: git-commit
    target: the working tree
    capabilities: [local-write, repo-mutation]
    sinks: []                             # committing itself is reversible
    feeds: [go-public]                    # ...but CI/sync publishes from it
    promotion_eligible: false
    reason: "taint — reversible in isolation, feeds an irreversible sink. Every step in the
             workflow-edit -> release-notes -> commit -> CI-publish chain is individually
             reversible; the chain is not."

  # ---- NOT promotion-eligible: unclassified ---------------------------------------
  - name: run-arbitrary-shell
    owner: (none — catch-all)
    mode: unrestricted
    target: unbounded
    capabilities: [read, local-write, network, dispatch, repo-mutation]
    sinks: [unknown]                      # unknown is NOT reversible
    feeds: [unknown]
    promotion_eligible: false
    reason: "sinks cannot be enumerated, so the floor cannot be decided mechanically. A class
             whose blast radius is unbounded is never a candidate, regardless of approval count."
