{
  "id": "P108",
  "slug": "feature-flags",
  "key": "P108-feature-flags",
  "title": "Feature flags",
  "summary": "Three environments compared as sets of flags - who is ahead, and what a promotion would turn on and off - and flags rolled out to a share of users by a bucket each user keeps, so a larger share only adds users; allow and deny lists; and the bucket against a new draw at every check.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P108 - Feature flags\n\nFeature flags in three environments, compared as sets. Flags can also be\nrolled out to a share of users, each user keeping one bucket per flag.\n\n- `main.eml` - the menu, the environments table, one user's flags, a\n  change of share, bucket against a new draw, and the allow and deny lists\n- `flags.eml` - the known flags, set questions on lists, the bucket hash,\n  the decision for one user, and counting over many users\n\nHow each part works:\n\n- Environments, as in the corpus case `feature-flag-rollout`: whether one\n  environment already has every flag of another is a subset question, and\n  what promoting it would change is a difference. Two environments can\n  each have a flag the other lacks, and then neither is ahead. Flags are\n  always listed in one fixed order.\n- A user's bucket for a flag is 0 to 9999. It comes from a hash of the\n  flag's name, a colon and the user's id: each character is folded in and\n  the result cubed, modulo the prime 4294967291, and the last step is\n  modulo 10000.\n  Cubing is one-to-one modulo that prime, and it is not linear. With a\n  plain polynomial hash, one user's buckets for two flags were correlated\n  (0.32), so a user early in one rollout tended to be early in the next.\n- The decision: the deny list first, then the allow list, then the user is\n  in when the bucket is under the share (in hundredths of a percent).\n  Because the bucket never changes, raising the share only adds users and\n  lowering it only removes them.\n- Against a new draw at every check, the share comes out about right too,\n  but users flip from one check to the next.\n\nWhat is checked: menu choices 1 to 6; user ids of up to 20 letters,\ndigits and - _ . @; a flag that is rolled out; a share from 0 to 100% with\nat most two decimals; allow or deny. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the environments: dev is ahead of staging, while staging and prod, and\n  dev and prod, each have a flag the other lacks;\n- the flags of ana (on by the allow list, off by the deny list), user42\n  and ops-bot;\n- new-checkout raised from 10% to 20% and 50% with nobody leaving, then\n  lowered to 5% with nobody joining;\n- search-v2 by bucket against a new draw: 0 users flip against 411;\n- user42 added to search-v2's allow list, and their flags again.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a cancel, a user id too long, and one with a !;\n- shares: a cancel, a flag that is not rolled out, a cancel, 101, abc and\n  12.345;\n- a cancel for bucket or new draw;\n- lists: neither allow nor deny, two cancels, and a user who is already\n  in the list.\n\nBuilt on the verified corpus case `feature-flag-rollout` (being ready to\npromote is a subset question, and what would change is a difference).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P108 feature flags: environments compared as sets of flags, one user's\n# flags and why, a rollout raised or lowered, and stable buckets against a\n# new draw at every check.\nimport flags\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef listed(xs):\n    if len(xs) == 0:\n        return \"(none)\"\n    \"\" => out\n    for x in xs:\n        if out != \"\":\n            out + \", \" => out\n        out + x => out\n    return out\n\ndef percent(bp):\n    # hundredths of a percent as a percent\n    bp % 100 => f\n    str(int((bp - f) / 100)) => s\n    if f == 0:\n        return s + \"%\"\n    if f < 10:\n        return s + \".0\" + str(f) + \"%\"\n    return s + \".\" + str(f) + \"%\"\n\ndef ask_user():\n    plain(input(\"user id (letters, digits, - _ . @)> \")) => u\n    if u == \"\":\n        return \"\"\n    if len(u) > 20:\n        \"Keep a user id to 20 characters.\" ^0\n        return \"!\"\n    for c in u:\n        if not ((c >= \"a\" and c <= \"z\") or (c >= \"A\" and c <= \"Z\") or (c >= \"0\" and c <= \"9\") or c == \"-\" or c == \"_\" or c == \".\" or c == \"@\"):\n            \"A user id has letters, digits and - _ . @ only.\" ^0\n            return \"!\"\n    return u\n\ndef ask_flag(rules):\n    plain(input(\"flag (\" + listed(rule_names(rules)) + \")> \")) => f\n    if f == \"\":\n        return \"\"\n    if not (f in rules):\n        \"Pick a flag that is rolled out.\" ^0\n        return \"!\"\n    return f\n\ndef rule_names(rules):\n    [] => out\n    for f in flags.known():\n        if f in rules:\n            out + [f] => out\n    return out\n\ndef environments(envs):\n    \"  environment  flags\" ^0\n    for e in [\"dev\", \"staging\", \"prod\"]:\n        (\"  \" + pad(e, 13) + listed(flags.in_order(envs[e]))) ^0\n    for pair in [[\"dev\", \"staging\"], [\"staging\", \"prod\"], [\"dev\", \"prod\"]]:\n        envs[pair[0]] => a\n        envs[pair[1]] => b\n        (\"Promoting \" + pair[0] + \" to \" + pair[1] + \" would turn on \" + listed(flags.minus(a, b)) + \" and turn off \" + listed(flags.minus(b, a)) + \".\") ^0\n        if flags.subset(a, b) and flags.subset(b, a):\n            (\"  The two have the same flags.\") ^0\n        elif flags.subset(a, b):\n            (\"  \" + pair[1] + \" already has every flag of \" + pair[0] + \".\") ^0\n        elif flags.subset(b, a):\n            (\"  \" + pair[0] + \" has every flag of \" + pair[1] + \" and more.\") ^0\n        else:\n            (\"  Neither has every flag of the other, so neither is ahead: there is only the difference.\") ^0\n\ndef one_user(rules):\n    ask_user() => u\n    if u == \"\":\n        \"Cancelled.\" ^0\n        return\n    if u == \"!\":\n        return\n    for f in rule_names(rules):\n        flags.decide(rules[f], f, u) => d\n        \"off\" => state\n        if d[0]:\n            \"on \" => state\n        (\"  \" + pad(f, 14) + state + \"  \" + d[1] + \" (\" + percent(rules[f][0]) + \" rolled out)\") ^0\n    \"The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.\" ^0\n\ndef roll(rules, people):\n    ask_flag(rules) => f\n    if f == \"\":\n        \"Cancelled.\" ^0\n        return rules\n    if f == \"!\":\n        return rules\n    plain(input(\"new share in percent (0 to 100, two decimals at most)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return rules\n    -1 => bp\n    \"\" => whole\n    \"\" => frac\n    False => dot\n    True => ok\n    for c in t:\n        if c == \".\" and not dot:\n            True => dot\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                frac + c => frac\n            else:\n                whole + c => whole\n        else:\n            False => ok\n    if ok and whole != \"\" and len(whole) <= 3 and len(frac) <= 2 and not (dot and frac == \"\"):\n        while len(frac) < 2:\n            frac + \"0\" => frac\n        int(whole) * 100 + int(frac) => bp\n    if bp < 0 or bp > 10000:\n        \"Type a share from 0 to 100, at most two decimals.\" ^0\n        return rules\n    rules[f] => old\n    [bp, old[1], old[2]] => new\n    flags.change(old, new, f, people) => c\n    (f + \" from \" + percent(old[0]) + \" to \" + percent(bp) + \", over \" + str(len(people)) + \" users:\") ^0\n    (\"  on before \" + str(c[0]) + \", on after \" + str(c[1]) + \": \" + str(c[2]) + \" stayed on, \" + str(c[3]) + \" joined, \" + str(c[4]) + \" left.\") ^0\n    if bp >= old[0]:\n        \"Raising the share only adds users: everyone whose bucket was under the old share is under the new one.\" ^0\n    else:\n        \"Lowering the share only removes users, those whose buckets lie between the new share and the old.\" ^0\n    new => rules[f]\n    return rules\n\ndef stable_vs_drawn(rules, people):\n    ask_flag(rules) => f\n    if f == \"\":\n        \"Cancelled.\" ^0\n        return\n    if f == \"!\":\n        return\n    rules[f] => r\n    flags.count_on(r, f, people) => n\n    flags.rerolled(r[0], people, 2026) => d\n    (f + \" at \" + percent(r[0]) + \", each of \" + str(len(people)) + \" users checked twice:\") ^0\n    (\"  by bucket:             \" + str(n) + \" on at the first check, \" + str(n) + \" at the second, 0 users flipped\") ^0\n    (\"  a new draw each time:  \" + str(d[0]) + \" on at the first check, \" + str(d[1]) + \" at the second, \" + str(d[2]) + \" users flipped\") ^0\n    \"The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.\" ^0\n\ndef lists(rules):\n    ask_flag(rules) => f\n    if f == \"\":\n        \"Cancelled.\" ^0\n        return rules\n    if f == \"!\":\n        return rules\n    plain(input(\"allow or deny> \")) => k\n    if k != \"allow\" and k != \"deny\":\n        if k == \"\":\n            \"Cancelled.\" ^0\n        else:\n            \"Type allow or deny.\" ^0\n        return rules\n    ask_user() => u\n    if u == \"\":\n        \"Cancelled.\" ^0\n        return rules\n    if u == \"!\":\n        return rules\n    rules[f] => r\n    1 => slot\n    if k == \"deny\":\n        2 => slot\n    if flags.has(r[slot], u):\n        (u + \" is already in the \" + k + \" list of \" + f + \".\") ^0\n        return rules\n    r[slot] + [u] => r[slot]\n    r => rules[f]\n    (u + \" added to the \" + k + \" list of \" + f + \": \" + listed(r[slot]) + \".\") ^0\n    return rules\n\n{\"dev\": [\"new-checkout\", \"dark-mode\", \"search-v2\", \"beta-banner\"], \"staging\": [\"dark-mode\", \"new-checkout\"], \"prod\": [\"dark-mode\", \"legacy-export\"]} => envs\n{\"new-checkout\": [1000, [\"ana\"], [\"ops-bot\"]], \"dark-mode\": [10000, [], []], \"search-v2\": [2500, [], [\"ana\"]]} => rules\nflags.users(1000) => people\n\n\"== Feature flags ==\" ^0\n\"Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        environments(envs)\n    elif choice == \"2\":\n        one_user(rules)\n    elif choice == \"3\":\n        roll(rules, people) => rules\n    elif choice == \"4\":\n        stable_vs_drawn(rules, people)\n    elif choice == \"5\":\n        lists(rules) => rules\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import flags\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef listed(xs):\n    if len(xs) == 0:\n        return \"(none)\"\n    out = \"\"\n    for x in xs:\n        if out != \"\":\n            out = out + \", \"\n        out = out + x\n    return out\n\ndef percent(bp):\n    f = bp % 100\n    s = str(int((bp - f) / 100))\n    if f == 0:\n        return s + \"%\"\n    if f < 10:\n        return s + \".0\" + str(f) + \"%\"\n    return s + \".\" + str(f) + \"%\"\n\ndef ask_user():\n    u = plain(input(\"user id (letters, digits, - _ . @)> \"))\n    if u == \"\":\n        return \"\"\n    if len(u) > 20:\n        print(\"Keep a user id to 20 characters.\")\n        return \"!\"\n    for c in u:\n        if not (c >= \"a\" and c <= \"z\" or c >= \"A\" and c <= \"Z\" or c >= \"0\" and c <= \"9\" or c == \"-\" or c == \"_\" or c == \".\" or c == \"@\"):\n            print(\"A user id has letters, digits and - _ . @ only.\")\n            return \"!\"\n    return u\n\ndef ask_flag(rules):\n    f = plain(input(\"flag (\" + listed(rule_names(rules)) + \")> \"))\n    if f == \"\":\n        return \"\"\n    if not f in rules:\n        print(\"Pick a flag that is rolled out.\")\n        return \"!\"\n    return f\n\ndef rule_names(rules):\n    out = []\n    for f in flags.known():\n        if f in rules:\n            out = out + [f]\n    return out\n\ndef environments(envs):\n    print(\"  environment  flags\")\n    for e in [\"dev\", \"staging\", \"prod\"]:\n        print(\"  \" + pad(e, 13) + listed(flags.in_order(envs[e])))\n    for pair in [[\"dev\", \"staging\"], [\"staging\", \"prod\"], [\"dev\", \"prod\"]]:\n        a = envs[pair[0]]\n        b = envs[pair[1]]\n        print(\"Promoting \" + pair[0] + \" to \" + pair[1] + \" would turn on \" + listed(flags.minus(a, b)) + \" and turn off \" + listed(flags.minus(b, a)) + \".\")\n        if flags.subset(a, b) and flags.subset(b, a):\n            print(\"  The two have the same flags.\")\n        elif flags.subset(a, b):\n            print(\"  \" + pair[1] + \" already has every flag of \" + pair[0] + \".\")\n        elif flags.subset(b, a):\n            print(\"  \" + pair[0] + \" has every flag of \" + pair[1] + \" and more.\")\n        else:\n            print(\"  Neither has every flag of the other, so neither is ahead: there is only the difference.\")\n\ndef one_user(rules):\n    u = ask_user()\n    if u == \"\":\n        print(\"Cancelled.\")\n        return\n    if u == \"!\":\n        return\n    for f in rule_names(rules):\n        d = flags.decide(rules[f], f, u)\n        state = \"off\"\n        if d[0]:\n            state = \"on \"\n        print(\"  \" + pad(f, 14) + state + \"  \" + d[1] + \" (\" + percent(rules[f][0]) + \" rolled out)\")\n    print(\"The bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.\")\n\ndef roll(rules, people):\n    f = ask_flag(rules)\n    if f == \"\":\n        print(\"Cancelled.\")\n        return rules\n    if f == \"!\":\n        return rules\n    t = plain(input(\"new share in percent (0 to 100, two decimals at most)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return rules\n    bp = -1\n    whole = \"\"\n    frac = \"\"\n    dot = False\n    ok = True\n    for c in t:\n        if c == \".\" and not dot:\n            dot = True\n        elif c >= \"0\" and c <= \"9\":\n            if dot:\n                frac = frac + c\n            else:\n                whole = whole + c\n        else:\n            ok = False\n    if ok and whole != \"\" and len(whole) <= 3 and len(frac) <= 2 and not (dot and frac == \"\"):\n        while len(frac) < 2:\n            frac = frac + \"0\"\n        bp = int(whole) * 100 + int(frac)\n    if bp < 0 or bp > 10000:\n        print(\"Type a share from 0 to 100, at most two decimals.\")\n        return rules\n    old = rules[f]\n    new = [bp, old[1], old[2]]\n    c = flags.change(old, new, f, people)\n    print(f + \" from \" + percent(old[0]) + \" to \" + percent(bp) + \", over \" + str(len(people)) + \" users:\")\n    print(\"  on before \" + str(c[0]) + \", on after \" + str(c[1]) + \": \" + str(c[2]) + \" stayed on, \" + str(c[3]) + \" joined, \" + str(c[4]) + \" left.\")\n    if bp >= old[0]:\n        print(\"Raising the share only adds users: everyone whose bucket was under the old share is under the new one.\")\n    else:\n        print(\"Lowering the share only removes users, those whose buckets lie between the new share and the old.\")\n    rules[f] = new\n    return rules\n\ndef stable_vs_drawn(rules, people):\n    f = ask_flag(rules)\n    if f == \"\":\n        print(\"Cancelled.\")\n        return\n    if f == \"!\":\n        return\n    r = rules[f]\n    n = flags.count_on(r, f, people)\n    d = flags.rerolled(r[0], people, 2026)\n    print(f + \" at \" + percent(r[0]) + \", each of \" + str(len(people)) + \" users checked twice:\")\n    print(\"  by bucket:             \" + str(n) + \" on at the first check, \" + str(n) + \" at the second, 0 users flipped\")\n    print(\"  a new draw each time:  \" + str(d[0]) + \" on at the first check, \" + str(d[1]) + \" at the second, \" + str(d[2]) + \" users flipped\")\n    print(\"The share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.\")\n\ndef lists(rules):\n    f = ask_flag(rules)\n    if f == \"\":\n        print(\"Cancelled.\")\n        return rules\n    if f == \"!\":\n        return rules\n    k = plain(input(\"allow or deny> \"))\n    if k != \"allow\" and k != \"deny\":\n        if k == \"\":\n            print(\"Cancelled.\")\n        else:\n            print(\"Type allow or deny.\")\n        return rules\n    u = ask_user()\n    if u == \"\":\n        print(\"Cancelled.\")\n        return rules\n    if u == \"!\":\n        return rules\n    r = rules[f]\n    slot = 1\n    if k == \"deny\":\n        slot = 2\n    if flags.has(r[slot], u):\n        print(u + \" is already in the \" + k + \" list of \" + f + \".\")\n        return rules\n    r[slot] = r[slot] + [u]\n    rules[f] = r\n    print(u + \" added to the \" + k + \" list of \" + f + \": \" + listed(r[slot]) + \".\")\n    return rules\n\nenvs = {\"dev\": [\"new-checkout\", \"dark-mode\", \"search-v2\", \"beta-banner\"], \"staging\": [\"dark-mode\", \"new-checkout\"], \"prod\": [\"dark-mode\", \"legacy-export\"]}\nrules = {\"new-checkout\": [1000, [\"ana\"], [\"ops-bot\"]], \"dark-mode\": [10000, [], []], \"search-v2\": [2500, [], [\"ana\"]]}\npeople = flags.users(1000)\nprint(\"== Feature flags ==\")\nprint(\"Environments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        environments(envs)\n    elif choice == \"2\":\n        one_user(rules)\n    elif choice == \"3\":\n        rules = roll(rules, people)\n    elif choice == \"4\":\n        stable_vs_drawn(rules, people)\n    elif choice == \"5\":\n        rules = lists(rules)\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "flags.eml",
      "eml": "# P108 feature flags - as in the corpus case feature-flag-rollout, whether\n# one environment already has every flag of another is a subset question,\n# and what promoting it would change is a difference. A flag can also be\n# rolled out to a share of users: each user falls in a bucket from 0 to\n# 9999 by a hash of the flag's name and the user's id, the same bucket\n# every time, so raising the share only ever adds users. An allow list\n# turns the flag on for a user and a deny list turns it off, the deny list\n# first.\n\ndef known():\n    # every flag, in the order reports list them\n    return [\"new-checkout\", \"dark-mode\", \"search-v2\", \"beta-banner\", \"legacy-export\"]\n\ndef has(xs, x):\n    for y in xs:\n        if y == x:\n            return True\n    return False\n\ndef subset(a, b):\n    # every flag of a is in b\n    for x in a:\n        if not has(b, x):\n            return False\n    return True\n\ndef minus(a, b):\n    # the flags of a not in b, in the known order\n    [] => out\n    for x in known():\n        if has(a, x) and not has(b, x):\n            out + [x] => out\n    return out\n\ndef in_order(a):\n    [] => out\n    for x in known():\n        if has(a, x):\n            out + [x] => out\n    return out\n\ndef codes():\n    # every character a flag name or a user id can hold, numbered from 1\n    \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@:\" => chars\n    {} => out\n    for i in [0:len(chars) - 1]:\n        i + 1 => out[chars[i]]\n    return out\n\ncodes() => code_table\n\ndef bucket(flag, user):\n    # 0 to 9999: each character of flag:user is folded in and the result\n    # cubed, modulo the prime P = 4294967291, then taken modulo 10000.\n    # Cubing is a one-to-one map modulo P (3 does not divide P - 1) and is\n    # not linear: a plain polynomial hash gives one user's buckets for two\n    # flags a correlation of 0.32, so a user early in one rollout would\n    # tend to be early in the next.\n    flag + \":\" + user => s\n    0 => h\n    for c in s:\n        (h * 257 + code_table[c]) % 4294967291 => t\n        t * t % 4294967291 * t % 4294967291 => h\n    return h % 10000\n\ndef decide(rule, flag, user):\n    # rule is [share in hundredths of a percent, allow list, deny list];\n    # [on, why]\n    if has(rule[2], user):\n        return [False, \"in the deny list\"]\n    if has(rule[1], user):\n        return [True, \"in the allow list\"]\n    bucket(flag, user) => b\n    if b < rule[0]:\n        return [True, \"bucket \" + str(b) + \" is under \" + str(rule[0])]\n    return [False, \"bucket \" + str(b) + \" is not under \" + str(rule[0])]\n\ndef users(n):\n    [] => out\n    for i in [1:n]:\n        out + [\"user\" + str(i)] => out\n    return out\n\ndef count_on(rule, flag, people):\n    0 => n\n    for u in people:\n        if decide(rule, flag, u)[0]:\n            n + 1 => n\n    return n\n\ndef change(old, new, flag, people):\n    # [on before, on after, stayed on, joined, left]\n    0 => before\n    0 => after\n    0 => stayed\n    0 => joined\n    0 => left\n    for u in people:\n        decide(old, flag, u)[0] => a\n        decide(new, flag, u)[0] => b\n        if a:\n            before + 1 => before\n        if b:\n            after + 1 => after\n        if a and b:\n            stayed + 1 => stayed\n        if b and not a:\n            joined + 1 => joined\n        if a and not b:\n            left + 1 => left\n    return [before, after, stayed, joined, left]\n\ndef rerolled(share, people, seed):\n    # every check draws a new number from 0 to 9999 instead of the bucket:\n    # [on at the first check, on at the second, users who flipped]\n    [seed] => x\n    0 => first\n    0 => second\n    0 => flipped\n    for u in people:\n        (1664525 * x[0] + 1013904223) % 4294967296 => x[0]\n        (int((x[0] - x[0] % 65536) / 65536) % 10000 < share) => a\n        (1664525 * x[0] + 1013904223) % 4294967296 => x[0]\n        (int((x[0] - x[0] % 65536) / 65536) % 10000 < share) => b\n        if a:\n            first + 1 => first\n        if b:\n            second + 1 => second\n        if a != b:\n            flipped + 1 => flipped\n    return [first, second, flipped]\n",
      "python": "def known():\n    return [\"new-checkout\", \"dark-mode\", \"search-v2\", \"beta-banner\", \"legacy-export\"]\n\ndef has(xs, x):\n    for y in xs:\n        if y == x:\n            return True\n    return False\n\ndef subset(a, b):\n    for x in a:\n        if not has(b, x):\n            return False\n    return True\n\ndef minus(a, b):\n    out = []\n    for x in known():\n        if has(a, x) and not has(b, x):\n            out = out + [x]\n    return out\n\ndef in_order(a):\n    out = []\n    for x in known():\n        if has(a, x):\n            out = out + [x]\n    return out\n\ndef codes():\n    chars = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@:\"\n    out = {}\n    for i in range(0, len(chars)):\n        out[chars[i]] = i + 1\n    return out\n\ncode_table = codes()\n\ndef bucket(flag, user):\n    s = flag + \":\" + user\n    h = 0\n    for c in s:\n        t = (h * 257 + code_table[c]) % 4294967291\n        h = t * t % 4294967291 * t % 4294967291\n    return h % 10000\n\ndef decide(rule, flag, user):\n    if has(rule[2], user):\n        return [False, \"in the deny list\"]\n    if has(rule[1], user):\n        return [True, \"in the allow list\"]\n    b = bucket(flag, user)\n    if b < rule[0]:\n        return [True, \"bucket \" + str(b) + \" is under \" + str(rule[0])]\n    return [False, \"bucket \" + str(b) + \" is not under \" + str(rule[0])]\n\ndef users(n):\n    out = []\n    for i in range(1, n+1):\n        out = out + [\"user\" + str(i)]\n    return out\n\ndef count_on(rule, flag, people):\n    n = 0\n    for u in people:\n        if decide(rule, flag, u)[0]:\n            n = n + 1\n    return n\n\ndef change(old, new, flag, people):\n    before = 0\n    after = 0\n    stayed = 0\n    joined = 0\n    left = 0\n    for u in people:\n        a = decide(old, flag, u)[0]\n        b = decide(new, flag, u)[0]\n        if a:\n            before = before + 1\n        if b:\n            after = after + 1\n        if a and b:\n            stayed = stayed + 1\n        if b and not a:\n            joined = joined + 1\n        if a and not b:\n            left = left + 1\n    return [before, after, stayed, joined, left]\n\ndef rerolled(share, people, seed):\n    x = [seed]\n    first = 0\n    second = 0\n    flipped = 0\n    for u in people:\n        x[0] = (1664525 * x[0] + 1013904223) % 4294967296\n        a = int((x[0] - x[0] % 65536) / 65536) % 10000 < share\n        x[0] = (1664525 * x[0] + 1013904223) % 4294967296\n        b = int((x[0] - x[0] % 65536) / 65536) % 10000 < share\n        if a:\n            first = first + 1\n        if b:\n            second = second + 1\n        if a != b:\n            flipped = flipped + 1\n    return [first, second, flipped]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n2\n\n2\na very long user identifier\n2\nbad id!\n3\n\n3\nfancy\n3\nsearch-v2\n\n3\nsearch-v2\n101\n3\nsearch-v2\nabc\n3\nsearch-v2\n12.345\n4\n\n5\ndark-mode\nmaybe\n5\ndark-mode\n\n5\ndark-mode\ndeny\n\n5\nnew-checkout\nallow\nana\n6\n",
      "screen": "== Feature flags ==\nEnvironments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> \nCancelled.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> a very long user identifier\nKeep a user id to 20 characters.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> bad id!\nA user id has letters, digits and - _ . @ only.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> \nCancelled.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> fancy\nPick a flag that is rolled out.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> search-v2\nnew share in percent (0 to 100, two decimals at most)> \nCancelled.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> search-v2\nnew share in percent (0 to 100, two decimals at most)> 101\nType a share from 0 to 100, at most two decimals.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> search-v2\nnew share in percent (0 to 100, two decimals at most)> abc\nType a share from 0 to 100, at most two decimals.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> search-v2\nnew share in percent (0 to 100, two decimals at most)> 12.345\nType a share from 0 to 100, at most two decimals.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 4\nflag (new-checkout, dark-mode, search-v2)> \nCancelled.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 5\nflag (new-checkout, dark-mode, search-v2)> dark-mode\nallow or deny> maybe\nType allow or deny.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 5\nflag (new-checkout, dark-mode, search-v2)> dark-mode\nallow or deny> \nCancelled.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 5\nflag (new-checkout, dark-mode, search-v2)> dark-mode\nallow or deny> deny\nuser id (letters, digits, - _ . @)> \nCancelled.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 5\nflag (new-checkout, dark-mode, search-v2)> new-checkout\nallow or deny> allow\nuser id (letters, digits, - _ . @)> ana\nana is already in the allow list of new-checkout.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\nana\n2\nuser42\n2\nops-bot\n3\nnew-checkout\n20\n3\nnew-checkout\n50\n3\nnew-checkout\n5\n4\nsearch-v2\n5\nsearch-v2\nallow\nuser42\n2\nuser42\n6\n",
      "screen": "== Feature flags ==\nEnvironments as sets of flags, and flags rolled out to a share of users by a bucket each user keeps.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 1\n  environment  flags\n  dev          new-checkout, dark-mode, search-v2, beta-banner\n  staging      new-checkout, dark-mode\n  prod         dark-mode, legacy-export\nPromoting dev to staging would turn on search-v2, beta-banner and turn off (none).\n  dev has every flag of staging and more.\nPromoting staging to prod would turn on new-checkout and turn off legacy-export.\n  Neither has every flag of the other, so neither is ahead: there is only the difference.\nPromoting dev to prod would turn on new-checkout, search-v2, beta-banner and turn off legacy-export.\n  Neither has every flag of the other, so neither is ahead: there is only the difference.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> ana\n  new-checkout  on   in the allow list (10% rolled out)\n  dark-mode     on   bucket 2435 is under 10000 (100% rolled out)\n  search-v2     off  in the deny list (25% rolled out)\nThe bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> user42\n  new-checkout  off  bucket 5396 is not under 1000 (10% rolled out)\n  dark-mode     on   bucket 5395 is under 10000 (100% rolled out)\n  search-v2     off  bucket 7645 is not under 2500 (25% rolled out)\nThe bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> ops-bot\n  new-checkout  off  in the deny list (10% rolled out)\n  dark-mode     on   bucket 7177 is under 10000 (100% rolled out)\n  search-v2     off  bucket 8997 is not under 2500 (25% rolled out)\nThe bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> new-checkout\nnew share in percent (0 to 100, two decimals at most)> 20\nnew-checkout from 10% to 20%, over 1000 users:\n  on before 76, on after 178: 76 stayed on, 102 joined, 0 left.\nRaising the share only adds users: everyone whose bucket was under the old share is under the new one.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> new-checkout\nnew share in percent (0 to 100, two decimals at most)> 50\nnew-checkout from 20% to 50%, over 1000 users:\n  on before 178, on after 499: 178 stayed on, 321 joined, 0 left.\nRaising the share only adds users: everyone whose bucket was under the old share is under the new one.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 3\nflag (new-checkout, dark-mode, search-v2)> new-checkout\nnew share in percent (0 to 100, two decimals at most)> 5\nnew-checkout from 50% to 5%, over 1000 users:\n  on before 499, on after 38: 38 stayed on, 0 joined, 461 left.\nLowering the share only removes users, those whose buckets lie between the new share and the old.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 4\nflag (new-checkout, dark-mode, search-v2)> search-v2\nsearch-v2 at 25%, each of 1000 users checked twice:\n  by bucket:             218 on at the first check, 218 at the second, 0 users flipped\n  a new draw each time:  257 on at the first check, 268 at the second, 411 users flipped\nThe share comes out about right either way; only the bucket keeps each user's answer the same from one check to the next.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 5\nflag (new-checkout, dark-mode, search-v2)> search-v2\nallow or deny> allow\nuser id (letters, digits, - _ . @)> user42\nuser42 added to the allow list of search-v2: user42.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 2\nuser id (letters, digits, - _ . @)> user42\n  new-checkout  off  bucket 5396 is not under 500 (5% rolled out)\n  dark-mode     on   bucket 5395 is under 10000 (100% rolled out)\n  search-v2     on   in the allow list (25% rolled out)\nThe bucket comes from the flag's name and the user's id, so it is the same at every check, and different flags put the same user in unrelated buckets.\n\n1) environments  2) one user  3) change a share  4) bucket or new draw  5) allow or deny  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "feature-flag-rollout",
      "caseId": "170-feature-flag-rollout",
      "title": "Promotion as a subset question"
    }
  ],
  "updated": "2026-10-16"
}
