<!-- canonical: efficientnewlanguage.org/ai/examples/304-rollup-double-counts-shared-parent | ai_layer_version: 0.1.0 | updated: 2026-08-08 -->

# Example 304 — Rollup double counts shared parent — the thing it is walking is not a tree

`rollup_double_counts_shared_parent.eml` runs the recursive rollup and a set-based rollup that visits each node once, then attributes the gap to specific nodes by counting how many distinct root-to-node paths reach each one.

## EML

```eml
# Self-authored for the EML case corpus (no external origin). The rollup walks
# the tree, and the thing it is walking is not a tree.
#
# Cost rollups, headcount rollups, storage rollups: sum a node's own value plus
# the rollups of its children, recursively. That is correct for a tree, and the
# structures it gets pointed at are almost never trees. A team reports into two
# departments after a reorg. A component is used by two products. A folder is
# linked into two projects. Each of those is a legitimate, deliberate,
# documented arrangement, and each turns the hierarchy into a DAG.
#
# On a DAG the recursive sum counts every shared node once per PATH that
# reaches it. The total at the root is not "the sum of everything below" but
# "the sum over all root-to-leaf paths", and those differ by exactly the shared
# subtrees. The number is stable, reproducible, and matches itself on every
# re-run, which is why it survives review.
#
# The measurement runs the recursive rollup and a set-based rollup that visits
# each node once, then reports the gap and attributes it to specific nodes -
# computed by counting how many distinct paths reach each one.

def children_of(node):
    if node in EDGES:
        return EDGES[node]
    return []

def own_cost(node):
    if node in COST:
        return COST[node]
    return 0

def rollup_recursive(node):
    # The obvious implementation. Correct on a tree.
    own_cost(node) => total
    for c in children_of(node):
        total + rollup_recursive(c) => total
    return total

def reachable(node):
    # Every distinct node at or below `node`, each once.
    [node] => seen
    [node] => stack
    while len(stack) > 0:
        stack[len(stack) - 1] => cur
        stack[0:len(stack) - 1] => stack
        for c in children_of(cur):
            if not (c in seen):
                seen + [c] => seen
                stack + [c] => stack
    return seen

def rollup_set(node):
    0 => total
    for n in reachable(node):
        total + own_cost(n) => total
    return total

def path_count(root, target):
    # How many distinct root-to-target paths exist. This is the multiplier the
    # recursive rollup applies, and it is derived here rather than assumed.
    if root == target:
        return 1
    0 => n
    for c in children_of(root):
        n + path_count(c, target) => n
    return n

{"org": ["eng", "ops"],
 "eng": ["platform", "shared-tools"],
 "ops": ["shared-tools", "support"],
 "platform": ["db-team"],
 "shared-tools": ["build-team"],
 "support": [],
 "db-team": [],
 "build-team": []} => EDGES

{"org": 0, "eng": 10, "ops": 8, "platform": 20, "shared-tools": 30,
 "support": 15, "db-team": 25, "build-team": 40} => COST

["org", "eng", "ops", "platform", "shared-tools", "support", "db-team", "build-team"] => NODES

"node           recursive  set-based  gap"^0
"-------------- ---------  ---------  ---"^0
0 => nodes_with_gap
for n in NODES:
    rollup_recursive(n) => r
    rollup_set(n) => s
    r - s => gap
    if not (gap == 0):
        nodes_with_gap + 1 => nodes_with_gap
    ((n + "               ")[0:15] + (str(r) + "          ")[0:11] + (str(s) + "          ")[0:11] + str(gap))^0

""^0
rollup_recursive("org") => root_r
rollup_set("org") => root_s
("root: recursive " + str(root_r) + ", set-based " + str(root_s) + ", overstated by " + str(root_r - root_s))^0
("nodes whose rollup is wrong: " + str(nodes_with_gap) + " of " + str(len(NODES)))^0

""^0
"paths from the root to each node"^0
0 => multi_path_nodes
0 => attributed
for n in NODES:
    path_count("org", n) => p
    if p > 1:
        multi_path_nodes + 1 => multi_path_nodes
        attributed + own_cost(n) * (p - 1) => attributed
    ((n + "               ")[0:15] + " paths: " + str(p) + "   own cost: " + str(own_cost(n)) + "   counted extra: " + str(own_cost(n) * (p - 1)))^0

""^0
("nodes reachable by more than one path: " + str(multi_path_nodes))^0
("overcount predicted from path multiplicity: " + str(attributed))^0
("overcount measured at the root:             " + str(root_r - root_s))^0

""^0
"the total is stable, which is why it survives"^0
("recursive rollup, run again: " + str(rollup_recursive("org")))^0
("recursive rollup, run again: " + str(rollup_recursive("org")))^0
"Same answer every time. Reproducibility is not correctness."^0

""^0
0 => checked
0 => passed

# The structure must not be a tree - some node must be reachable by more than
# one path, or there is nothing here.
checked + 1 => checked
if multi_path_nodes > 0:
    passed + 1 => passed

# The root rollup must be overstated.
checked + 1 => checked
if root_r > root_s:
    passed + 1 => passed

# The overcount must equal the sum of own-cost times (paths - 1) over all
# nodes. Both sides are computed - one by walking, one by counting paths - so
# this is a cross-check of the explanation and not a restatement of it.
checked + 1 => checked
if root_r - root_s == attributed:
    passed + 1 => passed

# A node whose own subtree contains no sharing must roll up correctly, so the
# defect is localised to the sharing and not to the recursion.
#
# The first version of this check asked `path_count("org", n) == 1` - how many
# paths reach n from the ROOT - and then required n's own rollup to be right.
# Those are different questions and the root is the counterexample: there is
# exactly one path from `org` to `org`, and `org` is the only node whose
# rollup is wrong. What decides a node's rollup is whether anything BELOW it
# is reachable two ways, so that is what gets computed.
checked + 1 => checked
0 => tree_subtrees
0 => tree_subtrees_wrong
for n in NODES:
    0 => shared_below
    for m in reachable(n):
        if path_count(n, m) > 1:
            shared_below + 1 => shared_below
    if shared_below == 0:
        tree_subtrees + 1 => tree_subtrees
        if not (rollup_recursive(n) == rollup_set(n)):
            tree_subtrees_wrong + 1 => tree_subtrees_wrong
if tree_subtrees_wrong == 0:
    if tree_subtrees >= 5:
        passed + 1 => passed

# The set-based rollup must be idempotent under repetition and the recursive
# one must be too - the failure is not flakiness, and a re-run proves nothing.
checked + 1 => checked
if rollup_recursive("org") == root_r:
    if rollup_set("org") == root_s:
        passed + 1 => passed

# A leaf must roll up to its own cost under both, so neither implementation is
# simply broken.
checked + 1 => checked
if rollup_recursive("support") == own_cost("support"):
    if rollup_set("support") == own_cost("support"):
        passed + 1 => passed

# And the shared node must be genuinely shared - reachable from two different
# parents, both of which legitimately claim it.
checked + 1 => checked
0 => parents_of_shared
for n in NODES:
    for c in children_of(n):
        if c == "shared-tools":
            parents_of_shared + 1 => parents_of_shared
if parents_of_shared == 2:
    passed + 1 => passed

("checks passed: " + str(passed) + "/" + str(checked))^0
if passed == checked:
    "Every shared node was counted once per path, and the total never moved." => verdict
else:
    "FAILED - the rollups did not behave as the checks describe." => verdict
verdict^0

""^0
"The recursion is correct code for a tree and the diagram on the wall is a"^0
"tree. What made it a DAG was a reorg, a shared service, a component reused"^0
"on purpose - decisions taken by people who were not thinking about the"^0
"rollup and had no reason to. Nothing in the data model forbids the second"^0
"parent, nothing in the rollup notices it, and the number it produces is"^0
"the same every single time it is asked."^0
```

