{
  "suite": "scope",
  "description": "An authority's scope of authority. Two questions over one match primitive, and getting either backwards has consequences an implementation cannot detect on its own: enforcement decides whether an authority MAY MINT, routing decides WHICH authority should own a subject.",
  "notes": [
    "ENFORCEMENT IS ALL-MUST-PASS ACROSS A RECORD'S PRODUCTS; ROUTING IS ANY-MATCH. With any-match enforcement, one in-scope package launders every other product on the record past a hard lock. With all-must-pass routing, a record touching one of your packages and one of somebody else's would route nowhere.",
    "ADVISORY NEVER AFFECTS AN OUTCOME IN EITHER DIRECTION, so it is filtered out of enforcement entirely. Let an ADVISORY match satisfy the closed list and an authority nullifies its own hard lock with one ADVISORY rule over every ecosystem. It still ROUTES, because an ADVISORY rule is precisely a statement of remit.",
    "HOLDING ONE HARD CLAIM MAKES THE WHOLE RULE SET A CLOSED LIST. A product matching no claim is refused when any HARD rule is present, flagged when only SOFT rules are, and allowed silently when there are neither.",
    "* IS THE ONLY METACHARACTER, and the rest of the pattern is escaped. Compiled as a bare regex, org.acme.* accepts orgXacmeY, so a rule written to hard-lock one namespace admits a neighbouring one. Over-matching a HARD rule is over-granting mint authority, which makes the escaping a security property rather than a nicety.",
    "SPECIFICITY IS A FIXED DIMENSION RANK, then pattern narrowness, then the rule id. It is not operator-tunable, because ranking decides which authority owns a subject and a tunable rank is a value two implementations could hold differently. The id tie-break exists so a routing decision does not change the day somebody adds an ordering to the query that loads rules."
  ],
  "enforcementCases": [
    {
      "id": "hard-rule-allows-what-it-claims",
      "rules": [
        {
          "id": "r-hard-npm-acme",
          "mode": "HARD",
          "dimension": "PACKAGE",
          "ecosystem": "npm",
          "packagePattern": "@acme/*"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@acme/widget"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW"
      }
    },
    {
      "id": "hard-rule-refuses-what-it-does-not-claim",
      "rules": [
        {
          "id": "r-hard-npm-acme",
          "mode": "HARD",
          "dimension": "PACKAGE",
          "ecosystem": "npm",
          "packagePattern": "@acme/*"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "outcome": "REFUSE"
      }
    },
    {
      "id": "one-in-scope-product-does-not-launder-the-others",
      "note": "THE enforcement case. All-must-pass is what stops a vendor hard-locked to its own packages minting a record that also names four belonging to somebody else. A genuinely mixed record needs a SOFT rule or a second authority, and forcing that conversation is the point.",
      "rules": [
        {
          "id": "r-hard-npm-acme",
          "mode": "HARD",
          "dimension": "PACKAGE",
          "ecosystem": "npm",
          "packagePattern": "@acme/*"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@acme/widget"
          },
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "outcome": "REFUSE"
      }
    },
    {
      "id": "soft-rule-allows-and-flags",
      "rules": [
        {
          "id": "r-soft-pypi",
          "mode": "SOFT",
          "dimension": "ECOSYSTEM",
          "ecosystem": "pypi"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "pypi",
            "packageName": "acme-parser"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW_FLAGGED"
      }
    },
    {
      "id": "no-claim-matches-and-a-soft-rule-exists",
      "note": "Only SOFT claims present, so the set is not a closed list; the unmatched product is flagged rather than refused.",
      "rules": [
        {
          "id": "r-soft-pypi",
          "mode": "SOFT",
          "dimension": "ECOSYSTEM",
          "ecosystem": "pypi"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW_FLAGGED"
      }
    },
    {
      "id": "advisory-only-never-refuses",
      "rules": [
        {
          "id": "r-advisory-any",
          "mode": "ADVISORY",
          "dimension": "ECOSYSTEM",
          "ecosystem": "*"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW"
      }
    },
    {
      "id": "advisory-cannot-satisfy-a-hard-lock",
      "note": "THE trap. If ADVISORY rules were left in the claim pool, adding one catch-all would nullify the hard lock beside it for every subject on earth, and the lock would become documentation of a constraint that enforces nothing.",
      "rules": [
        {
          "id": "r-hard-npm-acme",
          "mode": "HARD",
          "dimension": "PACKAGE",
          "ecosystem": "npm",
          "packagePattern": "@acme/*"
        },
        {
          "id": "r-advisory-any",
          "mode": "ADVISORY",
          "dimension": "ECOSYSTEM",
          "ecosystem": "*"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "outcome": "REFUSE"
      }
    },
    {
      "id": "no-rules-at-all-allows",
      "rules": [],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW"
      }
    },
    {
      "id": "empty-product-list-is-evaluated-once",
      "note": "'Nothing to check' is deliberately not the same as 'in scope'. A caller enforcing at mint must separately require that a record names what it affects, or this returns ALLOW for a record that asserts nothing.",
      "rules": [
        {
          "id": "r-hard-npm-acme",
          "mode": "HARD",
          "dimension": "PACKAGE",
          "ecosystem": "npm",
          "packagePattern": "@acme/*"
        }
      ],
      "subject": {
        "products": []
      },
      "expect": {
        "outcome": "REFUSE"
      }
    },
    {
      "id": "star-is-the-only-metacharacter",
      "note": "A dot in a pattern is a literal dot. Compiled as a bare regex, org.acme.* would accept orgXacmeY and the hard lock would admit a neighbouring namespace.",
      "rules": [
        {
          "id": "r-hard-maven",
          "mode": "HARD",
          "dimension": "PACKAGE",
          "ecosystem": "maven",
          "packagePattern": "org.acme.*"
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "maven",
            "packageName": "orgXacmeY"
          }
        ]
      },
      "expect": {
        "outcome": "REFUSE"
      }
    },
    {
      "id": "vendor-product-rule-matches-free-form-identity",
      "note": "Software with no package registry is still nameable; a rule can still claim it.",
      "rules": [
        {
          "id": "r-hard-vendor",
          "mode": "HARD",
          "dimension": "VENDOR_PRODUCT",
          "vendor": "acme",
          "product": "gateway"
        }
      ],
      "subject": {
        "products": [
          {
            "vendor": "acme",
            "product": "gateway"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW"
      }
    },
    {
      "id": "repository-rule-matches-a-normalised-repository",
      "note": "Both sides are normalised before matching, so a scheme and a .git suffix do not decide mint authority.",
      "rules": [
        {
          "id": "r-hard-repo",
          "mode": "HARD",
          "dimension": "REPOSITORY",
          "repository": "github.com/acme/*"
        }
      ],
      "subject": {
        "products": [
          {
            "repository": "https://github.com/acme/tool.git"
          }
        ]
      },
      "expect": {
        "outcome": "ALLOW"
      }
    }
  ],
  "routingCases": [
    {
      "id": "most-specific-dimension-wins",
      "note": "A package claim beats an ecosystem claim beats the catch-all.",
      "authorities": [
        {
          "authorityId": "a-last-resort",
          "rules": [
            {
              "id": "r-last-resort",
              "mode": "ADVISORY",
              "dimension": "LAST_RESORT"
            }
          ]
        },
        {
          "authorityId": "a-ecosystem",
          "rules": [
            {
              "id": "r-eco-npm",
              "mode": "HARD",
              "dimension": "ECOSYSTEM",
              "ecosystem": "npm"
            }
          ]
        },
        {
          "authorityId": "a-package",
          "rules": [
            {
              "id": "r-hard-npm-acme",
              "mode": "HARD",
              "dimension": "PACKAGE",
              "ecosystem": "npm",
              "packagePattern": "@acme/*"
            }
          ]
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@acme/widget"
          }
        ]
      },
      "expect": {
        "authorityId": "a-package",
        "ruleId": "r-hard-npm-acme"
      }
    },
    {
      "id": "last-resort-wins-only-when-nothing-else-matches",
      "authorities": [
        {
          "authorityId": "a-last-resort",
          "rules": [
            {
              "id": "r-last-resort",
              "mode": "ADVISORY",
              "dimension": "LAST_RESORT"
            }
          ]
        },
        {
          "authorityId": "a-package",
          "rules": [
            {
              "id": "r-hard-npm-acme",
              "mode": "HARD",
              "dimension": "PACKAGE",
              "ecosystem": "npm",
              "packagePattern": "@acme/*"
            }
          ]
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "authorityId": "a-last-resort",
        "ruleId": "r-last-resort"
      }
    },
    {
      "id": "routing-ignores-mode",
      "note": "An authority whose only matching rule is ADVISORY still routes, and then refuses nothing. Routing asks where a subject naturally belongs; whether that authority may mint is enforcement's separate question.",
      "authorities": [
        {
          "authorityId": "a-last-resort",
          "rules": [
            {
              "id": "r-last-resort",
              "mode": "ADVISORY",
              "dimension": "LAST_RESORT"
            }
          ]
        },
        {
          "authorityId": "a-advisory",
          "rules": [
            {
              "id": "r-adv-npm-acme",
              "mode": "ADVISORY",
              "dimension": "PACKAGE",
              "ecosystem": "npm",
              "packagePattern": "@acme/*"
            }
          ]
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@acme/widget"
          }
        ]
      },
      "expect": {
        "authorityId": "a-advisory",
        "ruleId": "r-adv-npm-acme"
      }
    },
    {
      "id": "any-match-routes-a-mixed-record",
      "note": "THE routing case, and the mirror of enforcement's all-must-pass. A record touching one of your packages and one of somebody else's is your business; requiring every product to match would leave it routing nowhere.",
      "authorities": [
        {
          "authorityId": "a-package",
          "rules": [
            {
              "id": "r-hard-npm-acme",
              "mode": "HARD",
              "dimension": "PACKAGE",
              "ecosystem": "npm",
              "packagePattern": "@acme/*"
            }
          ]
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          },
          {
            "ecosystem": "npm",
            "packageName": "@acme/widget"
          }
        ]
      },
      "expect": {
        "authorityId": "a-package",
        "ruleId": "r-hard-npm-acme"
      }
    },
    {
      "id": "nothing-matches-and-there-is-no-catch-all",
      "note": "null rather than an arbitrary pick. In practice the instance's last-resort authority carries a LAST_RESORT rule, which is what makes it the floor rather than a special case in the routing function.",
      "authorities": [
        {
          "authorityId": "a-package",
          "rules": [
            {
              "id": "r-hard-npm-acme",
              "mode": "HARD",
              "dimension": "PACKAGE",
              "ecosystem": "npm",
              "packagePattern": "@acme/*"
            }
          ]
        }
      ],
      "subject": {
        "products": [
          {
            "ecosystem": "npm",
            "packageName": "@other/widget"
          }
        ]
      },
      "expect": {
        "authorityId": null
      }
    }
  ]
}
