diff --git a/trios/.trinity/dashboard/t27-migration.json b/trios/.trinity/dashboard/t27-migration.json new file mode 100644 index 0000000000..0fbeb5f2bd --- /dev/null +++ b/trios/.trinity/dashboard/t27-migration.json @@ -0,0 +1,224 @@ +{ + "generatedAt": null, + "commit": "87f4fd324", + "law": "Everything below the interface is written in .t27 and generated to its target. The seed t27/bootstrap is the exception.", + "rings": [ + { + "id": "T27-00", + "what": "decision core: retry, review, merge gate, capacity", + "specFiles": 1, + "specLines": 209, + "handWrittenIn": "agent-server/queen-core/Sources/QueenPolicy/*.swift", + "handFiles": 4, + "handLines": 1805, + "generated": 0, + "state": "spec written, nothing generated, Swift answers", + "gate": "trios/tools/ring00-swift-parity.mjs (gHashTag/trios#1337)", + "depends": [] + }, + { + "id": "T27-01", + "what": "A2A protocol: message kinds, task states, agent liveness", + "specFiles": 1, + "specLines": 223, + "handWrittenIn": "agent-server/apps/server/src/api/routes/a2a.ts", + "handFiles": 1, + "handLines": 0, + "generated": 0, + "state": "spec and code share no vocabulary: 0 of 22 spec constants appear in the source", + "gate": "trios/docs/ring01-correspondence.md (gHashTag/trios#1342)", + "depends": [ + "T27-00" + ] + }, + { + "id": "T27-02", + "what": "orchestration: the Queen's tick, delegation, review sweep", + "specFiles": 0, + "specLines": 0, + "handWrittenIn": "agent-server/apps/server/src/api/services/queen-tick.ts", + "handFiles": 1, + "handLines": 1737, + "generated": 0, + "state": "no spec yet; 28 module-level functions await purity classification", + "gate": "trios/docs/t27-02-tick-survey.md (gHashTag/trios#1341)", + "depends": [ + "T27-00", + "T27-01" + ] + }, + { + "id": "T27-03", + "what": "transport: SSE, per-agent buffer, event ids", + "specFiles": 0, + "specLines": 0, + "handWrittenIn": "rings/SR-01/SSETransport.swift", + "handFiles": 1, + "handLines": 0, + "generated": 0, + "state": "not started; blocked behind T27-01", + "gate": "none yet", + "depends": [ + "T27-01" + ] + }, + { + "id": "T27-04", + "what": "scoring: salience, reliability, latency", + "specFiles": 0, + "specLines": 0, + "handWrittenIn": "agent-server/queen-core/Sources/QueenPolicy/QueenSalience.swift", + "handFiles": 1, + "handLines": 0, + "generated": 0, + "state": "not started; blocked behind T27-00", + "gate": "none yet", + "depends": [ + "T27-00" + ] + } + ], + "blockers": [ + { + "id": "t27c-switch-arms", + "what": "switch as a function body emits an empty match; the arms are dropped", + "where": "gHashTag/t27 seed", + "status": "open" + }, + { + "id": "t27c-mut-emit", + "what": "collect_mutable_names computes the mutable set and the emitter does not use it", + "where": "gHashTag/t27 seed", + "status": "open" + }, + { + "id": "t27c-enum-paths", + "what": "Enum.variant and the .variant shorthand", + "where": "gHashTag/t27 seed", + "status": "closed upstream in gHashTag/t27#2247" + }, + { + "id": "no-compiler-in-image", + "what": "the worker image has no rustc, swift, make, python3 or t27c: sh, bash, node, bun, git only", + "where": "agent-server/Dockerfile", + "status": "by design - generation is the operator's step" + } + ], + "series": [ + { + "day": "08-09", + "commit": "b5a960a08", + "spec_lines": 0, + "hand_lines": 0 + }, + { + "day": "08-10", + "commit": "2721de6f7", + "spec_lines": 0, + "hand_lines": 0 + }, + { + "day": "08-11", + "commit": "81c3eea1b", + "spec_lines": 0, + "hand_lines": 0 + }, + { + "day": "08-12", + "commit": "d3e2efbac", + "spec_lines": 0, + "hand_lines": 0 + }, + { + "day": "08-17", + "commit": "dbb41249e", + "spec_lines": 0, + "hand_lines": 0 + }, + { + "day": "08-18", + "commit": "8fff0d566", + "spec_lines": 0, + "hand_lines": 0 + }, + { + "day": "08-19", + "commit": "467f8098c", + "spec_lines": 210, + "hand_lines": 0 + }, + { + "day": "08-20", + "commit": "392b0ff18", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-21", + "commit": "713d060fa", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-22", + "commit": "89eb3d297", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-23", + "commit": "a249f1a10", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-24", + "commit": "4563ac7c1", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-26", + "commit": "4482d9aa5", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-27", + "commit": "c32b77a6d", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-28", + "commit": "0c529ca7d", + "spec_lines": 434, + "hand_lines": 0 + }, + { + "day": "08-29", + "commit": "d26a4abfc", + "spec_lines": 434, + "hand_lines": 2144 + }, + { + "day": "08-31", + "commit": "b7008d7da", + "spec_lines": 434, + "hand_lines": 3346 + }, + { + "day": "09-01", + "commit": "01e16a13b", + "spec_lines": 434, + "hand_lines": 3528 + }, + { + "day": "09-03", + "commit": "87f4fd324", + "spec_lines": 434, + "hand_lines": 3547 + } + ], + "seriesNote": "spec_lines counts rings/T27-00 and T27-01; hand_lines counts the Swift policy under queen-core/Sources/QueenPolicy and queen-tick.ts, which are the two hand-written statements of the laws those specs describe. Both are read by walking the tree at each day's last commit." +} \ No newline at end of file diff --git a/trios/agent-server/apps/server/src/api/routes/queen-roadmap.ts b/trios/agent-server/apps/server/src/api/routes/queen-roadmap.ts index 90a11e766e..561f2e5387 100644 --- a/trios/agent-server/apps/server/src/api/routes/queen-roadmap.ts +++ b/trios/agent-server/apps/server/src/api/routes/queen-roadmap.ts @@ -88,6 +88,36 @@ async function boardFacts(pool: Pool) { } } +/** + * The .t27 migration, read from the file the ledger tools write. + * + * It lives beside the roadmap rather than inside it because the two answer + * different questions: the roadmap is what somebody planned, this is how far + * one law has travelled from being written to being generated. A plan can be + * wrong; a line count cannot, which is why every number in this file is + * produced by walking the tree and none is typed. + */ +async function loadMigration(): Promise { + for (const path of migrationPaths()) { + try { + return JSON.parse(await readFile(path, 'utf8')) + } catch { + // The next candidate. Absent on a laptop that runs the server from + // somewhere else, and that is not an error - the panel simply says so. + } + } + return null +} + +function migrationPaths(): string[] { + const workspace = process.env.WORKSPACE_DIR || '/workspace' + return [ + `${workspace}/BrowserOS/trios/.trinity/dashboard/t27-migration.json`, + `${process.cwd()}/../.trinity/dashboard/t27-migration.json`, + `${process.cwd()}/.trinity/dashboard/t27-migration.json`, + ] +} + export function createQueenRoadmapDataRoute() { return new Hono().get('/', async (c) => { const url = queenLeaseDatabaseUrl() @@ -95,7 +125,11 @@ export function createQueenRoadmapDataRoute() { const roadmap = await loadRoadmap() const pool = new Pool({ connectionString: url }) try { - return c.json({ roadmap, board: await boardFacts(pool) }) + return c.json({ + roadmap, + migration: await loadMigration(), + board: await boardFacts(pool), + }) } finally { await pool.end() } @@ -167,7 +201,7 @@ const SHELL = ` .bar{display:grid;grid-template-columns:9rem 1fr 3rem;gap:var(--sp0); align-items:center;font-size:var(--f-2)} .bar .track{height:.6rem;background:#141414;border-radius:99px;overflow:hidden} - .bar .fill{height:100%;background:var(--red)} + .bar .fill{display:block;height:100%;background:var(--red)} .bar .num{font-family:var(--mono);color:var(--muted);text-align:right} table{width:100%;border-collapse:collapse;font-size:var(--f-2);margin-top:var(--sp1)} th{text-align:left;color:var(--muted);font-size:var(--f-3);letter-spacing:.1em; @@ -209,9 +243,14 @@ const SHELL = ` function draw(d){ var r=d.roadmap, b=d.board + // A missing roadmap file used to end the render here, which would have made + // the migration panel below unreachable in exactly the case it is most worth + // reading - a checkout without the planning file still has rings and still + // has git history. Each half now stands or falls on its own input. if(!r){$('app').innerHTML='
No roadmap index

'+ 'Expected .trinity/dashboard/roadmap.json in the checkout. This is a '+ - 'missing FILE, not an empty plan.

';return} + 'missing FILE, not an empty plan.

'+migrationPanel(d.migration) + return} var planned=r.epics.reduce(function(n,e){return n+e.issues.length},0) var html='' @@ -279,9 +318,108 @@ const SHELL = ` html+='

'+ esc(r.source.note)+'

' + html+=migrationPanel(d.migration) + $('app').innerHTML=html } + // The .t27 migration: the plan, and how far it has actually moved. + // + // Two things are drawn and they must not be confused. The TABLE is the plan - + // five rings, innermost first, each with the hand-written file that answers + // today. The CHART is the measurement: spec lines and hand-written lines per + // day, read out of git history rather than recorded by anyone. The chart is + // the honest part, and what it shows is that the two curves rose together - + // writing the specification did not remove one line of what it specifies. + function migrationPanel(m){ + if(!m) return '

.t27 migration

No migration ledger'+ + '

The server found no .trinity/dashboard/t27-migration.json. '+ + 'It is written by the ledger tools; without it this panel states nothing '+ + 'rather than guessing.

' + var rings=m.rings||[], series=m.series||[] + var specNow=rings.reduce(function(n,x){return n+(x.specLines||0)},0) + var handNow=rings.reduce(function(n,x){return n+(x.handLines||0)},0) + var genNow=rings.reduce(function(n,x){return n+(x.generated||0)},0) + var h='

.t27 migration · '+genNow+' generated of '+rings.length+' rings

' + h+='

'+esc(m.law)+'

' + h+='
'+ + bar('specification written', specNow, specNow+handNow, 'var(--accent)')+ + bar('still hand-written', handNow, specNow+handNow, 'var(--golden)')+ + bar('generated', genNow, rings.length, 'var(--accent)')+ + '
' + h+=chart(series) + h+=''+ + '' + rings.forEach(function(x){ + h+=''+ + ''+ + ''+ + '' + }) + h+='
ringwhatspechand-written todaystate
'+esc(x.id)+''+esc(x.what)+''+(x.specLines?x.specLines+' lines':'—')+''+esc(x.handWrittenIn)+ + (x.handLines?' · '+x.handLines+' lines':'')+''+esc(x.state)+'
' + h+='

what blocks generation

'+ + '' + ;(m.blockers||[]).forEach(function(b){ + h+=''+ + ''+ + '' + }) + h+='
idwhatwherestatus
'+esc(b.id)+''+esc(b.what)+''+esc(b.where)+''+esc(b.status)+'
' + return h + } + + function bar(label,n,total,colour){ + var pct=total?Math.round(n/total*100):0 + return '
'+esc(label)+''+ + ''+n+'/'+total+'
' + } + + // An inline SVG, drawn from the series with no library: two polylines over a + // shared scale, one point per day that has a commit. A day with no commit is + // absent rather than interpolated - a flat segment between two points is the + // truth that nothing landed, and inventing a point would make it look like it did. + function chart(series){ + if(series.length<2) return '' + var W=1100,H=260,P=44 + var max=1; series.forEach(function(p){ + max=Math.max(max,p.spec_lines||0,p.hand_lines||0)}) + max=Math.ceil(max/100)*100 + var x=function(i){return P+i*(W-P*2)/(series.length-1)} + var y=function(v){return H-P-(v/max)*(H-P*2)} + var line=function(key,colour){ + var pts=series.map(function(p,i){return x(i).toFixed(1)+','+y(p[key]||0).toFixed(1)}).join(' ') + return '' + } + var svg='' + for(var g=0;g<=4;g++){ + var gv=max*g/4, gy=y(gv) + svg+=''+ + ''+ + gv+'' + } + svg+=line('hand_lines','#FFD700')+line('spec_lines','#00FF88') + series.forEach(function(p,i){ + if(i%3&&i!==series.length-1) return + svg+=''+esc(p.day)+'' + }) + svg+='specification (.t27)'+ + 'hand-written' + return '

the two curves, from git history

'+ + '

One point '+ + 'per day that has a commit on this branch, counted by walking the tree at '+ + 'that commit. The specification appears on 08-19 and stops; nothing was '+ + 'generated and nothing hand-written was removed. A flat line here is a fact, '+ + 'not a gap in the data.

'+svg + } + function load(){ fetch('/queen/roadmap/data',token?{headers:{Authorization:'Bearer '+token}}:undefined) .then(function(res){