Skip to content

R7+R8: experimental validity layer, governance-graph typing, runtime-graph diff, CI with teeth - #32

Merged
LeoLin990405 merged 31 commits into
mainfrom
r7/validity
Aug 3, 2026
Merged

LeoLin990405 merged 31 commits into
mainfrom
r7/validity

Conversation

@LeoLin990405

Copy link
Copy Markdown
Owner

Stacks R7 (experimental validity) and R8 (graph engineering foundations + hardening) onto main.

Why this branch exists

R7 started from a smoke run that scored china/tang 10 and its random control 2.5. The control's transcript turned out to be the model asking the operator how to configure the experiment: the generator had replaced every office with "Baseline Agent N" and cut SOUL.md to a 571-byte disclaimer, so the run varied persona and topology at once. The number was confident and meaningless. Most of what follows is aimed at making that class of error visible rather than plausible.

R7 โ€” experimental validity

  • Controls vary topology only. SOUL.md is copied verbatim, office ids/labels/duties are preserved, and only the decision-flow prose and the mermaid diagram are rewritten. Leaving the source diagram in place made a control assert two contradictory wirings and follow whichever it read last.
  • topology.regime is derived from the destination directory (mirroring the validator's own rule); --dest stages a control at an exact runnable path; validateRegime accepts a single leading underscore so _baseline/* can actually run as a civ โ€” previously a control could be generated and validated but never used.
  • Judging: anonymization now blinds topology node labels and IDENTITY agent ids, not just regime ids; anchored 4-point rubric; order-swapped passes; up to three providers; shared verbosity budget; biasReport with per-provider variance, same-family vs cross-family gap, and position effect.
  • Episodic memory retrieval with stopword filtering and a deterministic tie-break (CURRENT_TIMESTAMP is second-granular).

R8 โ€” graph engineering foundations + hardening

  • Node typing (agent / gate / checkpoint / router), optional and defaulting to agent, plus gate_count / checkpoint_count kept deliberately separate from checks_cycles. No current regime declares a deterministic gate and the tests assert that: the Chancellery and the Directorate of Ceremonial are LLM reviewers, and the genuinely deterministic [VETO] sniffer lives in engine/mechanisms/veto.mjs attached to an agent node.
  • civagent runtime-graph <matchId> --diff reconstructs the actual call graph from the event span tree and compares it to the declaration. Its main result is a negative one: declared nodes are offices, runtime actors are regimes, and the id spaces do not overlap, so per-office edge verification is blocked on instrumentation rather than merely unimplemented. unexercised_ratio is null in that case instead of the 1.000 it would otherwise report for every regime in every match.
  • Idempotent tournament recording. Re-recording a tournament id stacked rows and silently inflated Bradley-Terry win counts. Uniqueness on episodic_memory is a partial index scoped to match_end โ€” a table-wide UNIQUE(match_id, regime, event_type) would have discarded the second veto_triggered event of a match, which for this project is the most damaging row to lose.
  • CI with teeth. The old badge was a hardcoded green image. Now: a real workflow badge, a Node 20/22 matrix, and examples/ + npm run smoke โ€” a zero-API-key end-to-end path (validate โ†’ metrics โ†’ control โ†’ validate) that was verified red as well as green.

Verification

npm run ci green on the integrated branch: 421 backend + 14 frontend + 57 regimes + smoke.

Every regression test in this branch was revert-verified โ€” undo the fix, watch it go red, restore. That discipline caught four tests that passed for the wrong reason, including one that asserted duplicate row-stacking as correct behaviour and had locked the defect in permanently.

Known limits, stated in the README rather than hidden

  • Rankings are over declared topologies (ยง9.6).
  • Multi-judge hedges position/verbosity/self-preference bias; it does not eliminate it (ยง9.5).

โ€ฆutcome loop

Three defects in the cross-match learning path, all found by reading the code โ€”
the local corpus is seven rows, small enough that none of them could surface in
a run.

queryEpisodicMemory applied LIMIT 10 in SQL and only then scored by keyword, so
everything a regime learned before its eleventh match was unreachable no matter
how well it matched. Scoring now runs over the regime's whole history behind a
cap set far above any realistic corpus. Worse than reported: the ordering was
'timestamp DESC' alone, and CURRENT_TIMESTAMP is second-granular, so memories
written in the same second tie and SQLite's order among them is unspecified โ€”
the old cap could just as easily have scanned the OLDEST rows. The AUTOINCREMENT
id now breaks the tie.

The retriever kept every token longer than one character, so 'the', 'of', 'and'
matched nearly every paragraph and ranking degenerated into 'return whatever
comes first'. Added an English/Chinese stopword set and a three-character floor,
exported as extractKeywords so tests can pin it directly.

The skill auditor judged prose plausibility, which measures ~46% against real
downstream outcomes โ€” chance. It now scores three properties that actually
predict transfer: whether the skill names a failure mechanism, whether it is
actionable at step level, and whether it generalizes a hazardous action.

Feeding the judge score into the extractor turned out to be impossible as
stated: sedimentation runs at each civ's match end, while judging only happens
once every civ has finished, so no score exists yet. Added engine/v5/
skill-outcome.mjs instead โ€” after judging, the tournament stamps score, rank and
provenance into each sedimented skill's frontmatter. Skills from a losing run
are the ones most likely to transfer badly, and nothing downstream could weight
or retire them without knowing. Best-effort: never fails the tournament.

17 tests, all revert-verified (reverting the LIMIT turns two red, reverting the
stopwords turns one red). ci: 321 backend green.
โ€ฆ anything

CivAgent claims that differences between 57 historical governance topologies
drive differences in governance quality. That claim has never been tested
against a null. It is fragile in a specific way: agentCount is 7 for 29 of the
57 regimes and 6-8 for 51 of them, so a ranking that looks like a structural
effect could be an agent-count effect.

Adds three controls, generated into regimes/_baseline/ and never touching a real
regime directory: solo (one agent, no edges), flat-N (same agent count, zero
edges, fully parallel) and random-N (same agent count, comparable edge count,
edge kinds sampled from the source distribution, connected, no self-loops).
Random generation is seeded and reproducible. All three pass validate:regimes,
and topology-bearing sources also produce valid topology.json.

Reproducibility fix found in review: the artifacts embedded a generatedAt wall
clock, so two runs of one seed were not byte-identical. Worse, the test excused
it by deleting the field before comparing โ€” legalising the very
non-determinism the seed exists to prevent. The stamp is gone, and the test now
also asserts structurally that no timestamp appears in the output: comparing two
back-to-back runs cannot catch this on its own, because both land in the same
millisecond and compare equal while the artifact is still irreproducible.

ci: 333 backend green (29 baseline tests, revert-verified).
Every conclusion CivAgent draws runs through blind judging plus Bradley-Terry,
and three known judge biases were neither controlled nor measured. Without that,
the rankings โ€” and any later regression on whether a learned skill helps โ€” have
no statistical footing.

Verbosity: transcripts are truncated to a shared budget before the prompt is
built, so the score stops partly measuring who wrote more. Truncation is
recorded rather than silent. Position: swap passes already averaged the two
orderings, which hides the effect; the per-regime forward-vs-swapped delta is
now reported, so a systematic seat advantage becomes visible instead of being
averaged away. Self-preference: judge providers and civ backends are mapped to
model families and the same-family vs cross-family score gap is reported.

All of it lands in a new biasReport field. The scores array keeps its exact
shape โ€” the frontend and the stats pipeline both read it, and the
Bradley-Terry maths is deliberately untouched this round.

ci: 319 backend green (15 calibration tests, fake judges only).
The smoke run that motivated this scored china/tang 10 and its random
control 2.5 โ€” but the control's transcript was the model asking the
operator how to configure the experiment. The generator had replaced
every office with "Baseline Agent N โ€” generic governance agent" and cut
SOUL.md to a 571-byte disclaimer, so the run varied persona and topology
at once and the measured gap was persona presence. Any ranking built on
those controls was measuring the wrong thing.

Controls now copy SOUL.md verbatim (plus a provenance comment), keep
every office id, label and duty from the source, and rewrite only the
Decision Flow prose and the mermaid diagram to describe their own edges.
The diagram matters: leaving the source's graph in place made a control
assert two contradictory wirings and follow whichever it read last,
quietly restoring the variable the control exists to remove.

Also:
- topology.regime is derived from the destination directory, mirroring
  the validator's own rule, instead of a synthetic name that fails the
  cross-check the moment a control is staged somewhere runnable.
- new --dest flag stages a control at an exact path; without it every
  control lands three levels deep where that cross-check never fires.
- validateRegime accepts a single leading underscore on the region, so
  _baseline/* and _ablated/* can run as civs at all. They could be
  generated and validated but never used โ€” which is how an experiment
  ends up with no control arm.
- the default-path test deleted the whole regimes/_baseline tree on
  cleanup, so a full `npm test` wiped committed control arms. It now
  removes only what it created, guarded by a sentinel.
- three revert-verified persona-parity tests; a stale hardcoded agent id
  in an existing test was a silent no-op replace and is now derived.

Re-run on cn:doubao: both arms stay in role and produce full governance
documents (control 19KB, source 3.4KB digest); control 10 vs source 7.9.
The remaining length asymmetry is verbosity bias, which R7-2's judge
budget addresses separately.

CI: 369 backend + 14 frontend + 57 regimes, all green.
The CI badge was a hardcoded green image. It stayed green whether or not
CI passed, which is the worst kind of README claim: the one a reader has
no way to check. It now points at the real ci.yml workflow status.

Other corrections, all verified against the code rather than memory:
- backend count 10 -> 11, and ยง6 no longer says "Ten-Model" in the title
  while saying "9 backends" in its own closing line. Enumerated instead.
- ยง8.4 claimed 9 unit tests; there are 369 backend + 14 frontend.
- ยง9.5 claimed a single judge and promised multi-judge as future work.
  Multi-judge, anonymization, anchored rubric, order swap, verbosity
  budget and the bias report all exist; the section now describes the
  residual bias instead of a solved problem.

Added:
- ยง2.1.1 places the project against "graph engineering", the term that
  spread in mid-2026, and states the two caveats that bound every claim
  in the file: rankings are over declared topologies, and controls are
  only valid while persona is held constant.
- ยง3.5 documents the R5-R7 validity layer.
- ยง9.6 states plainly that no declared edge has been verified as
  traversed, so a regime declaring an unused veto edge is behaviourally
  flat while scored as checked.
- ยง8.1 documents the model-free path, so someone without an API key can
  still run something. All four commands in it were executed before
  being written down.
- ยง12.1 now mentions topology.json and warns that a prose IDENTITY
  compiles to zero agents and passes silently.

Also gitignores .ccb/, the project-local agent runtime.
โ€ฆuter)

Nodes were untyped: every node was implicitly an LLM-backed office, so the
graph could not express the difference between a reviewer that may object
and a rule that always fires. `kind` is optional and defaults to agent, so
all 57 existing regimes are unchanged.

Validator constraints, each with a stated consequence: an unknown kind is
rejected rather than silently read as agent (which would under-count
gates); a non-agent kind cannot hold a content-producing functional_role;
a router may not emit review/veto edges (that is judgment, not dispatch);
a gate or checkpoint with no inbound edge is unreachable. Metrics gain
gate_count and checkpoint_count, deliberately separate from checks_cycles.

Review note: the first pass labelled menxia and silijian as gates. Both are
LLM reviewers exercising judgment, and "gate" is defined in this very
schema as "no model discretion". Labelling them would have made
gate_count report a hard mechanical veto where the regime has a
discretionary one. They are agents; only shangshu keeps an annotation
(router), on the strength of its SOUL text. No current regime declares a
deterministic gate, and the test now asserts that โ€” the genuinely
deterministic [VETO] sniffer lives in engine/mechanisms/veto.mjs, attached
to an agent node rather than modelled as one.
โ€ฆeclared one

Every ranking this project produces is over a declared topology; nothing
checked whether a declared edge was ever traversed. reconstructRuntimeGraph
rebuilds the actual call graph from the events.jsonl span tree, and
diffTopology reports unexercised edges, undeclared edges and per-edge
exercise counts. Both tolerate truncated streams: a SIGKILLed match has no
match_end, and orphan or duplicate spans are reported rather than thrown on.

The finding is more useful than the feature. Declared nodes are offices
(zhongshu, menxia); runtime actors are regimes (china/tang), because
run-v5.mjs emits every turn with actor=regime โ€” Claude Code runs all offices
in one process and the output is not tagged per agent. The two id spaces do
not overlap at all, so per-office edge verification is not merely
unimplemented, it is blocked on instrumentation.

unexercised_ratio is therefore null when the id spaces do not match. It
would otherwise read 1.000 for every regime in every match: a number that
looks like a measurement, is not one, and is exactly the kind of value that
ends up in a results table.

civagent runtime-graph <matchId> [--diff] [--json], match id validated
against the same SAFE_MATCH_ID contract replay.mjs uses.
CI ran lint, unit tests and regime validation but never exercised a full
path through the system, and someone cloning the repo without an API key
could run nothing at all โ€” every headline command needs a model backend.

examples/example-regime is a small invented regime (4 offices, clearly
labelled as an example so it cannot contaminate the 57-regime corpus) with
a markdown role table, which is also the format the parser requires: a
prose IDENTITY compiles to zero agents and passes silently. examples/smoke.mjs
chains validate -> metrics -> generate a flat control -> validate the
control, entirely on pure functions and local data. It writes to a temp
dir with a finally-cleanup, so a crash cannot leave a 58th regime behind.

Verified red as well as green: adding an edge to a nonexistent node makes
smoke exit 1 with the offending edge named.

Backend CI now runs on Node 20 and 22 (engines raised to >=20; 18 is EOL
and CI was already on 20), so the minimum supported line and the current
LTS are both exercised.
โ€ฆ events

Re-recording a tournament id stacked rows in match_results and
episodic_memory, silently inflating the win counts Bradley-Terry reads.
A UNIQUE(tournament_id, match_id) plus INSERT OR IGNORE makes a re-record
a no-op, the original row wins, and a skipped count is warned rather than
swallowed so a retry storm stays visible. Existing databases get the
constraint via CREATE UNIQUE INDEX IF NOT EXISTS, and a failure to create
it (pre-existing duplicates) is reported instead of aborting boot.

The pre-existing test asserted the stacking as correct behaviour, which is
how the defect survived; it now asserts the fixed contract.

Review note: the first pass put UNIQUE(match_id, regime, event_type) on the
whole episodic_memory table and swallowed collisions with INSERT OR IGNORE
in /api/history/sync. That silently discarded the second veto_triggered
event of a match. Two vetoes are two facts, not a duplicate โ€” and for a
project whose subject is checks and balances, that is the most damaging row
to drop. Uniqueness is now a PARTIAL index scoped to match_end, the one
event recordTournamentResult writes exactly once per (match, regime); /sync
keeps a plain INSERT, since its clear-then-insert already gives re-sync
idempotency. /sync also counted insert attempts rather than rows written,
so it reported more imported events than a client could read back.
README: 369 -> 421 backend tests, Node >=20, smoke in the CI sequence, and
runtime-graph in the CLI reference. ยง9.6 previously said the declared-vs-
exercised comparison had not shipped; it has, and what it established is
that the comparison cannot be made at office level at all, because the
event stream tags actors by regime. The section now says that rather than
implying the gap is merely unimplemented.

ESLint: .ccb/ holds the CCB agent runtime โ€” git worktrees and vendored
provider code checked out inside the repo. It is gitignored, but ESLint
does not read .gitignore, so lint:backend was reporting third-party files
as this project's errors and taking minutes to do it.
P1 โ€” skill outcome stamping crossed tournaments.
skillsForMatch matched on the last six characters of the match id. A
tournament child id is `${tournamentId}__${regime with / as -}`, so every
china/tang child ever run ends in "a-tang": stamping one tournament
overwrote outcome_score/rank/total/of on skills learned in unrelated
earlier tournaments, teaching the corpus that a losing pattern had won.
Attribution now reads the full match id from the provenance banner
writeSkillFile already writes. The old test used a made-up match-id shape
(`T-1__china-tang-abc123`) and stayed green against the broken path; the
fixtures now use the real shape, and a new test pins that stamping one
tournament leaves another's skills untouched.

P2 โ€” controls leaked the source wiring for non-mermaid diagrams.
rewriteMermaid only matched ```mermaid fences. china/ming draws its
dual-track chart as ASCII in a plain fence, so the Ming flat control
declared zero edges in topology.json while still showing the model
็š‡ๅธ โ†’ ๅ†…้˜/ๅธ็คผ็›‘ โ†’ ๅ…ญ้ƒจ. The model reads IDENTITY.md, not topology.json,
so that control isolated nothing. Any fenced diagram is now replaced, and
the control's Decision Flow says explicitly that the historical procedure
described above it is background and the flow below governs.

P2 โ€” the constraint migration did not migrate.
CREATE UNIQUE INDEX raises on a table that already violates it, and the
catch logged and continued, leaving INSERT OR IGNORE nothing to ignore
against. It now collapses pre-existing duplicates (earliest row wins,
matching OR IGNORE semantics) and retries, and reports loudly if
uniqueness still cannot be enforced. It also drops the superseded
table-wide ux_episodic_memory_mre index, which would otherwise make a
plain INSERT of a match's second veto fail. Both paths tested.

P2 โ€” judge events hung off a span that was never opened.
judgeSpanId was generated but no event carried it as its own span_id, so
runtime-graph reconstructed the whole judging step as orphans. A
judging_start event now opens the span. Judge-event assertions are scoped
to scoring events; the integration test additionally checks end to end
that every pass parents to the opener.

P2 โ€” the diff could not read control regimes.
loadDeclaredTopology hardcoded china|global, excluding _baseline/* and
_ablated/* โ€” the control arms this comparison exists to evaluate. It now
uses the same contract as civ-memory.mjs::validateRegime.

P3 โ€” dropped the shangshu "router" label.
regime-to-cc still compiles it into a model-backed subagent with a prompt,
while the schema defines router as making no model call and producing no
content. No regime now carries a kind annotation, and a test asserts it:
the typing mechanism stays, the unearned labels do not.

CI: 427 backend + 14 frontend + 57 regimes + smoke, green.
Five regimes and three scenarios leave enough freedom to narrate almost any
result after the fact. The predictions โ€” including the one that would count
against the project's own proposition, a ranking identical across all three
scenarios โ€” are committed before the first match runs.
Five regimes x three scenarios, all on cn:doubao. Pre-registered at 7841c96.

P1 held: the winner reorders across scenarios (ming / tang / qin), with tang
moving from last to first. P2 held on two of three. P3 failed on S3, where the
spread between first and last (1.25) is smaller than the position effect the
same judge showed between its own two passes (1.67) โ€” that cell produced no
measurement at all, so P2's failure there is not interpretable either.

The finding that outranks the predictions: score tracks transcript length
(Spearman +1.00 on S1, +0.70 on S2), and the three shortest transcripts are not
short answers but summaries of work that was never captured. china/tang's S1
transcript contains no policy, only a claim that an edict 'was already drafted,
debated through two rounds of Menxia veto ... and dispatched' โ€” none of which is
in the stream. It scored last. Three of fifteen cells are affected, and each is
the lowest score in its scenario.

So E1 cannot separate the two worlds it was built to separate: a third
explanation, whether the answer was captured at all, dominates both. Transcript
capture is now the blocker, ahead of control arms and repeat cells.

Also fixes a defect the experiment exposed: judge() computed biasReport and the
manifest writer dropped it, so every tournament ever run recorded a ranking with
no way to check it against the judge's own noise โ€” the exact question P3 asks.
Persisted, with the assertion added to the manifest contract test and
revert-verified. S1-S3 predate the fix; their position effects are recomputed by
hand from the per-pass JSON.
The backend was spawned as `claude --agents <json> -p "<task>"`. The default
output format for `-p` is `text`, which prints exactly one thing: the
coordinator's final assistant message. A regime's offices are Claude Code
subagents, so every word they exchanged went to the subagent channel and was
discarded.

This is what the E1 pilot was actually measuring. Three of fifteen transcripts
contained no policy at all, each scored last in its scenario, and score tracked
transcript length with Spearman +1.00 on the one scenario where the spread
exceeded the judge's noise. The tang cell's transcript claimed an edict "was
already drafted, debated through two rounds of Menxia veto ... and dispatched"
and I read that as the judge scoring a claim instead of work. The claim was
true. The work was happening; the harness was throwing it away.

Backend now runs with --output-format stream-json --verbose, and
engine/v5/stream-json.mjs renders the stream back to plain text. A delegation
carries input.subagent_type and the reply carries parent_tool_use_id, so turns
are attributed per office (china/tang#menxia). Turn events keep their shape, so
the judge, cleanTranscript and the dashboard are unchanged.

Verified on a real run, same regime and scenario as the failing E1 cell:
579 captured characters -> 72,828; 3 turn events -> 60; all three departments
attributed, with the Chancellery's three rounds of fengbo review present,
including substantive historical criticism of the draft.

--- and a second defect that the first fix exposed ---

With office prose finally reaching the mechanism engine, the first verification
run was SIGKILLed mid-draft. checkVeto matched the bare substring ้ฉณๅ›ž, and the
Secretariat had merely *described* the Chancellery's power to amend and return
documents. All three mechanisms shared the defect: checkEdict matched bare ่ฏไนฆ,
the commonest noun in a Tang governance document, and firing EDICT grants
vetoImmunity โ€” so a checks-and-balances regime would have silently lost the
behaviour under study. checkImpeach matched an unbracketed ๅผนๅŠพ๏ผšX.

Only [VETO] is ever taught to agents as a marker; 33 of the 57 regime files
contain ้ฉณๅ›ž/่ฏไนฆ/ๅœฃๆ—จ as ordinary vocabulary. All three now require bracketed
markers, with a regression test that feeds a regime's own description of its
constitution through the engine and asserts nothing fires. Both defects were
dormant only because the transcripts were empty.

E1 is superseded: its scores came from transcripts missing most of what the
regimes did, and the three lowest are the three worst-affected cells. The
pre-registration stands and should be re-run unchanged.

CI: 442 backend + 14 frontend + 57 regimes + smoke.
โ€ฆutruns the data

Now that transcript capture attributes turns per office (china/tang#menxia),
the runtime graph reconstructs at office level: which offices spoke, how often,
and the coordinator's dispatch sequence, parsed from the [โ†’ office] lines.

The investigation result matters more than the feature. Typed office-to-office
edges โ€” the thing topology.json actually declares โ€” still cannot be observed.
Claude Code subagents do not call each other; the coordinator relays every
hand-off, so `menxia โ†’ zhongshu [veto]` is physically indistinguishable from
`coordinator โ†’ menxia` followed by `coordinator โ†’ zhongshu`. No edge is
inferred: inferred.topology_edges is empty by design, and the comparability
gate refuses to treat a coincidental edge-kind match as evidence.

Review fix on top of that work: the diff still filled unexercised_edges with
all 15 declared edges and total_unexercised with 15 while comparable was false.
A consumer reading the count without also reading `comparable` concludes none
of Tang's edges were exercised โ€” and in the very match this was verified
against, the Chancellery reviewed the Secretariat's draft three times, each
time using the word ๅฐ้ฉณ. Those edges were exercised; they are unobservable.
A count that looks like a measurement and is not one is the defect this module
already rejected at the ratio level, one level down.

When comparable is false, unexercised_edges, total_unexercised and
edge_exercise_counts are now null, and the same list is published as
unobservable_edges / total_unobservable. Per-edge runtimeCount is withheld too:
a zero for an edge nobody could observe reads as "never fired". The CLI says
"Unobservable: 15 of 15 declared edges (not a count of non-exercise)".

Old matches without office actors still degrade to regime granularity.

CI: 457 backend + 14 frontend + 57 regimes + smoke.
R10 was orchestrated by Codex across six cluster agents. Verified
independently rather than accepted on report.

R10-1 (the one that mattered): the judge read `readMatchText(matchId, 6000)`,
whose implementation is `text.slice(-6000)` โ€” a blind tail. Fine when
transcripts were 1-20KB; after the stream-json capture fix a Tang match is
72,830 characters, and I measured what survived: zero of the Secretariat's four
segments, zero of the Chancellery's four, zero of the Department of State
Affairs' three. Only two coordinator fragments. The three rounds of fengbo
review โ€” the entire behavioural signal of a checks-and-balances regime โ€” were
outside the judge's view. Selection is now actor-stratified: on the same match,
3/4, 3/4 and 3/3 office segments reach the judge. Per-civ metadata records
originalLength, selectedLength, postAnonymizationLength, judgeViewLength and
omittedContentChars (67,867 on that match), so the omission is stated rather
than implied.

R10-2 was deliberately NOT implemented, and the reasoning is right:
caused_by_span_id alone cannot establish a typed officeโ†’office edge, and
"one observed handoff upgrades the whole graph to observable" does not follow โ€”
absent edges may be unexercised or merely uncaptured. unexercised_ratio keeps
returning null. Declining to ship a measurement is the correct outcome here.

R10-3 stages five seeded random controls and a dry-run-by-default experiment
orchestrator that refuses --execute without an operator-supplied AFP estimate,
since CivAgent cannot measure AFP itself. R10-4 renders office badges with
old-actor fallback. R10-5 declined a new API endpoint with a reason.

Review finding I fixed: nothing pinned the strategy the judge actually uses.
Flipping tournament.mjs back to "tail" left all 462 tests green โ€” the unit was
tested (selectTranscript samples across actors) and the wiring was not. That is
the seventh time this repo has had a suite that passes while the thing under
study is broken. test/judge-transcript-wiring.test.mjs now drives judge() over
a synthetic three-office stream and asserts every office's marker reaches the
prompt; it goes red under "tail".

CI: 464 backend + 17 frontend + 57 regimes + smoke.
โ€ฆnning

The orchestrator fixed the design but registered no claims. C1 names the
outcome that would count against the project's own proposition โ€” random wiring
winning or tying as often as it loses โ€” and requires it be reported as plainly
as the favourable one. C2 keeps pairs inside the judge's own noise band out of
the tally. The E1 smoke datapoint where the control beat the source 10 to 7.9
is recorded here so it cannot be quietly forgotten.
โ€ฆn a null

Three tournaments, five source/control pairs each, cn:doubao throughout,
--no-skill everywhere, anonymized and order-swapped. Predictions registered at
495fadd before the first execute.

Scored against the pre-registered rules: historical wiring wins 0, random
rewiring wins 1, unresolved 5, excluded 9. Every apparent source win in the raw
table came from a pair where at least one arm never delegated to any office.

That is the actual finding. In 13 of 30 arms the coordinator answered the task
itself and no topology โ€” historical or scrambled โ€” was exercised at all. This is
not a capture failure: raw-log fallback was 0/30 and every arm used
actor-stratified selection. The offices were simply never invoked.

Delegation turns out to be scenario-driven, not constitution-driven:
plague 0/10 arms failed to delegate, border-city 6/10, militarization 8/10.
Whether a regime runs its own constitution depends on the task text. And it
costs almost nothing not to: single-actor arms averaged 7.37 against 7.56 for
multi-actor arms, which is a plausible reason the models often skip it.

So the obstacle has moved rather than cleared. Before, the harness discarded the
deliberation; that is fixed. Now the regimes frequently do not deliberate, and
nothing in the design noticed until the pairs were tallied. Delegation has to
become a measured precondition at run time โ€” the dispatch count is already in
runtime-graph's observed block โ€” and the project has to decide whether
delegation is enforced or optional, because those are different experiments.

Transcript length spanned 2,321 to 441,914 characters, a 190x spread, median
16,209.

No claim that topology affects performance is supported by this run. Neither is
the opposite.
E1-control could not answer either question: 13 of 30 arms never called a
single office, so neither the historical nor the scrambled topology was
exercised, and delegation turned out to be driven by the scenario (plague
10/10 delegated, militarization 2/10) rather than by the constitution. Source
and control arms failed to delegate at exactly the same rate, 7/15 each.

R11 builds the instrument rather than picking a side. The coordinator first
states, with tools off, which offices it would call โ€” that plan is B's
dependent variable. Enforcement then requires the offices that are targets of
incoming edges in that arm's own topology, and only post-enforcement output is
eligible for A.

Verified independently rather than accepted on report:

- Enforcement is symmetric. requiredOffices comes from officesWithIncomingEdges
  on each arm's own topology; there is no branch on _baseline anywhere in
  dispatch-plan.mjs or plan-diff.mjs. The control is forced to run its scrambled
  wiring exactly as the source runs its historical one.
- plan-diff invents no composite score. Membership, order and omission are
  supported; directed_edge_alignment, edge_kind_alignment, multiedge_coverage
  and edge_exercise are named in an explicit unsupported_dimensions map with a
  reason each.
- Participation is a three-state observation (observed / not_observed /
  unknown), not an execution rate.
- The wiring is pinned, not just the units: cutting the enforcement injection
  out of run-v5 turns the R11 wiring tests red. That was last round's failure
  mode and it did not recur.

Review addition: the plan question is itself an intervention, and E2 did not
say so. Asking "which offices would you call" raises the salience of
delegation, so B measures stated intent when asked, not spontaneous behaviour.
E2's plan-stage adoption rate is therefore NOT comparable to E1-control's
13/30 baseline, which was collected with no plan question. The source-control
contrast inside E2 survives, since both arms get the identical prompt, but the
absolute level is inflated by an unknown amount โ€” and the gap against E1 is a
lower bound on the observer effect, to be reported rather than read as regimes
adopting their topologies more often than E1 suggested. Registered in
E2-preregistration.md before any E2 run, along with the decision not to add a
no-plan control arm.

Standing limit, unchanged: enforcement proves the coordinator called the
offices a topology names. It does not prove any command/review/info/veto edge
executed.

E2 is designed (3 scenarios x 5 repeats x 10 arms = 150 arms) and NOT run.
CI: 472 backend + 19 frontend + 57 regimes + smoke.
Bilingual pair in the repo's standard shape: two single-language files with a
toggle badge row at the top of each (current language green, the other grey and
linking across), matching README.md section for section โ€” 14 numbered sections,
same tables, same figures.

Both pages updated to the state after R8โ€“R11:
- test counts 421 -> 472 backend, 14 -> 19 frontend
- ยง3.5 gains transcript capture, actor-stratified transcript selection for
  judging, and the dispatch plan / equal-roster enforcement
- new ยง9.7 records what the two pilots actually showed, including the result
  that counts against this project's own proposition: scored against
  pre-registered rules, historical wiring won 0 resolved pairs, and 13 of 30
  arms never called a single office, so neither topology was exercised
- release history gains the R8-R11 row

The Chinese page is a translation of the same claims, not a softened summary โ€”
the limitations sections (ยง9.1-ยง9.7) carry the same numbers and the same
admissions as the English.
Pre-merge review of PR #32. Replacing planPrompt with the literal string
"noop" โ€” so the coordinator is asked nothing at all โ€” left all 472 tests
green. buildPlanPrompt's non-inducement wording was asserted in isolation
while nothing checked that the wording reached the model.

That hole sits on B's entire measurement. Under it the plan call could ask
nothing, or ask a leading question, and E2 would still collect numbers
labelled 'voluntary adoption' and generated from neither.

The run-v5 wiring test now asserts against the captured plan-phase invocation:
the adoption question is present, the non-inducement sentences are present, the
office roster is present, and no explicit demand to use all offices is. Both
failure modes revert-verified โ€” cutting the prompt reddens it, and rewriting
the wording into 'You must call all available offices' reddens it twice.

Seventh recurrence of unit-tested-but-unwired in this repo; the check that
found it is now part of the pre-merge routine.
Codex pre-merge review, P1, and it is my own regression. R10 correctly stopped
the mechanisms from matching bare words like ้ฉณๅ›ž and ่ฏไนฆ โ€” but that is only
half the contract. engine/regime-to-cc.mjs was still compiling every
veto-bearing office with "reject it explicitly by stating \"VETO:\"", while
checkVeto now accepts only [VETO] / [ๅฐ้ฉณ].

So every checks-and-balances regime carried a veto power no agent could
exercise. Tang's menxia and xingbu were both taught the inert form. Verified
directly: the compiled prompt teaches "VETO:", and checkVeto("VETO: legality
defect") returns false โ€” no event, no abort, the match records as a normal
completion.

All 472 tests stayed green because the mechanism tests hand-write the bracketed
marker. A test that supplies its own correct input cannot notice that the
production input is wrong. The new test takes the marker out of a really
compiled Tang prompt and feeds THAT to checkVeto, for every office granted
[CONSTITUTIONAL VETO POWER]. Revert-verified: restoring the old wording
reddens it.

Also corrects a published number. The single-actor count in E1-control is
14 of 30, not 13 โ€” 6 + 0 + 8 across the three scenarios. 13 appeared in
E1-control-results.md, both READMEs and E2-preregistration.md, and contradicted
the 7/15 + 7/15 split stated in the same paragraph.

Not merging. Three P1s from the same review remain open: mechanisms accept
markers from any text with no provenance or role authorisation (a tool result
quoting [VETO] can SIGKILL a match); dispatch and participation are inferred
from forgeable "[โ†’ office]" text rather than from the structured tool_use the
renderer already sees; and baseline --dest can write outside the regimes tree.
Codex pre-merge review, P1. Every generator unconditionally overwrites
IDENTITY.md, SOUL.md, metadata.json and topology.json at the destination, and
--dest went straight through unchecked. Reproduced against the real corpus:

  civagent baseline china/tang --type solo --dest regimes/china/tang

destroyed the Tang regime in one command, IDENTITY.md 1465 -> 526 bytes.
`--dest ../../anywhere` left the repository entirely.

--dest is now confined to regimes/_baseline/, and even inside it refuses a
non-empty directory that is not itself a generated control (checked via the
_baseline marker in metadata.json), while re-running a control the generator
produced stays idempotent.

The existing test named 'real regime files are never touched' only exercised a
temp outRoot and never the CLI's destDir, so it could not see any of this. Four
new tests, all revert-verified: removing the guard block reddens three of them.
โ€ฆfrom text

Codex pre-merge review, P1 x2, one root cause. StreamRenderer held the
structured evidence โ€” tool_use, tool id, subagent_type, message role โ€” and
run-v5 flattened it to text before the mechanism engine, enforcement and
runtime-graph each regex-guessed the structure back.

Two exploits followed, both reproduced:

- A user-role tool_result containing "[VETO]" โ€” external document text, or a
  prompt injection โ€” reached MechanismEngine.process() and SIGKILLed the match
  while emitting a forged veto_triggered. "[EDICT]" was worse: it grants
  vetoImmunity, so any untrusted content could silently disable the veto
  mechanism for the rest of the run.
- A model that called no subagent at all could print "[โ†’ zhongshu]" and
  "[โ†’ menxia]" in ordinary prose and obtain enforcement_passed plus
  participation_observed with dispatchCount 2 and officeTurnCount 0 โ€” the exact
  precondition E2 uses to decide which arms enter the quality comparison.

The stream-json capture fix is what widened this: subagent and tool-result
content that used to be discarded now flows through the same scanner.

Turn events now carry additive structured fields (message_role, content_items,
tool_uses). processStructured() gates on actor presence, assistant role and
absence of tool_result. Dispatch evidence comes only from tool_use items with a
real subagent_type. runtime-graph prefers tool_uses and falls back to text only
for legacy events that predate the field.

Review additions on top of the agent's work:

- dispatchOfficesFromText is deleted rather than left exported. It had no
  callers but manufacturing evidence is exactly what it did, and an exported
  helper that turns two printed strings into proof of delegation is a trap.
- A test pins that run-v5 calls only processStructured. The gated and ungated
  entry points both exist, so a future edit could drop back to the unguarded
  one while every gate test kept passing โ€” they call processStructured directly
  and would not notice. Revert-verified: swapping run-v5 back to process(text)
  reddens it.

CI: 502 backend + 19 frontend + 57 regimes + smoke.
โ€ฆmoke

The first version of the guard required --dest to live under regimes/_baseline/.
That is not the property worth enforcing, and CI caught it: examples/smoke.mjs
legitimately stages a control beside an example regime inside a disposable
sandbox (projectRoot = an OS temp dir), so the smoke went red on both Node
matrix legs while every backend unit test stayed green. A guard that is too
strict looks exactly like this โ€” green units, red end-to-end.

The property that actually matters is: never leave the project's regimes tree,
and never write over the source regime the control is derived from. The guard
now checks those two directly, plus the existing refusal to clobber a non-empty
directory that is not itself a generated control.

`civagent baseline china/tang --type solo --dest regimes/china/tang` is still
rejected โ€” now with a message that names the real reason rather than a path
convention.

Note for anyone revert-verifying this: removing the guard block and running the
suite actually executes the destructive case against regimes/china/tang. It
happened twice here and was restored from git both times. Verify it in a
scratch clone, not the working tree.

CI: 502 backend + 19 frontend + 57 regimes + smoke.
@LeoLin990405
LeoLin990405 merged commit 5aaa312 into main Aug 3, 2026
3 checks passed
LeoLin990405 added a commit that referenced this pull request Aug 6, 2026
โ€ฆgraph diff, CI with teeth (#32)

Stacks R7 through R12 onto main: the experimental-validity layer, governance-graph
typing, runtime-graph reconstruction, transcript capture and selection, dispatch
plan and enforcement, and the provenance fixes from the pre-merge review.

Merged after an independent pre-merge review returned "do not merge" with four
P1s. All four are fixed and revert-verified:

- The compiler taught reviewers to emit "VETO:" while the detector, after an
  earlier fix, accepted only "[VETO]". Every checks-and-balances regime had a
  veto power no agent could exercise, and 472 tests stayed green because the
  mechanism tests hand-write the correct marker instead of taking it from a real
  compiled prompt.
- Mechanisms had no provenance or role authorization: a user-role tool_result
  containing "[VETO]" could SIGKILL a match, and "[EDICT]" could grant
  vetoImmunity to any untrusted content.
- Dispatch and participation were inferred from forgeable text: printing
  "[โ†’ zhongshu]" without calling any subagent produced enforcement_passed and
  participation_observed.
- `baseline --dest` could overwrite a source regime or escape the repository;
  reproduced by destroying regimes/china/tang in one command.

Also corrected a published number: 14 of 30 arms never delegated, not 13. The
figure appeared in both READMEs and the E1-control results while the same
paragraph said 7/15 per side, which sums to 14.

CI on the merge commit: 502 backend + 19 frontend + 57 regimes + end-to-end
smoke, on Node 20 and 22.

Standing limits, unchanged and documented in README ยง9.5โ€“ยง9.7: rankings are over
declared topologies; enforcement proves the coordinator called the offices a
topology names, not that any typed edge executed; and no claim that governance
topology affects multi-agent performance is supported yet, nor the opposite.
LeoLin990405 added a commit that referenced this pull request Aug 7, 2026
P1 โ€” skill outcome stamping crossed tournaments.
skillsForMatch matched on the last six characters of the match id. A
tournament child id is `${tournamentId}__${regime with / as -}`, so every
china/tang child ever run ends in "a-tang": stamping one tournament
overwrote outcome_score/rank/total/of on skills learned in unrelated
earlier tournaments, teaching the corpus that a losing pattern had won.
Attribution now reads the full match id from the provenance banner
writeSkillFile already writes. The old test used a made-up match-id shape
(`T-1__china-tang-abc123`) and stayed green against the broken path; the
fixtures now use the real shape, and a new test pins that stamping one
tournament leaves another's skills untouched.

P2 โ€” controls leaked the source wiring for non-mermaid diagrams.
rewriteMermaid only matched ```mermaid fences. china/ming draws its
dual-track chart as ASCII in a plain fence, so the Ming flat control
declared zero edges in topology.json while still showing the model
็š‡ๅธ โ†’ ๅ†…้˜/ๅธ็คผ็›‘ โ†’ ๅ…ญ้ƒจ. The model reads IDENTITY.md, not topology.json,
so that control isolated nothing. Any fenced diagram is now replaced, and
the control's Decision Flow says explicitly that the historical procedure
described above it is background and the flow below governs.

P2 โ€” the constraint migration did not migrate.
CREATE UNIQUE INDEX raises on a table that already violates it, and the
catch logged and continued, leaving INSERT OR IGNORE nothing to ignore
against. It now collapses pre-existing duplicates (earliest row wins,
matching OR IGNORE semantics) and retries, and reports loudly if
uniqueness still cannot be enforced. It also drops the superseded
table-wide ux_episodic_memory_mre index, which would otherwise make a
plain INSERT of a match's second veto fail. Both paths tested.

P2 โ€” judge events hung off a span that was never opened.
judgeSpanId was generated but no event carried it as its own span_id, so
runtime-graph reconstructed the whole judging step as orphans. A
judging_start event now opens the span. Judge-event assertions are scoped
to scoring events; the integration test additionally checks end to end
that every pass parents to the opener.

P2 โ€” the diff could not read control regimes.
loadDeclaredTopology hardcoded china|global, excluding _baseline/* and
_ablated/* โ€” the control arms this comparison exists to evaluate. It now
uses the same contract as civ-memory.mjs::validateRegime.

P3 โ€” dropped the shangshu "router" label.
regime-to-cc still compiles it into a model-backed subagent with a prompt,
while the schema defines router as making no model call and producing no
content. No regime now carries a kind annotation, and a test asserts it:
the typing mechanism stays, the unearned labels do not.

CI: 427 backend + 14 frontend + 57 regimes + smoke, green.
LeoLin990405 added a commit that referenced this pull request Aug 7, 2026
Pre-merge review of PR #32. Replacing planPrompt with the literal string
"noop" โ€” so the coordinator is asked nothing at all โ€” left all 472 tests
green. buildPlanPrompt's non-inducement wording was asserted in isolation
while nothing checked that the wording reached the model.

That hole sits on B's entire measurement. Under it the plan call could ask
nothing, or ask a leading question, and E2 would still collect numbers
labelled 'voluntary adoption' and generated from neither.

The run-v5 wiring test now asserts against the captured plan-phase invocation:
the adoption question is present, the non-inducement sentences are present, the
office roster is present, and no explicit demand to use all offices is. Both
failure modes revert-verified โ€” cutting the prompt reddens it, and rewriting
the wording into 'You must call all available offices' reddens it twice.

Seventh recurrence of unit-tested-but-unwired in this repo; the check that
found it is now part of the pre-merge routine.
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.

1 participant