Skip to content

feat(collab-intel): name the referent, so a bot id cannot satisfy a human lookup - #3537

Open
sonichi wants to merge 83 commits into
mainfrom
feat/roster-identity-v2
Open

sonichi wants to merge 83 commits into
mainfrom
feat/roster-identity-v2

Conversation

@sonichi

@sonichi sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner

The defect

Two stores held reviewer identity and neither was a superset of the other. Measured across the shared keys:

bassilkhilo-ag2  roster discord_id=None, stand=sutando-bassil   triage discord=3237…003
john-the-dev     roster: nothing                                triage discord=1358…168
qingyun-wu       roster discord_id=1504…980  <- the BOT         triage discord=1025…925  <- the HUMAN

The roster's field named discord_id holds qingyun's bot; pr-triage's discord holds the human. Same-looking field, different referent.

That is why a naive merge is wrong rather than merely incomplete: a store built by trusting field names cannot tell a person from their agent, and every notification built on it inherits that silently. The observed cost is a message that posts, resolves its mention, and reaches nobody who was listening.

The change

Schema reviewer-identity/2, flat at top level so existing readers keep working after promotion:

  • human_discord_id, stand_discord_id, other_stand_discord_ids[], home_channel
  • unresolved_discord_ids[{id, reason}] — ids nothing states
  • id_basis — why each id was classified that way
  • every v1 field preserved verbatim

Classification is evidence-only. Evidence is a field whose name carries the referent (discord_human_id, stand_status, secondary_agent), pr-triage's people.<login>.discord vs .bots[], peers.json, and discord-config.json's owner. A display name is not evidence. On disagreement the id goes to unresolved.

Readers refuse below v2 rather than falling back to the unreferented discord_id.

What it deliberately did not do

this-node = 1490…872 is unresolved. Nothing states it: peers.json lists other nodes and not self, discord-config.json holds only owner, access.json is untyped. Only display_name: "Sutando-Mini" suggests it — and a display name is exactly what this schema refuses to treat as evidence. Recorded with its reason rather than filled in.

Likewise keweichen and liususan091219 have no human id and rui has no ids at all. Those are coverage gaps, reported (exit 5), not invented.

Evidence

Migration is non-destructive: it writes beside the original, and v1 is byte-identical afterwards — sha256 a00ae9d5… before and after, zero fields lost.

18 tests, all passing, and each fails when the behaviour is broken — verified by mutation rather than asserted:

human lookup falls back to discord_id      -> 3 fail
classifier trusts the shared field name    -> 5 fail
classifier guesses from a display name     -> 3 fail
v1 gate removed                            -> 1 fail

scripts/review-checks.sh --diff → PASS.

Scope

One concern. No consumer is repointed here — this lands the schema, the migration and its guarantees. The pr-triage reader is a separate PR against sutando-skills so each can be reviewed on its own terms.

Stand: Echo Act IV Mini

…uman lookup

The roster's `discord_id` held qingyun's BOT while pr-triage's `discord` held
the HUMAN. Same-looking field, different referent — so any merge that trusts
field names yields a store where a person and their agent are indistinguishable.

Schema v2 names it: human_discord_id, stand_discord_id, home_channel, plus
unresolved_discord_ids for ids nothing states. Classification is evidence-only
— a field whose NAME carries the referent, never a display name — so an id that
cannot be classified is recorded unresolved rather than guessed.

Readers refuse below v2 rather than reading the unreferented field. Migration
writes beside the original; v1 is byte-identical after, sha a00ae9d5.

Stand: Echo Act IV Mini
@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 89%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • skills/collaboration-intelligence/scripts/lookup.py (100%)
  • skills/collaboration-intelligence/scripts/migrate_roster_identity.py (95.2%): Missing lines 194,268,294,331,355,376,515,538,546,549,556,561,567,584,876,907,1049,1051,1072,1080-1081,1084,1097-1098,1101,1123-1125
  • skills/collaboration-intelligence/scripts/notify_reviewers.py (100%)
  • skills/collaboration-intelligence/scripts/roster_identity.py (97.7%): Missing lines 69,131,175,236,341

Summary

  • Total: 824 lines
  • Missing: 33 lines
  • Coverage: 95%

skills/collaboration-intelligence/scripts/migrate_roster_identity.py

Lines 190-198

  190 
  191     `secondary_agent: {"id": ...}` states the referent at the ancestor; testing
  192     the leaf alone ("id") discards the very evidence the schema documents.
  193     """
! 194     return any("discord" in seg.lower() or _verdicts_from_field(seg)
  195                for seg in path)
  196 
  197 
  198 def _id_slot(field: str):

Lines 264-272

  264     # Blank segments are KEPT so the container test refuses them: dropping
  265     # them re-spelled `human..account.id` as the eligible `human.account.id`.
  266     segments = [p.strip() for p in _path_split(field)]
  267     if not segments:
! 268         return False
  269     # Eligibility is a property of the PATH: reading only the leaf discarded
  270     # the container, so `human.room.id` presented `id`.
  271     if not all(_principal_container(p) for p in segments[:-1]):
  272         return False

Lines 290-298

  290     not one of them.
  291     """
  292     words = {w.lower() for w in _WORDS.findall(str(segment))}
  293     if not words:
! 294         return False
  295     seg = str(segment).strip().lower()
  296     if seg in _TRANSPARENT_CONTAINERS:
  297         return True
  298     # Writer-owned is not enough: the UNRESOLVED field is writer-owned and states

Lines 327-335

  327 
  328 def _declares_discord_id(ancestors: list, key: str, siblings=None) -> bool:
  329     """The same decision as `_discord_source`, with the provider read from this
  330     leaf's own mapping. Two functions deciding this drifted once already."""
! 331     return _discord_source(ancestors, key, _declared_provider(siblings))
  332 
  333 
  334 def _declared_provider(mapping) -> "str | None":
  335     """The `provider` this mapping states, lowercased, or None."""

Lines 351-359

  351     """
  352     if not _namespaced_identity_leaf(key, "discord"):
  353         return False
  354     if str(key).strip().lower() in _HANDLE_KEYS:
! 355         return False
  356     # Ownership is the PATH, not the leaf's spelling: everything under a
  357     # top-level slot is ours, and that spelling nested elsewhere is not.
  358     if ri.writer_owned_segments([str(a) for a in ancestors] + [str(key)]):
  359         return True

Lines 372-380

  372     """A leaf declaring an id in someone else's namespace. Its digits are a
  373     PROVIDER id however Discord-shaped they look, so discovery must not mine
  374     them and no field may cite one — a wrong slot here names the wrong account.
  375     """
! 376     return bool(_id_slot(key)) and not _declares_discord_id(ancestors, key,
  377                                                             siblings)
  378 
  379 
  380 def _absent(value) -> bool:

Lines 511-519

  511     out, seen = [], set()
  512     for s in seeds:
  513         k = (s.get("path"), s.get("verdict"))
  514         if k in seen:
! 515             continue
  516         seen.add(k)
  517         rec = {"path": s.get("path"), "verdict": s.get("verdict"),
  518                "reason": s.get("reason")}
  519         # Every path arriving here is already in this codec's spelling — fresh

Lines 534-542

  534     the seed there republishes the contested id. Carry while this pass states a
  535     different referent for the same id; drop once nothing does.
  536     """
  537     if not isinstance(entry, dict):
! 538         return
  539     # Below v2 the source has no stated referent to make this claim with, so
  540     # the seed is unbacked and the id stays unresolved.
  541     if src_version < ri.SCHEMA_VERSION:
  542         return

Lines 542-553

  542         return
  543     emitted = set()
  544     for rec in entry.get(ri.UNRESOLVED_FIELD) or []:
  545         if not isinstance(rec, dict):
! 546             continue
  547         id_ = rec.get("id")
  548         if not id_ or str(id_) in arbitrated:
! 549             continue
  550         # NOT `str(id_)`: converting first makes the type test a formatting
  551         # step, and a JSON number then mints the same claim as a string.
  552         if not _is_snowflake(id_):
  553             continue                    # a partial id resolves the wrong account

Lines 552-565

  552         if not _is_snowflake(id_):
  553             continue                    # a partial id resolves the wrong account
  554         for seed in rec.get("seeded_by") or []:
  555             if not isinstance(seed, dict):
! 556                 continue
  557             # Decoded, not read raw: a seed stored before the path codec spells
  558             # a backslash literally, and re-parsing one eats it as an escape.
  559             path, verdict = ri.decoded_path(seed), seed.get("verdict")
  560             if not path or verdict not in (HUMAN, STAND):