## Python (deterministic transpilation)

```python
def children_of(node):
    if node in EDGES:
        return EDGES[node]
    return []

def own_cost(node):
    if node in COST:
        return COST[node]
    return 0

def rollup_recursive(node):
    total = own_cost(node)
    for c in children_of(node):
        total = total + rollup_recursive(c)
    return total

def reachable(node):
    seen = [node]
    stack = [node]
    while len(stack) > 0:
        cur = stack[len(stack) - 1]
        stack = stack[0:len(stack) - 1]
        for c in children_of(cur):
            if not c in seen:
                seen = seen + [c]
                stack = stack + [c]
    return seen

def rollup_set(node):
    total = 0
    for n in reachable(node):
        total = total + own_cost(n)
    return total

def path_count(root, target):
    if root == target:
        return 1
    n = 0
    for c in children_of(root):
        n = n + path_count(c, target)
    return n

EDGES = {"org": ["eng", "ops"], "eng": ["platform", "shared-tools"], "ops": ["shared-tools", "support"], "platform": ["db-team"], "shared-tools": ["build-team"], "support": [], "db-team": [], "build-team": []}
COST = {"org": 0, "eng": 10, "ops": 8, "platform": 20, "shared-tools": 30, "support": 15, "db-team": 25, "build-team": 40}
NODES = ["org", "eng", "ops", "platform", "shared-tools", "support", "db-team", "build-team"]
print("node           recursive  set-based  gap")
print("-------------- ---------  ---------  ---")
nodes_with_gap = 0
for n in NODES:
    r = rollup_recursive(n)
    s = rollup_set(n)
    gap = r - s
    if not gap == 0:
        nodes_with_gap = nodes_with_gap + 1
    print((n + "               ")[0:15] + (str(r) + "          ")[0:11] + (str(s) + "          ")[0:11] + str(gap))
print("")
root_r = rollup_recursive("org")
root_s = rollup_set("org")
print("root: recursive " + str(root_r) + ", set-based " + str(root_s) + ", overstated by " + str(root_r - root_s))
print("nodes whose rollup is wrong: " + str(nodes_with_gap) + " of " + str(len(NODES)))
print("")
print("paths from the root to each node")
multi_path_nodes = 0
attributed = 0
for n in NODES:
    p = path_count("org", n)
    if p > 1:
        multi_path_nodes = multi_path_nodes + 1
        attributed = attributed + own_cost(n) * (p - 1)
    print((n + "               ")[0:15] + " paths: " + str(p) + "   own cost: " + str(own_cost(n)) + "   counted extra: " + str(own_cost(n) * (p - 1)))
print("")
print("nodes reachable by more than one path: " + str(multi_path_nodes))
print("overcount predicted from path multiplicity: " + str(attributed))
print("overcount measured at the root:             " + str(root_r - root_s))
print("")
print("the total is stable, which is why it survives")
print("recursive rollup, run again: " + str(rollup_recursive("org")))
print("recursive rollup, run again: " + str(rollup_recursive("org")))
print("Same answer every time. Reproducibility is not correctness.")
print("")
checked = 0
passed = 0
checked = checked + 1
if multi_path_nodes > 0:
    passed = passed + 1
checked = checked + 1
if root_r > root_s:
    passed = passed + 1
checked = checked + 1
if root_r - root_s == attributed:
    passed = passed + 1
checked = checked + 1
tree_subtrees = 0
tree_subtrees_wrong = 0
for n in NODES:
    shared_below = 0
    for m in reachable(n):
        if path_count(n, m) > 1:
            shared_below = shared_below + 1
    if shared_below == 0:
        tree_subtrees = tree_subtrees + 1
        if not rollup_recursive(n) == rollup_set(n):
            tree_subtrees_wrong = tree_subtrees_wrong + 1
if tree_subtrees_wrong == 0:
    if tree_subtrees >= 5:
        passed = passed + 1
checked = checked + 1
if rollup_recursive("org") == root_r:
    if rollup_set("org") == root_s:
        passed = passed + 1
checked = checked + 1
if rollup_recursive("support") == own_cost("support"):
    if rollup_set("support") == own_cost("support"):
        passed = passed + 1
checked = checked + 1
parents_of_shared = 0
for n in NODES:
    for c in children_of(n):
        if c == "shared-tools":
            parents_of_shared = parents_of_shared + 1
if parents_of_shared == 2:
    passed = passed + 1
print("checks passed: " + str(passed) + "/" + str(checked))
if passed == checked:
    verdict = "Every shared node was counted once per path, and the total never moved."
else:
    verdict = "FAILED - the rollups did not behave as the checks describe."
print(verdict)
print("")
print("The recursion is correct code for a tree and the diagram on the wall is a")
print("tree. What made it a DAG was a reorg, a shared service, a component reused")
print("on purpose - decisions taken by people who were not thinking about the")
print("rollup and had no reason to. Nothing in the data model forbids the second")
print("parent, nothing in the rollup notices it, and the number it produces is")
print("the same every single time it is asked.")
```

