kairos-chain 3.68.0 → 3.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/bin/kairos-chain +10 -1
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/kairos_hook_projector/lib/readable_gate_setup.rb +19 -3
- data/templates/skillsets/kairos_hook_projector/test/test_readable_gate_setup.rb +61 -0
- data/templates/skillsets/minimum_nomic/README.md +51 -0
- data/templates/skillsets/minimum_nomic/bin/mutate.rb +218 -0
- data/templates/skillsets/minimum_nomic/bin/score_detections.rb +134 -0
- data/templates/skillsets/minimum_nomic/report/report_template.html +247 -0
- data/templates/skillsets/minimum_nomic/skillset.json +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4277c267574d3ac35dc096a5a6ce3371e074a89d6840a7274d5c3c81397cd643
|
|
4
|
+
data.tar.gz: 0a07d054c3f1c3266d63184bf35e29239b91fcfacbebe3a2bbc2290603aca200
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f40163bd119d1d39c20ac8c986e05fed26e38c445fe7454afb72d35c80f03df8282122674f50d2fc5183261cb8f5a87ec50b1ca71e5ed1a9711cd2a6a007e2c2
|
|
7
|
+
data.tar.gz: b12d79ed73d1bc51a00dc7910be24903b53fca4a5c6d6a1549e4c7cf5bb4b384552a0af7af284e40fa0d220cfdf94e879c433f91c418ffb357d8e890c96a4215
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.69.0] - 2026-08-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **The mutation experiment and the report shape ship with `minimum_nomic`**
|
|
12
|
+
(SkillSet v0.2.0). Until now the bench could play a game and score it, and the
|
|
13
|
+
score could not rank models: over five games scored twice, the spread between
|
|
14
|
+
judges was 1.35–1.50 points while the spread between the judged was 0.67–0.71,
|
|
15
|
+
and re-scoring the same cell moved it 0.73. Asking a model for a number
|
|
16
|
+
measures the asker.
|
|
17
|
+
|
|
18
|
+
`bin/mutate.rb` replaces that with a question whose answer we hold: copy a
|
|
19
|
+
finished game, reverse exactly one vote, and see whether the analyst notices.
|
|
20
|
+
It writes one arm per seat plus a `clean/` control — not optional, because
|
|
21
|
+
without it a reported contradiction cannot be told from a confabulated one —
|
|
22
|
+
and a manifest naming what was planted where. It refuses to overwrite an
|
|
23
|
+
output directory, to make anything but exactly one substitution, and to leave
|
|
24
|
+
a grammatical scar.
|
|
25
|
+
|
|
26
|
+
`bin/score_detections.rb` prints the evidence and **returns no number**. A
|
|
27
|
+
first pass over 81 verdicts by keyword matching undercounted one analyst by
|
|
28
|
+
four, because its findings were written purely as a contrast — "its reasoning
|
|
29
|
+
says it will vote for its proposal, but it votes against" — which contains no
|
|
30
|
+
keyword. A script returning a count here would have shipped that error
|
|
31
|
+
silently.
|
|
32
|
+
|
|
33
|
+
`report/report_template.html` carries the chapter shape the 2026-08 report
|
|
34
|
+
settled on after being rewritten once: TL;DR before chapter 1, the run diagram
|
|
35
|
+
immediately after, then method, results, discussion, appendices. Placeholders
|
|
36
|
+
are in capitals and each structural choice carries its reason in a comment.
|
|
37
|
+
|
|
38
|
+
Three defects were found by running it rather than by reading it, and are
|
|
39
|
+
fixed here. Vote phrasing is not stable between games, so the flat pattern
|
|
40
|
+
list matched nothing in a fresh game whose ballots read `**Vote:** IN FAVOR`;
|
|
41
|
+
detection is now a marker search followed by a polarity flip within a short
|
|
42
|
+
window. Substituting by first occurrence rewrote a *quoted rule* — "adopted if
|
|
43
|
+
more than half of the players vote in favor of it" — instead of the speaker's
|
|
44
|
+
ballot; substitution is now by offset with the expected text asserted there.
|
|
45
|
+
And a copied arm inherits the original game's analyses, written before
|
|
46
|
+
anything was planted, which the worksheet was presenting as readings of the
|
|
47
|
+
mutated record; every analysis kind is now offset by what the arm inherited.
|
|
48
|
+
|
|
49
|
+
Demonstrated end to end from an installed gem: `skillset install
|
|
50
|
+
minimum_nomic`, a 6-turn game (11.3 minutes, analysts included), `mutate.rb`
|
|
51
|
+
planting three arms and a control, `reanalyse.rb` on two of them, and
|
|
52
|
+
`score_detections.rb`. All three analysts caught the planted reversal; the
|
|
53
|
+
control produced no false alarm.
|
|
54
|
+
|
|
7
55
|
## [3.67.0] - 2026-08-15
|
|
8
56
|
|
|
9
57
|
### Added
|
data/bin/kairos-chain
CHANGED
|
@@ -511,7 +511,16 @@ when 'gate'
|
|
|
511
511
|
puts "installed : #{installed['status']} — #{installed['detail']}"
|
|
512
512
|
puts "declaration: #{result.data['declaration']}"
|
|
513
513
|
puts
|
|
514
|
-
|
|
514
|
+
if result.data['already_declared']
|
|
515
|
+
# Says what happened, not what did not. An earlier line here read
|
|
516
|
+
# "Nothing was written" — false of a run that had just re-applied an
|
|
517
|
+
# edited threshold into the harness config.
|
|
518
|
+
puts "#{gate_mode} already declared the readable gate. Its declaration was left"
|
|
519
|
+
puts 'as it is and re-applied, so any thresholds edited in it are now live.'
|
|
520
|
+
puts 'The lines above are a fresh read of the result.'
|
|
521
|
+
else
|
|
522
|
+
puts 'Installed, and reporting only — blocking is false, as the catalogue ships it.'
|
|
523
|
+
end
|
|
515
524
|
# Reported, not treated as failure: the gate is on either way.
|
|
516
525
|
if result.data['verdict'] == 'OPEN_QUESTIONS'
|
|
517
526
|
puts
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -41,7 +41,22 @@ module KairosMcp
|
|
|
41
41
|
|
|
42
42
|
added = call(tool(:add), 'mode' => mode, 'gate' => GATE)
|
|
43
43
|
declaration = added['declaration']
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
# Already declared is the goal already met, not a failure. The add
|
|
46
|
+
# tool is append-only and refuses a second entry for the same gate on
|
|
47
|
+
# the same event — and it returns the declaration's path while doing
|
|
48
|
+
# so, which the first version of this reported as "wrote no
|
|
49
|
+
# declaration", a sentence its own argument disproved.
|
|
50
|
+
#
|
|
51
|
+
# It falls through rather than returning, so running this twice
|
|
52
|
+
# converges instead of refusing: the declaration keeps whatever
|
|
53
|
+
# thresholds it was tuned to, and the projector re-reads them. That
|
|
54
|
+
# is also what makes "edit the numbers, then run it again" true. An
|
|
55
|
+
# apply with nothing changed writes nothing — the projector reports
|
|
56
|
+
# up_to_date — so this is safe against a live blocking gate.
|
|
57
|
+
already = added['error'] == 'gate_already_declared' && !declaration.nil?
|
|
58
|
+
|
|
59
|
+
unless already || (declaration && %w[created appended].include?(added['action']))
|
|
45
60
|
return refuse('mode_hooks_add wrote no declaration: ' \
|
|
46
61
|
"#{added['refusal'] || added['error'] || added['action'] || added.inspect}")
|
|
47
62
|
end
|
|
@@ -52,7 +67,8 @@ module KairosMcp
|
|
|
52
67
|
return refuse("mode_hooks_project refused: #{plan['detail'] || plan.inspect}")
|
|
53
68
|
end
|
|
54
69
|
|
|
55
|
-
data = { 'declaration' => declaration, 'plan' => plan
|
|
70
|
+
data = { 'declaration' => declaration, 'plan' => plan,
|
|
71
|
+
'already_declared' => already }
|
|
56
72
|
return Result.new(status: :proposed, detail: 'nothing written', data: data) unless apply
|
|
57
73
|
|
|
58
74
|
applied = call(tool(:project), 'mode' => mode, 'apply' => true,
|
|
@@ -64,7 +80,7 @@ module KairosMcp
|
|
|
64
80
|
# The apply result asserts no liveness in either direction, by design.
|
|
65
81
|
# A fresh read is the only thing that answers whether the gate is on.
|
|
66
82
|
after = call(tool(:validate), 'mode' => mode)
|
|
67
|
-
Result.new(status: :ok, detail: 'installed',
|
|
83
|
+
Result.new(status: :ok, detail: already ? 'already declared' : 'installed',
|
|
68
84
|
data: data.merge('verdict' => after['verdict'],
|
|
69
85
|
'checks' => after['checks']))
|
|
70
86
|
end
|
|
@@ -94,6 +94,67 @@ class TestReadableGateSetup < Minitest::Test
|
|
|
94
94
|
refute project.calls.first.key?('apply')
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# Found by running it: `gate enable masa --apply` on a mode that already has
|
|
98
|
+
# the gate answered "mode_hooks_add wrote no declaration: gate_already_
|
|
99
|
+
# declared" and exited 1 — while the same reply carried the declaration's
|
|
100
|
+
# path, so the sentence disproved itself. Already declared is the goal
|
|
101
|
+
# already met. It carries on, so a second run converges and so that "edit the
|
|
102
|
+
# numbers, then run it again" is a true instruction rather than a hopeful one.
|
|
103
|
+
def test_a_mode_that_already_declares_the_gate_carries_on_instead_of_refusing
|
|
104
|
+
project = tool_class({ 'plan_sha256' => 'abc', 'up_to_date' => true },
|
|
105
|
+
{ 'action' => 'applied' })
|
|
106
|
+
out = run_with(add: tool_class('error' => 'gate_already_declared',
|
|
107
|
+
'declaration' => @decl, 'nothing_written' => true),
|
|
108
|
+
project: project,
|
|
109
|
+
validate: tool_class('verdict' => 'OK',
|
|
110
|
+
'checks' => { 'installed' => { 'status' => 'ok' } }),
|
|
111
|
+
apply: true)
|
|
112
|
+
assert_equal :ok, out.status, 'an already-declared gate is not a failure'
|
|
113
|
+
assert_equal true, out.data['already_declared'], 'and the caller is told which it was'
|
|
114
|
+
assert_equal 'OK', out.data['verdict']
|
|
115
|
+
assert_equal 2, project.calls.length,
|
|
116
|
+
're-applying is how an edited threshold reaches the harness'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The declaration is not rewritten from the catalogue on a second run: it may
|
|
120
|
+
# carry thresholds the mode tuned, and for masa a live blocking gate.
|
|
121
|
+
def test_an_already_declared_mode_keeps_its_tuned_declaration
|
|
122
|
+
File.write(@decl, JSON.generate(
|
|
123
|
+
'mode_name' => 'demo', 'version' => '1',
|
|
124
|
+
'hooks' => { 'Stop' => [{ 'gate' => 'readable_gate',
|
|
125
|
+
'section' => '§ tuned',
|
|
126
|
+
'blocking' => true,
|
|
127
|
+
'params' => { 'max_lines' => 12 } }] }
|
|
128
|
+
))
|
|
129
|
+
run_with(add: tool_class('error' => 'gate_already_declared', 'declaration' => @decl),
|
|
130
|
+
project: tool_class({ 'plan_sha256' => 'abc' }, { 'action' => 'applied' }),
|
|
131
|
+
validate: tool_class('verdict' => 'OK'), apply: true)
|
|
132
|
+
e = JSON.parse(File.read(@decl, encoding: 'UTF-8')).dig('hooks', 'Stop', 0)
|
|
133
|
+
assert_equal '§ tuned', e['section']
|
|
134
|
+
assert_equal true, e['blocking'], 'a live blocking gate must not be reset to the catalogue'
|
|
135
|
+
assert_equal 12, e.dig('params', 'max_lines')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The boundary of the clause above, and the reason it names one error rather
|
|
139
|
+
# than testing that an error is present. Six other refusals from the add tool
|
|
140
|
+
# ALSO carry a declaration path — declaration_not_json, declaration_unreadable
|
|
141
|
+
# (twice), mode_name_mismatch, declaration_ships_inside_the_skillset, and
|
|
142
|
+
# existing_declaration_refused. Widening "already declared" to "any error with
|
|
143
|
+
# a path" would send a corrupt or misnamed declaration on to the projector
|
|
144
|
+
# under the words "already declared". These must still refuse.
|
|
145
|
+
def test_other_add_refusals_that_also_carry_a_path_are_not_read_as_already_declared
|
|
146
|
+
%w[declaration_not_json declaration_unreadable mode_name_mismatch
|
|
147
|
+
declaration_ships_inside_the_skillset existing_declaration_refused].each do |err|
|
|
148
|
+
project = tool_class('plan_sha256' => 'abc')
|
|
149
|
+
out = run_with(add: tool_class('error' => err, 'declaration' => @decl,
|
|
150
|
+
'nothing_written' => true),
|
|
151
|
+
project: project, apply: true)
|
|
152
|
+
assert_equal :refused, out.status, "#{err}: a broken declaration is not an installed one"
|
|
153
|
+
assert_match(/#{err}/, out.detail)
|
|
154
|
+
assert_nil project.calls, "#{err}: nothing may be projected from it"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
97
158
|
def test_a_refused_declaration_stops_before_the_projector_runs
|
|
98
159
|
project = tool_class('plan_sha256' => 'abc')
|
|
99
160
|
out = run_with(add: tool_class('action' => 'refused', 'refusal' => 'unknown_gate'),
|
|
@@ -71,6 +71,57 @@ comparison that reuses both stored panels confounds generation with effort. Held
|
|
|
71
71
|
fixed at medium over the same 27 mutated records, the two generations tied at 16
|
|
72
72
|
detections each — and not on the same 16.
|
|
73
73
|
|
|
74
|
+
## Measuring an analyst instead of trusting its score
|
|
75
|
+
|
|
76
|
+
The 0-10 metacognition score the analysts return cannot rank models. Measured
|
|
77
|
+
over five games scored twice, the spread between judges was 1.35-1.50 points
|
|
78
|
+
while the spread between the judged was 0.67-0.71, and re-scoring the same cell
|
|
79
|
+
moved it 0.73. Asking a model for a number measures the asker.
|
|
80
|
+
|
|
81
|
+
`mutate.rb` replaces that with a question whose answer we already hold: copy a
|
|
82
|
+
finished game, reverse exactly one vote, and see whether the analyst notices.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
ruby .kairos/skillsets/minimum_nomic/bin/mutate.rb log/nomic/g1 --out log/nomic/g1_mut
|
|
86
|
+
ruby .kairos/skillsets/minimum_nomic/bin/reanalyse.rb log/nomic/g1_mut/clean
|
|
87
|
+
ruby .kairos/skillsets/minimum_nomic/bin/reanalyse.rb log/nomic/g1_mut/seat_A # and B, C
|
|
88
|
+
ruby .kairos/skillsets/minimum_nomic/bin/score_detections.rb log/nomic/g1_mut
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`mutate.rb` writes one arm per seat plus a `clean/` control, and a
|
|
92
|
+
`mutations.json` naming what was planted where. **The control is not optional**:
|
|
93
|
+
without it a reported contradiction cannot be told from a confabulated one.
|
|
94
|
+
Across 35 planted mutations the control arms produced zero false alarms, and
|
|
95
|
+
that is what makes the detections evidence.
|
|
96
|
+
|
|
97
|
+
It refuses three things: overwriting an output directory, making anything other
|
|
98
|
+
than exactly one substitution, and leaving a grammatical scar. The third check
|
|
99
|
+
exists because 4 of 27 substitutions on 2026-08-15 rewrote
|
|
100
|
+
`I vote **in favor** of X` as `I vote **against** of X`, stranding the *of* — a
|
|
101
|
+
clue visible without consulting the record at all. One analyst reverse-engineered
|
|
102
|
+
the edit from it.
|
|
103
|
+
|
|
104
|
+
`score_detections.rb` **does not decide**. It prints, per arm and per analyst,
|
|
105
|
+
the lines citing the mutated utterance, and the verdict is made by a person. A
|
|
106
|
+
first pass over 81 verdicts using keyword matching undercounted one analyst by
|
|
107
|
+
four, because its findings were written purely as a contrast — "its reasoning
|
|
108
|
+
says it will vote for its proposal, but it votes against" — which contains no
|
|
109
|
+
keyword. All 105 had to be re-read by hand. A script returning a number here
|
|
110
|
+
would have shipped that error silently.
|
|
111
|
+
|
|
112
|
+
## Writing it up
|
|
113
|
+
|
|
114
|
+
`report/report_template.html` is the shape the 2026-08 report settled on after
|
|
115
|
+
being rewritten once: a TL;DR before chapter 1, the run diagram immediately
|
|
116
|
+
after it, then method, results, discussion, appendices. Every placeholder is in
|
|
117
|
+
capitals and every structural choice carries the reason for it in a comment, so
|
|
118
|
+
that departing from it is a decision rather than an oversight. The stylesheet is
|
|
119
|
+
inline; there is nothing else to install.
|
|
120
|
+
|
|
121
|
+
Two rules the template enforces in its comments and the body should keep. Every
|
|
122
|
+
number carries its denominator. A claim the sample does not support is written
|
|
123
|
+
as "cannot be claimed" rather than rounded into a finding.
|
|
124
|
+
|
|
74
125
|
## What each participant is given
|
|
75
126
|
|
|
76
127
|
```
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Plant one false word in a copy of a finished game, so that an analyst can be
|
|
5
|
+
# measured against an answer key we wrote ourselves.
|
|
6
|
+
#
|
|
7
|
+
# Why this exists. The bench's own 0-10 metacognition score is not usable for
|
|
8
|
+
# comparing models: measured over five games scored twice, the spread between
|
|
9
|
+
# judges was 1.35-1.50 points while the spread between the judged was 0.67-0.71,
|
|
10
|
+
# and re-scoring the same cell moved it 0.73. Asking a model for a number
|
|
11
|
+
# measures the asker. Asking "did you notice the lie we planted" does not,
|
|
12
|
+
# because we know the answer before the analyst speaks.
|
|
13
|
+
#
|
|
14
|
+
# What it does. Copies a finished game once per seat, reverses exactly one vote
|
|
15
|
+
# in each copy, and writes a manifest naming what was changed where. It also
|
|
16
|
+
# copies the game unchanged, as the control: without a control arm, a reported
|
|
17
|
+
# contradiction cannot be told from a confabulated one. Across 35 planted
|
|
18
|
+
# mutations the control arms produced zero false alarms, which is what makes the
|
|
19
|
+
# detections evidence.
|
|
20
|
+
#
|
|
21
|
+
# What it refuses to do. It will not overwrite an existing output directory, it
|
|
22
|
+
# aborts unless exactly one substitution was made, and it rejects a replacement
|
|
23
|
+
# that leaves a grammatical scar. That last check is here because on 2026-08-15,
|
|
24
|
+
# 4 of 27 substitutions rewrote "I vote **in favor** of X" as
|
|
25
|
+
# "I vote **against** of X", stranding the *of* — a clue visible without
|
|
26
|
+
# consulting the record at all, which makes the mutation easier than intended.
|
|
27
|
+
# One analyst reverse-engineered the edit from it: "the malformed 'against of'
|
|
28
|
+
# suggests a botched edit from 'in favor of'".
|
|
29
|
+
#
|
|
30
|
+
# The game's own record is never touched. Everything is written under --out.
|
|
31
|
+
#
|
|
32
|
+
# Usage, from the project root:
|
|
33
|
+
# ruby .kairos/skillsets/minimum_nomic/bin/mutate.rb GAME_DIR --out MUT_DIR
|
|
34
|
+
# ruby .kairos/skillsets/minimum_nomic/bin/mutate.rb GAME_DIR --out MUT_DIR --seat B
|
|
35
|
+
#
|
|
36
|
+
# Then analyse each arm and score the result:
|
|
37
|
+
# ruby .../reanalyse.rb MUT_DIR/clean
|
|
38
|
+
# ruby .../reanalyse.rb MUT_DIR/seat_B
|
|
39
|
+
# ruby .../score_detections.rb MUT_DIR
|
|
40
|
+
|
|
41
|
+
require 'json'
|
|
42
|
+
require 'fileutils'
|
|
43
|
+
require 'optparse'
|
|
44
|
+
|
|
45
|
+
# Finding the vote is done in two stages, not with one big pattern list, because
|
|
46
|
+
# the phrasing is not stable between games. Eleven games produced "I vote in
|
|
47
|
+
# favor of", "I vote **YES**", "I vote FOR", a bare "NO", and "**Vote:** IN
|
|
48
|
+
# FAVOR" — and a flat list assembled from ten of them still failed on the
|
|
49
|
+
# eleventh. Worse, a flat list matches the wrong thing: an utterance that quotes
|
|
50
|
+
# a proposed rule reading "adopted if more than half of the players vote in
|
|
51
|
+
# favor of it" contains the phrase before the speaker's own ballot does, and
|
|
52
|
+
# flipping there corrupts a quotation rather than a vote.
|
|
53
|
+
#
|
|
54
|
+
# Stage one finds a marker that announces a ballot. Stage two flips the first
|
|
55
|
+
# polarity word within a short window after it. Anything the two stages miss is
|
|
56
|
+
# reported with the utterance quoted, so the caller can pass --from/--to.
|
|
57
|
+
VOTE_MARKERS = [
|
|
58
|
+
/\*\*Vote:?\*\*:?/i, # **Vote:** IN FAVOR
|
|
59
|
+
/\bVote:/i, # Vote: AGAINST
|
|
60
|
+
/\bI vote\b/i, # I vote in favor of ...
|
|
61
|
+
/\A/ # a bare ballot: the utterance is the vote
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
WINDOW = 32
|
|
65
|
+
|
|
66
|
+
POLARITY = [
|
|
67
|
+
[/\bIN FAVOU?R\b/i, ->(m) { m == m.upcase ? 'AGAINST' : 'against' }],
|
|
68
|
+
[/\bAGAINST\b/i, ->(m) { m == m.upcase ? 'IN FAVOR' : 'in favor of' }],
|
|
69
|
+
[/\bFOR\b/, ->(_) { 'AGAINST' }],
|
|
70
|
+
[/\bYES\b/i, ->(m) { m == m.upcase ? 'NO' : 'no' }],
|
|
71
|
+
[/\bNO\b/i, ->(m) { m == m.upcase ? 'YES' : 'yes' }]
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
# A replacement that produces any of these has changed the grammar as well as
|
|
75
|
+
# the vote, and the grammar is a clue the record does not have to be read to
|
|
76
|
+
# see. Refuse rather than silently ship an easier mutation.
|
|
77
|
+
SCARS = [
|
|
78
|
+
/against\*{0,2}\s+of\b/i,
|
|
79
|
+
/in favou?r\*{0,2}\s+(the|this|that|his|her|its|Player)\b/i,
|
|
80
|
+
/\bNO\*{0,2}\s+of\b/i,
|
|
81
|
+
/\bYES\*{0,2}\s+of\b/i
|
|
82
|
+
].freeze
|
|
83
|
+
|
|
84
|
+
options = { out: nil, seats: %w[A B C] }
|
|
85
|
+
OptionParser.new do |o|
|
|
86
|
+
o.banner = 'usage: mutate.rb GAME_DIR --out MUT_DIR [--seat A]'
|
|
87
|
+
o.on('--out DIR', 'output directory (must not already exist)') { |v| options[:out] = v }
|
|
88
|
+
o.on('--seat X', 'only this seat (default: all three)') { |v| options[:seats] = [v.upcase] }
|
|
89
|
+
end.parse!
|
|
90
|
+
|
|
91
|
+
game = ARGV[0] or abort 'usage: mutate.rb GAME_DIR --out MUT_DIR [--seat A]'
|
|
92
|
+
out = options[:out] or abort 'usage: mutate.rb GAME_DIR --out MUT_DIR [--seat A]'
|
|
93
|
+
abort "#{game}: not a game directory" unless File.directory?(File.join(game, 'records'))
|
|
94
|
+
abort "#{out}: already exists; a mutation set is never written over" if File.exist?(out)
|
|
95
|
+
|
|
96
|
+
utterances = File.readlines(File.join(game, 'records', 'utterances.jsonl'))
|
|
97
|
+
.reject { |l| l.strip.empty? }.map { |l| JSON.parse(l) }
|
|
98
|
+
public_log = utterances.select { |u| u['in_public_log'] }
|
|
99
|
+
|
|
100
|
+
# The polarity word of the first ballot in one utterance, or nil. "in favor of"
|
|
101
|
+
# is produced when flipping a lower-case "against", because "I vote against X"
|
|
102
|
+
# becomes "I vote in favor of X" and dropping the "of" would strand the object.
|
|
103
|
+
# The reverse direction takes the "of" with it for the same reason: leaving it
|
|
104
|
+
# behind is exactly the grammatical scar SCARS refuses.
|
|
105
|
+
def ballot(text)
|
|
106
|
+
VOTE_MARKERS.each do |marker|
|
|
107
|
+
m = text.match(marker) or next
|
|
108
|
+
window = text[m.end(0), WINDOW].to_s
|
|
109
|
+
POLARITY.each do |re, flip|
|
|
110
|
+
w = window.match(re) or next
|
|
111
|
+
|
|
112
|
+
from = w[0]
|
|
113
|
+
to = flip.call(from)
|
|
114
|
+
# Take a trailing " of" with a lower-case "against" so the object keeps
|
|
115
|
+
# its preposition, and drop one when moving the other way.
|
|
116
|
+
if from =~ /\Aagainst\z/ && window[w.end(0), 3] != ' of'
|
|
117
|
+
# "vote against X" -> "vote in favor of X": the "of" is added by flip.
|
|
118
|
+
elsif from =~ /\Ain favou?r\z/i && window[w.end(0), 3] == ' of'
|
|
119
|
+
from += ' of'
|
|
120
|
+
end
|
|
121
|
+
return { seq: nil, from: from, to: to, at: m.end(0) + w.begin(0) }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The first vote-bearing utterance of a seat. First rather than last because a
|
|
128
|
+
# mutation early in the record has more downstream text that must contradict it,
|
|
129
|
+
# and the downstream contradiction is the route an analyst is most likely to
|
|
130
|
+
# find.
|
|
131
|
+
def first_vote(rows, seat)
|
|
132
|
+
rows.select { |u| u['player'] == seat }.each do |u|
|
|
133
|
+
b = ballot(u['text']) or next
|
|
134
|
+
|
|
135
|
+
return { seq: u['seq'], player: seat, from: b[:from], to: b[:to], at: b[:at] }
|
|
136
|
+
end
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def plant(game, dest, hit)
|
|
141
|
+
FileUtils.cp_r(game, dest)
|
|
142
|
+
path = File.join(dest, 'records', 'utterances.jsonl')
|
|
143
|
+
rows = File.readlines(path).reject { |l| l.strip.empty? }.map { |l| JSON.parse(l) }
|
|
144
|
+
n = 0
|
|
145
|
+
rows.each do |u|
|
|
146
|
+
next unless u['seq'] == hit[:seq] && u['player'] == hit[:player]
|
|
147
|
+
|
|
148
|
+
# Substituting at the known offset rather than by first occurrence. An
|
|
149
|
+
# utterance that quotes a proposed rule — "adopted if more than half of the
|
|
150
|
+
# players vote in favor of it" — contains the phrase before the speaker's
|
|
151
|
+
# own ballot, and `sub` would silently rewrite the quotation instead.
|
|
152
|
+
found = u['text'][hit[:at], hit[:from].length]
|
|
153
|
+
raise "seq #{hit[:seq]}: expected #{hit[:from].inspect} at #{hit[:at]}, found #{found.inspect}" unless found == hit[:from]
|
|
154
|
+
|
|
155
|
+
u['text'] = u['text'].dup.tap { |t| t[hit[:at], hit[:from].length] = hit[:to] }
|
|
156
|
+
n += 1
|
|
157
|
+
end
|
|
158
|
+
raise "seq #{hit[:seq]}: expected exactly 1 substitution, made #{n}" unless n == 1
|
|
159
|
+
|
|
160
|
+
changed = rows.find { |u| u['seq'] == hit[:seq] && u['player'] == hit[:player] }
|
|
161
|
+
window = changed['text'][hit[:at], hit[:to].length + 24].to_s
|
|
162
|
+
scar = SCARS.find { |s| window =~ s }
|
|
163
|
+
raise "seq #{hit[:seq]}: replacement leaves a grammatical scar (#{window.strip.inspect})" if scar
|
|
164
|
+
|
|
165
|
+
File.write(path, rows.map { |u| JSON.generate(u) }.join("\n") + "\n")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
FileUtils.mkdir_p(out)
|
|
169
|
+
FileUtils.cp_r(game, File.join(out, 'clean'))
|
|
170
|
+
puts "#{out}/clean — control, unchanged"
|
|
171
|
+
|
|
172
|
+
manifest = { 'game' => File.expand_path(game), 'arms' => [] }
|
|
173
|
+
options[:seats].each do |seat|
|
|
174
|
+
hit = first_vote(public_log, seat)
|
|
175
|
+
if hit.nil?
|
|
176
|
+
first = public_log.find { |u| u['player'] == seat }
|
|
177
|
+
warn " seat #{seat}: no ballot found. Its first utterance opens:"
|
|
178
|
+
warn " #{first ? first['text'].to_s.gsub(/\s+/, ' ')[0, 120].inspect : '(this seat never spoke)'}"
|
|
179
|
+
warn ' If a vote is in there, the marker or the polarity word is one this script'
|
|
180
|
+
warn ' does not know. Add it to VOTE_MARKERS / POLARITY rather than widening a'
|
|
181
|
+
warn ' catch-all, which would start matching quoted rule text.'
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
dest = File.join(out, "seat_#{seat}")
|
|
185
|
+
begin
|
|
186
|
+
plant(game, dest, hit)
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
FileUtils.rm_rf(dest)
|
|
189
|
+
warn " seat #{seat}: #{e.message}"
|
|
190
|
+
next
|
|
191
|
+
end
|
|
192
|
+
manifest['arms'] << { 'dir' => "seat_#{seat}", 'seq' => hit[:seq], 'player' => seat,
|
|
193
|
+
'from' => hit[:from], 'to' => hit[:to] }
|
|
194
|
+
puts "#{dest} — [#{hit[:seq]}] #{seat}: #{hit[:from].inspect} -> #{hit[:to].inspect}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if manifest['arms'].empty?
|
|
198
|
+
# Leaving the control arm behind would make a retry hit "already exists" and
|
|
199
|
+
# look like the mutation set had been written when nothing was planted.
|
|
200
|
+
FileUtils.rm_rf(out)
|
|
201
|
+
abort "#{out}: no arm could be planted; nothing to measure (output removed)"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The row count each arm inherits, so scoring can tell this run's analyses from
|
|
205
|
+
# any the copied game already carried. `analyses` is in this list and must stay:
|
|
206
|
+
# a copied arm inherits the ORIGINAL game's analyses, written before anything
|
|
207
|
+
# was planted. Counting those as readings of the mutated record would show three
|
|
208
|
+
# analysts for an arm nobody has analysed yet, and every one of them would be a
|
|
209
|
+
# false miss.
|
|
210
|
+
%w[analyses analyses_rescored analyses_crossmodel].each do |kind|
|
|
211
|
+
manifest["baseline_#{kind}"] = Dir.glob(File.join(out, '*')).each_with_object({}) do |d, h|
|
|
212
|
+
f = File.join(d, 'records', "#{kind}.jsonl")
|
|
213
|
+
h[File.basename(d)] = File.exist?(f) ? File.readlines(f).size : 0
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
File.write(File.join(out, 'mutations.json'), JSON.pretty_generate(manifest))
|
|
217
|
+
puts "\n#{out}/mutations.json — what was planted where"
|
|
218
|
+
puts "next: run reanalyse.rb on every arm above, then score_detections.rb #{out}"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Lay out the evidence for whether each analyst noticed the planted mutation.
|
|
5
|
+
#
|
|
6
|
+
# This script does NOT decide. It extracts, for every arm and every analyst,
|
|
7
|
+
# the lines that cite the mutated utterance, and prints them next to what was
|
|
8
|
+
# planted there. The call is made by a person reading those lines.
|
|
9
|
+
#
|
|
10
|
+
# That division is deliberate and was learned the hard way. On 2026-08-15 the
|
|
11
|
+
# first pass over 81 verdicts used keyword matching — contradiction, mismatch,
|
|
12
|
+
# discrepancy, 矛盾 — and undercounted one analyst by four, because its findings
|
|
13
|
+
# were written purely as a contrast: "its reasoning says it will vote for its
|
|
14
|
+
# proposal, but it votes against". No word on the list appears in that sentence.
|
|
15
|
+
# All 105 verdicts had to be re-read by hand. A script that returned a number
|
|
16
|
+
# here would have shipped that error silently, so this one returns a worksheet.
|
|
17
|
+
#
|
|
18
|
+
# The criterion, which should be fixed before any result is read:
|
|
19
|
+
#
|
|
20
|
+
# detected names the vote at that utterance, or whether the rule passed
|
|
21
|
+
# or failed, as inconsistent with the record
|
|
22
|
+
# weak names only the reasoning-versus-utterance mismatch
|
|
23
|
+
# missed neither
|
|
24
|
+
# false alarm claims the same inconsistency in the CLEAN arm, where nothing
|
|
25
|
+
# was planted
|
|
26
|
+
#
|
|
27
|
+
# Usage, from the project root:
|
|
28
|
+
# ruby .kairos/skillsets/minimum_nomic/bin/score_detections.rb MUT_DIR
|
|
29
|
+
# ruby .kairos/skillsets/minimum_nomic/bin/score_detections.rb MUT_DIR --context 3
|
|
30
|
+
|
|
31
|
+
require 'json'
|
|
32
|
+
require 'optparse'
|
|
33
|
+
|
|
34
|
+
options = { context: 2 }
|
|
35
|
+
OptionParser.new do |o|
|
|
36
|
+
o.banner = 'usage: score_detections.rb MUT_DIR [--context N]'
|
|
37
|
+
o.on('--context N', Integer, 'lines to show per analyst (default 2)') { |v| options[:context] = v }
|
|
38
|
+
end.parse!
|
|
39
|
+
|
|
40
|
+
dir = ARGV[0] or abort 'usage: score_detections.rb MUT_DIR [--context N]'
|
|
41
|
+
manifest_path = File.join(dir, 'mutations.json')
|
|
42
|
+
abort "#{manifest_path}: not found; was this directory made by mutate.rb?" unless File.exist?(manifest_path)
|
|
43
|
+
|
|
44
|
+
manifest = JSON.parse(File.read(manifest_path))
|
|
45
|
+
|
|
46
|
+
# Every kind is offset by what the arm inherited when it was copied, `analyses`
|
|
47
|
+
# included. A copied arm carries the ORIGINAL game's analyses, written before
|
|
48
|
+
# anything was planted; showing them here would present three analysts for an
|
|
49
|
+
# arm nobody has analysed and score every one of them as a miss.
|
|
50
|
+
def analyses(arm_dir, baseline)
|
|
51
|
+
rows = []
|
|
52
|
+
KINDS.each do |kind|
|
|
53
|
+
path = File.join(arm_dir, 'records', "#{kind}.jsonl")
|
|
54
|
+
next unless File.exist?(path)
|
|
55
|
+
|
|
56
|
+
File.readlines(path)[baseline[kind].to_i..].to_a.each do |l|
|
|
57
|
+
next if l.strip.empty?
|
|
58
|
+
|
|
59
|
+
rows << JSON.parse(l).merge('kind' => kind)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
rows
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
KINDS = %w[analyses analyses_rescored analyses_crossmodel].freeze
|
|
66
|
+
|
|
67
|
+
def baseline_for(manifest, arm_dir)
|
|
68
|
+
KINDS.each_with_object({}) do |kind, h|
|
|
69
|
+
h[kind] = manifest.dig("baseline_#{kind}", arm_dir).to_i
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cite(seq)
|
|
74
|
+
/\[#{seq}\]|utterance #{seq}\b|turn.?#{seq}\b|message \[?#{seq}\]?/i
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "game: #{manifest['game']}"
|
|
78
|
+
puts
|
|
79
|
+
|
|
80
|
+
manifest['arms'].each do |arm|
|
|
81
|
+
arm_dir = File.join(dir, arm['dir'])
|
|
82
|
+
baseline = baseline_for(manifest, arm['dir'])
|
|
83
|
+
rows = analyses(arm_dir, baseline)
|
|
84
|
+
|
|
85
|
+
puts "===== #{arm['dir']} — planted at [#{arm['seq']}] #{arm['player']}: " \
|
|
86
|
+
"#{arm['from'].inspect} -> #{arm['to'].inspect}"
|
|
87
|
+
if rows.empty?
|
|
88
|
+
puts ' (no analysis yet — run reanalyse.rb or cross_model.rb on this arm first)'
|
|
89
|
+
puts
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
rows.each do |h|
|
|
94
|
+
label = "#{h['model']}#{h['effort'] ? " (effort #{h['effort']})" : ''}"
|
|
95
|
+
unless h['ok']
|
|
96
|
+
puts " -- #{label}: CALL FAILED — #{h['error']}"
|
|
97
|
+
next
|
|
98
|
+
end
|
|
99
|
+
hits = h['text'].to_s.lines.select { |l| l =~ cite(arm['seq']) }
|
|
100
|
+
puts " -- #{label}"
|
|
101
|
+
if hits.empty?
|
|
102
|
+
puts ' (never mentions that utterance) => missed, unless it says so elsewhere'
|
|
103
|
+
else
|
|
104
|
+
hits.first(options[:context]).each { |l| puts " #{l.strip[0, 240]}" }
|
|
105
|
+
puts " ... #{hits.length - options[:context]} more line(s)" if hits.length > options[:context]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
puts
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
clean_dir = File.join(dir, 'clean')
|
|
112
|
+
if File.directory?(clean_dir)
|
|
113
|
+
rows = analyses(clean_dir, baseline_for(manifest, 'clean'))
|
|
114
|
+
puts '===== clean — control. Anything below that names a contradiction at a planted'
|
|
115
|
+
puts ' position is a FALSE ALARM. Naming something else is not.'
|
|
116
|
+
if rows.empty?
|
|
117
|
+
puts ' (no analysis yet — the control arm must be analysed too, or the detections mean nothing)'
|
|
118
|
+
else
|
|
119
|
+
rows.each do |h|
|
|
120
|
+
label = "#{h['model']}#{h['effort'] ? " (effort #{h['effort']})" : ''}"
|
|
121
|
+
hits = manifest['arms'].flat_map { |a| h['text'].to_s.lines.select { |l| l =~ cite(a['seq']) } }.uniq
|
|
122
|
+
puts " -- #{label}"
|
|
123
|
+
if hits.empty?
|
|
124
|
+
puts ' (never mentions any planted position) => no false alarm'
|
|
125
|
+
else
|
|
126
|
+
hits.first(options[:context]).each { |l| puts " #{l.strip[0, 240]}" }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
puts
|
|
133
|
+
puts 'The verdict is yours, not this script\'s. Read the lines above against the'
|
|
134
|
+
puts 'criterion at the top of this file, and record detected / weak / missed per cell.'
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="LANG">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>TITLE</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light;
|
|
10
|
+
--plane: #f9f9f7;
|
|
11
|
+
--surface: #fcfcfb;
|
|
12
|
+
--ink: #0b0b0b;
|
|
13
|
+
--ink-2: #52514e;
|
|
14
|
+
--ink-muted: #898781;
|
|
15
|
+
--rule: #e1e0d9;
|
|
16
|
+
--axis: #c3c2b7;
|
|
17
|
+
--border: rgba(11,11,11,0.10);
|
|
18
|
+
--s1: #2a78d6; /* 青 — 上限まで進んだ */
|
|
19
|
+
--s2: #eb6834; /* 橙 — 参加者が終わりを作った */
|
|
20
|
+
--s3: #1baf7a; /* 緑青 — 全員一致の相互勝利 */
|
|
21
|
+
--pending: #898781;
|
|
22
|
+
}
|
|
23
|
+
@media (prefers-color-scheme: dark) {
|
|
24
|
+
:root:where(:not([data-theme="light"])) {
|
|
25
|
+
color-scheme: dark;
|
|
26
|
+
--plane: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7;
|
|
27
|
+
--ink-muted: #898781; --rule: #2c2c2a; --axis: #383835;
|
|
28
|
+
--border: rgba(255,255,255,0.10);
|
|
29
|
+
--s1: #3987e5; --s2: #d95926; --s3: #199e70;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
* { box-sizing: border-box; }
|
|
33
|
+
body {
|
|
34
|
+
margin: 0; padding: 0 1.25rem 5rem;
|
|
35
|
+
background: var(--plane); color: var(--ink);
|
|
36
|
+
font: 400 16px/1.85 system-ui, -apple-system, "Hiragino Sans", "Noto Sans JP", sans-serif;
|
|
37
|
+
}
|
|
38
|
+
main { max-width: 46rem; margin: 0 auto; }
|
|
39
|
+
header { padding: 3rem 0 1.5rem; border-bottom: 2px solid var(--ink); margin-bottom: 2rem; }
|
|
40
|
+
h1 { font-size: 1.75rem; line-height: 1.4; margin: 0 0 .6rem; }
|
|
41
|
+
.sub { color: var(--ink-2); font-size: 1.02rem; margin: 0 0 1rem; }
|
|
42
|
+
.meta { color: var(--ink-muted); font-size: .85rem; margin: 0; }
|
|
43
|
+
h2 {
|
|
44
|
+
font-size: 1.25rem; margin: 3rem 0 .25rem; padding-top: 1.5rem;
|
|
45
|
+
border-top: 1px solid var(--rule);
|
|
46
|
+
}
|
|
47
|
+
h2 .num { color: var(--ink-muted); font-weight: 400; margin-right: .5rem; }
|
|
48
|
+
h3 { font-size: 1.02rem; margin: 2rem 0 .5rem; }
|
|
49
|
+
p { margin: 0 0 1rem; }
|
|
50
|
+
.lede { font-size: 1.02rem; color: var(--ink-2); margin-bottom: 1.5rem; }
|
|
51
|
+
.banner {
|
|
52
|
+
background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--s2);
|
|
53
|
+
padding: .9rem 1.1rem; border-radius: 6px; font-size: .9rem; color: var(--ink-2);
|
|
54
|
+
margin: 0 0 2rem;
|
|
55
|
+
}
|
|
56
|
+
.banner strong { color: var(--ink); }
|
|
57
|
+
figure {
|
|
58
|
+
margin: 1.75rem 0; padding: 1.25rem; background: var(--surface);
|
|
59
|
+
border: 1px solid var(--border); border-radius: 8px;
|
|
60
|
+
}
|
|
61
|
+
figcaption { font-size: .85rem; color: var(--ink-2); margin-top: .9rem; line-height: 1.75; }
|
|
62
|
+
figcaption b { color: var(--ink); font-weight: 600; }
|
|
63
|
+
svg { display: block; width: 100%; height: auto; overflow: visible; }
|
|
64
|
+
table { width: 100%; border-collapse: collapse; font-size: .88rem; margin: 1.25rem 0; }
|
|
65
|
+
th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
|
|
66
|
+
th { color: var(--ink-2); font-weight: 600; font-size: .82rem; }
|
|
67
|
+
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
68
|
+
code {
|
|
69
|
+
font: 400 .85em/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
70
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
71
|
+
border-radius: 4px; padding: .1em .35em;
|
|
72
|
+
}
|
|
73
|
+
blockquote {
|
|
74
|
+
margin: 1.25rem 0; padding: .25rem 0 .25rem 1.1rem;
|
|
75
|
+
border-left: 3px solid var(--axis); color: var(--ink-2);
|
|
76
|
+
}
|
|
77
|
+
.key {
|
|
78
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
79
|
+
border-radius: 8px; padding: 1.1rem 1.25rem; margin: 1.5rem 0;
|
|
80
|
+
}
|
|
81
|
+
.key p:last-child { margin-bottom: 0; }
|
|
82
|
+
.legend { display: flex; flex-wrap: wrap; gap: 1rem; font-size: .82rem; color: var(--ink-2); margin-top: .9rem; }
|
|
83
|
+
.legend span { display: inline-flex; align-items: center; gap: .4rem; }
|
|
84
|
+
.swatch { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
|
|
85
|
+
ul { margin: 0 0 1rem; padding-left: 1.3rem; }
|
|
86
|
+
li { margin-bottom: .6rem; }
|
|
87
|
+
details { margin-top: .9rem; font-size: .88rem; }
|
|
88
|
+
summary { cursor: pointer; color: var(--ink-2); }
|
|
89
|
+
pre { background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
|
|
90
|
+
padding: .9rem 1.1rem; margin: 1.25rem 0; overflow-x: auto; line-height: 1.5;
|
|
91
|
+
font-size: .8rem; color: var(--ink-2);
|
|
92
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; }
|
|
93
|
+
.tldr ol { margin: .2rem 0 0; padding-left: 1.4rem; }
|
|
94
|
+
.tldr li { margin-bottom: .55rem; }
|
|
95
|
+
.foot { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--rule);
|
|
96
|
+
font-size: .82rem; color: var(--ink-muted); }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<main>
|
|
101
|
+
|
|
102
|
+
<!-- ══════════════════════════════════════════════════════════════════════════
|
|
103
|
+
Minimum Nomic — report template.
|
|
104
|
+
|
|
105
|
+
Fill every ALL-CAPS placeholder and delete every comment. The chapter
|
|
106
|
+
numbering, the class names and the order below are the shape the 2026-08
|
|
107
|
+
report settled on after being rewritten once; the reasons are stated in
|
|
108
|
+
each comment so that a departure is a decision rather than an oversight.
|
|
109
|
+
|
|
110
|
+
Rule for the whole document: every number carries its denominator, and a
|
|
111
|
+
claim the sample does not support is written as "cannot be claimed"
|
|
112
|
+
rather than rounded into a finding.
|
|
113
|
+
══════════════════════════════════════════════════════════════════════ -->
|
|
114
|
+
|
|
115
|
+
<header>
|
|
116
|
+
<h1>TITLE</h1>
|
|
117
|
+
<p class="sub">ONE SENTENCE SAYING WHAT IS BEING MEASURED, NOT WHAT WAS BUILT.</p>
|
|
118
|
+
<p class="meta">DRAFT vX.Y ・ KairosChain Bench ・ GAMES PLAYED YYYY-MM-DD TO YYYY-MM-DD</p>
|
|
119
|
+
</header>
|
|
120
|
+
|
|
121
|
+
<div class="banner">
|
|
122
|
+
<strong>Provisional.</strong> STATE THE SAMPLE SIZE AND THE HONESTY RULE: every figure
|
|
123
|
+
measured rather than estimated, and anything the sample does not support written as
|
|
124
|
+
"cannot be claimed".
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- TL;DR — put this before chapter 1, never after. A reader who stops here
|
|
128
|
+
should still leave with the findings, each with its denominator. Eight to
|
|
129
|
+
ten items; if an item needs a paragraph, it belongs in chapter 3. -->
|
|
130
|
+
<div class="key tldr">
|
|
131
|
+
<p style="margin:0 0 .7rem"><b>TL;DR</b> — before reading the rest.</p>
|
|
132
|
+
<ol>
|
|
133
|
+
<li><b>The setup.</b> WHAT WAS GIVEN TO WHOM, AND WHAT WAS DELIBERATELY WITHHELD.</li>
|
|
134
|
+
<li><b>FINDING.</b> CLAIM, WITH ITS DENOMINATOR.</li>
|
|
135
|
+
<li><b>FINDING.</b> ...</li>
|
|
136
|
+
<li><b>What could not be claimed.</b> THE STRONGEST NEGATIVE RESULT.</li>
|
|
137
|
+
</ol>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- The run diagram. Prose cannot carry a pipeline; replace the prose with
|
|
141
|
+
this rather than printing both. One line per stage, cost on the right. -->
|
|
142
|
+
<h3 style="margin-top:2.5rem">How a run proceeds</h3>
|
|
143
|
+
|
|
144
|
+
<pre>
|
|
145
|
+
1 PLAY A GAME one game = N turns / N model calls / ~N min
|
|
146
|
+
Player A ─┐
|
|
147
|
+
Player B ─┼──→ the game master decides only who speaks next
|
|
148
|
+
Player C ─┘ it compiles no rule set and adjudicates no legality
|
|
149
|
+
↓
|
|
150
|
+
2 RECORDS ACCUMULATE append-only, one directory per game
|
|
151
|
+
├ utterance log … every player reads this
|
|
152
|
+
├ reasoning log … shown to no player, ever
|
|
153
|
+
└ turn-control record … the game master's decisions and reasons
|
|
154
|
+
↓
|
|
155
|
+
3 MECHANICAL CHECKS check_gm.rb --falsify; no model is called
|
|
156
|
+
↓
|
|
157
|
+
4 ANALYSTS reanalyse.rb; the same models, fresh calls
|
|
158
|
+
↓
|
|
159
|
+
5 AUDIT audit_scores.rb; self-audit as the control
|
|
160
|
+
↓
|
|
161
|
+
6 MUTATION EXPERIMENT mutate.rb then score_detections.rb
|
|
162
|
+
the answer key is ours, so no model decides whether a detection happened
|
|
163
|
+
</pre>
|
|
164
|
+
|
|
165
|
+
<h2><span class="num">1</span>Introduction</h2>
|
|
166
|
+
|
|
167
|
+
<p class="lede">WHY THE ORDINARY EVALUATION SHAPE DOES NOT REACH WHAT IS BEING LOOKED FOR.</p>
|
|
168
|
+
|
|
169
|
+
<p>WHAT NOMIC IS, IN TWO SENTENCES, FOR A READER WHO HAS NEVER HEARD OF IT.</p>
|
|
170
|
+
|
|
171
|
+
<blockquote>THE QUESTION, STATED ONCE, IN ONE SENTENCE.</blockquote>
|
|
172
|
+
|
|
173
|
+
<p><b>The design rule.</b> STATE IT: what is to be observed is not written into the system.
|
|
174
|
+
Nothing instructs a participant to do the thing being measured, and nothing detects, scores
|
|
175
|
+
or reacts to it during play. A stall, a deadlock, a malformed move is a result.</p>
|
|
176
|
+
|
|
177
|
+
<h2><span class="num">2</span>Method</h2>
|
|
178
|
+
|
|
179
|
+
<h3>2.1 The game</h3>
|
|
180
|
+
<p>THE INITIAL RULES, AND WHICH ONE DOES WHAT. LINK TO THE APPENDIX FOR THE FULL TEXT.</p>
|
|
181
|
+
|
|
182
|
+
<h3>2.2 Who is at the table</h3>
|
|
183
|
+
<table>
|
|
184
|
+
<thead><tr><th>Seat</th><th>Tool</th><th>Model</th><th>Reasoning effort</th></tr></thead>
|
|
185
|
+
<tbody>
|
|
186
|
+
<tr><td>SEAT</td><td>CLI</td><td><code>MODEL</code></td><td>EFFORT OR "not settable"</td></tr>
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
<p><b>Record the effort setting even when it cannot be set.</b> A comparison that reuses two
|
|
190
|
+
stored panels confounds model with effort, and the confound is invisible unless both were
|
|
191
|
+
written down at the time.</p>
|
|
192
|
+
|
|
193
|
+
<h3>2.3 What each participant is given — and what it is not</h3>
|
|
194
|
+
<pre>
|
|
195
|
+
Player → its own identity + the INITIAL rules + every utterance so far
|
|
196
|
+
+ one line asking for its reasoning. Nothing else.
|
|
197
|
+
Game master → the roster + the INITIAL rules + every utterance so far.
|
|
198
|
+
Decides who speaks next. Renders nothing to anybody.
|
|
199
|
+
Analyst → the initial rules, both logs in full, the turn-control record.
|
|
200
|
+
A fresh call. NOT told which seat it held.
|
|
201
|
+
</pre>
|
|
202
|
+
|
|
203
|
+
<h3>2.4 What the game master is, exactly</h3>
|
|
204
|
+
<h3>2.5 Analysis and scoring</h3>
|
|
205
|
+
|
|
206
|
+
<h2><span class="num">3</span>Results</h2>
|
|
207
|
+
|
|
208
|
+
<!-- One subsection per finding. Each opens with the finding, then the evidence,
|
|
209
|
+
then what it does not show. Quote the record verbatim; a summary removes
|
|
210
|
+
the material a reader needs to disagree with you. -->
|
|
211
|
+
|
|
212
|
+
<h3>3.1 SHORTEST TRUE HEADLINE</h3>
|
|
213
|
+
|
|
214
|
+
<h3>3.N The scoring instrument</h3>
|
|
215
|
+
<p>IF A 0-10 SCORE IS USED, MEASURE IT BEFORE TRUSTING IT: judge-to-judge spread against
|
|
216
|
+
judged-to-judged spread, and the movement when the same cell is scored twice. Where the
|
|
217
|
+
first exceeds the second, say plainly that the score cannot rank models.</p>
|
|
218
|
+
|
|
219
|
+
<h3>3.N+1 Planting one lie in a record</h3>
|
|
220
|
+
<p>THE MUTATION RESULT. Include, without exception: the criterion declared BEFORE any result
|
|
221
|
+
was read; the control arm and its false-alarm count; the per-game spread as well as the
|
|
222
|
+
per-model rate; and the limits of the mutations themselves.</p>
|
|
223
|
+
|
|
224
|
+
<h2><span class="num">4</span>Discussion</h2>
|
|
225
|
+
|
|
226
|
+
<h3>4.1 What the games show</h3>
|
|
227
|
+
<h3>4.2 What cannot be claimed</h3>
|
|
228
|
+
<!-- Not a disclaimer section. Each item names the specific measurement that
|
|
229
|
+
would settle it, so the next round has a work list. -->
|
|
230
|
+
<h3>4.3 What comes next</h3>
|
|
231
|
+
|
|
232
|
+
<h2 id="appendix"><span class="num">A</span>Appendix A — the nine initial rules</h2>
|
|
233
|
+
<p>Verbatim. Participants read the original wording; a translation goes beside it, never
|
|
234
|
+
in place of it.</p>
|
|
235
|
+
|
|
236
|
+
<h2 id="appendix-b"><span class="num">B</span>Appendix B — QUOTED MATERIAL IN FULL</h2>
|
|
237
|
+
<p>Whatever the body quotes selectively belongs here in full, so that a reader who distrusts
|
|
238
|
+
the selection can check it.</p>
|
|
239
|
+
|
|
240
|
+
<div class="foot">
|
|
241
|
+
Records under <code>log/</code>, one directory per game, append-only.
|
|
242
|
+
Scripts: <code>.kairos/skillsets/minimum_nomic/bin/</code>.
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
</main>
|
|
246
|
+
</body>
|
|
247
|
+
</html>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimum_nomic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Minimum Nomic bench: three language models play a self-amending game with nine changeable rules, no goal and no termination condition, while a fourth acts as game master. Runnable scripts, not MCP tools — a game is started from the command line and its record is analysed, audited and mutation-tested afterwards.",
|
|
5
5
|
"author": "Masaomi Hatakeyama",
|
|
6
6
|
"layer": "L1",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.69.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
@@ -660,9 +660,12 @@ files:
|
|
|
660
660
|
- templates/skillsets/minimum_nomic/bin/audit_scores.rb
|
|
661
661
|
- templates/skillsets/minimum_nomic/bin/check_gm.rb
|
|
662
662
|
- templates/skillsets/minimum_nomic/bin/cross_model.rb
|
|
663
|
+
- templates/skillsets/minimum_nomic/bin/mutate.rb
|
|
663
664
|
- templates/skillsets/minimum_nomic/bin/reanalyse.rb
|
|
664
665
|
- templates/skillsets/minimum_nomic/bin/rules_initial.json
|
|
665
666
|
- templates/skillsets/minimum_nomic/bin/run_gm.rb
|
|
667
|
+
- templates/skillsets/minimum_nomic/bin/score_detections.rb
|
|
668
|
+
- templates/skillsets/minimum_nomic/report/report_template.html
|
|
666
669
|
- templates/skillsets/minimum_nomic/skillset.json
|
|
667
670
|
- templates/skillsets/mmp/config/meeting.yml
|
|
668
671
|
- templates/skillsets/mmp/knowledge/meeting_protocol_core/meeting_protocol_core.md
|