maelys-git

Level 2 — Structured deny codes

Keep machine-stable denial codes separate from human-readable messages.

Continue from Level 1 — Minimal gate. Keep the declarations and helpers from the earlier levels; this page extends the same wrapper in C and Python.

Level 2 separates machine-stable decision codes from human-readable messages. The policy derives facts such as deny("push", "GIT_FORCE_PUSH"), while the wrapper maps GIT_FORCE_PUSH to a user-facing message. This keeps audit records stable even when wording, localization, or UI messages change.

The principle

CODE
deny("push", "GIT_FORCE_PUSH")     ← stable code in policy
    ↓
C lookup table:
  "GIT_FORCE_PUSH"        → "force-push is forbidden"
  "GIT_PROTECTED_BRANCH"  → "direct push to protected branch"
  "GIT_RELEASE_BRANCH"    → "only release managers can push to release branches"
  "GIT_BLOCKED_USER"      → "user is not allowed to commit"
  "GIT_BAD_COMMIT_PREFIX" → "commit message must start with feat: fix: ..."
  "GIT_DELETE_PROTECTED"  → "deleting a protected branch is forbidden"
  "GIT_SENSITIVE_FILE"    → "this file requires explicit approval"

Human messages can change without touching the policy. The deny code is what appears in audit records and logs — it is stable across policy versions.

Message table in the wrapper

CODETurn stable denial codes into messages
static const struct { const char *code; const char *message; }
deny_messages[] = {
    { "GIT_FORCE_PUSH",        "force-push is forbidden" },
    { "GIT_PROTECTED_BRANCH",  "direct push to protected branch is forbidden" },
    { "GIT_BLOCKED_USER",      "user is not allowed to commit" },
    { "GIT_BAD_COMMIT_PREFIX", "commit message must start with feat: fix: chore: docs: refactor:" },
    { "GIT_DELETE_PROTECTED",  "deleting a protected branch is forbidden" },
    { "GIT_SENSITIVE_FILE",    "this file requires explicit approval" },
    { "GIT_RELEASE_BRANCH",   "only release managers can push to release branches" },
    { NULL, NULL }
};

static const char *message_for_code(const char *code) {
    for (int i = 0; deny_messages[i].code; i++)
        if (strcmp(deny_messages[i].code, code) == 0)
            return deny_messages[i].message;
    return code;  /* fallback: show code if unknown */
}

What you have learned

Keep this model

Separate denial codes from human messages
  • Policy produces stable codes

    A denial fact carries a machine-readable code identifying the reason. That code can remain stable while wording changes.

  • The application presents the message

    The wrapper maps the code to a readable explanation. Message wording and localization do not need to change the policy.

  • Membership and enumeration answer different questions

    A membership query checks a fully specified fact. To retrieve the codes present in a result, enumerate denial facts as shown in the audit level.

Continue