## stdout (executed)

```text
node           recursive  set-based  gap
-------------- ---------  ---------  ---
org            218        148        70
eng            125        125        0
ops            93         93         0
platform       45         45         0
shared-tools   70         70         0
support        15         15         0
db-team        25         25         0
build-team     40         40         0

root: recursive 218, set-based 148, overstated by 70
nodes whose rollup is wrong: 1 of 8

paths from the root to each node
org             paths: 1   own cost: 0   counted extra: 0
eng             paths: 1   own cost: 10   counted extra: 0
ops             paths: 1   own cost: 8   counted extra: 0
platform        paths: 1   own cost: 20   counted extra: 0
shared-tools    paths: 2   own cost: 30   counted extra: 30
support         paths: 1   own cost: 15   counted extra: 0
db-team         paths: 1   own cost: 25   counted extra: 0
build-team      paths: 2   own cost: 40   counted extra: 40

nodes reachable by more than one path: 2
overcount predicted from path multiplicity: 70
overcount measured at the root:             70

the total is stable, which is why it survives
recursive rollup, run again: 218
recursive rollup, run again: 218
Same answer every time. Reproducibility is not correctness.

checks passed: 7/7
Every shared node was counted once per path, and the total never moved.

The recursion is correct code for a tree and the diagram on the wall is a
tree. What made it a DAG was a reorg, a shared service, a component reused
on purpose - decisions taken by people who were not thinking about the
rollup and had no reason to. Nothing in the data model forbids the second
parent, nothing in the rollup notices it, and the number it produces is
the same every single time it is asked.
```

## Round-trip

`ok: true` — round-trip fixpoint reached (python1 == python2)

## Trace event types

eml:run:start · eml:def · eml:assign · eml:output · eml:call · eml:return · eml:run:done