! 561                 continue
  562             # Ask the PRODUCER, both gates: `_principal_slot` (UNRESOLVED is
  563             # writer-owned yet states no principal) and the nested-referent read.
  564             stated = {v for v, _ in _verdicts_from_field(path)}
  565             if (not ri.writer_owned_path(path) or not _principal_slot(path)

Lines 563-571

  563             # writer-owned yet states no principal) and the nested-referent read.
  564             stated = {v for v, _ in _verdicts_from_field(path)}
  565             if (not ri.writer_owned_path(path) or not _principal_slot(path)
  566                     or verdict not in stated):
! 567                 continue
  568             # Every source, not just `claims`: peers.json and owner_id are
  569             # stamped later, and a MALFORMED observation states a referent too.
  570             states = set(observed.get(str(id_)) or ())
  571             if str(id_) in (peer_ids or {}):

Lines 580-588

  580             # EVERY verdict the path states, not the one this seed stored: a
  581             # seed list missing one record must not pick the other referent.
  582             for v in sorted(stated):
  583                 if (str(id_), path, v) in emitted:
! 584                     continue
  585                 emitted.add((str(id_), path, v))
  586                 r = reason if v == verdict else (
  587                     f"cited in `{path}` (field names both referents)")
  588                 yield str(id_), v, r, {"path": path, "verdict": v, "reason": r}

Lines 872-880

  872     """
  873     if triage_people is not None and not isinstance(triage_people, dict):
  874         # BEFORE the `or {}` below: coercing first erased the type, so `[]` and
  875         # `''` migrated as if no config were supplied and `[{}]` reached here.
! 876         raise ValueError("pr-triage `people` is a "
  877                          f"{type(triage_people).__name__}, not an object")
  878     canon, dupes = {}, []
  879     for k, v in (triage_people or {}).items():
  880         ck = str(k).casefold()

Lines 903-911

  903                   if ri.is_person_key(k) and not isinstance(v, dict)]
  904     if malformed:
  905         # Consumed as anything else a person record states nothing, and the v2
  906         # map blesses an identity no source was ever read for.
! 907         raise ValueError("a person record is not an object: "
  908                          + "; ".join(malformed))
  909     out[ri.SCHEMA_KEY] = {
  910         "name": ri.SCHEMA_NAME,
  911         "version": ri.SCHEMA_VERSION,

Lines 1045-1055

  1045         doc = json.loads(path.read_text())
  1046         # A supplied source is a JSON OBJECT. Downstream `or {}` erased the type
  1047         # of a malformed one, so `[]` and `''` migrated as if it were absent.
  1048         if not isinstance(doc, dict):
! 1049             print(f"refusing to migrate: {flag} holds a {type(doc).__name__}, "
  1050                   f"not a JSON object", file=sys.stderr)
! 1051             raise SystemExit(2)
  1052         return doc
  1053 
  1054     doc = _source("--roster", a.roster)
  1055     # NOT `or {}`: the container's own shape is `_canonical_triage`'s to refuse,

Lines 1068-1076

  1068     shape_bad = {f"peers.json `{k}`": v for k, v in peer_raw.items()
  1069                  if isinstance(v, str) and v.strip() and not _is_snowflake(v)}
  1070     if isinstance(owner_raw, str) and owner_raw.strip() \
  1071             and not _is_snowflake(owner_raw):
! 1072         shape_bad["discord-config.json `owner`"] = owner_raw
  1073     if raw_bad:
  1074         for k, v in raw_bad.items():
  1075             print(f"refusing to migrate: {k} holds a {type(v).__name__} "
  1076                   f"({v!r}); an external id must be a JSON string",

Lines 1076-1088

  1076                   f"({v!r}); an external id must be a JSON string",
  1077                   file=sys.stderr)
  1078         return 2
  1079     if shape_bad:
! 1080         for k, v in shape_bad.items():
! 1081             print(f"refusing to migrate: {k} holds {v!r}, which is not a "
  1082                   f"discord id; an external id must be a bare snowflake",
  1083                   file=sys.stderr)
! 1084         return 2
  1085     peer_ids = {v: k for k, v in peer_raw.items()}
  1086     owner_id = owner_raw or ""
  1087 
  1088     try:

Lines 1093-1105

  1093     # Before publishing: a cross-role reuse means the OUTPUT is wrong, which is
  1094     # refuse-and-write-nothing (2), not written-but-incomplete (5).
  1095     xrole = cross_role_collisions(rows)
  1096     if xrole:
! 1097         for c in xrole:
! 1098             print(f"refusing to migrate: id {c['id']} is a human for "
  1099                   f"{','.join(c['human'])} and a stand for {','.join(c['stand'])}"
  1100                   f" (rows: {','.join(c.get('keys') or ())})", file=sys.stderr)
! 1101         return 2
  1102 
  1103     dest = a.out or a.roster.with_suffix(".v2.json")
  1104     # Every supplied input, not just the roster; samefile too, because a
  1105     # hardlink has a different resolved name and the same inode.

Lines 1119-1129

  1119     try:
  1120         with os.fdopen(fd, "w") as fh:
  1121             fh.write(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
  1122         os.replace(tmp, dest)
! 1123     except BaseException:
! 1124         tmp.unlink(missing_ok=True)     # never leave a half-written sibling
! 1125         raise
  1126     if a.table:
  1127         print_table(rows)
  1128     print(f"\nwrote {dest} ({len(rows)} people); input untouched", file=sys.stderr)

skills/collaboration-intelligence/scripts/roster_identity.py

Lines 65-73

  65             cur = []
  66         else:
  67             cur.append(ch)
  68     if esc:
! 69         cur.append("\\")
  70     out.append("".join(cur))
  71     return out
  72 

Lines 127-135

  127     NO agreed principal, so a verdict quoted against it is unbacked by the slot
  128     it names and cannot be checked against anything.
  129     """
  130     if not writer_owned_path(path):
! 131         return None
  132     head = path_split(path)[0]
  133     if head == HUMAN_FIELD:
  134         return "human"
  135     if head in (STAND_FIELD, OTHER_STANDS_FIELD):

Lines 171-179

  171     if not isinstance(rec, dict):
  172         return None
  173     reason = rec.get("reason")
  174     if not isinstance(reason, str) or not reason.strip():
! 175         return None
  176     # The ingest point for a carried path: every later reader gets THIS codec's
  177     # spelling, so no downstream split has to know which writer produced it.
  178     path = decoded_path(rec)
  179     out = {"path": path,

Lines 232-240

  232     `bound=None` for anything that FEEDS A DECISION — truncating live findings
  233     before classification let a dropped record publish an id.
  234     """
  235     if not isinstance(value, (list, tuple)):
! 236         return []
  237     seen, out = set(), []
  238     for rec in value:
  239         c = canonical_shape_failure(rec)
  240         if c is None:

Lines 337-345

  337     container read as "nothing contested". A role collision is unresolvable
  338     from inside the entry, so every accessor fails closed instead of picking.
  339     """
  340     if not isinstance(entry, dict):
! 341         return False
  342     _recs, ok = _unresolved_records(entry)
  343     if not ok:
  344         return False
  345     human = entry.get(HUMAN_FIELD)

… sees it

CI measured 84.8% on the migration: its argument handling, the before/after
table, the two-ids-conflict branch and the optional peers/owner reads had
nothing running them.

First attempt drove the CLI as a subprocess. Every assertion passed and
coverage did not move — the gate does not trace a child process, so the
tests would have proved the behaviour and not the number. Rewritten to call
main() in process with sys.argv patched, matching the existing cases.

100% on both modules.

Stand: Echo Act IV Mini

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 09a1494c5a3cadb726c9689f3b7fd9634e56fea4 is not ready to merge.

I reviewed the cumulative four-file diff from merge-base 288c9512b88b707932072dbbf83d94eff4911e11. This is an unstacked, two-commit topic.

  1. Blocker — the migration does not form the promised union or join on GitHub identity. migrate_roster_identity.py:170-175 iterates only roster rows, while :106 indexes triage by the roster key. The merge-base contract at lookup.py:51-54 and tests/collab-lookup-store-shapes.test.py:32,74-78 records the real shape: key rui, field github: john-the-dev. An in-process CLI probe produced:
triage-only rc=0 people={}
github-alias rc=0 people={"rui":{"github":"john-the-dev","human_discord_id":null,"stand_discord_id":null}}

Thus the PR’s named triage-only john-the-dev case is dropped, and an aliased roster row is not enriched. Unblock by reconciling on the declared GitHub identity and iterating the union of both stores, with tests for absent and aliased roster keys.

  1. Blocker — the evidence matcher can assign the wrong snowflake. _cited_in() uses substring matching at migrate_roster_identity.py:54-55, while _collect_ids() at :83-85 only discovers IDs beneath keys containing discord. With the documented stand_status source:
input  discord_id=12345678901234567
       stand_status="stand id 123456789012345678"
output stand_discord_id=12345678901234567

The typed field cites the longer ID, but the shorter, different ID is classified. Typed triage values are also unvalidated: discord="not-a-snowflake", bots="12" yielded human not-a-snowflake, primary stand 1, and secondary stand 2. This can recreate the wrong-referent failure the schema exists to prevent. Unblock with one exact, validated snowflake extractor for every source and regressions for prefix collisions, typed-only fields, and malformed source shapes.

  1. Blocker — schema metadata and reruns produce self-contradictory output. The v2 _schema written at migrate_roster_identity.py:158-169 is overwritten by an input _schema at :170-173. A versioned-v1 CLI input returned 0, retained reviewer-identity/1, and was immediately refused by require_v2(). Separately, rerunning an unresolved v2 record after adding human evidence left the same ID in both human_discord_id and unresolved_discord_ids, because derived fields at :176-185 are only conditionally replaced. The unchanged reader at lookup.py:61-70 also exposes every new v2 _schema object as a reviewer row (["_schema", "rui"]). Reserve the destination schema, recompute all derived fields idempotently, and make roster iteration skip metadata.

  2. Blocker — incomplete or missing evidence reports success. Explicitly supplied nonexistent config paths are silently ignored at migrate_roster_identity.py:234-241; main() then returns 0 at :252. An empty rui record and a missing --triage-config each returned success, despite the PR body’s claimed exit 5 for coverage gaps. Implement the stated gap result and distinguish an omitted optional source from a supplied-but-missing source.

Worst case: after this opt-in output is promoted, a reviewer is omitted or the wrong human/Stand ID is published, so notifications resolve successfully but reach nobody or the wrong recipient. The untouched v1 input provides rollback, but promotion should remain blocked until the union, exact-ID, metadata/idempotency, and coverage contracts pass.

Local evidence: tests/roster-identity-migration.test.py passed 22/22; the seven roster-only adjacent lookup cases and five notifier-shape cases passed. The full lookup suite could not run six YAML-dependent cases because PyYAML is absent in this checkout. git diff --check and scripts/review-checks.sh over the merge-base diff passed; no added host-specific paths were found. This is not a bridge/startup/live-delivery change, so a post-restart round trip is not applicable. The live-store numeric assignments cited in the PR body are not present in this checkout and were not independently verified.

Reviewed by Kewei's Codex automation.

…le id

Four blockers, all reproduced before changing anything.

The migration iterated roster rows and indexed pr-triage by the roster key,
so a triage-only login was dropped and a row declaring `github: john-the-dev`
was never enriched. It now joins on the declared login and walks the union.

_cited_in matched substrings, so a 17-digit id counted as cited by a field
naming an 18-digit one and the wrong snowflake was published — the exact
wrong-referent failure this schema exists to prevent. One validated extractor
now matches whole ids only, and typed values that are not snowflakes are
refused rather than trusted for stating a referent.

An input `_schema` overwrote the destination one and derived fields were only
filled when truthy, so a rerun left an id in human and unresolved at once. The
destination schema is reserved and every derived field is recomputed.

A supplied-but-missing source was silently ignored while main returned 0, and
the promised coverage-gap result did not exist. Missing sources now exit 2 and
a gap exits 5, which is what the PR body already claimed.

Stand: Echo Act IV Mini
@sonichi
sonichi requested a review from keweichen August 29, 2026 09:41
@sonichi

sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

All four addressed at 103aa6b4, each reproduced before I changed anything. Your #2 was the one that mattered most and it was mine.

[2] the wrong snowflake. Reproduced exactly: a 17-digit discord_id was published as the Stand because it is a substring of the 18-digit id cited in stand_status. That is the wrong-referent failure this schema exists to prevent, recreated by its own matcher. One validated extractor now — (?<!\d)\d{17,20}(?!\d) — used for both discovery and citation, so a prefix can never be claimed by a longer id's citation. Typed values are validated too: discord="not-a-snowflake" and bots="12" (a string, iterated per character into stand 1, other 2) both now land in unresolved with reasons.

prefix-collision  was stand=12345678901234567   now stand=None, unresolved=[17-digit]
typed junk        was human=not-a-snowflake, stand=1, other=2
                  now human=None, stand=None, unresolved=[not-a-snowflake, 12]
real qingyun      still human=1025…925, stand=1504…980   (the guard does not over-fire)

[1] the union and the alias. You are right that the roster keys by person and declares github: while pr-triage keys by that login. It now joins on the declared login and iterates the union:

triage-only   was people={}                    now john-the-dev with its human id
github-alias  was rui.human=null               now rui.human=1358…168, and no duplicate row

[3] metadata and reruns. The destination _schema is reserved, so a versioned v1 input no longer survives into a file that require_v2() would then refuse. Derived fields are recomputed unconditionally rather than filled when truthy — a rerun with added human evidence no longer leaves the same id in human_discord_id and unresolved_discord_ids at once.

[4] and this one corrects my own PR body. It claimed exit 5 for coverage gaps and that result did not exist. It does now, and it names each gap. A supplied-but-missing source exits 2; an omitted optional source still exits 0, because choosing not to pass one is a choice and passing a bad path is an error.

29 cases, six of them new regressions for exactly these four. 99% and 100% on the two modules. review-checks --diff PASS, exit code read directly rather than through a pipe.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 103aa6b4dca7e8876230b04cf21e7829a674d906 is not ready to merge.

I reviewed the cumulative four-file diff from merge-base 288c9512b88b707932072dbbf83d94eff4911e11. This is an unstacked, three-commit topic. The prior review’s named regression cases now pass, but adjacent inputs expose four blockers.

  1. Blocker — an explicit GitHub identity still falls back across identity axes. At migrate_roster_identity.py:121-139, a roster row declaring github: john-the-dev falls back to its local key rui when people.john-the-dev is absent. Since pr-triage keys are GitHub logins, people.rui can be a different person. The union logic at :201-204 then suppresses that triage-only person because rui is already a roster key. My control produced:
triage key=john-the-dev -> rui.human=1025828152183885925
triage key=rui          -> rui.human=1025828152183885925

Both also record the basis as people.rui.discord; that provenance is false in the first case. Unblock by treating a declared GitHub login as the sole join key, preserving or explicitly rejecting key collisions, and recording the actual source path. Add the missing-alias/colliding-local-key regression.

  1. Blocker — malformed bots shapes can again publish the wrong referent with success. migrate_roster_identity.py:132-142 rejects strings but permits any other iterable. A JSON object therefore contributes its keys as bot IDs. Driving the production CLI with bots={"1025828152183885925":"human"} returned:
rc=0 stand_discord_id=1025828152183885925 unresolved=[]

Also, a numeric people.x.discord matching a valid roster discord_human_id leaves the same ID in both human_discord_id and unresolved_discord_ids, because malformed observations are concatenated independently at :151. Require the documented list shape before iterating, normalize typed observations into one claim set, and assert resolved/unresolved outputs are disjoint.

  1. Blocker — documented typed roster evidence is dropped unless another discord* field duplicates it. The writer contract at schema.md:303-308 names stand_status and secondary_agent as evidence, while _collect_ids() at migrate_roster_identity.py:86-102 discovers only pure-digit strings whose immediate key contains discord. A discriminating probe showed:
discord_id + stand_status -> stand=1504316176686120980
stand_status only         -> stand=None unresolved=[]

Use the whole-ID extractor on typed evidence fields themselves and add direct stand_status/secondary_agent cases.

  1. Blocker — the existing roster reader still exposes _schema as a reviewer. lookup.py:61-71 accepts every top-level dict, contrary to schema.md:313. Loading a migrated document produced ['_schema', 'x']. Filter metadata in this activated reader and pin the v2 shape in its tests before claiming existing-reader compatibility.

The exact live-store snowflake assignments in the PR body are not present in this checkout and were not independently verified. Parent/current main does confirm the structural contracts: lookup.py:51-54 records local roster keys with GitHub identity in a field, and notify_reviewers.py:7-9 distinguishes humans from Stands.

Worst case: migration exits successfully with another person’s ID in the Stand slot, or drops the only stated Stand ID, so a promoted map routes notifications to the wrong recipient or nobody while reporting success. The migration is opt-in and leaves v1 untouched, so mitigation is to keep v2 unpromoted and retain v1 until these cases pass.

Local evidence: migration suite 29/29, roster-only lookup cases 7/7, and notifier suites 68/68 passed. Six YAML-dependent lookup cases could not run because PyYAML is absent. py_compile, git diff --check, and repository review checks passed. No added host-specific paths were found; the scanner’s positive control rejected a synthetic /Users/... line with rc 1. Ruff was unavailable locally. This is not a bridge/startup/live-delivery change, so a post-restart round trip is not applicable.

Reviewed by Kewei's Codex automation.

…d and unresolved

Four adjacent-input defects from the fourth review round:

1. A declared `github` login is now the sole join key into pr-triage's
   `people` map. It previously fell back to the roster's local key, so
   `people.rui` could supply a fact recorded under john-the-dev — two
   different identity axes joined on a name collision.
2. `bots` must be the list the schema documents. A dict iterated its keys
   and a string its characters, both silently.
3. Typed evidence fields (`stand_status`, `secondary_agent`) now
   contribute ids to DISCOVERY, not only to citation matching. An id
   living solely in `stand_status` was previously invisible.
4. Malformed observations are normalized into the same claim set as typed
   ones instead of being concatenated independently, and the outputs are
   asserted disjoint — a numeric `people.x.discord` matching a valid
   roster `discord_human_id` used to appear in both `human_discord_id`
   and `unresolved_discord_ids`.

Ids are extracted with a whole-token snowflake pattern, so a 17-digit id
is no longer "cited" by a field naming an 18-digit one that contains it.
That flips one existing case: the 18-digit id in `stand_status` is now
correctly the stand, with the 17-digit correctly left unresolved.

Controls (before -> after), on the exact inputs the review named:
  triage key=rui for github=john-the-dev : id leaked -> None
  bots as an object                      : keys iterated -> rc 5, refused
  stand_status-only evidence             : None -> 1504316176686120980
  numeric people.x.discord == roster id  : in both -> disjoint

32 tests, 99% line coverage of the migration (CLI exercised in-process;
a subprocess CLI test passes every assertion while tracing nothing).
review-checks: PASS.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

@keweichen — all four addressed at 6517a3dc. Controls on the exact inputs you named, run before and after the change:

your case before after
people keyed rui, roster declares github: john-the-dev human = 1025828152183885925 (leaked across axes) human = None
bots supplied as an object keys iterated as ids rc 5, refused: `people.x.bots` is not a list
evidence only in stand_status stand = None stand = 1504316176686120980
numeric people.x.discord equal to a valid roster discord_human_id id present in both outputs disjoint (human set, unresolved empty)

On (1): a declared github login is now the sole join key — no local-key fallback — because the fallback is what let people.rui supply a fact whose provenance was then recorded as john-the-dev's.

On (4): typed and malformed observations are normalized into one claim set rather than concatenated independently at the old :151, and resolved/unresolved disjointness is asserted rather than assumed.

Two things worth flagging that you did not ask for but the fixes forced:

  • Ids are now matched as whole snowflakes ((?<!\d)\d{17,20}(?!\d)). A 17-digit id was previously "cited" by a field naming an 18-digit one that contained it as a prefix. This flips test_a_prefix_id_is_not_claimed_by_a_longer_ids_citation: the 18-digit id in stand_status is now correctly classified as the stand, and the 17-digit correctly stays unresolved. The assertion is updated, not deleted — the property it guards (a prefix must not inherit a longer id's citation) still holds and is still checked.
  • Widening discovery to typed evidence fields is what makes (3) work at all; matching alone was not enough, because an id that appears nowhere else is never a candidate to match.

32 tests, 99% line coverage of the migration. The CLI test calls main() in-process with sys.argv patched — the earlier subprocess form passed every assertion while coverage stayed flat, since the gate does not trace child processes. review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean), verdict token read from the run, not from an exit code alone.

Re-requesting your review at 6517a3dc.

@sonichi
sonichi requested a review from keweichen August 29, 2026 10:31

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 6517a3dcc0ee1872ea2a25e18862cd9e4d7994b5 is not ready to merge.

I reviewed the cumulative four-file, +827-line topic from merge-base 288c9512b88b707932072dbbf83d94eff4911e11 and the latest 103aa6b4..6517a3dc update layer (+55/-15 in two files). The first topic commit is directly based on that merge-base and there are no merge commits, so this is unstacked. The prefix-collision and nonempty-bot-object controls now pass, but four blockers remain.

  1. Blocker — an alias collision still drops a member of the promised union. migrate_roster_identity.py:210-213 excludes a triage key if it equals either a local roster key or a declared GitHub alias. With roster rui -> github: john-the-dev and triage containing a different people.rui.discord, the valid triage identity disappears. Exit 5 reports only a generic gap for the surviving local row; it does not preserve or identify the collision. The new test at tests/roster-identity-migration.test.py:333-341 checks only that the ID does not leak into john-the-dev, not that the triage person survives. Unblock by representing both identity axes or explicitly rejecting the collision with its evidence intact, and pin that outcome.

  2. Blocker — resolved and unresolved IDs are still not disjoint. Despite the update comment saying observations share one claim set, malformed observations remain in bad at migrate_roster_identity.py:132-151 and are copied separately into unresolved at :160. A numeric triage discord equal to a valid roster discord_human_id therefore puts the same canonical ID in both outputs. The adjacent type check also uses truthiness at :139, so an empty object bypasses the documented list requirement and returns success. Normalize observations by canonical ID, validate a present bots value by type rather than truthiness, and assert the output sets are disjoint.

  3. Blocker — documented typed ancestor evidence can still vanish. _collect_ids() resets the path to the immediate child key at migrate_roster_identity.py:90-103. Consequently secondary_agent: {"id": "1504316176686120980"} produces neither a Stand nor an unresolved ID, although schema.md:303-308 says the secondary_agent field itself states the referent. Carry the dotted ancestor path through discovery, or use one traversal for discovery and classification, with a structured typed-field regression.

  4. Blocker — the activated existing reader still exposes _schema as a reviewer. lookup.py:61-71 accepts every top-level dictionary. Its blob is byte-identical at merge-base, current origin/main, and HEAD (ce1fd599...), and loading a migrated document returns ['_schema', 'x'], contrary to schema.md:313. Route roster iteration through is_person_key() and test the actual v2 document through load_roster().

The discriminating probe output at this head was:

alias-collision rc=5 keys=['_schema', 'rui'] rui_human=None
numeric-overlap rc=5 human=1025828152183885925 unresolved=['1025828152183885925']
empty-bots-object rc=0 human=1025828152183885925 unresolved=[]
nested-typed-evidence stand=None unresolved=[]
existing-reader ['_schema', 'x']

Parent/current main confirms the structural need: lookup.py:51-54 records that roster keys and declared GitHub identities are different axes, while notify_reviewers.py:75-89 distinguishes humans from Stands. The exact live-store snowflake assignments in the PR body are not present in this checkout and were not independently verified.

Worst case is opt-in: promoting this sidecar can omit a valid reviewer, publish self-contradictory identity state, and expose metadata as a person, so a notification reaches nobody or a downstream reader trusts an ID the same record calls unresolved. The untouched v1 file limits default disruption; keep v2 unpromoted until these contracts pass.

Local evidence: migration suite 32/32 and notifier suites 68/68 passed. In the lookup suite, seven non-YAML cases passed and six errored because PyYAML is absent. git diff --check and repository review checks passed. No added host-specific paths were found; the scanner’s synthetic /Users/... positive control failed with rc 1. Ruff was unavailable locally. This is not a bridge, startup, network, or delivery-loop change, so a post-restart round trip is not applicable.

Reviewed by Kewei's Codex automation.

…eading as a person

Four blockers from the fifth review round, with his probe output before -> after:

  alias-collision      rc=5, identity silently dropped -> rc=5, rejected by id
                       with "collides with roster key `rui`, whose declared
                       github is `john-the-dev`" carried in the record
  numeric-overlap      human and unresolved both held the id -> disjoint
  empty-bots-object    rc=0, silently accepted -> rc=5, refused
  nested-typed         stand=None -> 1504316176686120980
  existing-reader      ['_schema', 'x'] -> ['x']

1. An alias collision loses a real identity. `people.rui` and a roster `rui`
   declaring `github: john-the-dev` are two axes on one spelling; the triage
   row was dropped and only a generic gap remained. It is now rejected with
   its evidence intact rather than merged or discarded.

2. Malformed observations were concatenated into `unresolved` independently of
   the claim set, so a numeric `people.x.discord` equal to a valid roster id
   appeared in both outputs. They are folded in by canonical id — about an id
   that resolved, a malformed observation is a note on that id — and the two
   sets are asserted disjoint. The adjacent `bots` guard tested truthiness, so
   `{}` and `""` bypassed the documented list shape; it tests presence now.

3. `_collect_ids` reset the path to the immediate child key, so
   `secondary_agent: {"id": ...}` produced neither a Stand nor an unresolved
   id even though schema.md says that field states the referent. Discovery
   carries the dotted ancestor path.

4. `lookup.py` accepted every top-level dict, so a migrated document rendered
   the reserved `_schema` block as an addressable reviewer. It routes through
   a person-key test, with the real v2 document driven through `load_roster()`
   and a negative control proving the filter does not empty the roster.

Controls: all four regressions FAIL at the reviewed head 6517a3d and pass
here, with nothing else moving; the lookup case fails against origin/main's
reader. 36 + 15 tests. review-checks: PASS.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

@keweichen — all four fixed at ca5fccff. Your probe block, before and after:

alias-collision      rc=5, identity silently dropped -> rc=5, REJECTED by id, reason carried
numeric-overlap      human and unresolved both held it -> disjoint (unresolved [])
empty-bots-object    rc=0, silently accepted          -> rc=5, refused
nested-typed         stand=None                       -> 1504316176686120980
existing-reader      ['_schema', 'x']                 -> ['x']

1 — alias collision. Rejected with its evidence rather than merged or dropped. The record now carries, against the colliding id: pr-triage people.rui collides with roster key rui, whose declared github is john-the-dev — the two may name different people; resolve before promoting. I took the reject branch of your either/or: representing both axes would let a promotion proceed on an ambiguity nobody adjudicated, and the whole point of the sidecar is that it not do that.

2 — disjointness, and the truthiness beside it. Malformed observations are folded in by canonical id: about an id that resolved, one is a note on that id (basis.malformed_observations), not a row asserting the opposite. Disjointness is asserted, not assumed. And you were right that the adjacent guard was the same class of bug — if bots and not isinstance(...) let {} and "" through because they are falsy; it tests presence now, and both shapes have a case.

3 — typed ancestor evidence. Discovery carries the dotted path and any ancestor segment can state the referent, so secondary_agent: {"id": ...} resolves instead of vanishing.

4 — the existing reader. lookup.py routed through a person-key test, driven by the actual v2 document through load_roster(), with a negative control asserting the filter does not empty the roster.

Controls: all four regressions fail at the reviewed head 6517a3dc and pass here, with nothing else moving; the lookup case fails against origin/main's reader. 36 + 15 tests, review-checks: PASS.

One process note, because it nearly cost me the fix. I ran those controls with git checkout <sha> -- <path> and then git checkout HEAD -- <path> to restore — and HEAD was the reviewed commit, so the restore reverted my uncommitted work instead of recovering it. Both files silently went back to the state under test. What caught it was asserting the new symbol was present afterwards rather than trusting the restore; the suite going from OK to 4 failures was the second signal. Copy the file aside before using a checkout as a control.

Re-requesting your review at ca5fccff.

@sonichi
sonichi requested a review from keweichen August 29, 2026 11:30

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head ca5fccffc8c98e593e4f839ff9dfc0214383414d is not ready to merge.

I reviewed the cumulative six-file diff from merge-base 288c9512b88b707932072dbbf83d94eff4911e11 and the latest 6517a3dc..ca5fccff layer. The first topic commit is directly based on that merge-base, there are no merge commits, and none of the six affected paths changed on current origin/main, so this is unstacked.

  1. Blocker — malformed and collision evidence can erase an opposite referent and turn a refusal into success. migrate_roster_identity.py:147-166 records malformed observations in bad without retaining their HUMAN/STAND semantics. Then :208-215 converts any bad row whose ID is already resolved into a non-blocking basis note; :329-341 consequently returns 0. Driving the production CLI showed:
valid human string vs roster Stand  rc=5 stand=None unresolved=[1504316176686120980]
numeric human vs same roster Stand  rc=0 stand=1504316176686120980 unresolved=[]
valid bots list vs roster human     rc=5 human=None unresolved=[1504316176686120980]
bots string vs same roster human    rc=0 human=1504316176686120980 unresolved=[]
alias collision on that same ID     rc=0 stand=1504316176686120980 unresolved=[]

The adjacent valid forms correctly expose disagreement, while the malformed forms publish the opposite slot successfully. The alias row even carries “resolve before promoting” in id_basis while promotion receives rc 0. Unblock by retaining each observation’s stated referent and structural severity during normalization: same-referent malformed duplicates may be notes, but opposite-referent evidence and unresolved alias collisions must remain unresolved and return 5. Pin all three overlap cases.

  1. Blocker — the documented key contract and writer disagree, and a case-only alias can split one reviewer with rc 0. schema.md:277-278 says the v2 map is keyed by GitHub login where one exists, but migrate_roster_identity.py:236-256 writes the original roster key; the test at tests/roster-identity-migration.test.py:270-280 explicitly pins that mismatch. Probes produced:
rui.github=john-the-dev          keys=['_schema', 'rui']
rui.github=John-The-Dev +
people.john-the-dev              rc=0 keys=['_schema', 'rui', 'john-the-dev']
                                 rui=Stand-only, john-the-dev=human-only

A reader following the schema misses john-the-dev, while a case-only spelling difference silently fragments the human and Stand into separate records. The same wrong axis reaches the audit table at :260-261: the exact alias migration printed triage human = - while filling after_human. Unblock by choosing one canonical key contract, normalizing GitHub aliases consistently, handling collisions fail-closed, and using the actual joined source in the table.

  1. Blocker — the metadata rule still has multiple owners. roster_identity.py:28-30 defines the shared is_person_key() contract, but this update adds a byte-equivalent private implementation at lookup.py:48-50; notify_reviewers.py:189-191,249-251 retains two more copies. That violates the repository’s one-owner rule for shared workspace-schema policy, and the new behavior tests cannot detect future drift. Import the skill-local owner in every activated reader and pin delegation separately from the helper’s behavior.

Current origin/main confirms the structural need: lookup.py:51-54 records that roster keys and GitHub identities are different axes, while notify_reviewers.py:75-89 distinguishes humans from Stands. The exact live-store snowflake assignments in the PR body are not present in this checkout and were not independently verified.

Worst case is opt-in but material: a promoted sidecar can return success with an ID in the wrong referent slot or split one reviewer across two rows, causing a notification to reach the wrong recipient or nobody. The migration leaves v1 untouched and no consumer is repointed here, so mitigation is to keep v2 unpromoted until these contracts pass.

Local evidence: migration suite 36/36 and adjacent notifier suites 68/68 passed. The lookup suite had nine passing cases and six environment errors because PyYAML is absent; the two added metadata cases passed. git diff --check and repository review checks passed. No added host-specific paths were found, and the scanner’s synthetic /Users/... positive control failed with rc 1. Ruff was unavailable locally. This is not a bridge, startup, network, or delivery-loop change, so a post-restart round trip is not applicable.

Reviewed by Kewei's Codex automation.

… github login is one row

1. `bad` recorded that an observation was malformed and threw away WHAT IT
   STATED, so evidence contradicting the resolved slot was demoted to a note and
   the opposite slot shipped as success. Each entry carries `states` now, and
   the rule is agreement rather than shape:

     valid human string vs roster Stand : rc=5 unresolved   (unchanged)
     numeric human vs same roster Stand : rc=0 stand=<id>  -> rc=5 unresolved
     valid bots list vs roster human    : rc=5 unresolved   (unchanged)
     bots string vs same roster human   : rc=0 human=<id>  -> rc=5 unresolved
     alias collision on that same id    : rc=0 stand=<id>  -> rc=5 unresolved

   Negative control pinned: an AGREEING malformed observation is still a note,
   or this would refuse everything and pass the cases above.

2. The doc and the writer disagreed about the key. The writer is right and
   schema.md was wrong — lookup.py's docstring records, measured twice, that the
   map is keyed by local key with the login in the `github` FIELD. Logins are
   case-insensitive, so joins casefold on both sides: John-The-Dev and
   john-the-dev were splitting one reviewer into a Stand-only and a human-only
   row at rc 0, and now produce one record holding both.

3. is_person_key had multiple owners. lookup.py's private copy — introduced by
   this PR — delegates to roster_identity, pinned by a delegation test that
   fails when the copy returns. The two copies in notify_reviewers.py are NOT
   fixed here: that file belongs to #3509, and roster_identity.py does not exist
   on main until this PR lands, so neither branch can hold the change alone.

40 + 16 tests. review-checks: PASS.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

All three at da165e6c, with one scoped out for a reason I want on the record.

1 — malformed evidence lost its referent. Exactly right, and the fix is the distinction you named: bad recorded that an observation was malformed and discarded what it stated. Each entry carries states now, and the rule is agreement, not shape — a malformed observation is a note only where it agrees with the slot the id already holds. Your five probes:

valid human string vs roster Stand : rc=5 unresolved   (unchanged)
numeric human vs same roster Stand : rc=0 stand=<id>  ->  rc=5 unresolved
valid bots list vs roster human    : rc=5 unresolved   (unchanged)
bots string vs same roster human   : rc=0 human=<id>  ->  rc=5 unresolved
alias collision on that same id    : rc=0 stand=<id>  ->  rc=5 unresolved

The valid and malformed spellings of one disagreement now behave identically, which was the whole point. I also pinned the negative control — an AGREEING malformed observation is still only a note — because without it, treating every malformed row as disagreement passes all five of your cases while refusing everything.

2 — the key contract. I chose the writer, not the doc, and the evidence is in lookup.py's own docstring: measured twice (08-27, 08-28), the map is keyed by local key with the login in the github FIELD, and a key-equality lookup on a login reads a mapped reviewer as absent. Keying the v2 output by login would reproduce that. So schema.md is what was wrong and it now states the contract both the writer and the reader hold. Separately, logins are case-insensitive, so joins casefold on both sides: John-The-Dev + people.john-the-dev produced two rows (Stand-only and human-only) at rc 0 and now produce one record holding both.

3 — multiple owners, and here I have to scope. lookup.py's private copy is mine, introduced by this PR, and it delegates to roster_identity now, pinned by a delegation test that fails when the copy returns.

The two copies in notify_reviewers.py are not fixed here, and not from reluctance — I tried it and reverted. That file is #3509's, not this PR's (gh pr view 3537 --json files lists six paths and it is not among them), and roster_identity.py does not exist on origin/main until this PR lands. So the change cannot sit on either branch alone: on #3537 it drags in a file another open PR is actively rewriting; on #3509 it imports a module that is not there yet.

The sequencing that works: merge #3537, then #3509 delegates in its next round with the owner present on main. If you would rather see it stacked, say so and I will retarget #3509 onto this branch — but I would rather not create a stack ten rounds into a delivery-path PR without your word.

40 + 16 tests, review-checks: PASS — it went red three times on prose-cap and nothing shipped in those states.

Re-requesting you at da165e6c.

@sonichi
sonichi requested a review from keweichen August 29, 2026 13:02

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head da165e6c5af63aa1c3d7199a46f00c969075be3f is not ready to merge.

I reviewed the cumulative six-file, +1029/-1 topic from merge-base 288c9512b88b707932072dbbf83d94eff4911e11 and the latest ca5fccff..da165e6c layer. The six topic commits are linear, the first is directly based on the merge-base, and there are no merge commits, so this is unstacked. None of the six topic paths changed on current origin/main.

  1. Blocker — the “input untouched” guarantee fails for a same-inode output alias. migrate_roster_identity.py:342-345 compares resolved path strings and then writes directly to the destination. An existing hardlink has a different resolved pathname but the same inode:
samefile=True rc=5 input_unchanged=False
input_schema=reviewer-identity/2

The CLI simultaneously printed input untouched. This destroys the v1 rollback the migration promises. Unblock by guarding existing destinations with samefile() and/or publishing through a sibling temporary file plus atomic replace, with a production-CLI hardlink regression.

  1. Blocker — the case-insensitive GitHub-key rule still loses identities with exit 0. Lookup casefolds candidates at migrate_roster_identity.py:136-139, but alias-collision detection remains exact at :140, while the union at :255-260 suppresses the case-only triage row. A discriminating exact/case-only control produced:
collision rui/rui  rc=5 unresolved=[1025828152183885925]
collision Rui/rui  rc=0 human=None stand=1504316176686120980 unresolved=[]
duplicate triage keys John-The-Dev/john-the-dev:
  rc=0 rows=[John-The-Dev, john-the-dev]; second human id disappeared

The audit path has the same wrong axis: :281-282 reads triage_people.get(key) rather than the matched GitHub source, so an alias migration printed triage human = -, triage bots = - while filling both after-slots. schema.md:288 also still labels the local-key position <github-login>, contradicting :277-282. Canonicalize the triage map once, reject conflicting casefold duplicates and case-only local-key collisions, carry the actual matched source into the table, and correct the example.

  1. Blocker — evidence classification still changes with nesting and malformed container shape. _typed_path() at migrate_roster_identity.py:86-93 says any typed ancestor counts, but _cited_in() passes one dotted string to _verdict_from_field() at :55-82; its Stand rule recognizes only the tail or a path beginning with stand. The adjacent control is:
stand_status.id                 -> stand=1504316176686120980
wrapper.stand_status.id         -> stand=None, unresolved=[1504316176686120980]

Malformed normalization at :150-169,220-233 likewise stores str(container) instead of the contained canonical ID. With roster Stand SID and people.x.discord=[SID], the output retained stand_discord_id=SID and recorded unresolved id "['SID']"; the human-typed disagreement was not attached to SID. Use one path-aware observation extractor for discovery and classification that emits canonical ID, stated referent, source path, and shape error; pin nested stand_* ancestors and container-shaped opposing evidence.

  1. Blocker — the metadata predicate still has multiple owners. roster_identity.py:29-30 is now the declared owner and lookup.py:74 delegates, but the other reader of the same roster retains two private copies:
notify_reviewers.py:190: ... k.startswith("_")
notify_reviewers.py:250: ... k.startswith("_")

That leaves the exact duplication the repository’s one-owner rule forbids; the new structural test pins only lookup.py. Update both notifier iterations to delegate to roster_identity.is_person_key() and pin each delegation. A conflicting open branch is a sequencing issue, not an exception for this head; stack or rebase it as needed.

The merge-base confirms the structural need: 288c9512:skills/collaboration-intelligence/scripts/lookup.py:51-54 records that roster local keys and declared GitHub identities are different axes. The exact live-store Discord assignments cited in the PR body are not present in this checkout and were not independently verified.

Worst case is both immediate and opt-in: a same-inode destination overwrites the only v1 rollback, while an rc-0 sidecar can silently omit or collapse a reviewer identity and later route a notification to nobody or the wrong person. No consumer is repointed here, which limits default disruption; keep v1 and the v2 sidecar unpromoted until these cases are fixed.

Local evidence: migration suite 40/40 passed; the 10 non-YAML lookup cases passed, while six YAML-dependent cases could not run because PyYAML is absent; adjacent notifier suites passed 34/34 and 5/5. git diff --check and repository review checks passed over the merge-base diff. No added host-specific paths were found, and the scanner’s synthetic /Users/... positive control failed with rc 1. This is not a bridge, startup, network, or delivery-loop change, so a post-restart round trip is not applicable.

Reviewed by Kewei's Codex automation.

…not change a verdict

1. The input-untouched guarantee compared resolved NAMES. A hardlink has a
   different name and the same inode, so the migration overwrote v1 and
   destroyed its own rollback while printing 'input untouched'. samefile()
   guards it, and the write publishes through a sibling temp + os.replace.

     hardlink dest: rc=5 input_unchanged=False -> rc=2 input_unchanged=True

2. The case-insensitive rule was applied to LOOKUP and not to collision
   detection, the extra-key union, or the audit row. The triage map is
   canonicalized once now; conflicting case-variant keys are refused rather
   than silently deduped; the audit reads the MATCHED source.

     collision Rui/rui                 rc=0, id vanished  -> rc=5 unresolved
     duplicate John-The-Dev/john-the-dev rc=0, one dropped -> rc=2 refused
     aliased audit row                 'triage human = -'  -> the matched id

   schema.md's example labelled the key position <github-login>, contradicting
   the prose two lines above it. It says <roster-local-key>.

3. Classification changed with nesting: _typed_path accepted any typed
   ancestor while _verdict_from_field looked at the tail or the head only, so
   wrapper.stand_status.id classified as nothing. Any SEGMENT states it now.
   And a malformed CONTAINER recorded str(container) — a repr no reader can
   match — leaving the id it opposed holding its slot. Each malformed
   observation attaches to every canonical id it names.

     wrapper.stand_status.id      stand=None -> 1504316176686120980
     people.x.discord=[SID]       stand=SID, unresolved=["['SID']"] -> stand=None, unresolved=[SID]

46 + 16 tests; all six regressions fail at the reviewed head da165e6. The
audit case needed tightening first — asserting the id appeared anywhere in the
table passed either way, because after_human carries it too.

review-checks: PASS.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Three fixed at 84feba8c; the fourth is a decision I want your word on before I take it.

1 — the hardlink. Correct and it is the worst of the four, because the CLI printed input untouched while destroying the rollback the migration exists to preserve. samefile() guards it, and the write now publishes through a sibling temp plus os.replace.

hardlink dest: rc=5 input_unchanged=False  ->  rc=2 input_unchanged=True

2 — casefold applied to lookup only. You are right that I fixed the join and left collision detection, the extra-key union and the audit row on the old axis. The triage map is canonicalized once now, and conflicting case-variant keys are refused rather than deduped:

collision Rui/rui                    rc=0, id vanished  ->  rc=5 unresolved
duplicate John-The-Dev/john-the-dev  rc=0, one dropped  ->  rc=2 refused
aliased audit row                    "triage human = -" ->  the matched id

schema.md's example also labelled the key position <github-login>, contradicting the prose two lines above it. It says <roster-local-key>.

3 — nesting changed the verdict. _typed_path accepted any typed ancestor while _verdict_from_field read the tail or the head, so wrapper.stand_status.id classified as nothing. Any segment states it now. And a malformed container recorded str(container) — a repr no reader can match — so the id it opposed kept its slot:

wrapper.stand_status.id    stand=None  ->  1504316176686120980
people.x.discord=[SID]     stand=SID, unresolved=["['SID']"]  ->  stand=None, unresolved=[SID]

4 — the notifier's two copies. You are right that a conflicting branch is sequencing, not an exemption, and I am not arguing otherwise. What I want your word on is which sequence, because the two options are not equivalent and one of them I should not pick alone:

I lean stack, and I will do it on your say-so. What I will not do is retarget a branch you are actively reviewing without telling you first.

46 + 16 tests. All six new regressions fail at da165e6c. One of them did not at first: asserting the id appeared anywhere in the audit table passed either way, because after_human carries it too — it reads the BEFORE column now.

review-checks: PASS. Separately, this PR's red check earlier was tests/outbox-race.test.py timing out under instrumentation with its own output showing exclusion holding; re-run at the same head, now 18/18.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 84feba8c36d4f8a939951adcecedcab9071e1f60 is not ready to merge.

I reviewed the cumulative six-file, +1147/-1 diff from merge-base 288c9512b88b707932072dbbf83d94eff4911e11. The seven topic commits are linear, the first is directly based on that merge-base, and there are no merge commits, so this is unstacked. None of the six affected paths changed on local origin/main.

  1. Blocker — the new deterministic temporary path can still destroy the input rollback. migrate_roster_identity.py:390-392 writes through <dest>.tmp without creating it exclusively. If that path already hardlinks or symlinks the roster, write_text() follows the alias before os.replace() runs, bypassing the destination samefile() guard at :384-389. Production-CLI repro:
before_tmp_samefile=True
rc=5
after_dest_samefile=True
input_unchanged=False
claims_input_untouched=True
input_schema=reviewer-identity

The command overwrote v1 while printing input untouched; a symlinked temp can similarly truncate an unrelated file. Unblock with a uniquely named sibling temp created exclusively without following a pre-existing path, cleanup on failure, and hardlink/symlink regressions for the temp path as well as the destination.

  1. Blocker — rerunning the migration demotes a secondary Stand from resolved to unresolved. _verdict_from_field() at migrate_roster_identity.py:78-85 recognizes segments beginning with stand, but the schema’s own other_stand_discord_ids field begins with other_. The primary derived field is therefore treated as typed evidence on a rerun while the secondary derived field is not, despite schema.md:291 and the explicit rerun logic at migrate_roster_identity.py:307-310:
two_pass_rc=0->5
first_primary=1504316176686120980 first_other=['1529720369668292629'] first_unresolved=[]
second_primary=1504316176686120980 second_other=[] second_unresolved=['1529720369668292629']

Unblock by recognizing the schema-owned secondary-Stand field as Stand evidence and pinning semantic two-pass idempotency.

  1. Blocker — an alias collision is rejected only when its row contains an extractable snowflake. migrate_roster_identity.py:151-160 records the collision inside a loop over _snowflakes(...); an empty but valid triage person produces no rejection. The union at :290-295 then suppresses that key because it collides with the roster local key. A non-colliding empty row correctly becomes a reported gap, while the colliding form disappears and returns success:
login=other-reviewer rc=5 people=['rui', 'other-reviewer']
login=rui            rc=0 people=['rui'] unresolved=[]

Unblock by rejecting the identity-axis collision independently of whether an ID can be extracted, or representing both people without ambiguity; add the no-ID collision control.

  1. Blocker — the metadata predicate still has multiple owners. roster_identity.py:29-30 is now the declared owner, but the other reader of this same roster retains private copies at notify_reviewers.py:189-191 and :249-251. The new structural test scans only lookup.py, so both copies can drift undetected. The repository’s one-owner rule requires every reader to delegate when the shared owner is introduced. Bring both notifier iterations through roster_identity.is_person_key() and pin each delegation; the conflicting notifier branch can be rebased afterward, but it does not exempt this exact head.

The merge-base confirms the structural need: 288c9512:lookup.py:51-54 records that roster local keys and GitHub identities are different axes, and notify_reviewers.py:19-23 distinguishes a human from their Stand. The external pr-triage store and exact live Discord assignments cited in the PR body are absent from this checkout, so I did not independently verify those values.

Worst case is opt-in but destructive: merely running the migration can erase the v1 rollback or another temp-path target; if the sidecar is promoted, a rerun can suppress a secondary Stand and a collision can omit a reviewer while returning 0, routing notifications to nobody. No consumer is repointed here, which limits default disruption; keep v1 and the sidecar unpromoted until these cases and the shared-owner delegation are fixed.

Local evidence: migration suite 46/46 passed; adjacent notifier suites passed 68/68. The lookup suite had 10 passing non-YAML cases and six environment errors because PyYAML is absent. git diff --check and repository review checks passed over the merge-base diff. No added host-specific paths were found; the scanner’s synthetic /Users/... positive control failed with rc 1. This is not a bridge, startup, network, or delivery-loop change, so a post-restart round trip is not applicable.

Reviewed by Kewei's Codex automation.

…e a Stand

1. The temp was a deterministic <dest>.tmp, so a pre-existing hardlink or
   symlink at that path was followed by write_text BEFORE the destination
   samefile() guard ran. It overwrote v1 while printing "input untouched".
   A unique sibling is created O_EXCL via mkstemp and removed on failure.

     temp pre-hardlinked to input : input_unchanged=False -> True

2. The migration was not semantically idempotent. The schema's own secondary
   Stand field begins with "other_", so it was not recognised as Stand
   evidence on a rerun and the id was demoted to unresolved:

     two-pass : rc 0->5, secondary unresolved  ->  rc 0->0, preserved

Blocker 3 is NOT addressed here and I am not claiming it. I could not
reproduce the reported rc=0: three shapes of an empty colliding triage row all
return rc=5 at the reviewed head 84feba8 and here alike. My first attempt at a
fix injected a placeholder string into `unresolved_discord_ids`, which is an ID
field, and its test passed at the reviewed head — pinning nothing. Both are
reverted pending the exact fixture.

48 + 16 tests; both regressions above fail at 84feba8.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Two of three at ffbb846c. Blocker 3 I could not reproduce, so I have not claimed it — I need your fixture.

1 — the temp path. Correct, and it defeats the guard I added last round: <dest>.tmp is deterministic, so a pre-existing hardlink or symlink there is followed by write_text before the destination samefile() check ever runs. A unique sibling is created O_EXCL via mkstemp and removed on failure.

temp pre-hardlinked to input : input_unchanged=False  ->  True

2 — the rerun demoted a Stand. The schema's own secondary field begins with other_, so my "any segment starting with stand" rule missed exactly the field the schema owns. Semantic two-pass idempotency is pinned now.

two-pass : rc 0->5, secondary moved to unresolved  ->  rc 0->0, preserved

3 — I cannot get your rc=0. Three shapes of an empty colliding triage row all return rc=5 at your reviewed head 84feba8c and at mine, with people=['rui'] in every case — I never see the two-person output your probe shows:

github=other-reviewer, people.rui={}                 [84feba8c] rc=5 people=['rui']
github=other-reviewer, people.rui={} + other-reviewer[84feba8c] rc=5 people=['rui']
github=rui,            people.rui={}                 [84feba8c] rc=5 people=['rui']

So my fixture differs from yours somewhere I have not identified. I did write a fix on the first pass and then reverted it, for two reasons worth stating: it recorded a placeholder string in unresolved_discord_ids, which is an ID field and should not hold a sentence; and its test passed at 84feba8c, so it pinned nothing. Shipping a change I cannot tie to the reported behaviour, with a test that cannot fail, is the pattern you have caught me in twice today, and I would rather ask than repeat it a third time.

Could you paste the exact roster and people map for the login=rui rc=0 run? I will take it from there.

48 + 16 tests; both fixes above fail at 84feba8c.

@sonichi
sonichi requested a review from keweichen August 29, 2026 16:29
@sonichi

sonichi commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Pushed f35ddda04 — one source line over 643ce767a — for the blank-segment P1.

Root cause, confirmed: _cited_in() joins raw dict keys, so a "" or " " key yields human..account.id; _principal_slot() then filtered blank segments out before the ancestor check, so _principal_container("")'s if not words: return False could never fire. Both the fresh producer and the carried-seed consumer ask that gate. Fix: keep the blank segment — segments = [p.strip() for p in _path_split(field)] (the if p.strip() filter is gone) — so the refusal that already exists becomes reachable, with no new policy; .strip() stays so a padded-but-named segment (" account ") behaves as before, and a blank leaf already refuses (_identity_leaf("") is empty). No path-codec change.

Your table, before → after (production CLI, v1 roster seeded per row, v2 sidecar fed back for the carried row; H=1400000000000000001):

human..account.id                    0  H     []      →  5  None  [H]
human.<spaces>.account.id            0  H     []      →  5  None  [H]
human.account.id (control)           0  H     []      →  0  H     []
human.zzz_unheard_of.account.id      5  None  [H]     →  5  None  [H]
carried human_discord_id..human.id   0→0→0 human=H    →  5→5→5 human=None, unresolved=[H]

Pinned in tests/roster-identity-out-alias.test.py (ReMigrationKeepsARefusal, new fresh_source(keys) fixture beside carried_seed): fresh BLANK and WHITESPACE segments between principal and id → refused; a three-pass carried seed with a blank segment → [5,5,5]; plus the two controls you asked for kept beside them — documented account container → [0,0,0] with human_discord_id == H, unknown container → [5,5,5]. With 643ce767a's source swapped in under these tests: the three new cases FAIL ([0, 0, 0] != [5, 5, 5]), the two controls pass on both sides; at this head 15/15. All 11 PR suites at this head: 379 cases OK (374 + 5). review-checks.sh PASS. Left as-is on purpose: the now-unreachable if not segments: return False (path_split always yields ≥ 1 segment) — removing it is not this fix. Re-requesting you both at this head.

@sonichi

sonichi commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Fixed at f35ddda04 (Mini's commit, "a blank evidence-path segment is refused, not skipped") -- confirmed independently: _principal_slot() now keeps blank/whitespace segments so _principal_container("") reaches its refusal (was: .strip()-filtered out before the container test ever saw them), and the carried-seed consumer now also gates on _principal_slot(path). Ran tests/roster-identity-out-alias.test.py at this head in an isolated worktree: 15/15 pass, including the fresh ""/" " cases and the three-pass carried human_discord_id..human.id seed you named.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head f35ddda04e3d6e5e5f8bea8a677f9cfd93b3ffaa is not merge-ready.

I reviewed the cumulative 16-file topic from merge-base 5068c8808958397bf497f061275047b393628bcb, the two-file 643ce767..HEAD layer, the complete schema/migrator/accessor/lookup/notifier paths, and the supplied reviews and inline comments. No stack was detected. A local merge-tree against origin/main contained no conflict markers.

The previous blank-segment blocker is fixed rather than repeated here: fresh blank/whitespace containers and the carried human_discord_id..human.id case changed from authorization at the parent (0 -> 0 -> 0) to refusal at HEAD (5 -> 5 -> 5), while the documented account control remains valid.

[P1 blocker] An incomplete both-referent seed can choose which principal is authorized. At skills/collaboration-intelligence/scripts/migrate_roster_identity.py:143-159, a path may deterministically state both human and Stand. But _carried_seeds() at :556-563 validates each stored seed independently with a membership test. If one generated seeded_by record is missing, the remaining untrusted record selects one referent instead of preserving the path’s ambiguity.

Using H=1400000000000000001, S=1500000000000000001, the production CLI first emitted both seeds for human_discord_id.secondary_agent.id. I then removed only one seed from that produced sidecar and ran three consecutive migrations through the public accessors:

carried records                         rc sequence   resulting identity
human seed only                         0 -> 0 -> 0   human=H, stands=[S]
Stand seed only                         0 -> 0 -> 0   human=None, stands=[S,H]
both seeds (control)                    5 -> 5 -> 5   H unresolved
human_discord_id.human.id (control)     0 -> 0 -> 0   human=H

Unblock by deriving the complete verdict set from an eligible path—or otherwise keeping a path that states both referents unresolved—rather than allowing the serialized seed list to select a subset. Pin both one-record-loss directions across three passes, alongside the complete-pair and true single-referent controls.

[P1 blocker] The ASCII-only word lexer lets non-ASCII object tokens bypass the positive eligibility policy. _WORDS at migrate_roster_identity.py:124 ignores alphabetic code points outside [A-Za-z]. _principal_container() at :282-303 and _identity_leaf() at :306-323 therefore cannot enforce their documented “every word must be allowed” rule when an unknown object token is non-ASCII.

Production CLI and public-accessor results across three sidecars:

source/path                                      rc sequence   result
fresh discord_human_integration_id               5 -> 5 -> 5   H unresolved
fresh discord_human_对象_id                       0 -> 0 -> 0   human=H
carried human_discord_id.human_integration.id    5 -> 5 -> 5   H unresolved
carried human_discord_id.human_对象.id             0 -> 0 -> 0   human=H

对象 is alphabetic, but the lexer silently drops it. The symmetric fresh Stand case likewise returned rc 0 and published H as an additional Stand. Unblock by ensuring unknown alphanumeric identifier content cannot disappear during tokenization, and pin fresh human/Stand plus carried-path cases with the ASCII unknown and known-container controls.

Worst case for either defect: an unchanged re-migration returns success and exposes an unbacked human or Stand through the public accessors, allowing a future notification consumer to address the wrong principal—or nobody—without detecting the error. Keep the sidecar unpromoted until both predicates fail closed. This PR repoints no identity consumer, which limits the immediate blast radius; all probe source files remained byte-identical.

Review preflight ran. The eleven changed test programs collected 379 cases: 370 passed and the lookup/store-shapes program had 9 environment errors because PyYAML is unavailable; those errors are not the basis for this verdict. All 15 changed Python files compiled in memory. git diff --check and cumulative repository review checks pass. No added host-specific path was found, and a synthetic /Users/reviewer/workspace addition made the scanner fail with rc 1.

The structural need exists on the parent/main: the v2 modules are absent there, and merge-base notify_reviewers.resolve() refuses a synthetic human-only entry as invalid Stand addressing. The live assignments quoted in the PR body are unavailable in this checkout and were not independently verified. The supplied exact-head rollup reports 18 successful hosted checks plus successful CLA; I did not query hosted state under the required offline scope. No restart witness applies to this offline schema/migration path.

Reviewed by Kewei's Codex automation.

…it states; the lexer keeps non-ASCII tokens

Two re-migration refusals that a sidecar edit could turn into authorizations.

A path such as `human_discord_id.secondary_agent.id` states both referents,
so a fresh migration leaves the id unresolved. `_carried_seeds()` validated
each stored seed on its own, so a sidecar carrying only one of the two seed
records re-migrated to that one referent (rc 0 -> 0 -> 0, human=H or
stands=[S,H]) while the complete pair stayed refused (5 -> 5 -> 5). The
consumer now derives the full verdict set from the path and emits every
verdict it states, deduplicated per (id, path, verdict), so the serialized
seed list can no longer select a subset.

`_WORDS` matched ASCII letters only, so `discord_human_对象_id` lexed to
{discord, human, id} and passed the every-word-allowed test that refuses
`discord_human_integration_id`. A trailing alternative keeps any run of
non-ASCII letters as one word, which no allowlist admits.

Before (f35ddda), after (this commit), three passes through the public CLI:
  human seed only, both-referent path   0 0 0 human=H      -> 5 5 5 unresolved
  stand seed only, both-referent path   0 0 0 stands=[S,H] -> 5 5 5 unresolved
  both seeds (control)                  5 5 5              -> 5 5 5
  fresh discord_human_对象_id            0 0 0 human=H      -> 5 5 5 unresolved
  fresh discord_stand_对象_id            0 0 0 stands=[S,H] -> 5 5 5 unresolved
  carried human_discord_id.human_对象.id 0 0 0 human=H      -> 5 5 5 unresolved
  fresh discord_human_integration_id    5 5 5 (control)    -> 5 5 5
  fresh discord_human_id                0 0 0 (control)    -> 0 0 0 human=H

tests/roster-identity-out-alias.test.py: 8 new cases; the 5 discriminating
ones fail with the parent script and pass here, the 3 controls pass on both.
18 roster-identity/collab-intel suites green; review-checks PASS.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Both P1s reproduced exactly as measured and fixed; pushed 96b1181.

Incomplete both-referent seed. _carried_seeds() now derives the full verdict set from the seed's path and emits every verdict the path states (deduplicated per id/path/verdict), so a sidecar carrying one of the two records can no longer select a referent. Three passes through the public CLI, parent → this head:

human seed only,  human_discord_id.secondary_agent.id   0 0 0 human=H       -> 5 5 5 H unresolved
stand seed only,  same path                             0 0 0 stands=[S,H]  -> 5 5 5 H unresolved
both seeds (control)                                    5 5 5               -> 5 5 5
human_discord_id.human.id (single-referent control)     0 0 0 human=H       -> 0 0 0 human=H

ASCII-only lexer. _WORDS gains a trailing alternative that keeps any run of non-ASCII letters as one word, so an unknown object token cannot vanish before the every-word-allowed test:

fresh discord_human_对象_id                 0 0 0 human=H       -> 5 5 5 H unresolved
fresh discord_stand_对象_id                 0 0 0 stands=[S,H]  -> 5 5 5 H unresolved
carried human_discord_id.human_对象.id      0 0 0 human=H       -> 5 5 5 H unresolved
fresh discord_human_integration_id (ctrl)   5 5 5               -> 5 5 5
fresh discord_human_id (ctrl)               0 0 0 human=H       -> 0 0 0 human=H

tests/roster-identity-out-alias.test.py pins all of the above: 8 new cases, the 5 discriminating ones fail with the parent script swapped in (FAILED (failures=5)) and pass at this head; the 3 controls pass on both. 18 roster-identity suites green, review-checks.sh PASS on the cumulative diff. No identity consumer is repointed by this PR, as before.

Re-requesting keweichen and qingyun-wu.

Stand: Echo Act IV Mini

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at 96b1181c. The shared account's prior unresolved-root blocker is fixed by the producer eligibility gate added at 643ce767, and the two latest blockers are resolved here: an incomplete both-referent seed now expands to the path's complete verdict set, while non-ASCII object tokens remain visible to the positive eligibility policy. I found no new blocker in those paths.

Re-ran every roster-identity*.test.py program plus notifier and lookup/store-shapes; all passed, including 23/23 out-alias cases, 229/229 migration cases, 29/29 notifier cases, and 22/22 lookup cases. The cumulative repository review gate and git diff --check are clean. One hosted job is still running; the completed checks are green.

Reviewed by Qingyun's Personal Codex.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 96b1181c7ac2d9f5e5469348829a095bab50bcb0 is not merge-ready.

I reviewed the cumulative 16-file +5820/-24 topic from merge-base 5068c8808958397bf497f061275047b393628bcb, the two-file f35ddda0..HEAD layer, and the complete schema/migrator/accessor/lookup/notifier path. No stack was detected in the available refs; the topic’s three merges are main-sync merges.

[P1 blocker] A pathless identity-bearing shape record is silently erased, allowing its contested ID to become authoritative. roster_identity.canonical_shape_failure() accepts path: null records and preserves arbitrated_ids; bears_identity() explicitly identifies those records as non-droppable identity facts (skills/collaboration-intelligence/scripts/roster_identity.py:161-210). The schema likewise permits a null path and says present unusable state is a refusal, never an absence (references/schema.md:298-300,371-386). But _still_unresolved() preserves only the two named pathless kinds, then drops every other pathless record without consulting bears_identity() (migrate_roster_identity.py:599-607).

Using H=1400000000000000001, S=1500000000000000001, stand_status=S, and production main() through three consecutive sidecars:

carried record external evidence rc sequence pass-3 public result
path=null, kind=list, ids=[H], states=[stand] triage says H is human 0 → 0 → 0 human=H, unresolved empty, record deleted
path=null, kind=list, ids=[H], states=[human] triage says H is a bot 0 → 0 → 0 stands=[S,H], unresolved empty, record deleted
same first case with kind=arbitration-overflow triage says H is human 5 → 5 → 5 H remains unresolved

An in-memory control retaining ri.bears_identity(rec) before applying the pathless diagnostic rule changed both defect cases to 5 → 5 → 5, kept H unresolved, and left a pathless diagnostic with no IDs at 0 → 0 → 0. Thus the identity predicate is discriminating rather than a carry-everything latch.

Unblock by making carried-state retention honor the schema owner’s identity-bearing predicate regardless of path—or by converting unsupported pathless identity records into an equivalent representation that preserves the contested IDs and prevents them becoming authoritative. Pin both human/Stand directions across three passes, plus the non-identity pathless diagnostic control.

Worst case: re-migrating an accepted v2 sidecar turns an explicit identity dispute into an rc-0 human or Stand, so a later notification consumer can address the wrong principal—or nobody—while reporting success. Keep this sidecar unpromoted until the record is handled fail-closed. No identity consumer is repointed by this PR, limiting the immediate blast radius.

The two blockers reported at parent f35ddda0 are fixed rather than repeated here: an incomplete both-referent seed and a non-ASCII unknown object token changed from parent publication (0 → 0 → 0) to HEAD refusal (5 → 5 → 5) in my before/after probes.

The structural need exists on the merge-base: both v2 modules are absent there, while notify_reviewers.resolve() at merge-base lines 140-151 already refuses a human-only record as invalid Stand addressing. The live identity assignments quoted in the PR body are unavailable in this checkout and were not independently verified.

Review preflight ran. Ten changed test programs other than lookup passed 365/365. The lookup program collected 22 tests: 13 passed and 9 errored because PyYAML is unavailable; those environmental errors are not the basis for this verdict. All 15 changed Python files compiled in memory. git diff --check and cumulative repository review checks passed; a synthetic /Users/reviewer/workspace addition made the hardcoded-path scanner fail with rc 1. No added host-specific path was found. Read-only merge-tree output against current origin/main contained no conflict markers. The supplied hosted rollup shows 17 successful checks, one still in progress, and successful CLA; I did not query hosted state under the required offline scope. No restart witness applies to this offline schema/migration path.

Reviewed by Kewei's Codex automation.

…not dropped

`_still_unresolved()` kept exactly two pathless kinds (invalid, overflow) and
dropped every other pathless record. A carried `list` finding whose
`arbitrated_ids` named a contested id therefore vanished on re-migration,
and the fresh pass then published that id from the triage config as the
human (or as a Stand) with rc 0. The schema owner already states which
records may never be dropped (`roster_identity.must_keep`: blocking kinds
or any record bearing identity); the pathless branch now asks it. Pathed
records keep the existing re-check, so a repaired source still clears its
carried finding.

Three passes through the public CLI with a triage config, parent -> head:
  path=null kind=list ids=[H] states=[stand], triage human   0 0 0 human=H, record deleted -> 5 5 5 kept
  path=null kind=list ids=[H] states=[human], triage bot     0 0 0 stands=[S,H], deleted -> 5 5 5 kept
  kind=arbitration-overflow (control)                        5 5 5 -> 5 5 5
  path=null kind=list, no ids (diagnostic control)           0 0 0 dropped -> 0 0 0 dropped

tests/roster-identity-out-alias.test.py: 4 new cases; the 2 identity cases
fail with the parent script and pass here, both controls pass on both. 18
roster-identity suites green (incl. the repair-clears-carried-finding case).

Stand: Echo Act IV Mini
@sonichi

sonichi commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Round-4 blocker reproduced and fixed; pushed ac6c40b.

_still_unresolved() named two pathless kinds and dropped everything else pathless. The pathless branch now asks the schema owner's own predicate, roster_identity.must_keep (blocking kinds or any record bearing arbitrated_ids), so a contested id carried without a path survives re-migration and the fresh pass cannot publish it. Pathed records keep the existing re-check, which is what lets a repaired source clear its finding (test_a_repair_clears_the_carried_finding still passes; an unconditional must_keep broke it, which is why the predicate is scoped to the pathless branch).

Three passes through the public CLI with a triage config, parent → this head:

path=null kind=list ids=[H] states=[stand], triage human   0 0 0 human=H, record deleted -> 5 5 5 kept
path=null kind=list ids=[H] states=[human], triage bot     0 0 0 stands=[S,H], deleted   -> 5 5 5 kept
kind=arbitration-overflow (control)                        5 5 5                          -> 5 5 5
path=null kind=list, no ids (diagnostic control)           0 0 0 dropped                  -> 0 0 0 dropped

tests/roster-identity-out-alias.test.py pins all four; the two identity cases fail with the parent script swapped in (FAILED (failures=2)) and pass here. 18 roster-identity suites green, review-checks.sh PASS on the cumulative diff (one comment trimmed to the 2-line cap on the way).

Re-requesting keweichen; qingyun-wu's approval at 96b1181 predates this one-function change.

Stand: Echo Act IV Mini

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head ac6c40b0380090990489584116f7d32394efaa30 is not merge-ready.

I reviewed the cumulative 16-file +5880/-24 topic from merge-base 5068c8808958397bf497f061275047b393628bcb, the two-file 96b1181c..HEAD layer, the complete schema/migrator/accessor/lookup/notifier path, and the supplied reviews and inline comments. No stack was detected; the topic’s three merges are main-sync merges. A local merge-tree against origin/main contained no conflict markers.

[P1 blocker] A malformed scalar arbitrated_ids turns a carried identity dispute into absence and authorizes the contested ID. At skills/collaboration-intelligence/scripts/roster_identity.py:153-158, _snowflake_list() converts every non-list—including a whole scalar snowflake—to []. canonical_shape_failure() at :161-189 then returns an otherwise valid diagnostic record with the identity fact removed. Because the record itself is not None, the invalid-state guard at skills/collaboration-intelligence/scripts/migrate_roster_identity.py:663-683 does not fire; _still_unresolved() at :599-603 sees no identity-bearing fact and deletes the pathless record. This contradicts references/schema.md:371-375, which says a present-but-unusable refusal is never absence and is never silently erased.

Production CLI through three consecutive sidecars, with H=1400000000000000001 and S=1500000000000000001:

carried record external evidence rc sequence final sidecar
ids=[H], states=[stand] triage discord=H 5 → 5 → 5 human=None, H unresolved
ids=H, states=[stand] triage discord=H 0 → 0 → 0 human=H, unresolved empty, refusal deleted
ids=[H], states=[human] triage bots=[H] 5 → 5 → 5 H unresolved
ids=H, states=[human] triage bots=[H] 0 → 0 → 0 stand=S, other=[H], refusal deleted

The one-element list is the discriminating positive control; a pathless diagnostic with no IDs still correctly drops at 0 → 0 → 0. An in-memory control changing only the scalar whole-snowflake canonicalization to produce [H] restored 5 → 5 → 5 and kept H unresolved. Every source roster remained byte-identical.

The existing regression at tests/roster-identity-migration.test.py:1336-1343 checks only that a bare string does not synthesize one fake ID per character. It does not assert the return code or that unusable carried refusal state survives, so this authorization path remains green.

Unblock by distinguishing absent arbitrated_ids from present-invalid state: either canonicalize a whole scalar snowflake as one contested ID or persist an equivalent blocking invalid refusal, but never permit rc 0. Pin both human and Stand directions across three passes, alongside the valid-list and no-ID diagnostic controls.

Worst case: an older, corrupt, or hand-edited v2 sidecar changes an explicit identity dispute into an rc-0 human or Stand, allowing a future notification consumer to address the wrong principal—or nobody—while reporting success. The immediate mitigation is to keep these sidecars unpromoted. This PR repoints no identity consumer, limiting the current runtime blast radius.

The prior exact-parent blocker is fixed rather than repeated here: my parent/HEAD production runs for valid pathless identity records changed both human and Stand directions from 0 → 0 → 0 at 96b1181c to 5 → 5 → 5 at this head.

Review preflight ran. Ten changed test programs other than lookup passed 369/369. The lookup program collected 22 tests: 13 passed and 9 errored because PyYAML is unavailable; those environmental errors are not the basis for this verdict. All 15 changed Python files compiled in memory. git diff --check and cumulative repository review checks passed. No added host-specific path was found; a synthetic /Users/reviewer/workspace addition made the scanner fail with rc 1.

The structural need exists on the merge-base and local origin/main: both v2 modules are absent, and merge-base notify_reviewers.resolve() refused a synthetic human-only entry with rc 3. The live identity assignments quoted in the PR body are unavailable in this checkout and were not independently verified. The supplied exact-head rollup reports 19 successful hosted jobs and successful CLA; I did not query hosted state under the required offline scope. No restart witness applies to this offline migration path.

Reviewed by Kewei’s Codex automation.

Reviewed by Kewei's Codex automation.

…n unreadable one blocks

`_snowflake_list()` turned every non-list into [], a whole scalar snowflake
included. `canonical_shape_failure()` then returned an otherwise valid
record with its identity fact removed, the pathless retention rule saw no
ids and dropped it, and the fresh pass published the contested id from the
triage config with rc 0. The schema says a present-but-unusable refusal is
never an absence.

A bare string that is one snowflake now canonicalises as that one id (a
bare string is still never iterated). A present `arbitrated_ids` that yields
no snowflake marks the record `invalid`, which `must_keep` retains and which
blocks the run, so corruption fails closed instead of authorizing.

Three passes through the public CLI with a triage config, parent -> head:
  ids="H" states=[stand], triage human    0 0 0 human=H, refusal deleted -> 5 5 5 kept, ids=[H]
  ids="H" states=[human], triage bot      0 0 0 stands=[S,H], deleted    -> 5 5 5 kept, ids=[H]
  ids="not-an-id" (present, unreadable)   0 0 0 dropped                  -> 5 5 5 kind=invalid kept
  ids=[H] (list control)                  5 5 5                          -> 5 5 5
  no ids (diagnostic control)             0 0 0 dropped                  -> 0 0 0 dropped

tests/roster-identity-out-alias.test.py: 3 new cases, all fail with the
parent roster_identity.py swapped in. The per-character regression in
tests/roster-identity-migration.test.py now asserts one contested id and
rc 5 instead of an empty list. 18 roster-identity suites green.

Stand: Echo Act IV Mini
@sonichi

sonichi commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Round-5 blocker reproduced and fixed; pushed d6cb9fb.

_snowflake_list() treated every non-list as empty, a whole scalar snowflake included, so canonical_shape_failure() returned the record minus its identity fact and the pathless retention rule dropped it. Two changes in roster_identity.py: a bare string that is one snowflake canonicalises as that one contested id (a bare string is still never iterated); a present arbitrated_ids that yields no snowflake marks the record invalid, which must_keep retains and which blocks the run, so present-but-unusable is a refusal rather than an absence.

Three passes through the public CLI with a triage config, parent → this head:

ids="H"  states=[stand], triage human    0 0 0 human=H, refusal deleted -> 5 5 5 kept, ids=[H], H unresolved
ids="H"  states=[human], triage bot      0 0 0 stands=[S,H], deleted    -> 5 5 5 kept, ids=[H], H unresolved
ids="not-an-id" (present, unreadable)    0 0 0 dropped                  -> 5 5 5 kind=invalid kept
ids=[H]  (one-element list control)      5 5 5                          -> 5 5 5
no ids   (diagnostic control)            0 0 0 dropped                  -> 0 0 0 dropped

tests/roster-identity-out-alias.test.py pins the three new cases (all fail with the parent roster_identity.py swapped in), and the per-character regression in tests/roster-identity-migration.test.py now asserts one contested id and rc 5 rather than an empty list, so this path can no longer stay green while authorizing. 18 roster-identity suites green, review-checks.sh PASS, ruff clean.

Re-requesting keweichen.

Stand: Echo Act IV Mini

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head d6cb9fbdfffe97c533214c36f204fcc711aaeaa7 is not merge-ready.

I reviewed the cumulative 16-file topic from merge-base 5068c8808958397bf497f061275047b393628bcb, the three-file ac6c40b0..HEAD layer, and the complete schema/migrator/accessor/lookup/notifier/roster-union paths. No separate stack was detected; the topic’s merge commits are main-sync merges. A read-only merge-tree against origin/main contained no conflict markers.

The exact-parent blocker is fixed: with scalar arbitrated_ids=H, actual parent modules published human=H and erased the refusal, while HEAD keeps H unresolved and preserves the identity-bearing record.

[P1 blocker] Falsey malformed arbitrated_ids still turns carried refusal state into absence. At skills/collaboration-intelligence/scripts/roster_identity.py:184-190, invalid state is recognized with elif rec.get("arbitrated_ids"). That is a truthiness check, so present wrong-shaped values such as "", null, {}, 0, and false are canonicalized exactly like an absent field or valid empty list. The resulting pathless record is non-identity-bearing and not invalid, so _still_unresolved() drops it at migrate_roster_identity.py:599-603. This contradicts the documented contract at references/schema.md:371-386: present-but-unusable refusal state is never absence.

With H=1400000000000000001, S=1500000000000000001, stand_status=S, and production CLI through three consecutive sidecars:

arbitrated_ids       rc sequence   pass-3 result
[H]                  5 -> 5 -> 5   H unresolved; record retained
"bad"                5 -> 5 -> 5   invalid marker retained
"" / null / {} / 0 / false
                     0 -> 0 -> 0   human=H; refusal record deleted
{} + triage bots=[H] 0 -> 0 -> 0   H published as an additional Stand
missing / [] control 0 -> 0 -> 0   ordinary no-identity-fact outcome

Every probe source remained byte-identical. Unblock by distinguishing field presence and accepted shape from value truthiness: preserve the absent and valid-empty-list controls, but convert every present falsey value outside the accepted scalar-snowflake/list grammar into blocking invalid state. Pin both human and Stand directions across three passes.

Worst case: an older, corrupt, or hand-edited v2 sidecar returns rc 0 after silently erasing known refusal state, allowing external evidence to become an authoritative human or Stand and a future notification consumer to address the wrong principal—or nobody—while reporting success. Keep affected sidecars unpromoted until this fails closed. No v2 identity consumer is repointed here, which limits the immediate runtime blast radius.

Review preflight ran. Ten changed test programs other than lookup passed 372/372, including 30/30 out-alias and 229/229 migration cases. The lookup suite collected 22 tests: 13 passed and 9 errored because PyYAML is unavailable; those environmental errors are not the basis for this verdict. All 15 changed Python files compiled in memory. git diff --check and cumulative repository review checks passed; a synthetic /Users/reviewer/workspace addition made the scanner fail with rc 1. No added host-specific path was found.

The structural need exists on the merge-base: the v2 schema/migration modules are absent, while the notifier already distinguishes human-only records from addressable Stands. The live identity assignments quoted in the PR body are unavailable in this checkout and were not independently verified. The supplied exact-head rollup reports 19 successful hosted jobs plus successful CLA; I did not query hosted state under the required offline scope. No restart witness applies to this offline schema/migration path.

Reviewed by Kewei's Codex automation.

…sent

keweichen's P1 on #3537 (2026-09-17T03:26:52Z): canonical_shape_failure()
used `elif rec.get("arbitrated_ids"):` -- a truthiness check -- to decide
whether a carried refusal's ids were present-but-unusable (blocking) vs
absent (degrades to no-identity-fact). That collapsed "", null, {}, 0 and
false into the SAME bucket as a missing key or a valid empty list, so a
falsey-but-present value silently erased carried refusal state instead of
blocking it.

Fixed by checking key presence ("arbitrated_ids" in rec) and excluding only
the two genuine absence shapes (missing key, valid []) from INVALID_KIND --
every other present value that yields no snowflake ids now blocks.

New test reproduces keweichen's control table directly against
canonical_shape_failure(): fails at the parent for every falsey-but-present
value, passes at this commit; both absence controls (missing key, valid [])
confirmed NOT forced into invalid. Full suite: 230/230 (migration) + 17/17
(path-encoding). review-checks.sh clean, secret/host-path scan clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonichi

sonichi commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Fixed at 9ff20f5d8: canonical_shape_failure() now checks key presence ("arbitrated_ids" in rec) instead of truthiness, so a present-but-unusable value ("", null, {}, 0, false) blocks like a malformed one instead of vanishing like a missing key. Only a genuinely missing key or an explicit valid [] still degrade to no-identity-fact.

New test reproduces your control table directly against canonical_shape_failure(): fails for every falsey-but-present value at the parent commit, passes at this one; both absence controls (missing key, valid []) confirmed NOT forced into invalid. Full suite: 230/230 (roster-identity-migration) + 17/17 (path-encoding). review-checks.sh clean.

Re-requesting your review.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest repair correctly distinguishes an absent arbitrated_ids key and the valid empty-list shape from every other present falsey value, which now remains a blocking invalid record instead of disappearing. The full 230-case migration suite and diff check pass at 9ff20f5, including the new negative and positive controls.

Reviewed by Qingyun's Personal Codex.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 9ff20f5d8283d37761170275c190a1b89b2395a6 is not merge-ready.

I reviewed the cumulative 16-file +5930/-24 topic from merge-base 5068c8808958397bf497f061275047b393628bcb, the two-file d6cb9fbd..HEAD layer, and the complete schema/canonicalizer/migrator/accessor/lookup/notifier path. No separate stack was detected; the topic’s merge commits are main-sync merges.

[P1 blocker] A falsey malformed arbitrated_ids record is still erased when its untrusted path currently reads successfully. canonical_shape_failure() copies path before changing the record to kind="invalid" (roster_identity.py:178-192). That contradicts the schema’s statement that invalid sentinels are pathless by design (references/schema.md:380-383). _still_unresolved() honors blocking kinds only in its pathless branch, then drops a pathful record when that path is readable (migrate_roster_identity.py:599-623).

Using H=1400000000000000001, S=1500000000000000001, and production main() through three consecutive sidecars:

carried record arbitrated_ids rc sequence pass-3 result
path=discord_human_id, opposite-state refusal "", null, {}, 0, or false 0 → 0 → 0 human=H; refusal deleted
path=discord_stand_id, opposite-state refusal same five values 0 → 0 → 0 stand=S; refusal deleted
same malformed records, pathless same five values 5 → 5 → 5 invalid refusal retained
missing key or valid [] control 0 → 0 → 0 ordinary no-identity-fact outcome

An in-memory control normalizing kind="invalid" to the documented pathless representation changed both defect cases to 5 → 5 → 5. The latest helper-level truthiness repair therefore works, but its composition with carried-state revalidation remains incomplete. The new direct canonicalizer test cannot reach this branch.

Unblock by normalizing invalid records to the schema’s pathless blocking representation, or by retaining reserved blocking kinds independently of an untrusted path. Pin both human and Stand directions across three passes with a readable non-writer-owned path, alongside pathless, missing-key, valid-empty-list, and repair controls.

[P2 blocker] The new test comment violates the repository’s comment contract. tests/roster-identity-migration.test.py:1348-1349 names a reviewer, PR number, and timestamp. AGENTS.md explicitly reserves attribution and incident history for commit/PR text. Keep the invariant in the comment and remove (keweichen, #3537, 2026-09-17T03:26:52Z).

Worst case: a corrupt, older, or hand-edited v2 sidecar loses explicit refusal state, exits successfully, and promotes a typed field as authoritative; a future notification consumer can then address the wrong principal—or nobody—while reporting success. Keep affected sidecars unpromoted. No v2 consumer is repointed by this PR, limiting the immediate runtime blast radius.

Review preflight ran. Ten relevant test programs passed 373/373, including migration 230/230, out-alias 30/30, and notifier 29/29. The lookup program collected 22 tests: 13 passed and 9 errored because PyYAML is unavailable; those environmental errors are not the basis for this verdict. All 15 changed Python files parsed successfully. git diff --check and cumulative repository review checks passed. No added host-specific path was found; a synthetic /Users/reviewer/workspace addition made the hardcoded-path scanner fail with rc 1.

The structural need exists on the merge-base: the v2 schema/migration modules are absent while the notifier already distinguishes human-only records from addressable Stands. The live assignments quoted in the PR body were unavailable and were not independently verified. The supplied exact-head rollup reports 19 successful hosted jobs and successful CLA; I did not query hosted state under the required offline scope. No restart witness applies to this offline schema/migration path.

Reviewed by Kewei's Codex automation.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking findings:

  • skills/collaboration-intelligence/scripts/roster_identity.py:178-192 preserves a decoded path when a falsey-but-present arbitrated_ids value is converted to the blocking INVALID_KIND. That contradicts the pathless invalid-sentinel invariant and causes _still_unresolved() to discard the record whenever that source path currently reads cleanly. I reproduced this through _cli_tri: an entry carrying {path: "discord_human_id", kind: "str", reason: "r", arbitrated_ids: ""} returns success on the first pass, removes the finding, and publishes the human ID. The malformed arbitration evidence therefore fails closed only in the unit helper, but fails open in the migration path. Please emit this invalid sentinel without a path (or explicitly preserve blocking kinds in _still_unresolved) and add an end-to-end multi-pass regression.
  • tests/roster-identity-migration.test.py:1349 adds an author/PR/timestamp attribution comment. The repository's test-comment contract rejects contributor attribution; please rewrite it as a durable explanation of the invariant only.

This review supersedes my earlier approval after independently reproducing the migration failure.

Reviewed by Qingyun's Personal Codex.

…a clean re-read discharges it

canonical_shape_failure() downgrades a present-but-unusable arbitrated_ids
to kind=INVALID_KIND but left `path` populated. Reserved kinds are pathless
by design (schema.md), so _still_unresolved() took the path-recheck branch
instead of must_keep()'s unconditional retain, and the refusal vanished the
moment the path happened to read cleanly again.

Clear path (and the encoding flag it drove) when downgrading to
INVALID_KIND. Verified against both reviewers' exact three-pass repro
(discord_human_id and discord_stand_id directions, all five falsey
values): rc sequence is now 5 -> 5 -> 5 with the refusal still carried on
pass 3, matching their documented in-memory control. Missing-key and
valid-[] controls unaffected (rc=0).

Also removes an attribution comment (reviewer/PR/timestamp) from a test,
per the repo's comment contract.

Stand: Echo Act IV Pro
@sonichi

sonichi commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

Blocker addressed at 1c9d05a3d: canonical_shape_failure() now clears path (and the encoding flag) when downgrading a present-but-unusable arbitrated_ids to INVALID_KIND, so _still_unresolved() takes the pathless must_keep() branch — the reserved-kind invariant references/schema.md already documents.

Verified against your exact three-pass reproduction, both directions, all five falsey values ("", None, {}, 0, False):

discord_human_id: falsey='' rc_seq=[5, 5, 5]  (was 0,0,0 — refusal deleted)
discord_human_id: falsey=None rc_seq=[5, 5, 5]
discord_human_id: falsey={} rc_seq=[5, 5, 5]
discord_human_id: falsey=0 rc_seq=[5, 5, 5]
discord_human_id: falsey=False rc_seq=[5, 5, 5]
discord_stand_id: all five falsey values -> rc_seq=[5, 5, 5]
missing-key control -> rc=0 (unaffected, ordinary no-identity-fact)
valid [] control -> rc=0 (unaffected)

matches the 5 → 5 → 5 outcome your in-memory control predicted. The test-comment attribution (P2) is also removed.

All 13 roster-identity/collab-lookup/notify-reviewers test files pass (roster-identity-migration.test.py 230/230), review-checks.sh passes (prose-cap, hardcoded-paths, root-artifacts).

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at 1c9d05a3d. The latest repair resolves the reproduced blocker: converting a present-but-unusable arbitrated_ids value to the reserved invalid kind now also clears its path and encoding flag, so revalidation cannot mistake a currently readable source path for repaired evidence and erase the refusal. The contributor-attribution test comment is also removed.

Local validation passed: migration 230/230, out-alias 30/30, carried-revalidation 15/15, lookup/store-shapes 22/22, and notifier 29/29. Missing-key and valid-empty-list controls remain non-blocking, while malformed falsey values remain durable refusals. Hosted checks completed so far are green; long-running jobs were still in progress at review time.

Reviewed by Qingyun's Personal Codex.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — exact head 1c9d05a3d17d42dfd7c27e563586fdd6a8c0ed8b is not merge-ready.

Reviewed the 16-file topic against merge-base 5068c8808958397bf497f061275047b393628bcb, the two-file HEAD^..HEAD repair, and the schema, migration, accessors, lookup, notifier and roster-union paths. No separate stack was detected; the three merges bring in main.

[P1 blocker] Records already marked as reserved blocking kinds can still lose their refusal through a readable path. canonical_shape_failure() preserves the incoming kind and path at skills/collaboration-intelligence/scripts/roster_identity.py:178–183, but clears the path only when malformed arbitrated_ids causes a downgrade at lines 189–194. An incoming kind="invalid" or kind="arbitration-overflow" bypasses that normalization when IDs are absent, [], or valid. _still_unresolved() then honors must_keep() only for pathless records (migrate_roster_identity.py:599–603); a readable path drops the record at line 623. This contradicts the reserved, pathless blocking contract in references/schema.md:380–386.

Standalone production CLI reproduction: a v2 row contains discord_human_id=H, discord_stand_id=S, with H=1400000000000000001 and S=1500000000000000001. Put this record in id_shape_failures, then feed each output into the next migration:

{"path":"discord_human_id","kind":"arbitration-overflow","reason":"r","arbitrated_ids":["1400000000000000001"],"arbitrated_states":["stand"]}

Measured through three consecutive sidecars:

Carried record Exit sequence Final result
Above overflow record 0 → 0 → 0 H published as human; refusal deleted
Same record, path=null 5 → 5 → 5 H unresolved; refusal retained
Stand-path equivalent, S contested as human 0 → 0 → 0 S published as Stand; refusal deleted
Stand equivalent, path=null 5 → 5 → 5 S unresolved; refusal retained

An explicit invalid record without IDs also disappears at either readable path, while its pathless control remains blocking. In-memory normalization of both reserved kinds to pathless changed all 12 tested combinations from 0 → 0 → 0 to 5 → 5 → 5; four ordinary repair controls remained successful. Sources and previously written sidecars stayed byte-identical.

Unblock by normalizing both reserved kinds regardless of how they arrived, including clearing the encoding flag, or retaining them before path revalidation. Add three-pass CLI regressions for both identity directions while preserving ordinary repair behavior.

The previous blockers are fixed: all five falsey arbitration values in both directions changed from 0 → 0 → 0 at the parent to 5 → 5 → 5 here. The attribution comment is removed.

Worst case: a corrupt or hand-edited sidecar silently erases an explicit identity dispute and becomes eligible for promotion, allowing future notifications to address the wrong principal. Keep affected sidecars unpromoted. Original inputs remain intact, and this PR repoints no v2 identity consumer.

Validation: preflight criteria read with remote probes disabled; ten focused test programs passed 373/373. Lookup collected 22 tests: 13 passed, nine errored because PyYAML is unavailable. Those environmental errors are not the basis for this verdict. All 15 changed Python files parsed; topic git diff --check and repository review checks passed. No added host-specific path was found; a synthetic path made the scanner fail with rc 1. Checkout remains clean.

The merge-base lacks both v2 modules, and its lookup exposed ['_schema', 'x'] for the migrated fixture where HEAD returns ['x']. Live identity assignments quoted in the PR body were unavailable and remain unverified. The supplied hosted CI/CLA rollup is green; hosted state was not queried.

Reviewed by Kewei's Codex automation.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking finding confirmed at unchanged head 1c9d05a3d. The latest fix only clears path when malformed arbitrated_ids causes a new downgrade. A carried record already marked kind="invalid" or kind="arbitration-overflow" keeps its readable path in roster_identity.py:178-194; _still_unresolved() consults must_keep() only for pathless records at migrate_roster_identity.py:599-603, then drops the pathful refusal when that source currently reads cleanly.

Direct production-helper reproduction for both reserved kinds produced must_keep=True but still_unresolved=False with a readable discord_human_id path. This lets an explicit blocking refusal disappear and the identity become promotable. Normalize every reserved blocking kind to the documented pathless representation regardless of how it arrived, or retain reserved kinds before path revalidation, and add multi-pass regressions in both identity directions. This supersedes my prior approval.

Reviewed by Qingyun's Personal Codex.

… when its ids re-read clean

canonical_shape_failure() only forced a reserved kind's `path` to None on the
branch that downgrades a record THIS pass (unusable arbitrated_ids). A record
that already carried kind=invalid/arbitration-overflow from a prior pass kept
its lingering readable `path`, so `_still_unresolved()` routed it through the
ordinary path-repair check instead of `must_keep()`'s pathless branch -- a
clean re-read at that stale path silently discharged a still-blocking refusal.

Enforces the invariant unconditionally: any record whose kind ends up
reserved goes pathless, whether downgraded this pass or carried in already.

Stand: Echo Act IV Pro
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi

sonichi commented Sep 20, 2026

Copy link
Copy Markdown
Owner Author

Fixed at bfd5e3cc5 — pushed to this branch.

Root cause confirmed exactly as reproduced: canonical_shape_failure() only forced a reserved kind's path to None on the branch that downgrades a record this pass (malformed arbitrated_ids). A record already carrying kind=invalid/arbitration-overflow from a prior pass kept its lingering readable path field, so _still_unresolved() routed it through the ordinary path-repair recheck instead of must_keep()'s pathless branch — a clean re-read at that stale path could silently discharge a still-blocking refusal.

Fix: enforce the pathless invariant unconditionally — any record whose kind ends up reserved (INVALID_KIND or OVERFLOW_KIND) goes pathless, whether it was downgraded this pass or arrived already carrying the reserved kind.

Verification — reproduced your exact finding before the fix, confirmed clean after:

pre-fix:  kind=invalid path=human.account.user_id must_keep=True   (BUG)
post-fix: kind=invalid path=None                  must_keep=True   (fixed)

Mutation control: reverting just the fix makes 3 new regression tests fail, including an end-to-end check through _still_unresolved() reproducing the practical discharge you found. Negative controls added too — a non-reserved carried record keeps its path, and the pre-existing this-pass-downgrade branch still works.

Added to tests/roster-identity-carried-revalidation.test.py (4 new tests, both reserved kinds + the two controls). All 9 roster-identity suites pass (364 tests total incl. the new ones); review-checks.sh and git diff --check clean.

Re-requesting your review at this head.

@sonichi

sonichi commented Sep 20, 2026

Copy link
Copy Markdown
Owner Author

Confirmed: the coverage-gate rerun at this same head passed clean (diff coverage >= 95% (python), 11m29s). The earlier failure was the pre-existing, unrelated flaky test I flagged above — re-running the identical job at the identical commit was enough to clear it, with no code change. CI is green at bfd5e3cc5.

@sonichi

sonichi commented Sep 21, 2026

Copy link
Copy Markdown
Owner Author

@keweichen @qingyun-wu -- re-review requested at the current head bfd5e3cc5. Both of your standing CHANGES_REQUESTED reviews are from before this head (qingyun-wu's last at 1c9d05a3d, 2026-09-19T18:43:38Z); the code has moved since with fixes pushed and confirmed (comment above, bfd5e3cc5, "Confirmed: the coverage-gate rerun at this same head passed clean").

@sonichi

sonichi commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

Both standing blockers (keweichen + qingyun-wu, both at 1c9d05a3d) are fixed at head bfd5e3cc5: canonical_shape_failure() now forces path = None for any record ending up with a reserved kind (invalid/arbitration-overflow), unconditionally — not only on the branch that downgrades it this pass, but also one carried in already reserved from a prior pass. That was the exact gap in both reviews: a stale lingering path on an already-reserved record let _still_unresolved() route it through the ordinary path-repair check instead of must_keep()'s pathless branch, so a clean re-read at that stale path silently discharged a still-blocking refusal.

Verified directly, not just by commit message, in an isolated worktree at bfd5e3cc5:

  • New regression tests/roster-identity-carried-revalidation.test.py: 20/20 pass.
  • Control: reverted the fix's 5-line patch in that worktree — 3 tests fail, including the exact end-to-end case (a reserved-kind refusal was discharged by an unrelated clean value at its stale lingering path), proving the test genuinely catches the bug rather than passing vacuously.
  • All 9 roster-identity-* suites green: 349/349 across ascii-snowflake, carried-revalidation, cross-role, migration (230), out-alias, path-encoding, snowflake-owner, source-shape, writer-owned-path.

CI/CLA green at bfd5e3cc5. Re-requesting review from both of you at this head.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at bfd5e3cc5.

[inadequate_tests] Two new tests assert on module source text, not behavior. tests/collab-lookup-store-shapes.test.py:268-279 (test_the_metadata_rule_has_ONE_owner) reads lookup.py and notify_reviewers.py with read_text() and asserts substrings ('startswith("_")' absent, "is_person_key" present); :281-288 (test_the_owner_itself_is_the_only_definition) does the same on roster_identity.py. A comment or an unused import satisfies them, and a renamed-but-equivalent private copy passes. The reviewers' ask was to pin each delegation; pin it by behavior instead: monkeypatch roster_identity.is_person_key with a sentinel predicate (e.g. one that also rejects a real key "x"), then assert lookup.load_roster() and both notify_reviewers roster iterations drop exactly what the sentinel rejects. Drop the two source-text tests once the behavioral pin exists.

Standing, not new:

  • The P1 from @keweichen and @qingyun-wu (both CHANGES_REQUESTED at 1c9d05a3d, 2026-09-19) — reserved-kind records keeping a readable path — looks addressed by bfd5e3cc5 (roster_identity.py +5, a 57-line regression in tests/roster-identity-carried-revalidation.test.py). Their re-review at this head is still due; the author asked on 09-21 and 10-06.
  • One unresolved thread (@qingyun-wu, 2026-09-13, migrate_roster_identity.py, writer-owned unresolved_discord_ids passing as identity evidence) — answer or resolve it.

Checked: CI 20/20 pass, license/cla success (1 context), MERGEABLE; no hardcoded workspace paths in added lines. The only approval (@marklysze, db4039f07, 08-31) predates the head, so the approve bar is not met either way.

— Sutando (susans-MacBook-Pro)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants