diff --git a/.github/workflows/website-checks.yml b/.github/workflows/website-checks.yml index 1bcf207b19..ad8741c14c 100644 --- a/.github/workflows/website-checks.yml +++ b/.github/workflows/website-checks.yml @@ -294,7 +294,7 @@ jobs: # and the nightly t27 world scan -- the only place these ran -- then # failed for three nights. Pure, no browser. - name: Queen map close-ups stay readable - run: npm run check:queen-displays && npm run check:queen-continuous + run: npm run check:queen-displays && npm run check:queen-continuous && npm run check:queen-issue-proof && npm run check:queen-worlds # The PASSPORT argues a result is worthless without the record of how it # was taken, so its own figures must not drift from its own prose: every # plotted quantity carries the literal the text prints for it, in both diff --git a/SOUL.md b/SOUL.md index 2357902633..779478f683 100644 --- a/SOUL.md +++ b/SOUL.md @@ -1,8 +1,10 @@ -# QUEEN hive status-fill agent +# Corona issue proof reader - Type: Custom / UI -- Bound GitHub issue: gHashTag/trinity#890 -- Mission: implement truthful, overview-readable issue-status hex fills. +- Bound GitHub issue: gHashTag/trinity#1360 +- Evidence dependency: gHashTag/tt-trinity-corona#12. +- Mission: show honey only for a closed allowlisted issue whose exact native + seal, conformance vectors, verifier and CI match the current canonical source. - Allowed: scoped local source/tests/docs/skill edits, read-only repository and public browser inspection, local test/build/preview, issue-bound journaling. - Stop: unrelated changes, unavailable provenance, external publication/secrets diff --git a/apps/website/.trinity/seals/queen_queen_issue_proof.json b/apps/website/.trinity/seals/queen_queen_issue_proof.json new file mode 100644 index 0000000000..b25621f7cc --- /dev/null +++ b/apps/website/.trinity/seals/queen_queen_issue_proof.json @@ -0,0 +1,12 @@ +{ + "gen_hash_c": "sha256:2dd94ece3724a73e039e6f6051fce5c6b66a62d7fd65eaeec4a994f9c35e5a68", + "gen_hash_rust": "sha256:17be9c555632c81e3f30e42d37abc7cedbf5e1c79f50f6f07c44fc0729c74e1e", + "gen_hash_verilog": "sha256:37ed904ae570e8d46b1a99a4e79d4b3608b3dbe51274bb2f6d085ba80dea9fd0", + "gen_hash_zig": "sha256:0f93bc44dc9f89ef77a0854963488e1b1d16e454d717285802f64c6d89a54240", + "module": "queen_issue_proof", + "ring": 12, + "sealed_at": "2026-10-04T12:58:19Z", + "sealed_by": "t27c-bootstrap@0.4.0", + "spec_hash": "sha256:76c068c85d3af34aaa6f4a571742f6ad00de727a37f0ffe419c83bbdb1b84c71", + "spec_path": "specs/queen/issue_proof.t27" +} \ No newline at end of file diff --git a/apps/website/conformance/queen_issue_proof.json b/apps/website/conformance/queen_issue_proof.json new file mode 100644 index 0000000000..71373f6e17 --- /dev/null +++ b/apps/website/conformance/queen_issue_proof.json @@ -0,0 +1,70 @@ +{ + "spec_path": "specs/queen/issue_proof.t27", + "spec_hash": "76c068c85d3af34aaa6f4a571742f6ad00de727a37f0ffe419c83bbdb1b84c71", + "vectors": [ + { + "mask": 0, + "expected": 0 + }, + { + "mask": 1, + "expected": 0 + }, + { + "mask": 2, + "expected": 0 + }, + { + "mask": 3, + "expected": 0 + }, + { + "mask": 4, + "expected": 0 + }, + { + "mask": 5, + "expected": 0 + }, + { + "mask": 6, + "expected": 0 + }, + { + "mask": 7, + "expected": 0 + }, + { + "mask": 8, + "expected": 0 + }, + { + "mask": 9, + "expected": 0 + }, + { + "mask": 10, + "expected": 0 + }, + { + "mask": 11, + "expected": 0 + }, + { + "mask": 12, + "expected": 0 + }, + { + "mask": 13, + "expected": 0 + }, + { + "mask": 14, + "expected": 0 + }, + { + "mask": 15, + "expected": 1 + } + ] +} diff --git a/apps/website/package.json b/apps/website/package.json index 4f55d17b93..f3bab3f07c 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -54,6 +54,7 @@ "check:queen-coverage": "node --experimental-strip-types qa/queen-coverage-contract.mjs", "check:queen-displays": "node --experimental-strip-types qa/queen-hive-display-contract.mjs", "check:queen-worlds": "node --experimental-strip-types qa/queen-repository-worlds.mjs", + "check:queen-issue-proof": "node scripts/issue-proof-from-spec.mjs --check && node --experimental-strip-types qa/queen-issue-proof.mjs", "check:queen-dead-api": "node qa/queen-viewport-contract.mjs --dead-api", "check:queen-touch": "node qa/queen-touch-contract.mjs", "check:queen-round": "node qa/queen-round-contract.mjs", diff --git a/apps/website/qa/queen-continuous-map.mjs b/apps/website/qa/queen-continuous-map.mjs index 46338c8fb0..196ba92ff9 100644 --- a/apps/website/qa/queen-continuous-map.mjs +++ b/apps/website/qa/queen-continuous-map.mjs @@ -52,6 +52,9 @@ assert.equal(hiveEpicProgress(epic,[{...issue,number:7,state:'open'},{...issue,r const before=new Map(world.displays.flatMap((row,i)=>row?[[row.key,world.map.positions[i]]]:[])); updated.displays.forEach((row,i)=>{if(row)assert.deepEqual(updated.map.positions[i],before.get(row.key),'Observation must not teleport other cells');}); assert.equal(updated.displays.find(i=>i?.key===issue.key).state,'closed'); +const complete=data.catalogUniverse(atlas,[{...issue,state:'closed'}],[issue.repo]); +assert.equal(complete.map.cells.find(c=>c?.kind==='repo'&&c.repo===issue.repo).open,0,'complete observed backlog replaces dated open count'); +assert.equal(updated.map.cells.find(c=>c?.kind==='repo'&&c.repo===issue.repo).open,core.cells.find(c=>c?.kind==='repo'&&c.repo===issue.repo).open,'partial observation keeps snapshot backlog count'); const extra={...issue,key:`${issue.repo}#999999`,number:999999,state:'closed'}; const extended=data.catalogUniverse(atlas,[extra]); for(const [i,row] of extended.displays.entries())if(row&&before.has(row.key))assert.deepEqual(extended.map.positions[i],before.get(row.key),'An observed old issue appends without moving snapshot cells'); diff --git a/apps/website/qa/queen-issue-proof.mjs b/apps/website/qa/queen-issue-proof.mjs new file mode 100644 index 0000000000..d1d838ae0e --- /dev/null +++ b/apps/website/qa/queen-issue-proof.mjs @@ -0,0 +1,72 @@ +import assert from 'node:assert/strict'; +import {createHash} from 'node:crypto'; +import {readFileSync} from 'node:fs'; +import {createIssueProofReader} from '../src/lib/queenIssueProof.ts'; +import {issueProofPolicy as production} from '../src/lib/queenIssueProof.generated.ts'; +import {hiveTaskPaint} from '../src/components/queenHiveDisplay.ts'; + +// Public HTTP boundary fixtures with real SHA-256, no network and no fake digest. +const hash=text=>createHash('sha256').update(text).digest('hex'); +const sha='a'.repeat(40),signal=new AbortController().signal,repo=production.REPO; +const row=(overrides={})=>({key:`${repo}#1`,repo,number:1,title:'Phase A',kind:'issue',state:'closed',closedAt:'2026-10-03T13:05:56Z',children:[],coverage:'unknown',...overrides}); +function fixture({source,seal:editSeal,vectors:editVectors}={}) { + const spec=source??production.ISSUES.map(n=>`; https://github.com/${repo}/issues/${n}`).join('\n'); + const seal={spec_path:production.SPEC,spec_hash:`sha256:${hash(spec)}`,...Object.fromEntries(['c','rust','verilog','zig'].map(b=>[`gen_hash_${b}`,`sha256:${'b'.repeat(64)}`]))}; + const vectors={spec_path:production.SPEC,spec_hash:seal.spec_hash,vectors:[{expected:false},{expected:true}]}; + editSeal?.(seal);editVectors?.(vectors); + const files=new Map([[production.SPEC,spec],[production.SEAL,JSON.stringify(seal)],[production.VECTORS,JSON.stringify(vectors)],[production.VERIFIER,'verifier fixture'],[production.WORKFLOW,'workflow fixture'],[production.MAKEFILE,'make fixture']]); + const policy={...production,VECTOR_COUNT:2,...Object.fromEntries(['SPEC','SEAL','VECTORS','VERIFIER','WORKFLOW','MAKEFILE'].map(k=>[`${k}_HASH`,hash(files.get(production[k]))]))}; + const runs=[production.WORKFLOW,production.GDS_WORKFLOW].map((path,i)=>({id:i+1,head_sha:sha,event:'push',path,status:'completed',conclusion:'success',html_url:`https://github.com/${repo}/actions/runs/${i+1}`,repository:{full_name:repo}})); + const reads=[]; + const fetcher=async(url,opts)=>{ + reads.push({url,opts});assert.equal(opts.credentials,'omit');assert.equal(opts.cache,'no-store');assert.equal(opts.headers.Authorization,undefined); + if(url.endsWith('/commits/HEAD'))return Response.json({sha}); + if(url.includes('/actions/runs?'))return Response.json({workflow_runs:runs}); + const prefix=`https://raw.githubusercontent.com/${repo}/${sha}/`; + assert.ok(url.startsWith(prefix),'all evidence is pinned to the resolved SHA'); + const bytes=files.get(url.slice(prefix.length)); + return new Response(bytes??'missing',{status:bytes===undefined?404:200}); + }; + return {files,runs,reads,fetcher,reader:createIssueProofReader(policy)}; +} +let cases=0; +async function check(label,mutate,expected='unknown',options){ + const f=fixture(options);mutate?.(f); + const result=(await f.reader.proveWorldIssues([row()],repo,signal,f.fetcher))[0]; + assert.equal(result.coverage,expected,label); + assert.equal(Boolean(result.proof),expected==='t27',label); + if(expected==='t27'){assert.equal(result.proof.commit,sha);assert.equal(hiveTaskPaint(result).tone,'honey');} + else assert.notEqual(hiveTaskPaint(result).tone,'honey'); + cases++; +} +await check('complete canonical evidence',null,'t27'); +for(const path of [production.SPEC,production.SEAL,production.VECTORS,production.VERIFIER,production.WORKFLOW,production.MAKEFILE]){ + await check(`tampered ${path}`,f=>f.files.set(path,f.files.get(path)+' changed')); + await check(`missing ${path}`,f=>f.files.delete(path)); +} +for(const edit of [r=>r.head_sha='c'.repeat(40),r=>r.event='pull_request',r=>r.repository.full_name='someone/fork',r=>r.status='in_progress',r=>r.conclusion='failure',r=>r.html_url='https://evil.test/fake',r=>r.path='some-other.yml'])await check('wrong CI identity/result',f=>edit(f.runs[0])); +await check('GDS failure',f=>f.runs[1].conclusion='failure'); +await check('newest failed run supersedes older successful run',f=>f.runs.push({...f.runs[0],id:3,html_url:`https://github.com/${repo}/actions/runs/3`,conclusion:'failure'})); +await check('missing issue binding',null,'unknown',{source:`; https://github.com/${repo}/issues/123`}); +await check('seal has stale source',null,'unknown',{seal:s=>s.spec_hash=`sha256:${'c'.repeat(64)}`}); +await check('seal blocked tests',null,'unknown',{seal:s=>s.tests={blocked:'missing compiler'}}); +await check('seal missing backend',null,'unknown',{seal:s=>delete s.gen_hash_rust}); +await check('vectors missing cases',null,'unknown',{vectors:v=>v.vectors.pop()}); +await check('vectors bound to another source',null,'unknown',{vectors:v=>v.spec_hash=`sha256:${'c'.repeat(64)}`}); +await check('anonymous rate limit',f=>f.fetcher=async()=>new Response('rate limited',{status:403})); +await check('invalid head',f=>f.fetcher=async()=>Response.json({sha:'main'})); +const f=fixture(); +const accepted=(await f.reader.proveWorldIssues([row()],repo,signal,f.fetcher))[0]; +const before=f.reads.length; +await f.reader.proveWorldIssues([row()],repo,signal,f.fetcher);assert.equal(f.reads.length,before,'minute cache avoids repeated anonymous reads'); +f.reader.invalidateIssueProof(repo); +const unavailable=async()=>new Response('limited',{status:429}); +assert.equal((await f.reader.proveWorldIssues([accepted],repo,signal,unavailable))[0].coverage,'unknown','failed refresh removes a cached positive'); +assert.equal((await f.reader.proveWorldIssues([{...accepted,state:'open'}],repo,signal,f.fetcher))[0].coverage,'unknown','reopening removes proof'); +assert.equal((await f.reader.proveWorldIssues([row({number:99})],repo,signal,f.fetcher))[0].coverage,'unknown','other issues never inherit proof'); +const unbound=fixture();await unbound.reader.proveWorldIssues([row({repo:'another/repo'})],'another/repo',signal,unbound.fetcher);assert.equal(unbound.reads.length,0); +cases+=5; +const matrix=JSON.parse(readFileSync(new URL('../conformance/queen_issue_proof.json',import.meta.url))); +assert.equal(matrix.vectors.length,16); +for(const v of matrix.vectors)assert.equal(production.ACCEPT[v.mask],v.expected,`conformance mask ${v.mask}`); +console.log(`Issue proof reader: ${cases} cases and ${matrix.vectors.length} conformance vectors PASS`); diff --git a/apps/website/scripts/issue-proof-from-spec.mjs b/apps/website/scripts/issue-proof-from-spec.mjs new file mode 100644 index 0000000000..0c8c8684d3 --- /dev/null +++ b/apps/website/scripts/issue-proof-from-spec.mjs @@ -0,0 +1,30 @@ +import {readFileSync,writeFileSync,mkdirSync} from 'node:fs'; +import {join} from 'node:path'; +import {SITE,loadCompiler,constsOf,checkSchema,compilerErrors,verdictOf,sha256} from './agents-from-specs.mjs'; +import {runSpecTests} from './viewport-from-spec.mjs'; +const source='specs/queen/issue_proof.t27'; +const text=readFileSync(join(SITE,source),'utf8'); +const analyze=await loadCompiler(readFileSync(join(SITE,'public/t27/t27_compiler.wasm'))); +const analysis=analyze(text),v=verdictOf(analysis),constants=constsOf(analysis); +if(compilerErrors(analysis).length||!v.typecheckOk||!v.hirOk||v.discarded||analysis.ast?.name!=='queen_issue_proof')throw new Error('Issue proof spec rejected'); +const schema={REPO:'str',ISSUES:'arr-u32',SPEC:'str',SPEC_HASH:'str',SEAL:'str',SEAL_HASH:'str',VECTORS:'str',VECTORS_HASH:'str',VERIFIER:'str',VERIFIER_HASH:'str',MAKEFILE:'str',MAKEFILE_HASH:'str',WORKFLOW:'str',WORKFLOW_HASH:'str',GDS_WORKFLOW:'str',VECTOR_COUNT:'u32',CACHE_MS:'u32',ACCEPT:'arr-u8'}; +const problems=checkSchema(constants,schema,{},source); +if(problems.length)throw new Error(problems.join('; ')); +const fields=Object.fromEntries(Object.entries(constants).map(([k,v])=>[k,v.value])); +const result=runSpecTests(analysis,fields); +if(result.tests!==2||result.asserts!==19||result.failures.length)throw new Error(JSON.stringify(result)); +if(fields.ACCEPT.length!==16||fields.ACCEPT.some((v,i)=>v!==Number(i===15)))throw new Error('Incomplete acceptance truth table'); +if(process.argv.includes('--check')){ + const seal=JSON.parse(readFileSync(join(SITE,'.trinity/seals/queen_queen_issue_proof.json'),'utf8')); + if(seal.spec_path!==source||seal.spec_hash!==`sha256:${sha256(Buffer.from(text))}`||seal.tests?.blocked)throw new Error('Missing or stale native issue proof seal'); +} +for(const [k,v] of Object.entries(fields))if(k.endsWith('_HASH')&&!/^[a-f0-9]{64}$/.test(v))throw new Error(`Invalid ${k}`); +const outputs={ + 'src/lib/queenIssueProof.generated.ts':`// GENERATED from ${source}; sha256 ${sha256(Buffer.from(text))}\nexport const issueProofPolicy = ${JSON.stringify(fields,null,2)} as const;\n`, + 'conformance/queen_issue_proof.json':JSON.stringify({spec_path:source,spec_hash:sha256(Buffer.from(text)),vectors:fields.ACCEPT.map((expected,mask)=>({mask,expected}))},null,2)+'\n', +}; +for(const [path,bytes] of Object.entries(outputs)){ + if(process.argv.includes('--check')){if(readFileSync(join(SITE,path),'utf8')!==bytes)throw new Error(`Stale ${path}`);} + else {mkdirSync(join(SITE,path,'..'),{recursive:true});writeFileSync(join(SITE,path),bytes);} +} +console.log(`Issue proof spec: ${result.tests} tests, ${result.asserts} assertions; 16 conformance rows`); diff --git a/apps/website/specs/queen/issue_proof.t27 b/apps/website/specs/queen/issue_proof.t27 new file mode 100644 index 0000000000..581cd382db --- /dev/null +++ b/apps/website/specs/queen/issue_proof.t27 @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +; Issue: https://github.com/gHashTag/trinity/issues/1360 +; A closed issue becomes honey only after exact, canonical executable evidence. +; These pins describe Corona PR13; unmerged/failing/pending evidence stays unknown. +module queen_issue_proof; + +pub const REPO : str = "ghashtag/tt-trinity-corona"; +pub const ISSUES : [2]u32 = [1, 12]; +pub const SPEC : str = "specs/corona/phase_a.t27"; +pub const SPEC_HASH : str = "7d4f7bbd507fc35cc05d88cab97582f42d619f4c5ced93584894ffd2aeeb567a"; +pub const SEAL : str = ".trinity/seals/corona_corona_phase_a.json"; +pub const SEAL_HASH : str = "78babf1f37f9e36c45caa169e4868067ea17da61f86fa3a9bef249b2b940e673"; +pub const VECTORS : str = "conformance/corona_phase_a.json"; +pub const VECTORS_HASH : str = "48e7ae489c76a00a3745ce96f82a3f1a6ae423be4dbbaa8df62e822b2737117c"; +pub const VERIFIER : str = "tools/phase_a_conformance.py"; +pub const VERIFIER_HASH : str = "2adcd80666cf08882707ca2bfc1a5b9ed7ed06fbf78838c9549fe986b11dd369"; +pub const MAKEFILE : str = "Makefile"; +pub const MAKEFILE_HASH : str = "8e7ca522a8f3dfbff6a8e9ec47f329f36b8682a173d08ec95da863dc4a1ba4cd"; +pub const WORKFLOW : str = ".github/workflows/ci.yml"; +pub const WORKFLOW_HASH : str = "e1193c3add7fd5cf7af94b1447dbca89547c83da4b2e33e4c399ab21baace75c"; +pub const GDS_WORKFLOW : str = ".github/workflows/gds.yml"; +pub const VECTOR_COUNT : u32 = 1920; +pub const CACHE_MS : u32 = 60000; +; bit 0 = closed; bit 1 = exact source/issue; bit 2 = native seal/vectors; +; bit 3 = canonical CI and GDS succeeded at the current default-branch SHA. +; Observed backlog counts may replace snapshot counts only for a complete page. +; The inspector links to the spec and both runs at the observed source revision. +pub const ACCEPT : [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]; + +test missing_evidence_is_never_honey { + assert ACCEPT[0] == 0; + assert ACCEPT[1] == 0; + assert ACCEPT[2] == 0; + assert ACCEPT[3] == 0; + assert ACCEPT[4] == 0; + assert ACCEPT[5] == 0; + assert ACCEPT[6] == 0; + assert ACCEPT[7] == 0; + assert ACCEPT[8] == 0; + assert ACCEPT[9] == 0; + assert ACCEPT[10] == 0; + assert ACCEPT[11] == 0; + assert ACCEPT[12] == 0; + assert ACCEPT[13] == 0; + assert ACCEPT[14] == 0; +} +test complete_proof_is_honey { + assert ACCEPT[15] == 1; + assert VECTOR_COUNT == 1920; + assert ISSUES[0] == 1; + assert ISSUES[1] == 12; +} diff --git a/apps/website/src/components/QueenCatalogHive.tsx b/apps/website/src/components/QueenCatalogHive.tsx index 1178c68c01..cb43003f1a 100644 --- a/apps/website/src/components/QueenCatalogHive.tsx +++ b/apps/website/src/components/QueenCatalogHive.tsx @@ -9,14 +9,28 @@ import {SELECTION_KEY,validSelection} from '../lib/queenEmbed'; import {atlasAgentPacket,type UniverseAtlas,type AtlasIssue} from '../lib/queenUniverseAtlas'; import type {CombHandle} from './queenHud'; import type {HiveDisplayProjection,HiveDisplay} from './queenHiveDisplay'; -import type {WorldIssue} from './queenRepositoryWorld'; +import {loadWorldIssues,type WorldIssue} from './queenRepositoryWorld'; +import {supportsIssueProof} from '../lib/queenIssueProof'; import './QueenCatalogHive.css'; export function QueenCatalogHive({atlas,lang,handleRef,foundationVisible=true,fitInset=0,onInspect}:{atlas:UniverseAtlas;lang:'ru'|'en';handleRef:Ref;foundationVisible?:boolean;fitInset?:number;onInspect?:()=>void}) { const ru=lang==='ru'; const [params,setParams]=useSearchParams(),focus=catalogFocus(`#/queen?${params}`,atlas),repo=focus?.repo??null; const [observed,setObserved]=useState>({}); - const world=useMemo(()=>catalogUniverse(atlas,Object.values(observed)),[atlas,observed]),map=world.map; + const [completeWorlds,setCompleteWorlds]=useState([]); + useEffect(()=>{ + if(!repo||!supportsIssueProof(repo))return; + const request=new AbortController(); + // Refresh this small supported world when selected; a static atlas predating + // closure must not override the exact GitHub/native-proof observation. + loadWorldIssues(repo,1,request.signal).then(({rows,hasMore})=>{ + if(request.signal.aborted)return; + setObserved(prev=>({...Object.fromEntries(Object.entries(prev).filter(([,row])=>row.repo!==repo)),...Object.fromEntries(rows.map(row=>[row.key,row]))})); + setCompleteWorlds(prev=>[...prev.filter(r=>r!==repo),...(!hasMore?[repo]:[])]); + }).catch(()=>{/* The dated atlas remains explicitly a snapshot on failure. */}); + return()=>request.abort(); + },[repo]); + const world=useMemo(()=>catalogUniverse(atlas,Object.values(observed),completeWorlds),[atlas,observed,completeWorlds]),map=world.map; const issueRows=world.displays.filter(row=>row?.repo===repo); const cards=useMemo(()=>world.displays.map(row=>row?{number:row.number,title:row.title,column:row.state}:null),[world]); const issueKey=focus?.number?`${repo}#${focus.number}`:null; diff --git a/apps/website/src/components/QueenCellStage.tsx b/apps/website/src/components/QueenCellStage.tsx index de98dc602e..c107120af2 100644 --- a/apps/website/src/components/QueenCellStage.tsx +++ b/apps/website/src/components/QueenCellStage.tsx @@ -78,6 +78,8 @@ const COPY = { packetNote: 'The packet does not claim the work. Check assignees and open pull requests before starting.', specs: 'Related specs · not proof', noSpecs: 'The catalog links no .t27 spec to this cell.', + proof: 'Verified Phase A evidence', + observed: 'Checked', post: 'Post to GitHub', posted: 'Posted', postFailed: 'GitHub post failed', @@ -126,6 +128,8 @@ const COPY = { packetNote: 'Пакет не назначает задачу. Перед работой проверьте исполнителя и открытые pull request.', specs: 'Связанные спеки · не доказательство', noSpecs: 'Каталог не связывает с этой сотой ни одной спеки .t27.', + proof: 'Проверенные доказательства Phase A', + observed: 'Проверено', post: 'Отправить в GitHub', posted: 'Отправлено', postFailed: 'Не удалось отправить в GitHub', @@ -291,6 +295,13 @@ export default function QueenCellStage({

)}

{t.description}

+ {issue?.proof &&
+ {t.proof}{' · '} + .t27{' · '} + CI{' · '} + GDS +
{t.observed}: +
}
             {issue ? (issue.body || t.noBody) : issueError ? t.failed : t.loading}
           
diff --git a/apps/website/src/components/queenCatalogData.ts b/apps/website/src/components/queenCatalogData.ts index a7a19f98d0..c917a301c7 100644 --- a/apps/website/src/components/queenCatalogData.ts +++ b/apps/website/src/components/queenCatalogData.ts @@ -52,7 +52,7 @@ export function catalogHive(input:UniverseAtlas):CatalogHive { } /** One coordinate space. Focus never changes membership or replaces the core. */ -export function catalogUniverse(input:UniverseAtlas,observed:WorldIssue[]=[]) { +export function catalogUniverse(input:UniverseAtlas,observed:WorldIssue[]=[],completeWorlds:string[]=[]) { const atlas=keyWorldAtlas(input),core=catalogHive(atlas); const cells=[...core.cells],positions:CatalogPosition[]=cells.map((_,i)=>({...hexToWorld(spiralAxial(i)),scale:1})); const displays:(WorldIssue|null)[]=cells.map(()=>null),regions:CatalogRegion[]=[]; @@ -61,6 +61,8 @@ export function catalogUniverse(input:UniverseAtlas,observed:WorldIssue[]=[]) { core.cells.forEach((portal,portalIndex)=>{ if(portal?.kind!=='repo')return; const rows=catalogIssueRows(atlas,portal.repo,observed); + // Partial pages and a single inspected issue cannot establish the backlog. + if(completeWorlds.includes(portal.repo))cells[portalIndex]={...portal,open:observed.filter(r=>r.repo===portal.repo&&r.state==='open').length}; // Reserve the snapshot's slots: a newly observed older issue appends instead of shifting existing cells. const snapshot=catalogIssueRows(atlas,portal.repo),byKey=new Map(rows.map(r=>[r.key,r])); const ordered=[...snapshot.map(r=>byKey.get(r.key)!),...observed.filter(r=>r.repo===portal.repo&&byKey.has(r.key)&&!snapshot.some(s=>s.key===r.key))]; @@ -89,7 +91,7 @@ export function catalogUniverse(input:UniverseAtlas,observed:WorldIssue[]=[]) { } regions.push(region); }); - const map:CatalogHive={...core,cells,positions,regions,specLinks,coreCount:core.cells.length,signature:JSON.stringify([core.signature,regions,specLinks,displays.map(r=>r?.key??null)])}; + const map:CatalogHive={...core,cells,positions,regions,specLinks,coreCount:core.cells.length,signature:JSON.stringify([core.signature,completeWorlds,cells.filter(c=>c?.kind==='repo'),regions,specLinks,displays.map(r=>r?.key??null)])}; return {map,displays}; } diff --git a/apps/website/src/components/queenHiveDisplay.ts b/apps/website/src/components/queenHiveDisplay.ts index 84f07f7371..8ec4aae3dd 100644 --- a/apps/website/src/components/queenHiveDisplay.ts +++ b/apps/website/src/components/queenHiveDisplay.ts @@ -9,8 +9,8 @@ export interface HiveDisplay { state: string; closedAt: string | null; children: EpicRecord['children']; - /** The public issue ledger supplies no issue-to-module coverage proof. */ - coverage: 'unknown'; + /** GitHub lifecycle alone is unknown; only the separate native-proof reader may promote it. */ + coverage: 'unknown' | 't27'; } type Snapshot = { repo: string; closedIssues: FoundationIssue[]; epics: EpicRecord[] }; diff --git a/apps/website/src/components/queenRepositoryWorld.ts b/apps/website/src/components/queenRepositoryWorld.ts index 06ab1b6989..60e69f4ca5 100644 --- a/apps/website/src/components/queenRepositoryWorld.ts +++ b/apps/website/src/components/queenRepositoryWorld.ts @@ -1,4 +1,5 @@ import type { HiveDisplay } from './queenHiveDisplay'; +import {proveWorldIssues,invalidateIssueProof,type IssueProof} from '../lib/queenIssueProof.ts'; export const PINNED_WORLDS = ['ghashtag/trios', 'ghashtag/t27']; export const WORLD_STORAGE = 'queen.public-worlds.v1'; @@ -19,7 +20,7 @@ export function savedWorlds(raw: string | null): string[] { catch { return [...PINNED_WORLDS]; } } function object(value: unknown): Record { return value!==null&&typeof value==='object'&&!Array.isArray(value)?value as Record:{}; } -export type WorldIssue = HiveDisplay & { updatedAt:string|null; body?:string; assignees?:string[] }; +export type WorldIssue = HiveDisplay & { updatedAt:string|null; body?:string; assignees?:string[]; proof?:IssueProof }; export type WorldMetadata = { repo:string; description:string; issuesEnabled:boolean }; export function githubWorldMetadata(repo:string,raw:unknown):WorldMetadata { const value=object(raw); @@ -57,7 +58,7 @@ export async function loadWorldIssues(repo:string,page:number,signal:AbortSignal const response=await githubRead(repo,`/issues?state=all&sort=created&direction=asc&per_page=100&page=${page}`,signal,fetcher); const raw:unknown=await response.json(); const link=response.headers.get('link'); - return {rows:githubWorldIssues(repo,raw),hasMore:link?link.includes('rel="next"'):Array.isArray(raw)&&raw.length===100}; + return {rows:await proveWorldIssues(githubWorldIssues(repo,raw),repo,signal,fetcher),hasMore:link?link.includes('rel="next"'):Array.isArray(raw)&&raw.length===100}; } export async function loadWorldBacklog(repo:string,page:number,signal:AbortSignal,fetcher:Fetcher=fetch) { if(!Number.isInteger(page)||page<1||page>100)throw new Error('invalid-page'); @@ -80,6 +81,7 @@ const DETAILS_TTL_MS=60_000,DETAILS_MAX=100; export async function loadWorldIssueDetailsCached(repo:string,number:number,signal:AbortSignal,fresh=false,fetcher:Fetcher=fetch) { const key=`${repo}#${number}`,cached=detailsCache.get(key); if(!fresh&&cached&&Date.now()-cached.at=DETAILS_MAX)detailsCache.delete(detailsCache.keys().next().value!); detailsCache.set(key,{at:Date.now(),row}); @@ -93,5 +95,5 @@ export async function loadWorldIssueDetails(repo:string,number:number,signal:Abo if(!row)throw new Error('issue-identity'); const assignees=object(raw).assignees; row.assignees=Array.isArray(assignees)?assignees.flatMap(a=>{const login=object(a).login;return typeof login==='string'&&/^[a-z0-9-]+$/i.test(login)?[login]:[];}):[]; - return row; + return (await proveWorldIssues([row],repo,signal,fetcher))[0]; } diff --git a/apps/website/src/lib/queenIssueProof.generated.ts b/apps/website/src/lib/queenIssueProof.generated.ts new file mode 100644 index 0000000000..ad46bff1d1 --- /dev/null +++ b/apps/website/src/lib/queenIssueProof.generated.ts @@ -0,0 +1,41 @@ +// GENERATED from specs/queen/issue_proof.t27; sha256 76c068c85d3af34aaa6f4a571742f6ad00de727a37f0ffe419c83bbdb1b84c71 +export const issueProofPolicy = { + "REPO": "ghashtag/tt-trinity-corona", + "ISSUES": [ + 1, + 12 + ], + "SPEC": "specs/corona/phase_a.t27", + "SPEC_HASH": "7d4f7bbd507fc35cc05d88cab97582f42d619f4c5ced93584894ffd2aeeb567a", + "SEAL": ".trinity/seals/corona_corona_phase_a.json", + "SEAL_HASH": "78babf1f37f9e36c45caa169e4868067ea17da61f86fa3a9bef249b2b940e673", + "VECTORS": "conformance/corona_phase_a.json", + "VECTORS_HASH": "48e7ae489c76a00a3745ce96f82a3f1a6ae423be4dbbaa8df62e822b2737117c", + "VERIFIER": "tools/phase_a_conformance.py", + "VERIFIER_HASH": "2adcd80666cf08882707ca2bfc1a5b9ed7ed06fbf78838c9549fe986b11dd369", + "MAKEFILE": "Makefile", + "MAKEFILE_HASH": "8e7ca522a8f3dfbff6a8e9ec47f329f36b8682a173d08ec95da863dc4a1ba4cd", + "WORKFLOW": ".github/workflows/ci.yml", + "WORKFLOW_HASH": "e1193c3add7fd5cf7af94b1447dbca89547c83da4b2e33e4c399ab21baace75c", + "GDS_WORKFLOW": ".github/workflows/gds.yml", + "VECTOR_COUNT": 1920, + "CACHE_MS": 60000, + "ACCEPT": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ] +} as const; diff --git a/apps/website/src/lib/queenIssueProof.ts b/apps/website/src/lib/queenIssueProof.ts new file mode 100644 index 0000000000..84e3094bed --- /dev/null +++ b/apps/website/src/lib/queenIssueProof.ts @@ -0,0 +1,72 @@ +import {issueProofPolicy as policy} from './queenIssueProof.generated.ts'; + +export type IssueProof = {commit:string;specUrl:string;ciUrl:string;gdsUrl:string;observedAt:number}; +type Row = {repo:string;number:number;state:string;coverage:'unknown'|'t27';proof?:IssueProof}; +type Fetcher=typeof fetch; +type Run = {id?:number;head_sha?:string;event?:string;path?:string;status?:string;conclusion?:string;html_url?:string;repository?:{full_name?:string}}; +export type IssueProofPolicy={REPO:string;ISSUES:readonly number[];SPEC:string;SEAL:string;VECTORS:string;VERIFIER:string;MAKEFILE:string;WORKFLOW:string;GDS_WORKFLOW:string;SPEC_HASH:string;SEAL_HASH:string;VECTORS_HASH:string;VERIFIER_HASH:string;MAKEFILE_HASH:string;WORKFLOW_HASH:string;VECTOR_COUNT:number;CACHE_MS:number;ACCEPT:readonly number[]}; +const hex=async(bytes:ArrayBuffer)=>Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),x=>x.toString(16).padStart(2,'0')).join(''); + +async function read(url:string,signal:AbortSignal,fetcher:Fetcher):Promise{ + const response=await fetcher(url,{signal,credentials:'omit',cache:'no-store',headers:{Accept:'application/vnd.github+json'}}); + if(!response.ok)throw new Error(`proof read ${response.status}`); + return response; +} +function acceptedRun(raw:unknown,repo:string,commit:string,path:string):Run|null{ + const runs=(raw as {workflow_runs?:Run[]})?.workflow_runs; + if(!Array.isArray(runs))return null; + const relevant=runs.filter(r=>r&&r.head_sha===commit&&r.event==='push'&&r.path===path&&r.repository?.full_name?.toLowerCase()===repo&&Number.isSafeInteger(r.id)); + const latest=relevant.sort((a,b)=>b.id!-a.id!)[0]; + if(latest?.status!=='completed'||latest.conclusion!=='success'||latest.html_url?.toLowerCase()!==`https://github.com/${repo}/actions/runs/${latest.id}`)return null; + return latest; +} + +/** Policy comes from the checked-in spec; never from a URL or API response. */ +export function createIssueProofReader(policy:IssueProofPolicy){ +const cache=new Map(); +function supportsIssueProof(repo:string):boolean{return repo===policy.REPO;} +function invalidateIssueProof(repo:string):void{cache.delete(repo);} +async function repositoryProof(repo:string,signal:AbortSignal,fetcher:Fetcher):Promise{ + const held=cache.get(repo); + if(held&&Date.now()-held.observedAt{ + const bytes=await (await read(`https://raw.githubusercontent.com/${repo}/${commit}/${path}`,signal,fetcher)).arrayBuffer(); + if(await hex(bytes)!==hashes[i])throw new Error(`proof hash mismatch: ${path}`); + return new TextDecoder().decode(bytes); + })); + const seal=JSON.parse(files[1]),vectors=JSON.parse(files[2]); + if(seal.spec_path!==policy.SPEC||seal.spec_hash!==`sha256:${policy.SPEC_HASH}`||seal.tests?.blocked|| + vectors.spec_path!==policy.SPEC||vectors.spec_hash!==seal.spec_hash||vectors.vectors?.length!==policy.VECTOR_COUNT|| + !['zig','rust','c','verilog'].every(b=>/^sha256:[a-f0-9]{64}$/.test(seal[`gen_hash_${b}`]??'')))throw new Error('incomplete native evidence'); + for(const n of policy.ISSUES)if(!new RegExp(`https://github.com/${repo}/issues/${n}(?![0-9])`,'i').test(files[0]))throw new Error('issue not bound to spec'); + const runs=await (await read(`${api}/actions/runs?head_sha=${commit}&event=push&per_page=100`,signal,fetcher)).json(); + const ci=acceptedRun(runs,repo,commit,policy.WORKFLOW),gds=acceptedRun(runs,repo,commit,policy.GDS_WORKFLOW); + if(!ci||!gds)throw new Error('canonical CI or GDS is not successful at current HEAD'); + const proof={commit,specUrl:`https://github.com/${repo}/blob/${commit}/${policy.SPEC}`,ciUrl:ci.html_url!,gdsUrl:gds.html_url!,observedAt:Date.now()}; + cache.set(repo,proof); + return proof; +} + +/** Supported exact evidence only; failures retain the observed GitHub lifecycle. */ +async function proveWorldIssues(rows:T[],repo:string,signal:AbortSignal,fetcher:Fetcher=fetch):Promise{ + // Reopened/unsupported observations cannot retain a previous positive proof. + rows=rows.map(row=>({...row,coverage:'unknown' as const,proof:undefined})); + if(!supportsIssueProof(repo)||!rows.some(r=>r.repo===repo&&r.state==='closed'&&(policy.ISSUES as readonly number[]).includes(r.number)))return rows; + let proof:IssueProof; + try{proof=await repositoryProof(repo,signal,fetcher);}catch{return rows.map(r=>({...r,coverage:'unknown' as const,proof:undefined}));} + if(signal.aborted)return rows; + return rows.map(row=>{ + const mask=Number(row.state==='closed')+2*Number(row.repo===repo&&(policy.ISSUES as readonly number[]).includes(row.number))+4+8; + return policy.ACCEPT[mask]===1?{...row,coverage:'t27' as const,proof}:row; + }); +} +return {supportsIssueProof,invalidateIssueProof,proveWorldIssues}; +} +export const {supportsIssueProof,invalidateIssueProof,proveWorldIssues}=createIssueProofReader(policy); diff --git a/docs/QUEEN_CORONA_PROOF.md b/docs/QUEEN_CORONA_PROOF.md new file mode 100644 index 0000000000..c22e8f8112 --- /dev/null +++ b/docs/QUEEN_CORONA_PROOF.md @@ -0,0 +1,56 @@ +# Corona issue proof in the public map + +Issue: https://github.com/gHashTag/trinity/issues/1360 +Dependency: https://github.com/gHashTag/tt-trinity-corona/pull/13 + +The GitHub issue adapter formerly assigned `coverage: unknown` to every issue. +Closing Corona #1 therefore left a red cell even if a native proof was added. +The reader now implements `apps/website/specs/queen/issue_proof.t27` for the +explicitly supported Corona issues #1 and #12. Other repositories stay unknown. + +Acceptance requires a closed issue, exact SHA-256 pins for the spec, native +seal, vectors, replay tool, Makefile and CI workflow, plus successful canonical +push CI and GDS at the current default-branch SHA. PR/fork runs cannot substitute +for that accepted source. The latest matching failed run overrides an older +successful run. A failed refresh clears a cached positive result. Public reads +omit credentials; the successful proof cache lasts one minute. + +The selected small Corona world is read from GitHub so dated open issue rows do +not override observed closure. Only a complete first page can update the portal +backlog count. A single inspected issue cannot establish the repository total. +Accepted cells show the immutable spec and exact CI/GDS source links and the +observation timestamp in the inspector. + +## Reproduce + +From `apps/website`: + +```sh +npm ci +npm run check:queen-issue-proof +npm run check:queen-worlds +npm run check:queen-displays +npm run check:queen-continuous +npm run typecheck:ratchet +npm run build:ci +``` + +The checked generator analyzes/types the `.t27` with the vendored compiler, +executes its two tests (19 assertions), and verifies generated policy and all +16 conformance vectors. The reader test uses real SHA-256 over isolated HTTP +fixtures for acceptance, tampering, missing files, reopen, CI identity/failure, +rate limits and cache invalidation. The `.trinity/seals` receipt is created by +native t27c, not assembled by the reader. + +To update evidence, first review the upstream accepted source and CI, then edit +the spec pins, regenerate with `node scripts/issue-proof-from-spec.mjs`, recreate +the native seal and run all checks. Do not weaken the reader to make a cell gold. + +## Boundaries + +Until Corona PR13 is accepted with successful canonical push CI/GDS, this reader +must leave its cells unverified. Unit fixtures are not live acceptance evidence. +The existing TypeScript ratchet has 179 baseline errors across 26 files; the +new change must add none. The game embedding at app.t27.ai has a separate Queen +source pin and requires deployment verification after the source PR is accepted. +This change establishes neither a fabricated chip nor reward/payment settlement.