From 5a58e3e0bfce81faca688568d2b862c901747f40 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 17:22:24 +0000 Subject: [PATCH 1/4] Feed the atlas resume view from node-status records Add the interface v0 schema, three current views for existing graph nodes, and a validator that refuses an owner without an evidence URL. The atlas inspector passes that view into resumeProjection. Static pages stay on the snapshot-only call. Scientific effect NONE. Co-authored-by: Dylan Roy --- architecture/node-status.schema.json | 75 ++++++ docs/site/README.md | 2 +- docs/site/atlas.mjs | 9 +- docs/site/cite.html | 2 +- docs/site/current-model.mjs | 37 ++- docs/site/current.mjs | 2 +- docs/site/dependencies.html | 4 +- docs/site/explore.html | 2 +- docs/site/index.html | 2 +- docs/site/measure.html | 2 +- docs/site/museum.html | 2 +- docs/site/node-status.json | 74 ++++++ docs/site/reproduce.html | 2 +- docs/site/workspace.html | 2 +- .../eng.d0-packet-allowlist-fix.json | 24 ++ records/node_status/eng.hard-gate.json | 24 ++ .../node_status/math.side24-coefficient.json | 24 ++ tests/test_node_status_atlas.mjs | 126 ++++++++++ tools/current_site_history.py | 74 +++++- tools/validate_node_status.py | 234 ++++++++++++++++++ 20 files changed, 704 insertions(+), 19 deletions(-) create mode 100644 architecture/node-status.schema.json create mode 100644 docs/site/node-status.json create mode 100644 records/node_status/eng.d0-packet-allowlist-fix.json create mode 100644 records/node_status/eng.hard-gate.json create mode 100644 records/node_status/math.side24-coefficient.json create mode 100644 tests/test_node_status_atlas.mjs create mode 100644 tools/validate_node_status.py diff --git a/architecture/node-status.schema.json b/architecture/node-status.schema.json new file mode 100644 index 000000000..7e8ef3dda --- /dev/null +++ b/architecture/node-status.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "architecture/node-status.schema.json", + "title": "Node status record v0", + "description": "Current view of one graph node. status is freshness of this view only, never scientific validity. scientific_effect stays NONE. owner is null unless evidence_url is a PICKUP or assignment comment URL. Receipts are referenced by sources[].url and sources[].rev and are not copied.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "node_id", + "owner", + "next_action", + "missing_input", + "stops", + "sources", + "source_rev", + "observed_at", + "status", + "scientific_effect" + ], + "properties": { + "schema_version": {"const": 0}, + "node_id": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*$" + }, + "owner": { + "oneOf": [ + {"type": "null"}, + { + "type": "object", + "additionalProperties": false, + "required": ["id", "evidence_url"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "evidence_url": { + "type": "string", + "pattern": "^https://github\\.com/d6g8k5htny-coder/(?:main|Math-)/(?:issues|pull)/[1-9][0-9]*#issuecomment-[1-9][0-9]*$" + } + } + } + ] + }, + "next_action": {"type": "string", "minLength": 1}, + "missing_input": { + "type": "array", + "items": {"type": "string", "minLength": 1} + }, + "stops": { + "type": "array", + "items": {"type": "string", "minLength": 1} + }, + "sources": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["url", "rev"], + "properties": { + "url": {"type": "string", "minLength": 1}, + "rev": {"type": "string", "pattern": "^[0-9a-f]{40}$"} + } + } + }, + "source_rev": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "observed_at": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" + }, + "status": {"enum": ["fresh", "stale", "unknown"]}, + "scientific_effect": {"const": "NONE"} + } +} diff --git a/docs/site/README.md b/docs/site/README.md index 76618868a..033e8b1ba 100644 --- a/docs/site/README.md +++ b/docs/site/README.md @@ -21,7 +21,7 @@ The front and technical layers use the same existing evidence. No second scienti `tools/current_site_data.py` resolves public main and Math- commits once, verifies their complete Git trees and the seven acquired source files, and extracts exact indented proof displays. Other inventory rows bind paths and Git blobs; their document bytes are not downloaded. The page calls equation blocks excerpts and keeps their surrounding proof scope available by pinned line links. `tools/current_site_build.py` renders one payload offline into marked current sections and static node pages; Research and Formal stay script-free. Current observations and original dated reading cuts have separate identities. -"Resume this node" appears in the atlas inspector and on each current static node page. Both come from `resumeProjection` in `current-model.mjs`; `tools/current_site_history.py` keeps a Python copy, and `tests/test_current_site_resume.mjs` checks that the two agree on every node. The block names the snapshot's graph commit, path, SHA-256 and observation time, and warns readers to refresh from the live record before any ownership- or head-dependent action. Recorded `review_*` fields are labelled as review records, not as the current task, an owner or completion. Next action, missing input, owner evidence and retained stop read "not recorded in this snapshot", because the graph records none of them. A bare `review_issue` number names no repository, so it is not linked. Continuation links come only from the small site-maintained `RESUME_TASK_RECORDS` table of explicitly identified operational pointers; they are not graph metadata, scientific acceptance or source-binding admission. Archived snapshot pages are unchanged. +"Resume this node" appears in the atlas inspector and on each current static node page. Both come from `resumeProjection` in `current-model.mjs`; `tools/current_site_history.py` keeps a Python copy, and `tests/test_current_site_resume.mjs` checks that the two agree on every node. The block names the snapshot's graph commit, path, SHA-256 and observation time, and warns readers to refresh from the live record before any ownership- or head-dependent action. Recorded `review_*` fields are labelled as review records, not as the current task, an owner or completion. Next action, missing input, owner evidence and retained stop read "not recorded in this snapshot" on the snapshot-only projection, because the graph records none of them. The atlas inspector also passes `node-status.json`, the published copy of `records/node_status`, into that same projection. Static node pages stay on the snapshot-only call. An owner is shown only when the record has an evidence URL. The record's status is freshness of that view, not scientific validity, and `scientific_effect` stays NONE. A bare `review_issue` number names no repository, so it is not linked. Continuation links come only from the small site-maintained `RESUME_TASK_RECORDS` table of explicitly identified operational pointers; they are not graph metadata, scientific acceptance or source-binding admission. Archived snapshot pages are unchanged. The configured `.github/workflows/current-site.yml` builds on main pushes and a 30-minute schedule and deploys the complete tested docs artifact through Pages Actions. Each candidate needs its own hosted success and deployed-source acceptance. A failed source refresh cannot reach deployment. Source observations are not mathematical revalidation. Existing diagrams on Explore keep their defined teaching scope; the new source panel links the advancing work. diff --git a/docs/site/atlas.mjs b/docs/site/atlas.mjs index 766be1f63..60a260ccc 100644 --- a/docs/site/atlas.mjs +++ b/docs/site/atlas.mjs @@ -1,5 +1,5 @@ -import {currentSnapshot,element,anchor} from './current.mjs?site-release=8a66eb0ad9971fbffe07579998488e81195a024161995c3c4c7d4b34e1ec7262'; -import {hasNode,readState,neighborhood,searchGraph,downstreamReach,requiredDependencyPaths,REQUIRED_ROUTE_SEMANTICS,evidenceAxes,sourceURL,sourceForNode,nodePage,resumeProjection,RESUME_NOT_RECORDED} from './current-model.mjs?site-release=713e81d352fe16f86116e405448ab16604f861c360841ba0f7d7396fd9ccb594'; +import {currentSnapshot,element,anchor} from './current.mjs?site-release=433be464efd229238533aa4e1ec4c6f9831229cd1cd9ba4f4a941f54cc169e49'; +import {hasNode,readState,neighborhood,searchGraph,downstreamReach,requiredDependencyPaths,REQUIRED_ROUTE_SEMANTICS,evidenceAxes,sourceURL,sourceForNode,nodePage,resumeProjection,RESUME_NOT_RECORDED} from './current-model.mjs?site-release=307d77d57733b20953326e167591665192a0a7e004a026b1c9bb4d523e2912b8'; import {createAtlasShell,viewForViewport} from './atlas-shell.mjs?site-release=9fce5f707e3486ef152ffdaa1cf1e4adf7cd9cf9dac116407a69b6528b4c656c'; const byId=id=>document.getElementById(id); const svgElement=(tag,attrs)=>{const n=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,String(v));return n;}; @@ -9,8 +9,11 @@ function resumeSection(view){const section=element('section',undefined,'resume-n const s=view.snapshot,warning=element('p',undefined,'resume-warning');warning.dataset.freshness=view.freshness;warning.append(element('strong',`As of ${s.observed_at} · ${view.freshness}.`),` ${view.warning}${view.freshness==='current'?'':' Do not rely on this snapshot for current ownership or heads.'}`); section.append(heading,warning,element('p',`Snapshot identity: graph ${s.graph_repository} commit ${s.graph_commit} · ${s.graph_path} · SHA-256 ${s.graph_sha256} · main commit ${s.main_commit||RESUME_NOT_RECORDED}.`),element('h5','Recorded graph state'),fields(view.state),element('h5','Recorded review record'),element('p',view.review.label),view.review.fields.length?fields(view.review.fields):element('p',`Review metadata: ${RESUME_NOT_RECORDED}.`),element('h5','Operational task state'),fields(view.operational),element('h5','Continuation')); const task=view.continuation,p=element('p');if(task)p.append(anchor(task.label,task.url),`. ${task.meaning} Site-maintained pointer, not part of the graph snapshot; identified on ${task.identified_on} in `,anchor("the coordinator's activation record",task.identified_in),'. The linked comment is mutable; read it live before acting.');else p.textContent=view.continuation_text;section.append(p);return section;} +// Published copy of records/node_status. A missing file leaves the snapshot-only projection. +async function loadNodeStatus(){try{const response=await fetch('node-status.json',{cache:'no-store',credentials:'omit'});if(!response.ok)return null;const body=await response.json();return body&&typeof body==='object'&&!Array.isArray(body)?body:null;}catch{return null;}} function linkRecord(label,value){const p=element('p');if(typeof value==='string'){try{const u=new URL(value);if(u.protocol==='https:'&&u.hostname==='github.com'&&!u.username&&!u.password&&!u.port){p.append(anchor(label,value));return p;}}catch{}}p.textContent=`${label}: ${value||'not recorded'}`;return p;} async function start() { + const nodeStatus=await loadNodeStatus(); const root=byId('current-atlas');if(!root)return; const data=await currentSnapshot; if(!data){byId('atlas-status').textContent='Unavailable: current snapshot did not verify. Use the static source index and preserved snapshot below.';return;} @@ -37,7 +40,7 @@ async function start() { const reachPanel=element('p',reachText);reachPanel.id='atlas-reach';panel.append(reachPanel,element('p',`Complete indexed snapshot query at ${data.graph.identity.commit}. Each count excludes the selected node and counts distinct downstream nodes. All edges includes context; required edges uses required edges at every step. These counts do not identify blocked theorems or prove completeness.`));byId('atlas-diagnostic-selection').textContent=`Selected ${selected}: ${reachText} No scientific disposition is inferred.`; panel.append(element('p',n.scope||n.notes||'Statement and scope extraction not recorded. Inspect the source reference.')); panel.append(anchor('Read this node without JavaScript',nodePage(selected))); - panel.append(resumeSection(resumeProjection(data,selected,Date.now()))); + panel.append(resumeSection(resumeProjection(data,selected,Date.now(),nodeStatus))); const dl=element('dl',undefined,'atlas-axes');for(const a of evidenceAxes(n)){dl.append(element('dt',a.axis),element('dd',`${a.axis[0].toUpperCase()+a.axis.slice(1)} metadata ${a.state==='recorded'?'recorded':'not recorded'} · ${a.state}`));}panel.append(dl,element('p','These labels describe metadata in this graph snapshot. Evidence values, applicability and custody are separate. No explicit graph-to-artifact binding · Artifact roles: unknown · Custody: unknown.')); const source=sourceForNode(data,n);if(source){panel.append(anchor('Immutable source reference (document bytes not acquired by this graph view)',sourceURL(source)));panel.append(element('p',`Git blob ${source.blob} · ${source.bytes} bytes in the pinned tree. Reference presence is not mathematical verification.`));}else panel.append(element('p',n.source?`Unresolved source reference: ${n.source}. No matched public file in this pinned source inventory.`:'Source reference not recorded.')); for(const key of Object.keys(n).filter(k=>k.startsWith('review_'))){const value=n[key];const pinned=key==='review_source'?data.sources.find(s=>s.repository===data.graph.identity.repository&&s.path===value):null;if(pinned)panel.append(anchor('Recorded review source at the pinned commit',sourceURL(pinned)));else panel.append(linkRecord(`Recorded ${key}`,value&&typeof value==='object'?JSON.stringify(value):value));} diff --git a/docs/site/cite.html b/docs/site/cite.html index 8d847fffd..400a75c02 100644 --- a/docs/site/cite.html +++ b/docs/site/cite.html @@ -7,7 +7,7 @@ - +
UNIVERSAL LAW MATHEMATICS IN THE OPEN
diff --git a/docs/site/current-model.mjs b/docs/site/current-model.mjs index 7636274b7..046f2ee32 100644 --- a/docs/site/current-model.mjs +++ b/docs/site/current-model.mjs @@ -79,17 +79,50 @@ export const RESUME_TASK_RECORDS=Object.freeze({ }); const REVIEW_URL=/^https:\/\/github\.com\/d6g8k5htny-coder\/(?:main|Math-)\/(?:issues|pull)\/[1-9][0-9]*(?:#[A-Za-z0-9_-]+)?$/; const metadataText=value=>typeof value==='string'?value:JSON.stringify(value); +const NODE_STATUS_FORBIDDEN=new Set(['lemma_closed','prizes_solved','discharges_OBL_H5_JETMOD','certified_C_H','freeze','inventable_attempt_accepted']); +const NODE_STATUS_STATE=/^(?:fresh|stale|unknown)$/; +const NODE_STATUS_OBSERVED=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; +function nodeStatusForbidden(value){ + if(!value||typeof value!=='object')return false; + return Object.entries(value).some(([key,item])=>NODE_STATUS_FORBIDDEN.has(key)||nodeStatusForbidden(item)); +} +// A record without an evidence URL is not applied. Freshness is not scientific validity. +function nodeStatusRecord(statusRecords,id){ + if(!statusRecords||typeof statusRecords!=='object'||Array.isArray(statusRecords)||!Object.prototype.hasOwnProperty.call(statusRecords,id))return null; + const record=statusRecords[id]; + if(!record||typeof record!=='object'||Array.isArray(record)||record.scientific_effect!=='NONE'||nodeStatusForbidden(record))return null; + if(!Object.prototype.hasOwnProperty.call(record,'owner'))return null; + const owner=record.owner; + if(owner!==null&&(typeof owner!=='object'||Array.isArray(owner)||typeof owner.id!=='string'||!owner.id||typeof owner.evidence_url!=='string'||!owner.evidence_url))return null; + return record; +} +function nodeStatusList(value){return Array.isArray(value)&&value.every(item=>typeof item==='string'&&item)&&value.length?value.join('; '):null;} +function nodeStatusOperational(record){ + const owner=record.owner; + return RESUME_OPERATIONAL.map(([field,label])=>{ + const row={field,label,text:RESUME_NOT_RECORDED}; + if(field==='next_action'&&typeof record.next_action==='string'&&record.next_action){ + row.text=record.next_action; + if(typeof record.status==='string'&&NODE_STATUS_STATE.test(record.status)&&typeof record.observed_at==='string'&&NODE_STATUS_OBSERVED.test(record.observed_at))row.note=`Node-status view ${record.status} as of ${record.observed_at}. Scientific effect NONE. Freshness is not scientific validity.`; + }else if(field==='missing_input'){const text=nodeStatusList(record.missing_input);if(text)row.text=text;} + else if(field==='owner_evidence'){row.text=owner===null?'No owner recorded in this node-status view.':owner.id;if(owner&&owner.evidence_url)row.url=owner.evidence_url;} + else if(field==='retained_stop')row.text=nodeStatusList(record.stops)||'No retained stop recorded in this node-status view.'; + return row; + }); +} // One projection feeds the atlas inspector and the static node pages; only `now` adds a freshness state. -export function resumeProjection(snapshot,id,now) { +// statusRecords is optional. The snapshot-only call stays unchanged for static pages. +export function resumeProjection(snapshot,id,now,statusRecords) { const graph=snapshot?.graph?.data;if(!hasNode(graph,id))return null; const node=graph.nodes[id],identity=snapshot.graph.identity,own=key=>Object.prototype.hasOwnProperty.call(node,key); const task=Object.prototype.hasOwnProperty.call(RESUME_TASK_RECORDS,id)?RESUME_TASK_RECORDS[id]:null; + const record=nodeStatusRecord(statusRecords,id); return {node:id, snapshot:{observed_at:snapshot.observed_at,graph_repository:identity.repository,graph_commit:identity.commit,graph_path:identity.path,graph_sha256:identity.sha256,main_commit:snapshot.repositories.find(r=>r.repository==='d6g8k5htny-coder/main')?.commit??null}, freshness:Number.isFinite(now)?freshness(snapshot.observed_at,now):null,warning:RESUME_FRESHNESS_WARNING, state:['classification','controlling'].map(field=>({field,text:own(field)&&node[field]!==null?metadataText(node[field]):RESUME_NOT_RECORDED})), review:{label:RESUME_REVIEW_LABEL,fields:Object.keys(node).filter(key=>key.startsWith('review_')).map(field=>{const value=node[field];return {field,text:metadataText(value),url:typeof value==='string'&&REVIEW_URL.test(value)?value:null,note:field==='review_issue'&&typeof value==='number'?RESUME_ISSUE_NOTE:null};})}, - operational:RESUME_OPERATIONAL.map(([field,label])=>({field,label,text:RESUME_NOT_RECORDED})), + operational:record?nodeStatusOperational(record):RESUME_OPERATIONAL.map(([field,label])=>({field,label,text:RESUME_NOT_RECORDED})), continuation:task?{...task}:null,continuation_text:task?null:RESUME_NO_TASK}; } export const MAX_WINDOW_NODES=150; diff --git a/docs/site/current.mjs b/docs/site/current.mjs index 771822592..6525e571f 100644 --- a/docs/site/current.mjs +++ b/docs/site/current.mjs @@ -1,4 +1,4 @@ -import {fetchVerifiedSnapshot,freshness,filterSources,sourceURL,projectCurrentAcquisitions,ACQUISITION_LABELS,ACQUISITION_UNAVAILABLE} from './current-model.mjs?site-release=713e81d352fe16f86116e405448ab16604f861c360841ba0f7d7396fd9ccb594'; +import {fetchVerifiedSnapshot,freshness,filterSources,sourceURL,projectCurrentAcquisitions,ACQUISITION_LABELS,ACQUISITION_UNAVAILABLE} from './current-model.mjs?site-release=307d77d57733b20953326e167591665192a0a7e004a026b1c9bb4d523e2912b8'; export function element(tag,text,className) { const e=document.createElement(tag);if(text!==undefined)e.textContent=text;if(className)e.className=className;return e; } diff --git a/docs/site/dependencies.html b/docs/site/dependencies.html index 518434ce0..9feb696fb 100644 --- a/docs/site/dependencies.html +++ b/docs/site/dependencies.html @@ -13,8 +13,8 @@ - - + + diff --git a/docs/site/explore.html b/docs/site/explore.html index 228f1f2b2..529ccf0a8 100644 --- a/docs/site/explore.html +++ b/docs/site/explore.html @@ -13,7 +13,7 @@ - + diff --git a/docs/site/index.html b/docs/site/index.html index 4bf00582d..ef1b1eb1c 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -7,7 +7,7 @@ - +
UNIVERSAL LAW MATHEMATICS IN THE OPEN
diff --git a/docs/site/measure.html b/docs/site/measure.html index 30fd847bd..4510dafa1 100644 --- a/docs/site/measure.html +++ b/docs/site/measure.html @@ -13,7 +13,7 @@ - + diff --git a/docs/site/museum.html b/docs/site/museum.html index 6fcda90c7..e35c2b194 100644 --- a/docs/site/museum.html +++ b/docs/site/museum.html @@ -12,7 +12,7 @@ - + diff --git a/docs/site/node-status.json b/docs/site/node-status.json new file mode 100644 index 000000000..c35554433 --- /dev/null +++ b/docs/site/node-status.json @@ -0,0 +1,74 @@ +{ + "eng.d0-packet-allowlist-fix": { + "schema_version": 0, + "node_id": "eng.d0-packet-allowlist-fix", + "owner": null, + "next_action": "The pinned graph note says a portable fix was verified locally and a writer with push access was still awaited. This view records no owner.", + "missing_input": [ + "No PICKUP comment URL is recorded for an owner of this node." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + }, + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/patches/d0_packet_allowlist.patch", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE" + }, + "eng.hard-gate": { + "schema_version": 0, + "node_id": "eng.hard-gate", + "owner": null, + "next_action": "Use the pinned downstream-gate package as the recorded engineering control. This view sets no mathematical status.", + "missing_input": [ + "The graph source is the directory frontiers/downstream_gate_20260925/, so this view does not pin one document byte as the control text." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + }, + { + "url": "https://github.com/d6g8k5htny-coder/Math-/tree/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE" + }, + "math.side24-coefficient": { + "schema_version": 0, + "node_id": "math.side24-coefficient", + "owner": null, + "next_action": "Read the pinned coefficient proof and its recorded review as a review record only. This view records no owner and sets no mathematical status.", + "missing_input": [ + "No owner evidence URL is recorded for this node." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + }, + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/coefficients/side24_v1/PROOF.md", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE" + } +} diff --git a/docs/site/reproduce.html b/docs/site/reproduce.html index c075f279f..6e5b5b31a 100644 --- a/docs/site/reproduce.html +++ b/docs/site/reproduce.html @@ -8,7 +8,7 @@ - +
UNIVERSAL LAW MATHEMATICS IN THE OPEN
diff --git a/docs/site/workspace.html b/docs/site/workspace.html index 1e04f5543..09751d559 100644 --- a/docs/site/workspace.html +++ b/docs/site/workspace.html @@ -10,7 +10,7 @@ - + diff --git a/records/node_status/eng.d0-packet-allowlist-fix.json b/records/node_status/eng.d0-packet-allowlist-fix.json new file mode 100644 index 000000000..0a7f209b5 --- /dev/null +++ b/records/node_status/eng.d0-packet-allowlist-fix.json @@ -0,0 +1,24 @@ +{ + "schema_version": 0, + "node_id": "eng.d0-packet-allowlist-fix", + "owner": null, + "next_action": "The pinned graph note says a portable fix was verified locally and a writer with push access was still awaited. This view records no owner.", + "missing_input": [ + "No PICKUP comment URL is recorded for an owner of this node." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + }, + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/patches/d0_packet_allowlist.patch", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE" +} diff --git a/records/node_status/eng.hard-gate.json b/records/node_status/eng.hard-gate.json new file mode 100644 index 000000000..6a502b1d8 --- /dev/null +++ b/records/node_status/eng.hard-gate.json @@ -0,0 +1,24 @@ +{ + "schema_version": 0, + "node_id": "eng.hard-gate", + "owner": null, + "next_action": "Use the pinned downstream-gate package as the recorded engineering control. This view sets no mathematical status.", + "missing_input": [ + "The graph source is the directory frontiers/downstream_gate_20260925/, so this view does not pin one document byte as the control text." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + }, + { + "url": "https://github.com/d6g8k5htny-coder/Math-/tree/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE" +} diff --git a/records/node_status/math.side24-coefficient.json b/records/node_status/math.side24-coefficient.json new file mode 100644 index 000000000..ecf9c4272 --- /dev/null +++ b/records/node_status/math.side24-coefficient.json @@ -0,0 +1,24 @@ +{ + "schema_version": 0, + "node_id": "math.side24-coefficient", + "owner": null, + "next_action": "Read the pinned coefficient proof and its recorded review as a review record only. This view records no owner and sets no mathematical status.", + "missing_input": [ + "No owner evidence URL is recorded for this node." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + }, + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/coefficients/side24_v1/PROOF.md", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE" +} diff --git a/tests/test_node_status_atlas.mjs b/tests/test_node_status_atlas.mjs new file mode 100644 index 000000000..25af64a84 --- /dev/null +++ b/tests/test_node_status_atlas.mjs @@ -0,0 +1,126 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {execFileSync} from 'node:child_process'; +import {mkdtemp, readFile, readdir, rm, writeFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import * as m from '../docs/site/current-model.mjs'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +const recordsDir = fileURLToPath(new URL('../records/node_status/', import.meta.url)); +const FORBIDDEN = ['lemma_closed', 'prizes_solved', 'discharges_OBL_H5_JETMOD', 'certified_C_H', 'freeze', 'inventable_attempt_accepted']; +const commit = 'a'.repeat(40), main = 'c'.repeat(40), digest = 'b'.repeat(64); +const snapshot = nodes => ({schema_version: 1, observed_at: '2026-10-13T12:00:00Z', scientific_effect: 'NONE', repositories: [{repository: 'd6g8k5htny-coder/main', commit: main, tree: main}, {repository: 'd6g8k5htny-coder/Math-', commit, tree: commit}], sources: [], graph: {identity: {repository: 'd6g8k5htny-coder/Math-', commit, path: 'frontiers/x/GRAPH.json', blob: commit, bytes: 20, sha256: digest}, data: {nodes, edges: []}}, equations: [], references: []}); + +function python(args, options = {}) { + try { + const stdout = execFileSync('python3', ['-B', '-S', ...args], {encoding: 'utf8', cwd: root, stdio: ['pipe', 'pipe', 'pipe'], ...options}); + return {status: 0, stdout, stderr: ''}; + } catch (error) { + return {status: error.status ?? 1, stdout: String(error.stdout ?? ''), stderr: String(error.stderr ?? '')}; + } +} + +async function records() { + const names = (await readdir(recordsDir)).filter(name => name.endsWith('.json')).sort(); + const entries = await Promise.all(names.map(async name => [name.slice(0, -5), JSON.parse(await readFile(join(recordsDir, name), 'utf8'))])); + return Object.fromEntries(entries); +} + +function flags(value, found = []) { + if (value && typeof value === 'object') for (const [key, item] of Object.entries(value)) { + if (FORBIDDEN.includes(key)) found.push(key); + flags(item, found); + } + return found; +} + +test('published node-status records are real graph nodes with no owner and no mathematical flags', async () => { + const view = await records(); + const ids = Object.keys(view); + assert.ok(ids.length >= 2 && ids.length <= 3, ids.join(',')); + const schema = JSON.parse(await readFile(new URL('../architecture/node-status.schema.json', import.meta.url), 'utf8')); + assert.deepEqual(schema.required, ['schema_version', 'node_id', 'owner', 'next_action', 'missing_input', 'stops', 'sources', 'source_rev', 'observed_at', 'status', 'scientific_effect']); + assert.equal(schema.additionalProperties, false); + assert.deepEqual(schema.properties.status.enum, ['fresh', 'stale', 'unknown']); + assert.equal(schema.properties.scientific_effect.const, 'NONE'); + const graph = JSON.parse(await readFile(new URL('../docs/site/current.json', import.meta.url), 'utf8')).graph.data.nodes; + const bundle = JSON.parse(await readFile(new URL('../docs/site/node-status.json', import.meta.url), 'utf8')); + assert.deepEqual(bundle, view); + assert.deepEqual(Object.keys(bundle), [...ids].sort()); + for (const [id, record] of Object.entries(view)) { + assert.equal(record.node_id, id); + assert.equal(record.schema_version, 0); + assert.equal(record.owner, null); + assert.equal(record.scientific_effect, 'NONE'); + assert.ok(['fresh', 'stale', 'unknown'].includes(record.status)); + assert.equal(flags(record).length, 0); + assert.ok(Object.hasOwn(graph, id), id); + } + const checked = python(['tools/validate_node_status.py', 'records/node_status']); + assert.equal(checked.status, 0, checked.stderr); + assert.match(checked.stdout, /node-status records valid/); + assert.doesNotMatch(checked.stdout + checked.stderr, /skip/i); +}); + +test('a record with an owner but no evidence_url fails validation', async () => { + const base = (await records())['eng.hard-gate']; + const directory = await mkdtemp(join(tmpdir(), 'node-status-')); + const bad = {...base, owner: {id: 'agent 9'}}; + await writeFile(join(directory, 'eng.hard-gate.json'), JSON.stringify(bad)); + const failed = python(['tools/validate_node_status.py', directory]); + assert.notEqual(failed.status, 0); + assert.match(failed.stderr, /evidence_url/); + const shaped = {...base, owner: {id: 'shape-only', evidence_url: 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-6346301123'}}; + const okDir = await mkdtemp(join(tmpdir(), 'node-status-ok-')); + await writeFile(join(okDir, 'eng.hard-gate.json'), JSON.stringify(shaped)); + const passed = python(['tools/validate_node_status.py', okDir]); + assert.equal(passed.status, 0, passed.stderr); + const flagged = {...base, lemma_closed: true}; + const flagDir = await mkdtemp(join(tmpdir(), 'node-status-flag-')); + await writeFile(join(flagDir, 'eng.hard-gate.json'), JSON.stringify(flagged)); + const refused = python(['tools/validate_node_status.py', flagDir]); + assert.notEqual(refused.status, 0); + assert.match(refused.stderr, /lemma_closed/); + await rm(directory, {recursive: true}); + await rm(okDir, {recursive: true}); + await rm(flagDir, {recursive: true}); +}); + +test('resume projection shows a node-status view and refuses an owner without evidence', async () => { + const view = await records(); + const nodes = Object.fromEntries(Object.keys(view).map(id => [id, {kind: 'candidate', classification: 'OPEN_ACTIVE', next_action: 'merge now', owner: 'not an owner'}])); + nodes.other = {kind: 'candidate', classification: 'OPEN_ACTIVE'}; + const data = snapshot(nodes); + for (const id of Object.keys(view)) { + const shown = m.resumeProjection(data, id, undefined, view); + assert.equal(shown.operational.find(row => row.field === 'next_action').text, view[id].next_action); + assert.match(shown.operational.find(row => row.field === 'next_action').note, /Freshness is not scientific validity/); + assert.equal(shown.operational.find(row => row.field === 'owner_evidence').text, 'No owner recorded in this node-status view.'); + assert.equal(shown.operational.find(row => row.field === 'owner_evidence').url, undefined); + assert.equal(shown.scientific_effect, undefined); + assert.doesNotMatch(JSON.stringify(shown.operational), /merge now|not an owner/); + assert.deepEqual(flags(shown), []); + } + const bare = m.resumeProjection(data, 'other', undefined, view); + assert.ok(bare.operational.every(row => row.text === m.RESUME_NOT_RECORDED)); + const omitted = m.resumeProjection(data, Object.keys(view)[0]); + assert.ok(omitted.operational.every(row => row.text === m.RESUME_NOT_RECORDED)); + const ownerless = {[Object.keys(view)[0]]: {...view[Object.keys(view)[0]], owner: {id: 'agent 9'}}}; + const hidden = m.resumeProjection(data, Object.keys(view)[0], undefined, ownerless); + assert.ok(hidden.operational.every(row => row.text === m.RESUME_NOT_RECORDED)); + assert.doesNotMatch(JSON.stringify(hidden), /agent 9/); + const tools = fileURLToPath(new URL('../tools/', import.meta.url)); + const script = 'import json,sys\nsys.path.insert(0,sys.argv[1])\nfrom current_site_history import resume_projection\npayload=json.load(sys.stdin)\nprint(json.dumps([resume_projection(payload["data"], i, payload["records"]).get("operational") for i in payload["ids"]]))'; + const ids = [...Object.keys(view), 'other']; + const py = JSON.parse(python(['-c', script, tools], {input: JSON.stringify({data, ids, records: view})}).stdout); + assert.deepEqual(py, ids.map(id => m.resumeProjection(data, id, undefined, view).operational)); +}); + +test('the atlas resume section passes the published node-status view into resumeProjection', async () => { + const source = await readFile(new URL('../docs/site/atlas.mjs', import.meta.url), 'utf8'); + assert.match(source, /fetch\('node-status\.json'/); + assert.match(source, /resumeProjection\(data,selected,Date\.now\(\),nodeStatus\)/); + assert.match(source, /function resumeSection\(view\)/); +}); diff --git a/tools/current_site_history.py b/tools/current_site_history.py index b641e44ca..b6aae04f7 100644 --- a/tools/current_site_history.py +++ b/tools/current_site_history.py @@ -1554,14 +1554,81 @@ def _resume_text(value): return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, separators=(",", ":")) -def resume_projection(data, node_id): - """The static form of current-model.mjs resumeProjection; a static page evaluates no freshness.""" +_NODE_STATUS_FORBIDDEN = frozenset({ + "lemma_closed", "prizes_solved", "discharges_OBL_H5_JETMOD", "certified_C_H", + "freeze", "inventable_attempt_accepted"}) +_NODE_STATUS_STATE = re.compile(r"(?:fresh|stale|unknown)\Z") +_NODE_STATUS_OBSERVED = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z") + + +def _node_status_forbidden(value): + if isinstance(value, dict): + return any(key in _NODE_STATUS_FORBIDDEN or _node_status_forbidden(item) for key, item in value.items()) + if isinstance(value, list): + return any(_node_status_forbidden(item) for item in value) + return False + + +def _node_status_record(status_records, node_id): + """Optional current view. An owner without evidence_url drops the whole record.""" + if not isinstance(status_records, dict) or node_id not in status_records: + return None + record = status_records[node_id] + if (not isinstance(record, dict) or record.get("scientific_effect") != "NONE" + or _node_status_forbidden(record) or "owner" not in record): + return None + owner = record["owner"] + if owner is not None and (not isinstance(owner, dict) or not isinstance(owner.get("id"), str) or not owner.get("id") + or not isinstance(owner.get("evidence_url"), str) or not owner.get("evidence_url")): + return None + return record + + +def _node_status_list(value): + if isinstance(value, list) and value and all(isinstance(item, str) and item for item in value): + return "; ".join(value) + return None + + +def _node_status_operational(record): + owner = record["owner"] + rows = [] + for field, label in RESUME_OPERATIONAL: + row = {"field": field, "label": label, "text": RESUME_NOT_RECORDED} + if field == "next_action" and isinstance(record.get("next_action"), str) and record["next_action"]: + row["text"] = record["next_action"] + if (_NODE_STATUS_STATE.fullmatch(str(record.get("status"))) + and isinstance(record.get("observed_at"), str) + and _NODE_STATUS_OBSERVED.fullmatch(record["observed_at"])): + row["note"] = (f"Node-status view {record['status']} as of {record['observed_at']}. " + "Scientific effect NONE. Freshness is not scientific validity.") + elif field == "missing_input": + text = _node_status_list(record.get("missing_input")) + if text: + row["text"] = text + elif field == "owner_evidence": + row["text"] = "No owner recorded in this node-status view." if owner is None else owner["id"] + if owner and owner.get("evidence_url"): + row["url"] = owner["evidence_url"] + elif field == "retained_stop": + row["text"] = _node_status_list(record.get("stops")) or "No retained stop recorded in this node-status view." + rows.append(row) + return rows + + +def resume_projection(data, node_id, status_records=None): + """The static form of current-model.mjs resumeProjection; a static page evaluates no freshness. + + status_records is the optional node-status map. The snapshot-only call matches the + JavaScript default and is what static pages render. + """ nodes = data["graph"]["data"]["nodes"] if not isinstance(node_id, str) or node_id not in nodes: return None node, identity = nodes[node_id], data["graph"]["identity"] task = RESUME_TASK_RECORDS.get(node_id) main = next((repo["commit"] for repo in data["repositories"] if repo["repository"] == "d6g8k5htny-coder/main"), None) + record = _node_status_record(status_records, node_id) return { "node": node_id, "snapshot": {"observed_at": data["observed_at"], "graph_repository": identity["repository"], @@ -1575,7 +1642,8 @@ def resume_projection(data, node_id): "url": value if isinstance(value, str) and _RESUME_REVIEW_URL.fullmatch(value) else None, "note": RESUME_ISSUE_NOTE if field == "review_issue" and isinstance(value, (int, float)) and not isinstance(value, bool) else None} for field, value in node.items() if field.startswith("review_")]}, - "operational": [{"field": field, "label": label, "text": RESUME_NOT_RECORDED} for field, label in RESUME_OPERATIONAL], + "operational": _node_status_operational(record) if record else [ + {"field": field, "label": label, "text": RESUME_NOT_RECORDED} for field, label in RESUME_OPERATIONAL], "continuation": dict(task) if task else None, "continuation_text": None if task else RESUME_NO_TASK, } diff --git a/tools/validate_node_status.py b/tools/validate_node_status.py new file mode 100644 index 000000000..dc9ba5395 --- /dev/null +++ b/tools/validate_node_status.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +"""Validate records/node_status against architecture/node-status.schema.json. + +Status is freshness of the current view only. scientific_effect must stay NONE. +An owner is refused unless evidence_url is present. No network and no writes. +""" +from __future__ import annotations + +import argparse +import json +from datetime import datetime +from pathlib import Path, PurePosixPath +import re +import sys + +ROOT = Path(__file__).resolve().parents[1] +REQUIRED = ( + "schema_version", "node_id", "owner", "next_action", "missing_input", "stops", + "sources", "source_rev", "observed_at", "status", "scientific_effect", +) +FORBIDDEN = frozenset({ + "lemma_closed", "prizes_solved", "discharges_OBL_H5_JETMOD", "certified_C_H", + "freeze", "inventable_attempt_accepted", +}) +NODE_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*\Z") +SHA = re.compile(r"[0-9a-f]{40}\Z") +OBSERVED = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z") +COMMENT = re.compile( + r"https://github\.com/d6g8k5htny-coder/(?:main|Math-)/(?:issues|pull)/" + r"[1-9][0-9]*#issuecomment-[1-9][0-9]*\Z") +SOURCE_URL = re.compile( + r"https://github\.com/d6g8k5htny-coder/(?:main|Math-)/(?:blob|tree)/" + r"[0-9a-f]{40}/[A-Za-z0-9_./-]+\Z") +STATUSES = frozenset({"fresh", "stale", "unknown"}) +MAX_FILES = 256 +MAX_BYTES = 65536 + + +def unique_pairs(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +def forbidden_keys(value, found): + if isinstance(value, dict): + for key, item in value.items(): + if key in FORBIDDEN: + found.append(key) + forbidden_keys(item, found) + elif isinstance(value, list): + for item in value: + forbidden_keys(item, found) + return found + + +def load_json(path, max_bytes=MAX_BYTES): + if path.is_symlink(): + raise ValueError("symlink refused") + if path.stat().st_size > max_bytes: + raise ValueError("record too large") + return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_pairs) + + +def check_schema_file(root): + path = root / "architecture" / "node-status.schema.json" + problems = [] + try: + schema = load_json(path) + except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as error: + return [f"schema: {error}"] + if schema.get("required") != list(REQUIRED): + problems.append("schema: required fields differ from interface v0") + if schema.get("additionalProperties") is not False: + problems.append("schema: additional properties must be refused") + status = schema.get("properties", {}).get("status", {}).get("enum") + if status != ["fresh", "stale", "unknown"]: + problems.append("schema: status must be fresh, stale, or unknown") + effect = schema.get("properties", {}).get("scientific_effect", {}).get("const") + if effect != "NONE": + problems.append("schema: scientific_effect must be NONE") + owner = schema.get("properties", {}).get("owner", {}) + if not any(item.get("type") == "null" for item in owner.get("oneOf", [])): + problems.append("schema: owner must allow null") + return problems + + +def graph_ids(root): + path = root / "docs" / "site" / "current.json" + data = load_json(path, max_bytes=2_000_000) + nodes = data["graph"]["data"]["nodes"] + if not isinstance(nodes, dict): + raise ValueError("current graph nodes are not an object") + return set(nodes) + + +def check_owner(record, problems): + owner = record["owner"] + if owner is None: + return + if not isinstance(owner, dict): + problems.append("owner requires evidence_url") + return + if "evidence_url" not in owner or not isinstance(owner.get("evidence_url"), str) or not owner.get("evidence_url"): + problems.append("owner requires evidence_url") + return + if set(owner) != {"id", "evidence_url"}: + problems.append("owner allows only id and evidence_url") + if not isinstance(owner.get("id"), str) or not owner["id"]: + problems.append("owner id must be a non-empty string") + if not COMMENT.fullmatch(owner["evidence_url"]): + problems.append("owner evidence_url must be a GitHub comment URL") + + +def check_record(record, filename, known_ids): + problems = [] + if not isinstance(record, dict): + return ["record must be an object"] + flags = forbidden_keys(record, []) + if flags: + problems.append("forbidden mathematical status flag: " + ", ".join(sorted(set(flags)))) + if set(record) != set(REQUIRED): + extra = sorted(set(record) - set(REQUIRED)) + missing = sorted(set(REQUIRED) - set(record)) + if extra: + problems.append("unexpected fields: " + ", ".join(extra)) + if missing: + problems.append("missing fields: " + ", ".join(missing)) + return problems + if type(record["schema_version"]) is not int or record["schema_version"] != 0: + problems.append("schema_version must be 0") + node_id = record["node_id"] + if not isinstance(node_id, str) or not NODE_ID.fullmatch(node_id): + problems.append("invalid node_id") + elif filename != node_id + ".json": + problems.append("filename must match node_id") + elif node_id not in known_ids: + problems.append("node_id is not in the current graph") + check_owner(record, problems) + if not isinstance(record["next_action"], str) or not record["next_action"]: + problems.append("next_action must be a non-empty string") + for field in ("missing_input", "stops"): + value = record[field] + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + problems.append(f"{field} must be an array of non-empty strings") + sources = record["sources"] + if not isinstance(sources, list) or not sources: + problems.append("sources must be a non-empty array") + else: + for index, source in enumerate(sources): + if not isinstance(source, dict) or set(source) != {"url", "rev"}: + problems.append(f"sources[{index}] must have url and rev") + continue + if not isinstance(source["url"], str) or not SOURCE_URL.fullmatch(source["url"]): + problems.append(f"sources[{index}].url must be a pinned GitHub blob or tree URL") + if not isinstance(source["rev"], str) or not SHA.fullmatch(source["rev"]): + problems.append(f"sources[{index}].rev must be a 40-character commit") + elif source["rev"] not in source["url"]: + problems.append(f"sources[{index}].rev must appear in its url") + if not isinstance(record["source_rev"], str) or not SHA.fullmatch(record["source_rev"]): + problems.append("source_rev must be a 40-character commit") + observed = record["observed_at"] + if not isinstance(observed, str) or not OBSERVED.fullmatch(observed): + problems.append("observed_at must be UTC Z") + else: + try: + datetime.strptime(observed, "%Y-%m-%dT%H:%M:%SZ") + except ValueError: + problems.append("observed_at is not a real timestamp") + if record["status"] not in STATUSES: + problems.append("status must be fresh, stale, or unknown") + if record["scientific_effect"] != "NONE": + problems.append("scientific_effect must be NONE") + return problems + + +def validate_directory(root, directory): + root = Path(root).resolve() + directory = Path(directory) + if not directory.is_absolute(): + directory = (Path.cwd() / directory).resolve() + problems = [f"schema: {item}" if not item.startswith("schema:") else item for item in check_schema_file(root)] + if not directory.is_dir() or directory.is_symlink(): + return problems + ["records directory missing"] + try: + known = graph_ids(root) + except (OSError, UnicodeError, ValueError, json.JSONDecodeError, KeyError) as error: + return problems + [f"current graph: {error}"] + files = sorted(directory.iterdir(), key=lambda item: item.name) + selected = [item for item in files if item.name != ".gitkeep"] + if not selected: + problems.append("no node-status records") + if len(selected) > MAX_FILES: + problems.append("too many node-status records") + seen = set() + for path in selected: + label = path.name + if not path.is_file() or path.is_symlink() or path.suffix != ".json" or PurePosixPath(path.name).name != path.name: + problems.append(f"{label}: expected a JSON record file") + continue + try: + record = load_json(path) + except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as error: + problems.append(f"{label}: {error}") + continue + node_id = record.get("node_id") if isinstance(record, dict) else None + if isinstance(node_id, str): + if node_id in seen: + problems.append(f"{label}: duplicate node_id") + seen.add(node_id) + for problem in check_record(record, path.name, known): + problems.append(f"{label}: {problem}") + return problems + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", type=Path) + parser.add_argument("--root", type=Path, default=ROOT) + args = parser.parse_args(argv) + problems = validate_directory(args.root, args.directory) + if problems: + for problem in problems: + print(problem, file=sys.stderr) + return 1 + print(f"node-status records valid: {args.directory}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 49f089e80442f54f2bf4a2b8aef183d7c88ae4d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 17:52:02 +0000 Subject: [PATCH 2/4] Address the node-status AMEND on interface v0 Keep the schema closed except the Lane 4 handoff fields, refresh_error, and owner.released_url. source_rev must equal sources[0].rev. Share one evidence URL pattern, show a released owner as released, and mark a view stale when observed_at is more than 24 hours before the render clock. Co-authored-by: Dylan Roy --- .github/workflows/node-status.yml | 42 ++++ architecture/node-status.schema.json | 92 +++++++- docs/site/README.md | 2 +- docs/site/atlas.mjs | 6 +- docs/site/cite.html | 2 +- docs/site/current-model.mjs | 45 +++- docs/site/current.mjs | 2 +- docs/site/dependencies.html | 4 +- docs/site/explore.html | 2 +- docs/site/index.html | 2 +- docs/site/measure.html | 2 +- docs/site/museum.html | 2 +- docs/site/reproduce.html | 2 +- docs/site/workspace.html | 2 +- .../node_status/math-463-released.json | 39 ++++ tests/test_node_status_atlas.mjs | 213 ++++++++++++++++-- tools/current_site_history.py | 75 ++++-- tools/validate_node_status.py | 206 ++++++++++++++--- 18 files changed, 637 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/node-status.yml create mode 100644 tests/fixtures/node_status/math-463-released.json diff --git a/.github/workflows/node-status.yml b/.github/workflows/node-status.yml new file mode 100644 index 000000000..0fa4d7c55 --- /dev/null +++ b/.github/workflows/node-status.yml @@ -0,0 +1,42 @@ +# Node-status acceptance. This job is not a required check and is not part of +# workspace-landing. It does not edit any existing workflow step. +name: Node status + +on: + pull_request: + paths: + - 'architecture/node-status.schema.json' + - 'records/node_status/**' + - 'docs/site/**' + - 'tools/validate_node_status.py' + - 'tools/current_site_history.py' + - 'tests/test_node_status_atlas.mjs' + - 'tests/fixtures/node_status/**' + - '.github/workflows/node-status.yml' + push: + branches: [main] + paths: + - 'architecture/node-status.schema.json' + - 'records/node_status/**' + - 'docs/site/**' + - 'tools/validate_node_status.py' + - 'tools/current_site_history.py' + - 'tests/test_node_status_atlas.mjs' + - 'tests/fixtures/node_status/**' + - '.github/workflows/node-status.yml' + +permissions: + contents: read + +jobs: + node-status: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - name: Node-status atlas test and validator + run: | + node --test tests/test_node_status_atlas.mjs + python3 -B -S tools/validate_node_status.py records/node_status diff --git a/architecture/node-status.schema.json b/architecture/node-status.schema.json index 7e8ef3dda..8cb9de6cf 100644 --- a/architecture/node-status.schema.json +++ b/architecture/node-status.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "architecture/node-status.schema.json", "title": "Node status record v0", - "description": "Current view of one graph node. status is freshness of this view only, never scientific validity. scientific_effect stays NONE. owner is null unless evidence_url is a PICKUP or assignment comment URL. Receipts are referenced by sources[].url and sources[].rev and are not copied.", + "description": "Current view of one graph node. status is freshness of this view only, never scientific validity. scientific_effect stays NONE. owner is null or an object; released_url, when present, stays on that object and does not clear it. source_rev equals sources[0].rev, one value, never comma-joined. Receipts are referenced by sources[].url and sources[].rev and are not copied. predecessor, successor, carried_forward, and refresh_error are optional.", "type": "object", "additionalProperties": false, "required": [ @@ -36,7 +36,11 @@ "id": {"type": "string", "minLength": 1}, "evidence_url": { "type": "string", - "pattern": "^https://github\\.com/d6g8k5htny-coder/(?:main|Math-)/(?:issues|pull)/[1-9][0-9]*#issuecomment-[1-9][0-9]*$" + "pattern": "^https://github\\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\\d*#(issuecomment-[1-9]\\d*|pullrequestreview-[1-9]\\d*)$" + }, + "released_url": { + "type": "string", + "pattern": "^https://github\\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\\d*#(issuecomment-[1-9]\\d*|pullrequestreview-[1-9]\\d*)$" } } } @@ -55,21 +59,87 @@ "type": "array", "minItems": 1, "items": { - "type": "object", - "additionalProperties": false, - "required": ["url", "rev"], - "properties": { - "url": {"type": "string", "minLength": 1}, - "rev": {"type": "string", "pattern": "^[0-9a-f]{40}$"} - } + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["url", "rev"], + "properties": { + "url": { + "type": "string", + "pattern": "^https://github\\.com/d6g8k5htny-coder/(main|Math-)/(blob|tree)/[0-9a-f]{40}/[A-Za-z0-9_./-]+$" + }, + "rev": {"type": "string", "pattern": "^[0-9a-f]{40}$"} + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["url", "rev"], + "properties": { + "url": { + "type": "string", + "pattern": "^https://github\\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\\d*(#(issuecomment-[1-9]\\d*|pullrequestreview-[1-9]\\d*))?$" + }, + "rev": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$"} + } + } + ] } }, - "source_rev": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "source_rev": { + "type": "string", + "pattern": "^(?:[0-9a-f]{40}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)$" + }, "observed_at": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" }, "status": {"enum": ["fresh", "stale", "unknown"]}, - "scientific_effect": {"const": "NONE"} + "scientific_effect": {"const": "NONE"}, + "predecessor": { + "oneOf": [ + {"type": "null"}, + { + "type": "object", + "additionalProperties": false, + "required": ["node_id", "url"], + "properties": { + "node_id": {"type": "string", "minLength": 1}, + "url": {"type": "string", "pattern": "^https?://\\S+$"} + } + } + ] + }, + "successor": { + "oneOf": [ + {"type": "null"}, + { + "type": "object", + "additionalProperties": false, + "required": ["node_id", "url"], + "properties": { + "node_id": {"type": "string", "minLength": 1}, + "url": {"type": "string", "pattern": "^https?://\\S+$"} + } + } + ] + }, + "carried_forward": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "text", "source_url", "status"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "text": {"type": "string", "minLength": 1}, + "source_url": {"type": "string", "minLength": 1}, + "status": {"enum": ["open", "resolved"]}, + "resolution_url": {"type": "string", "minLength": 1} + } + } + }, + "refresh_error": {"type": ["string", "null"]} } } diff --git a/docs/site/README.md b/docs/site/README.md index 033e8b1ba..a6742fa68 100644 --- a/docs/site/README.md +++ b/docs/site/README.md @@ -21,7 +21,7 @@ The front and technical layers use the same existing evidence. No second scienti `tools/current_site_data.py` resolves public main and Math- commits once, verifies their complete Git trees and the seven acquired source files, and extracts exact indented proof displays. Other inventory rows bind paths and Git blobs; their document bytes are not downloaded. The page calls equation blocks excerpts and keeps their surrounding proof scope available by pinned line links. `tools/current_site_build.py` renders one payload offline into marked current sections and static node pages; Research and Formal stay script-free. Current observations and original dated reading cuts have separate identities. -"Resume this node" appears in the atlas inspector and on each current static node page. Both come from `resumeProjection` in `current-model.mjs`; `tools/current_site_history.py` keeps a Python copy, and `tests/test_current_site_resume.mjs` checks that the two agree on every node. The block names the snapshot's graph commit, path, SHA-256 and observation time, and warns readers to refresh from the live record before any ownership- or head-dependent action. Recorded `review_*` fields are labelled as review records, not as the current task, an owner or completion. Next action, missing input, owner evidence and retained stop read "not recorded in this snapshot" on the snapshot-only projection, because the graph records none of them. The atlas inspector also passes `node-status.json`, the published copy of `records/node_status`, into that same projection. Static node pages stay on the snapshot-only call. An owner is shown only when the record has an evidence URL. The record's status is freshness of that view, not scientific validity, and `scientific_effect` stays NONE. A bare `review_issue` number names no repository, so it is not linked. Continuation links come only from the small site-maintained `RESUME_TASK_RECORDS` table of explicitly identified operational pointers; they are not graph metadata, scientific acceptance or source-binding admission. Archived snapshot pages are unchanged. +"Resume this node" appears in the atlas inspector and on each current static node page. Both come from `resumeProjection` in `current-model.mjs`; `tools/current_site_history.py` keeps a Python copy, and `tests/test_current_site_resume.mjs` checks that the two agree on every node. The block names the snapshot's graph commit, path, SHA-256 and observation time, and warns readers to refresh from the live record before any ownership- or head-dependent action. Recorded `review_*` fields are labelled as review records, not as the current task, an owner or completion. Next action, missing input, owner evidence and retained stop read "not recorded in this snapshot" on the snapshot-only projection, because the graph records none of them. The atlas inspector also passes `node-status.json`, the published copy of `records/node_status`, into that same projection. Static node pages stay on the snapshot-only call. An owner object is shown only when `evidence_url` matches the shared comment pattern; `released_url`, when present, stays on that object and the atlas says released. The panel shows the recorded status and `observed_at`, and says stale when `observed_at` is more than 24 hours before the render clock. Status is freshness of that view, not scientific validity, and `scientific_effect` stays NONE. A bare `review_issue` number names no repository, so it is not linked. Continuation links come only from the small site-maintained `RESUME_TASK_RECORDS` table of explicitly identified operational pointers; they are not graph metadata, scientific acceptance or source-binding admission. Archived snapshot pages are unchanged. The configured `.github/workflows/current-site.yml` builds on main pushes and a 30-minute schedule and deploys the complete tested docs artifact through Pages Actions. Each candidate needs its own hosted success and deployed-source acceptance. A failed source refresh cannot reach deployment. Source observations are not mathematical revalidation. Existing diagrams on Explore keep their defined teaching scope; the new source panel links the advancing work. diff --git a/docs/site/atlas.mjs b/docs/site/atlas.mjs index 60a260ccc..b65d28566 100644 --- a/docs/site/atlas.mjs +++ b/docs/site/atlas.mjs @@ -1,11 +1,11 @@ -import {currentSnapshot,element,anchor} from './current.mjs?site-release=433be464efd229238533aa4e1ec4c6f9831229cd1cd9ba4f4a941f54cc169e49'; -import {hasNode,readState,neighborhood,searchGraph,downstreamReach,requiredDependencyPaths,REQUIRED_ROUTE_SEMANTICS,evidenceAxes,sourceURL,sourceForNode,nodePage,resumeProjection,RESUME_NOT_RECORDED} from './current-model.mjs?site-release=307d77d57733b20953326e167591665192a0a7e004a026b1c9bb4d523e2912b8'; +import {currentSnapshot,element,anchor} from './current.mjs?site-release=c678bd088183b6548c4b094e8f7db287c9ab07f1ad2064737dc6eee9b82332d8'; +import {hasNode,readState,neighborhood,searchGraph,downstreamReach,requiredDependencyPaths,REQUIRED_ROUTE_SEMANTICS,evidenceAxes,sourceURL,sourceForNode,nodePage,resumeProjection,RESUME_NOT_RECORDED,resumeFieldHref} from './current-model.mjs?site-release=e2a859394fa5607936ba766a76875392cf43b7c424f0c52542b68a539dcdc1dd'; import {createAtlasShell,viewForViewport} from './atlas-shell.mjs?site-release=9fce5f707e3486ef152ffdaa1cf1e4adf7cd9cf9dac116407a69b6528b4c656c'; const byId=id=>document.getElementById(id); const svgElement=(tag,attrs)=>{const n=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,String(v));return n;}; // Mirrors the static node page block rendered from the same projection by tools/current_site_history.py. function resumeSection(view){const section=element('section',undefined,'resume-node');section.id='atlas-resume';section.setAttribute('aria-labelledby','atlas-resume-heading');const heading=element('h4','Resume this node');heading.id='atlas-resume-heading'; - const fields=items=>{const dl=element('dl',undefined,'resume-fields');for(const item of items){const dd=element('dd');if(item.url)dd.append(anchor(item.text,item.url));else dd.append(element('code',item.text));if(item.note)dd.append(` ${item.note}`);dl.append(element('dt',item.label||item.field),dd);}return dl;}; + const fields=items=>{const dl=element('dl',undefined,'resume-fields');for(const item of items){const dd=element('dd');const href=resumeFieldHref(item);if(href)dd.append(anchor(item.text,href));else dd.append(element('code',item.text));if(item.note)dd.append(` ${item.note}`);dl.append(element('dt',item.label||item.field),dd);}return dl;}; const s=view.snapshot,warning=element('p',undefined,'resume-warning');warning.dataset.freshness=view.freshness;warning.append(element('strong',`As of ${s.observed_at} · ${view.freshness}.`),` ${view.warning}${view.freshness==='current'?'':' Do not rely on this snapshot for current ownership or heads.'}`); section.append(heading,warning,element('p',`Snapshot identity: graph ${s.graph_repository} commit ${s.graph_commit} · ${s.graph_path} · SHA-256 ${s.graph_sha256} · main commit ${s.main_commit||RESUME_NOT_RECORDED}.`),element('h5','Recorded graph state'),fields(view.state),element('h5','Recorded review record'),element('p',view.review.label),view.review.fields.length?fields(view.review.fields):element('p',`Review metadata: ${RESUME_NOT_RECORDED}.`),element('h5','Operational task state'),fields(view.operational),element('h5','Continuation')); const task=view.continuation,p=element('p');if(task)p.append(anchor(task.label,task.url),`. ${task.meaning} Site-maintained pointer, not part of the graph snapshot; identified on ${task.identified_on} in `,anchor("the coordinator's activation record",task.identified_in),'. The linked comment is mutable; read it live before acting.');else p.textContent=view.continuation_text;section.append(p);return section;} diff --git a/docs/site/cite.html b/docs/site/cite.html index 400a75c02..e86bd44a6 100644 --- a/docs/site/cite.html +++ b/docs/site/cite.html @@ -7,7 +7,7 @@ - +
UNIVERSAL LAW MATHEMATICS IN THE OPEN
diff --git a/docs/site/current-model.mjs b/docs/site/current-model.mjs index 046f2ee32..02264b21d 100644 --- a/docs/site/current-model.mjs +++ b/docs/site/current-model.mjs @@ -82,11 +82,25 @@ const metadataText=value=>typeof value==='string'?value:JSON.stringify(value); const NODE_STATUS_FORBIDDEN=new Set(['lemma_closed','prizes_solved','discharges_OBL_H5_JETMOD','certified_C_H','freeze','inventable_attempt_accepted']); const NODE_STATUS_STATE=/^(?:fresh|stale|unknown)$/; const NODE_STATUS_OBSERVED=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; +// Same characters as EVIDENCE_URL_PATTERN in tools/validate_node_status.py. +export const NODE_STATUS_EVIDENCE_URL_PATTERN = String.raw`^https://github\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\d*#(issuecomment-[1-9]\d*|pullrequestreview-[1-9]\d*)$`; +export const NODE_STATUS_EVIDENCE_URL = new RegExp(NODE_STATUS_EVIDENCE_URL_PATTERN); +const NODE_STATUS_STALE_MS = 24 * 60 * 60 * 1000; function nodeStatusForbidden(value){ if(!value||typeof value!=='object')return false; return Object.entries(value).some(([key,item])=>NODE_STATUS_FORBIDDEN.has(key)||nodeStatusForbidden(item)); } +export function nodeStatusEvidenceURL(url){ + return typeof url==='string' && NODE_STATUS_EVIDENCE_URL.test(url) ? url : null; +} +// Owner evidence is linked only when it matches the shared comment pattern. +export function resumeFieldHref(item){ + if(!item || typeof item.url!=='string' || !item.url)return null; + if(item.field==='owner_evidence' && !NODE_STATUS_EVIDENCE_URL.test(item.url))return null; + return item.url; +} // A record without an evidence URL is not applied. Freshness is not scientific validity. +// released_url stays on the owner object; this reader does not replace that object with null. function nodeStatusRecord(statusRecords,id){ if(!statusRecords||typeof statusRecords!=='object'||Array.isArray(statusRecords)||!Object.prototype.hasOwnProperty.call(statusRecords,id))return null; const record=statusRecords[id]; @@ -97,15 +111,38 @@ function nodeStatusRecord(statusRecords,id){ return record; } function nodeStatusList(value){return Array.isArray(value)&&value.every(item=>typeof item==='string'&&item)&&value.length?value.join('; '):null;} -function nodeStatusOperational(record){ +function nodeStatusNote(record, now){ + const recorded=record.status, observed=record.observed_at; + if(typeof recorded!=='string'||!NODE_STATUS_STATE.test(recorded)||typeof observed!=='string'||!NODE_STATUS_OBSERVED.test(observed))return null; + let shown=recorded; + if(Number.isFinite(now)){ + const age=now-Date.parse(observed); + if(Number.isFinite(age)&&age>NODE_STATUS_STALE_MS)shown='stale'; + } + return `Node-status view ${shown} as of ${observed}. Recorded status ${recorded}. Scientific effect NONE. Freshness is not scientific validity.`; +} +function nodeStatusOperational(record, now){ const owner=record.owner; return RESUME_OPERATIONAL.map(([field,label])=>{ const row={field,label,text:RESUME_NOT_RECORDED}; if(field==='next_action'&&typeof record.next_action==='string'&&record.next_action){ row.text=record.next_action; - if(typeof record.status==='string'&&NODE_STATUS_STATE.test(record.status)&&typeof record.observed_at==='string'&&NODE_STATUS_OBSERVED.test(record.observed_at))row.note=`Node-status view ${record.status} as of ${record.observed_at}. Scientific effect NONE. Freshness is not scientific validity.`; + const note=nodeStatusNote(record, now); + if(note)row.note=note; }else if(field==='missing_input'){const text=nodeStatusList(record.missing_input);if(text)row.text=text;} - else if(field==='owner_evidence'){row.text=owner===null?'No owner recorded in this node-status view.':owner.id;if(owner&&owner.evidence_url)row.url=owner.evidence_url;} + else if(field==='owner_evidence'){ + if(owner===null)row.text='No owner recorded in this node-status view.'; + else if(owner&&Object.prototype.hasOwnProperty.call(owner,'released_url')){ + row.text='released'; + const link=nodeStatusEvidenceURL(owner.released_url); + if(link)row.url=link; + else if(typeof owner.released_url==='string'&&owner.released_url)row.text=owner.released_url; + }else if(owner){ + const link=nodeStatusEvidenceURL(owner.evidence_url); + if(link){row.text=owner.id;row.url=link;} + else if(typeof owner.evidence_url==='string'&&owner.evidence_url)row.text=owner.evidence_url; + } + } else if(field==='retained_stop')row.text=nodeStatusList(record.stops)||'No retained stop recorded in this node-status view.'; return row; }); @@ -122,7 +159,7 @@ export function resumeProjection(snapshot,id,now,statusRecords) { freshness:Number.isFinite(now)?freshness(snapshot.observed_at,now):null,warning:RESUME_FRESHNESS_WARNING, state:['classification','controlling'].map(field=>({field,text:own(field)&&node[field]!==null?metadataText(node[field]):RESUME_NOT_RECORDED})), review:{label:RESUME_REVIEW_LABEL,fields:Object.keys(node).filter(key=>key.startsWith('review_')).map(field=>{const value=node[field];return {field,text:metadataText(value),url:typeof value==='string'&&REVIEW_URL.test(value)?value:null,note:field==='review_issue'&&typeof value==='number'?RESUME_ISSUE_NOTE:null};})}, - operational:record?nodeStatusOperational(record):RESUME_OPERATIONAL.map(([field,label])=>({field,label,text:RESUME_NOT_RECORDED})), + operational:record?nodeStatusOperational(record, now):RESUME_OPERATIONAL.map(([field,label])=>({field,label,text:RESUME_NOT_RECORDED})), continuation:task?{...task}:null,continuation_text:task?null:RESUME_NO_TASK}; } export const MAX_WINDOW_NODES=150; diff --git a/docs/site/current.mjs b/docs/site/current.mjs index 6525e571f..936eefb4c 100644 --- a/docs/site/current.mjs +++ b/docs/site/current.mjs @@ -1,4 +1,4 @@ -import {fetchVerifiedSnapshot,freshness,filterSources,sourceURL,projectCurrentAcquisitions,ACQUISITION_LABELS,ACQUISITION_UNAVAILABLE} from './current-model.mjs?site-release=307d77d57733b20953326e167591665192a0a7e004a026b1c9bb4d523e2912b8'; +import {fetchVerifiedSnapshot,freshness,filterSources,sourceURL,projectCurrentAcquisitions,ACQUISITION_LABELS,ACQUISITION_UNAVAILABLE} from './current-model.mjs?site-release=e2a859394fa5607936ba766a76875392cf43b7c424f0c52542b68a539dcdc1dd'; export function element(tag,text,className) { const e=document.createElement(tag);if(text!==undefined)e.textContent=text;if(className)e.className=className;return e; } diff --git a/docs/site/dependencies.html b/docs/site/dependencies.html index 9feb696fb..6b213d5c0 100644 --- a/docs/site/dependencies.html +++ b/docs/site/dependencies.html @@ -13,8 +13,8 @@ - - + + diff --git a/docs/site/explore.html b/docs/site/explore.html index 529ccf0a8..9957d5fbb 100644 --- a/docs/site/explore.html +++ b/docs/site/explore.html @@ -13,7 +13,7 @@ - + diff --git a/docs/site/index.html b/docs/site/index.html index ef1b1eb1c..655ee67a4 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -7,7 +7,7 @@ - +
UNIVERSAL LAW MATHEMATICS IN THE OPEN
diff --git a/docs/site/measure.html b/docs/site/measure.html index 4510dafa1..3febbaa3f 100644 --- a/docs/site/measure.html +++ b/docs/site/measure.html @@ -13,7 +13,7 @@ - + diff --git a/docs/site/museum.html b/docs/site/museum.html index e35c2b194..01dfcfca5 100644 --- a/docs/site/museum.html +++ b/docs/site/museum.html @@ -12,7 +12,7 @@ - + diff --git a/docs/site/reproduce.html b/docs/site/reproduce.html index 6e5b5b31a..ad09f8b86 100644 --- a/docs/site/reproduce.html +++ b/docs/site/reproduce.html @@ -8,7 +8,7 @@ - +
UNIVERSAL LAW MATHEMATICS IN THE OPEN
diff --git a/docs/site/workspace.html b/docs/site/workspace.html index 09751d559..b7a9a10f8 100644 --- a/docs/site/workspace.html +++ b/docs/site/workspace.html @@ -10,7 +10,7 @@ - + diff --git a/tests/fixtures/node_status/math-463-released.json b/tests/fixtures/node_status/math-463-released.json new file mode 100644 index 000000000..9c0c4ecf7 --- /dev/null +++ b/tests/fixtures/node_status/math-463-released.json @@ -0,0 +1,39 @@ +{ + "schema_version": 0, + "node_id": "eng.hard-gate", + "owner": { + "id": "agent-old", + "evidence_url": "https://github.com/d6g8k5htny-coder/Math-/pull/463#issuecomment-6198606495", + "released_url": "https://github.com/d6g8k5htny-coder/Math-/pull/463#pullrequestreview-5537612171" + }, + "next_action": "Read the historical pickup as released. This fixture sets no mathematical status and records no occupied role.", + "missing_input": [ + "The pickup comment was later released." + ], + "stops": [], + "sources": [ + { + "url": "https://github.com/d6g8k5htny-coder/Math-/blob/dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde/frontiers/downstream_gate_20260925/GRAPH.json", + "rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde" + } + ], + "source_rev": "dbe6c0a1b1a22c7404b0fd173ee0043b98a08cde", + "observed_at": "2026-10-22T17:00:35Z", + "status": "fresh", + "scientific_effect": "NONE", + "predecessor": { + "node_id": "board-old", + "url": "https://github.com/example/repo/issues/1" + }, + "successor": null, + "carried_forward": [ + { + "id": "math-463-pickup", + "text": "Historical pickup later released by native review.", + "source_url": "https://github.com/d6g8k5htny-coder/Math-/pull/463#issuecomment-6198606495", + "status": "resolved", + "resolution_url": "https://github.com/d6g8k5htny-coder/Math-/pull/463#pullrequestreview-5537612171" + } + ], + "refresh_error": null +} diff --git a/tests/test_node_status_atlas.mjs b/tests/test_node_status_atlas.mjs index 25af64a84..f47959fa9 100644 --- a/tests/test_node_status_atlas.mjs +++ b/tests/test_node_status_atlas.mjs @@ -10,6 +10,11 @@ import * as m from '../docs/site/current-model.mjs'; const root = fileURLToPath(new URL('..', import.meta.url)); const recordsDir = fileURLToPath(new URL('../records/node_status/', import.meta.url)); const FORBIDDEN = ['lemma_closed', 'prizes_solved', 'discharges_OBL_H5_JETMOD', 'certified_C_H', 'freeze', 'inventable_attempt_accepted']; +const PATTERN = String.raw`^https://github\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\d*#(issuecomment-[1-9]\d*|pullrequestreview-[1-9]\d*)$`; +const DAY = 24 * 60 * 60 * 1000; +const PICKUP = 'https://github.com/d6g8k5htny-coder/Math-/pull/463#issuecomment-6198606495'; +const RELEASED = 'https://github.com/d6g8k5htny-coder/Math-/pull/463#pullrequestreview-5537612171'; +const COMMENT = 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-6346301123'; const commit = 'a'.repeat(40), main = 'c'.repeat(40), digest = 'b'.repeat(64); const snapshot = nodes => ({schema_version: 1, observed_at: '2026-10-13T12:00:00Z', scientific_effect: 'NONE', repositories: [{repository: 'd6g8k5htny-coder/main', commit: main, tree: main}, {repository: 'd6g8k5htny-coder/Math-', commit, tree: commit}], sources: [], graph: {identity: {repository: 'd6g8k5htny-coder/Math-', commit, path: 'frontiers/x/GRAPH.json', blob: commit, bytes: 20, sha256: digest}, data: {nodes, edges: []}}, equations: [], references: []}); @@ -22,6 +27,12 @@ function python(args, options = {}) { } } +function pythonJson(args, options) { + const result = python(args, options); + if (!result.stdout) throw new Error(`python ${args[0]} failed (${result.status}): ${result.stderr}`); + return JSON.parse(result.stdout); +} + async function records() { const names = (await readdir(recordsDir)).filter(name => name.endsWith('.json')).sort(); const entries = await Promise.all(names.map(async name => [name.slice(0, -5), JSON.parse(await readFile(join(recordsDir, name), 'utf8'))])); @@ -36,15 +47,48 @@ function flags(value, found = []) { return found; } +async function validate(record) { + const directory = await mkdtemp(join(tmpdir(), 'node-status-')); + const name = `${record.node_id}.json`; + const path = join(directory, name); + await writeFile(path, JSON.stringify(record)); + const result = python(['tools/validate_node_status.py', directory]); + const saved = JSON.parse(await readFile(path, 'utf8')); + await rm(directory, {recursive: true, force: true}); + return {...result, saved}; +} + +function ownerRow(view) { + return view.operational.find(row => row.field === 'owner_evidence'); +} + +function noteOf(view) { + return view.operational.find(row => row.field === 'next_action').note; +} + test('published node-status records are real graph nodes with no owner and no mathematical flags', async () => { const view = await records(); const ids = Object.keys(view); assert.ok(ids.length >= 2 && ids.length <= 3, ids.join(',')); const schema = JSON.parse(await readFile(new URL('../architecture/node-status.schema.json', import.meta.url), 'utf8')); assert.deepEqual(schema.required, ['schema_version', 'node_id', 'owner', 'next_action', 'missing_input', 'stops', 'sources', 'source_rev', 'observed_at', 'status', 'scientific_effect']); + for (const name of ['predecessor', 'successor', 'carried_forward', 'refresh_error']) { + assert.equal(schema.required.includes(name), false, name); + assert.ok(schema.properties[name], name); + } assert.equal(schema.additionalProperties, false); assert.deepEqual(schema.properties.status.enum, ['fresh', 'stale', 'unknown']); assert.equal(schema.properties.scientific_effect.const, 'NONE'); + const ownerObject = schema.properties.owner.oneOf.find(item => item.type === 'object'); + assert.equal(ownerObject.type, 'object'); + assert.equal(ownerObject.additionalProperties, false); + assert.deepEqual(ownerObject.required, ['id', 'evidence_url']); + assert.equal(ownerObject.properties.evidence_url.pattern, PATTERN); + assert.equal(ownerObject.properties.released_url.pattern, PATTERN); + assert.equal(m.NODE_STATUS_EVIDENCE_URL_PATTERN, PATTERN); + const printed = python(['-c', 'import sys; sys.path.insert(0, "tools"); from validate_node_status import EVIDENCE_URL_PATTERN; from current_site_history import EVIDENCE_URL_PATTERN as shared; assert shared == EVIDENCE_URL_PATTERN; print(EVIDENCE_URL_PATTERN)']); + assert.equal(printed.status, 0, printed.stderr); + assert.equal(printed.stdout.trim(), PATTERN); const graph = JSON.parse(await readFile(new URL('../docs/site/current.json', import.meta.url), 'utf8')).graph.data.nodes; const bundle = JSON.parse(await readFile(new URL('../docs/site/node-status.json', import.meta.url), 'utf8')); assert.deepEqual(bundle, view); @@ -55,6 +99,7 @@ test('published node-status records are real graph nodes with no owner and no ma assert.equal(record.owner, null); assert.equal(record.scientific_effect, 'NONE'); assert.ok(['fresh', 'stale', 'unknown'].includes(record.status)); + assert.equal(record.source_rev, record.sources[0].rev); assert.equal(flags(record).length, 0); assert.ok(Object.hasOwn(graph, id), id); } @@ -66,26 +111,110 @@ test('published node-status records are real graph nodes with no owner and no ma test('a record with an owner but no evidence_url fails validation', async () => { const base = (await records())['eng.hard-gate']; - const directory = await mkdtemp(join(tmpdir(), 'node-status-')); - const bad = {...base, owner: {id: 'agent 9'}}; - await writeFile(join(directory, 'eng.hard-gate.json'), JSON.stringify(bad)); - const failed = python(['tools/validate_node_status.py', directory]); + const failed = await validate({...base, owner: {id: 'agent 9'}}); assert.notEqual(failed.status, 0); - assert.match(failed.stderr, /evidence_url/); - const shaped = {...base, owner: {id: 'shape-only', evidence_url: 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-6346301123'}}; - const okDir = await mkdtemp(join(tmpdir(), 'node-status-ok-')); - await writeFile(join(okDir, 'eng.hard-gate.json'), JSON.stringify(shaped)); - const passed = python(['tools/validate_node_status.py', okDir]); + assert.match(failed.stderr, /eng\.hard-gate\.json: owner requires evidence_url/); + assert.doesNotMatch(failed.stderr, /Traceback/); + const shaped = {...base, owner: {id: 'shape-only', evidence_url: COMMENT}}; + const passed = await validate(shaped); assert.equal(passed.status, 0, passed.stderr); const flagged = {...base, lemma_closed: true}; - const flagDir = await mkdtemp(join(tmpdir(), 'node-status-flag-')); - await writeFile(join(flagDir, 'eng.hard-gate.json'), JSON.stringify(flagged)); - const refused = python(['tools/validate_node_status.py', flagDir]); + const refused = await validate(flagged); assert.notEqual(refused.status, 0); - assert.match(refused.stderr, /lemma_closed/); - await rm(directory, {recursive: true}); - await rm(okDir, {recursive: true}); - await rm(flagDir, {recursive: true}); + assert.match(refused.stderr, /forbidden mathematical status flag: lemma_closed/); + const nested = {...base, carried_forward: [{id: 'x', text: 'y', source_url: COMMENT, status: 'open', lemma_closed: true}]}; + const nestedRefused = await validate(nested); + assert.match(nestedRefused.stderr, /forbidden mathematical status flag: lemma_closed/); + const notDict = await validate({...base, owner: 'agent 9'}); + assert.match(notDict.stderr, /owner must be null or an object with evidence_url/); + assert.doesNotMatch(notDict.stderr, /Traceback/); + const widened = await validate({...base, status: 'current'}); + assert.match(widened.stderr, /status must be fresh, stale, or unknown/); + const looseSha = {...base, sources: [{url: base.sources[0].url, rev: 'not-a-timestamp'}], source_rev: 'not-a-timestamp'}; + const sha = await validate(looseSha); + assert.match(sha.stderr, /sources\[0\]\.rev must be a 40-character commit/); +}); + +test('closed schema, evidence pattern, and primary source_rev are enforced', async () => { + const base = (await records())['eng.hard-gate']; + const extra = await validate({...base, not_in_schema: 1}); + assert.notEqual(extra.status, 0); + assert.match(extra.stderr, /unexpected fields: not_in_schema/); + const script = await validate({...base, owner: {id: 'agent 9', evidence_url: 'javascript:alert(1)'}}); + assert.notEqual(script.status, 0); + assert.match(script.stderr, /owner evidence_url does not match the comment pattern/); + assert.doesNotMatch(script.stderr, /Traceback/); + const zeros = [ + 'https://github.com/d6g8k5htny-coder/main/issues/0#issuecomment-1', + 'https://github.com/d6g8k5htny-coder/main/issues/01#issuecomment-12', + 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-0', + 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-01', + 'https://github.com/d6g8k5htny-coder/main/pull/463#pullrequestreview-0', + 'https://github.com/d6g8k5htny-coder/Math-/pull/0463#issuecomment-6198606495', + 'https://github.com/d6g8k5htny-coder/main/issues/475#pullrequestreview-01', + ]; + for (const url of zeros) { + assert.equal(m.NODE_STATUS_EVIDENCE_URL.test(url), false, url); + const rejected = await validate({...base, owner: {id: 'agent 9', evidence_url: url}}); + assert.notEqual(rejected.status, 0, url); + assert.match(rejected.stderr, /owner evidence_url does not match the comment pattern/); + const released = await validate({...base, owner: {id: 'agent 9', evidence_url: COMMENT, released_url: url}}); + assert.notEqual(released.status, 0, url); + assert.match(released.stderr, /owner released_url does not match the comment pattern/); + } + const ten = 'https://github.com/d6g8k5htny-coder/main/issues/10#issuecomment-10'; + assert.equal(m.NODE_STATUS_EVIDENCE_URL.test(ten), true); + assert.equal((await validate({...base, owner: {id: 'agent 9', evidence_url: ten}})).status, 0); + const older = '2020-01-01T00:00:00Z'; + const newer = '2026-10-22T17:00:35Z'; + const pin = base.sources[0]; + const comment = {url: COMMENT, rev: newer}; + const other = {url: 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-6346276856', rev: older}; + const primary = await validate({...base, sources: [{...other, rev: older}, comment], source_rev: older}); + assert.equal(primary.status, 0, primary.stderr); + const notPrimary = await validate({...base, sources: [other, comment], source_rev: newer}); + assert.notEqual(notPrimary.status, 0); + assert.match(notPrimary.stderr, /source_rev must equal sources\[0\]\.rev/); + const pinPrimary = await validate({...base, sources: [pin, comment], source_rev: pin.rev}); + assert.equal(pinPrimary.status, 0, pinPrimary.stderr); + const joined = await validate({...base, sources: [other, comment], source_rev: `${older},${newer}`}); + assert.notEqual(joined.status, 0); + assert.match(joined.stderr, /source_rev must be a single rev/); + assert.doesNotMatch(joined.stderr, /source_rev must equal sources\[0\]\.rev/); +}); + +test('released_url keeps the owner object and the atlas says released', async () => { + const fixture = JSON.parse(await readFile(new URL('./fixtures/node_status/math-463-released.json', import.meta.url), 'utf8')); + assert.equal(fixture.owner.evidence_url, PICKUP); + assert.equal(fixture.owner.released_url, RELEASED); + assert.notEqual(fixture.owner, null); + const checked = await validate(fixture); + assert.equal(checked.status, 0, checked.stderr); + assert.equal(typeof checked.saved.owner, 'object'); + assert.notEqual(checked.saved.owner, null); + assert.equal(checked.saved.owner.id, 'agent-old'); + assert.equal(checked.saved.owner.evidence_url, PICKUP); + assert.equal(checked.saved.owner.released_url, RELEASED); + assert.equal(checked.saved.predecessor.node_id, 'board-old'); + assert.equal(checked.saved.successor, null); + assert.equal(checked.saved.carried_forward[0].status, 'resolved'); + assert.equal(checked.saved.refresh_error, null); + const data = snapshot({[fixture.node_id]: {kind: 'candidate', classification: 'OPEN_ACTIVE'}}); + const before = fixture.owner; + const shown = m.resumeProjection(data, fixture.node_id, Date.parse(fixture.observed_at), {[fixture.node_id]: fixture}); + assert.equal(fixture.owner, before); + assert.notEqual(fixture.owner, null); + const row = ownerRow(shown); + assert.equal(row.text, 'released'); + assert.equal(row.url, RELEASED); + assert.equal(m.resumeFieldHref(row), RELEASED); + assert.doesNotMatch(JSON.stringify(row), /current owner/); + assert.doesNotMatch(JSON.stringify(row), /agent-old/); + assert.equal(row.label, 'Owner evidence'); + assert.doesNotMatch(row.text, /No owner recorded/); + const script = 'import json,sys\nsys.path.insert(0,sys.argv[1])\nfrom current_site_history import resume_projection\npayload=json.load(sys.stdin)\nprint(json.dumps(resume_projection(payload["data"], payload["id"], payload["records"], payload["now"])["operational"]))'; + const py = pythonJson(['-c', script, fileURLToPath(new URL('../tools/', import.meta.url))], {input: JSON.stringify({data, id: fixture.node_id, records: {[fixture.node_id]: fixture}, now: Date.parse(fixture.observed_at)})}); + assert.deepEqual(py, shown.operational); }); test('resume projection shows a node-status view and refuses an owner without evidence', async () => { @@ -96,9 +225,10 @@ test('resume projection shows a node-status view and refuses an owner without ev for (const id of Object.keys(view)) { const shown = m.resumeProjection(data, id, undefined, view); assert.equal(shown.operational.find(row => row.field === 'next_action').text, view[id].next_action); - assert.match(shown.operational.find(row => row.field === 'next_action').note, /Freshness is not scientific validity/); - assert.equal(shown.operational.find(row => row.field === 'owner_evidence').text, 'No owner recorded in this node-status view.'); - assert.equal(shown.operational.find(row => row.field === 'owner_evidence').url, undefined); + assert.match(noteOf(shown), /Node-status view fresh as of 2026-10-22T17:00:35Z\. Recorded status fresh\./); + assert.match(noteOf(shown), /Freshness is not scientific validity/); + assert.equal(ownerRow(shown).text, 'No owner recorded in this node-status view.'); + assert.equal(ownerRow(shown).url, undefined); assert.equal(shown.scientific_effect, undefined); assert.doesNotMatch(JSON.stringify(shown.operational), /merge now|not an owner/); assert.deepEqual(flags(shown), []); @@ -107,15 +237,48 @@ test('resume projection shows a node-status view and refuses an owner without ev assert.ok(bare.operational.every(row => row.text === m.RESUME_NOT_RECORDED)); const omitted = m.resumeProjection(data, Object.keys(view)[0]); assert.ok(omitted.operational.every(row => row.text === m.RESUME_NOT_RECORDED)); - const ownerless = {[Object.keys(view)[0]]: {...view[Object.keys(view)[0]], owner: {id: 'agent 9'}}}; - const hidden = m.resumeProjection(data, Object.keys(view)[0], undefined, ownerless); + const id = Object.keys(view)[0]; + const ownerless = {[id]: {...view[id], owner: {id: 'agent 9'}}}; + const hidden = m.resumeProjection(data, id, undefined, ownerless); assert.ok(hidden.operational.every(row => row.text === m.RESUME_NOT_RECORDED)); assert.doesNotMatch(JSON.stringify(hidden), /agent 9/); + const flagged = {[id]: {...view[id], lemma_closed: true}}; + assert.ok(m.resumeProjection(data, id, undefined, flagged).operational.every(row => row.text === m.RESUME_NOT_RECORDED)); + const effect = {[id]: {...view[id], scientific_effect: 'SOME'}}; + assert.ok(m.resumeProjection(data, id, undefined, effect).operational.every(row => row.text === m.RESUME_NOT_RECORDED)); + const hostile = {[id]: {...view[id], owner: {id: 'agent 9', evidence_url: 'javascript:alert(1)'}}}; + const plain = m.resumeProjection(data, id, undefined, hostile); + assert.equal(ownerRow(plain).url, undefined); + assert.equal(ownerRow(plain).text, 'javascript:alert(1)'); + assert.equal(m.resumeFieldHref({field: 'owner_evidence', text: 'javascript:alert(1)', url: 'javascript:alert(1)'}), null); + assert.equal(m.nodeStatusEvidenceURL('javascript:alert(1)'), null); + const observed = view[id].observed_at; + const instant = Date.parse(observed); + const boundary = m.resumeProjection(data, id, instant + DAY, view); + assert.match(noteOf(boundary), /Node-status view fresh as of /); + assert.doesNotMatch(noteOf(boundary), /Node-status view stale/); + const past = m.resumeProjection(data, id, instant + DAY + 1, view); + assert.match(noteOf(past), /Node-status view stale as of /); + assert.match(noteOf(past), /Recorded status fresh/); + const unknown = {[id]: {...view[id], status: 'unknown'}}; + const overridden = m.resumeProjection(data, id, instant + DAY + 1, unknown); + assert.match(noteOf(overridden), /Node-status view stale as of /); + assert.match(noteOf(overridden), /Recorded status unknown/); const tools = fileURLToPath(new URL('../tools/', import.meta.url)); - const script = 'import json,sys\nsys.path.insert(0,sys.argv[1])\nfrom current_site_history import resume_projection\npayload=json.load(sys.stdin)\nprint(json.dumps([resume_projection(payload["data"], i, payload["records"]).get("operational") for i in payload["ids"]]))'; + const script = 'import json,sys\nsys.path.insert(0,sys.argv[1])\nfrom current_site_history import resume_projection\npayload=json.load(sys.stdin)\nprint(json.dumps([resume_projection(payload["data"], i, payload["records"], payload.get("now")).get("operational") for i in payload["ids"]]))'; const ids = [...Object.keys(view), 'other']; - const py = JSON.parse(python(['-c', script, tools], {input: JSON.stringify({data, ids, records: view})}).stdout); - assert.deepEqual(py, ids.map(id => m.resumeProjection(data, id, undefined, view).operational)); + const py = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids, records: view})}); + assert.deepEqual(py, ids.map(node => m.resumeProjection(data, node, undefined, view).operational)); + const pyPast = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: view, now: instant + DAY + 1})}); + assert.deepEqual(pyPast, [m.resumeProjection(data, id, instant + DAY + 1, view).operational]); + const pyEdge = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: view, now: instant + DAY})}); + assert.deepEqual(pyEdge, [m.resumeProjection(data, id, instant + DAY, view).operational]); + const pyScript = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: hostile})}); + assert.deepEqual(pyScript, [plain.operational]); + const pyOwner = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: ownerless})}); + assert.deepEqual(pyOwner, [hidden.operational]); + const pyFlag = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: flagged})}); + assert.deepEqual(pyFlag, [m.resumeProjection(data, id, undefined, flagged).operational]); }); test('the atlas resume section passes the published node-status view into resumeProjection', async () => { @@ -123,4 +286,6 @@ test('the atlas resume section passes the published node-status view into resume assert.match(source, /fetch\('node-status\.json'/); assert.match(source, /resumeProjection\(data,selected,Date\.now\(\),nodeStatus\)/); assert.match(source, /function resumeSection\(view\)/); + assert.match(source, /resumeFieldHref\(item\)/); + assert.match(source, /import \{[^}]*resumeFieldHref\} from '\.\/current-model\.mjs/); }); diff --git a/tools/current_site_history.py b/tools/current_site_history.py index b6aae04f7..1c205ac26 100644 --- a/tools/current_site_history.py +++ b/tools/current_site_history.py @@ -8,7 +8,7 @@ bytes. Restore admits all downloads and the durable manifest pin before writes. """ import argparse -from datetime import datetime +from datetime import datetime, timezone import hashlib from html.parser import HTMLParser import io @@ -24,6 +24,7 @@ import zipfile from current_inventory_acquisition import (project_current_acquisitions, current_source_list, inventory_legend, acquisition_notice, date_explanation) +from validate_node_status import EVIDENCE_URL_PATTERN from current_site_data import parse_json from site_asset_release import transform, without_release @@ -1559,6 +1560,8 @@ def _resume_text(value): "freeze", "inventable_attempt_accepted"}) _NODE_STATUS_STATE = re.compile(r"(?:fresh|stale|unknown)\Z") _NODE_STATUS_OBSERVED = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z") +_NODE_STATUS_EVIDENCE = re.compile(EVIDENCE_URL_PATTERN) +_NODE_STATUS_STALE_MS = 24 * 60 * 60 * 1000 def _node_status_forbidden(value): @@ -1590,37 +1593,73 @@ def _node_status_list(value): return None -def _node_status_operational(record): +def _node_status_evidence(url): + return url if isinstance(url, str) and _NODE_STATUS_EVIDENCE.fullmatch(url) else None + + +def _node_status_note(record, now): + recorded = record.get("status") + observed = record.get("observed_at") + if not (isinstance(recorded, str) and _NODE_STATUS_STATE.fullmatch(recorded) + and isinstance(observed, str) and _NODE_STATUS_OBSERVED.fullmatch(observed)): + return None + shown = recorded + if isinstance(now, (int, float)) and not isinstance(now, bool): + try: + parsed = datetime.strptime(observed, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + except ValueError: + parsed = None + if parsed is not None: + age = now - int(parsed.timestamp()) * 1000 + if age > _NODE_STATUS_STALE_MS: + shown = "stale" + return (f"Node-status view {shown} as of {observed}. Recorded status {recorded}. " + "Scientific effect NONE. Freshness is not scientific validity.") + + +def _node_status_operational(record, now=None): owner = record["owner"] rows = [] for field, label in RESUME_OPERATIONAL: row = {"field": field, "label": label, "text": RESUME_NOT_RECORDED} if field == "next_action" and isinstance(record.get("next_action"), str) and record["next_action"]: row["text"] = record["next_action"] - if (_NODE_STATUS_STATE.fullmatch(str(record.get("status"))) - and isinstance(record.get("observed_at"), str) - and _NODE_STATUS_OBSERVED.fullmatch(record["observed_at"])): - row["note"] = (f"Node-status view {record['status']} as of {record['observed_at']}. " - "Scientific effect NONE. Freshness is not scientific validity.") + note = _node_status_note(record, now) + if note: + row["note"] = note elif field == "missing_input": text = _node_status_list(record.get("missing_input")) if text: row["text"] = text elif field == "owner_evidence": - row["text"] = "No owner recorded in this node-status view." if owner is None else owner["id"] - if owner and owner.get("evidence_url"): - row["url"] = owner["evidence_url"] + if owner is None: + row["text"] = "No owner recorded in this node-status view." + elif isinstance(owner, dict) and "released_url" in owner: + row["text"] = "released" + link = _node_status_evidence(owner.get("released_url")) + if link: + row["url"] = link + elif isinstance(owner.get("released_url"), str) and owner["released_url"]: + row["text"] = owner["released_url"] + elif isinstance(owner, dict): + link = _node_status_evidence(owner.get("evidence_url")) + if link: + row["text"] = owner["id"] + row["url"] = link + elif isinstance(owner.get("evidence_url"), str) and owner["evidence_url"]: + row["text"] = owner["evidence_url"] elif field == "retained_stop": row["text"] = _node_status_list(record.get("stops")) or "No retained stop recorded in this node-status view." rows.append(row) return rows -def resume_projection(data, node_id, status_records=None): +def resume_projection(data, node_id, status_records=None, now=None): """The static form of current-model.mjs resumeProjection; a static page evaluates no freshness. status_records is the optional node-status map. The snapshot-only call matches the - JavaScript default and is what static pages render. + JavaScript default and is what static pages render. now is the render clock in + milliseconds, used only for the node-status stale display. """ nodes = data["graph"]["data"]["nodes"] if not isinstance(node_id, str) or node_id not in nodes: @@ -1642,7 +1681,7 @@ def resume_projection(data, node_id, status_records=None): "url": value if isinstance(value, str) and _RESUME_REVIEW_URL.fullmatch(value) else None, "note": RESUME_ISSUE_NOTE if field == "review_issue" and isinstance(value, (int, float)) and not isinstance(value, bool) else None} for field, value in node.items() if field.startswith("review_")]}, - "operational": _node_status_operational(record) if record else [ + "operational": _node_status_operational(record, now) if record else [ {"field": field, "label": label, "text": RESUME_NOT_RECORDED} for field, label in RESUME_OPERATIONAL], "continuation": dict(task) if task else None, "continuation_text": None if task else RESUME_NO_TASK, @@ -1651,8 +1690,16 @@ def resume_projection(data, node_id, status_records=None): def _resume_html(view): snapshot = view["snapshot"] + def _href(item): + url = item.get("url") + if not isinstance(url, str) or not url: + return None + if item.get("field") == "owner_evidence" and not _NODE_STATUS_EVIDENCE.fullmatch(url): + return None + return url + rows = lambda items: "".join(f'
{_v1_escape(item.get("label", item["field"]))}
' + ( - f'{_v1_escape(item["text"])}' if item.get("url") else f'{_v1_escape(item["text"])}') + f'{_v1_escape(item["text"])}' if _href(item) else f'{_v1_escape(item["text"])}') + (f' {_v1_escape(item["note"])}' if item.get("note") else "") + '
' for item in items) review = (f'
{rows(view["review"]["fields"])}
' if view["review"]["fields"] else f'

Review metadata: {RESUME_NOT_RECORDED}.

') diff --git a/tools/validate_node_status.py b/tools/validate_node_status.py index dc9ba5395..912ca87f1 100644 --- a/tools/validate_node_status.py +++ b/tools/validate_node_status.py @@ -2,7 +2,9 @@ """Validate records/node_status against architecture/node-status.schema.json. Status is freshness of the current view only. scientific_effect must stay NONE. -An owner is refused unless evidence_url is present. No network and no writes. +An owner is null or an object. released_url may be present on that object and +is never a reason to replace the owner with null. source_rev must equal +sources[0].rev, one value, never a comma-joined list. No network and no writes. """ from __future__ import annotations @@ -12,12 +14,14 @@ from pathlib import Path, PurePosixPath import re import sys +from urllib.parse import urlparse ROOT = Path(__file__).resolve().parents[1] REQUIRED = ( "schema_version", "node_id", "owner", "next_action", "missing_input", "stops", "sources", "source_rev", "observed_at", "status", "scientific_effect", ) +OPTIONAL = frozenset({"predecessor", "successor", "carried_forward", "refresh_error"}) FORBIDDEN = frozenset({ "lemma_closed", "prizes_solved", "discharges_OBL_H5_JETMOD", "certified_C_H", "freeze", "inventable_attempt_accepted", @@ -25,12 +29,23 @@ NODE_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*\Z") SHA = re.compile(r"[0-9a-f]{40}\Z") OBSERVED = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z") -COMMENT = re.compile( - r"https://github\.com/d6g8k5htny-coder/(?:main|Math-)/(?:issues|pull)/" - r"[1-9][0-9]*#issuecomment-[1-9][0-9]*\Z") -SOURCE_URL = re.compile( - r"https://github\.com/d6g8k5htny-coder/(?:main|Math-)/(?:blob|tree)/" - r"[0-9a-f]{40}/[A-Za-z0-9_./-]+\Z") +# Canonical evidence_url and released_url pattern. The same characters are +# NODE_STATUS_EVIDENCE_URL_PATTERN in docs/site/current-model.mjs. +EVIDENCE_URL_PATTERN = r"^https://github\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\d*#(issuecomment-[1-9]\d*|pullrequestreview-[1-9]\d*)$" +EVIDENCE_URL = re.compile(EVIDENCE_URL_PATTERN) +PINNED_URL = re.compile( + r"^https://github\.com/d6g8k5htny-coder/(main|Math-)/(blob|tree)/" + r"[0-9a-f]{40}/[A-Za-z0-9_./-]+$") +GITHUB_OBJECT_URL = re.compile( + r"^https://github\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\d*" + r"(#(issuecomment-[1-9]\d*|pullrequestreview-[1-9]\d*))?$") +UTC_REV = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z") +CARRIED_SOURCE_URL = re.compile( + r"^https://github\.com/[\w.-]+/[\w.-]+/(?:issues|pull)/\d+" + r"(?:#(?:issuecomment|pullrequestreview)-\d+)?$") +CARRIED_COMMENT_URL = re.compile( + r"^https://github\.com/[\w.-]+/[\w.-]+/(?:issues|pull)/\d+" + r"#(?:issuecomment|pullrequestreview)-\d+$") STATUSES = frozenset({"fresh", "stale", "unknown"}) MAX_FILES = 256 MAX_BYTES = 65536 @@ -65,6 +80,31 @@ def load_json(path, max_bytes=MAX_BYTES): return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_pairs) +def _http_url(url): + if not isinstance(url, str) or not url or any(ch.isspace() for ch in url): + return False + parsed = urlparse(url) + return parsed.scheme.lower() in ("http", "https") and bool(parsed.netloc) + + +def _closed_schema_problems(record): + extra = sorted(set(record) - set(REQUIRED) - OPTIONAL) + missing = sorted(set(REQUIRED) - set(record)) + problems = [] + if extra: + problems.append("unexpected fields: " + ", ".join(extra)) + if missing: + problems.append("missing fields: " + ", ".join(missing)) + return problems + + +def _owner_object(schema): + for item in schema.get("properties", {}).get("owner", {}).get("oneOf", []): + if item.get("type") == "object": + return item + return None + + def check_schema_file(root): path = root / "architecture" / "node-status.schema.json" problems = [] @@ -76,15 +116,32 @@ def check_schema_file(root): problems.append("schema: required fields differ from interface v0") if schema.get("additionalProperties") is not False: problems.append("schema: additional properties must be refused") - status = schema.get("properties", {}).get("status", {}).get("enum") + props = schema.get("properties", {}) + for name in ("predecessor", "successor", "carried_forward", "refresh_error"): + if name not in props: + problems.append(f"schema: optional field {name} missing") + if name in schema.get("required", []): + problems.append(f"schema: {name} must stay optional") + status = props.get("status", {}).get("enum") if status != ["fresh", "stale", "unknown"]: problems.append("schema: status must be fresh, stale, or unknown") - effect = schema.get("properties", {}).get("scientific_effect", {}).get("const") + effect = props.get("scientific_effect", {}).get("const") if effect != "NONE": problems.append("schema: scientific_effect must be NONE") - owner = schema.get("properties", {}).get("owner", {}) + owner = props.get("owner", {}) if not any(item.get("type") == "null" for item in owner.get("oneOf", [])): problems.append("schema: owner must allow null") + owner_object = _owner_object(schema) + if owner_object is None: + problems.append("schema: owner with released_url must stay an object") + else: + owner_props = owner_object.get("properties", {}) + if owner_props.get("evidence_url", {}).get("pattern") != EVIDENCE_URL_PATTERN: + problems.append("schema: evidence_url pattern drift") + if owner_props.get("released_url", {}).get("pattern") != EVIDENCE_URL_PATTERN: + problems.append("schema: released_url pattern drift") + if "released_url" in owner_object.get("required", []): + problems.append("schema: released_url must stay optional") return problems @@ -98,21 +155,105 @@ def graph_ids(root): def check_owner(record, problems): + """Read owner. A released_url keeps the owner object; it is not cleared.""" owner = record["owner"] if owner is None: return if not isinstance(owner, dict): - problems.append("owner requires evidence_url") + problems.append("owner must be null or an object with evidence_url") return - if "evidence_url" not in owner or not isinstance(owner.get("evidence_url"), str) or not owner.get("evidence_url"): + extra = sorted(set(owner) - {"id", "evidence_url", "released_url"}) + if extra: + problems.append("owner allows only id, evidence_url, and released_url") + evidence = owner.get("evidence_url") + if "evidence_url" not in owner or not isinstance(evidence, str) or evidence == "": problems.append("owner requires evidence_url") return - if set(owner) != {"id", "evidence_url"}: - problems.append("owner allows only id and evidence_url") - if not isinstance(owner.get("id"), str) or not owner["id"]: + if not isinstance(owner.get("id"), str) or owner.get("id") == "": problems.append("owner id must be a non-empty string") - if not COMMENT.fullmatch(owner["evidence_url"]): - problems.append("owner evidence_url must be a GitHub comment URL") + if not EVIDENCE_URL.fullmatch(evidence): + problems.append("owner evidence_url does not match the comment pattern") + if "released_url" in owner: + released = owner.get("released_url") + if not isinstance(released, str) or not EVIDENCE_URL.fullmatch(released): + problems.append("owner released_url does not match the comment pattern") + + +def check_link(value, field, problems): + if value is None: + return + if not isinstance(value, dict) or set(value) != {"node_id", "url"}: + problems.append(f"{field} must be null or an object with node_id and url") + return + node_id = value.get("node_id") + if not isinstance(node_id, str) or node_id == "" or any(ch.isspace() for ch in node_id): + problems.append(f"{field}.node_id must be a non-empty string") + if not _http_url(value.get("url")): + problems.append(f"{field}.url must be an http(s) URL") + + +def check_carried(value, problems): + if not isinstance(value, list): + problems.append("carried_forward must be a list") + return + seen = set() + allowed = {"id", "text", "source_url", "status", "resolution_url"} + required = {"id", "text", "source_url", "status"} + for index, item in enumerate(value): + label = f"carried_forward[{index}]" + if not isinstance(item, dict) or not required <= set(item) or not set(item) <= allowed: + problems.append(f"{label} must have id, text, source_url, and status") + continue + item_id = item["id"] + if not isinstance(item_id, str) or item_id == "": + problems.append(f"{label}.id must be a non-empty string") + elif item_id in seen: + problems.append(f"{label} duplicates id {item_id}") + else: + seen.add(item_id) + if not isinstance(item["text"], str) or item["text"] == "": + problems.append(f"{label}.text must be a non-empty string") + source = item["source_url"] + if not isinstance(source, str) or not CARRIED_SOURCE_URL.fullmatch(source): + problems.append(f"{label}.source_url must be a GitHub issue or comment URL") + if item["status"] not in ("open", "resolved"): + problems.append(f"{label}.status must be open or resolved") + elif item["status"] == "resolved" and not CARRIED_COMMENT_URL.fullmatch(str(item.get("resolution_url", ""))): + problems.append(f"{label} resolved without a resolution_url") + + +def check_source(source, index, problems): + label = f"sources[{index}]" + if not isinstance(source, dict) or set(source) != {"url", "rev"}: + problems.append(f"{label} must have url and rev") + return + url, rev = source.get("url"), source.get("rev") + if isinstance(url, str) and PINNED_URL.fullmatch(url): + if not isinstance(rev, str) or not SHA.fullmatch(rev): + problems.append(f"{label}.rev must be a 40-character commit") + elif rev not in url: + problems.append(f"{label}.rev must appear in its url") + return + if isinstance(url, str) and GITHUB_OBJECT_URL.fullmatch(url): + if not isinstance(rev, str) or not UTC_REV.fullmatch(rev): + problems.append(f"{label}.rev must be a UTC timestamp") + return + try: + datetime.strptime(rev, "%Y-%m-%dT%H:%M:%SZ") + except ValueError: + problems.append(f"{label}.rev must be a UTC timestamp") + return + problems.append(f"{label}.url must be a pinned blob or tree, or a GitHub issue, pull, or comment") + + +def _source_rev_problems(record, sources): + source_rev = record["source_rev"] + primary = sources[0].get("rev") if isinstance(sources[0], dict) else None + if not isinstance(source_rev, str) or source_rev == "" or "," in source_rev: + return ["source_rev must be a single rev"] + if source_rev != primary: + return ["source_rev must equal sources[0].rev"] + return [] def check_record(record, filename, known_ids): @@ -122,13 +263,8 @@ def check_record(record, filename, known_ids): flags = forbidden_keys(record, []) if flags: problems.append("forbidden mathematical status flag: " + ", ".join(sorted(set(flags)))) - if set(record) != set(REQUIRED): - extra = sorted(set(record) - set(REQUIRED)) - missing = sorted(set(REQUIRED) - set(record)) - if extra: - problems.append("unexpected fields: " + ", ".join(extra)) - if missing: - problems.append("missing fields: " + ", ".join(missing)) + problems.extend(_closed_schema_problems(record)) + if problems: return problems if type(record["schema_version"]) is not int or record["schema_version"] != 0: problems.append("schema_version must be 0") @@ -151,17 +287,15 @@ def check_record(record, filename, known_ids): problems.append("sources must be a non-empty array") else: for index, source in enumerate(sources): - if not isinstance(source, dict) or set(source) != {"url", "rev"}: - problems.append(f"sources[{index}] must have url and rev") - continue - if not isinstance(source["url"], str) or not SOURCE_URL.fullmatch(source["url"]): - problems.append(f"sources[{index}].url must be a pinned GitHub blob or tree URL") - if not isinstance(source["rev"], str) or not SHA.fullmatch(source["rev"]): - problems.append(f"sources[{index}].rev must be a 40-character commit") - elif source["rev"] not in source["url"]: - problems.append(f"sources[{index}].rev must appear in its url") - if not isinstance(record["source_rev"], str) or not SHA.fullmatch(record["source_rev"]): - problems.append("source_rev must be a 40-character commit") + check_source(source, index, problems) + problems.extend(_source_rev_problems(record, sources)) + for field in ("predecessor", "successor"): + if field in record: + check_link(record[field], field, problems) + if "carried_forward" in record: + check_carried(record["carried_forward"], problems) + if "refresh_error" in record and record["refresh_error"] is not None and not isinstance(record["refresh_error"], str): + problems.append("refresh_error must be a string or null") observed = record["observed_at"] if not isinstance(observed, str) or not OBSERVED.fullmatch(observed): problems.append("observed_at must be UTC Z") @@ -182,7 +316,7 @@ def validate_directory(root, directory): directory = Path(directory) if not directory.is_absolute(): directory = (Path.cwd() / directory).resolve() - problems = [f"schema: {item}" if not item.startswith("schema:") else item for item in check_schema_file(root)] + problems = list(check_schema_file(root)) if not directory.is_dir() or directory.is_symlink(): return problems + ["records directory missing"] try: From 5a3115b04a197b7afbda59c74a440122fac21332 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 18:12:37 +0000 Subject: [PATCH 3/4] Require the shared comment pattern for resolution_url carried_forward resolution URLs were still matched with the loose comment regex, which accepted a zero id and comments on other repositories. Reuse EVIDENCE_URL for that field. Co-authored-by: Dylan Roy --- architecture/node-status.schema.json | 6 ++++- tests/test_node_status_atlas.mjs | 39 ++++++++++++++++++++++++++++ tools/validate_node_status.py | 20 +++++++++----- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/architecture/node-status.schema.json b/architecture/node-status.schema.json index 8cb9de6cf..2bad95d30 100644 --- a/architecture/node-status.schema.json +++ b/architecture/node-status.schema.json @@ -136,7 +136,11 @@ "text": {"type": "string", "minLength": 1}, "source_url": {"type": "string", "minLength": 1}, "status": {"enum": ["open", "resolved"]}, - "resolution_url": {"type": "string", "minLength": 1} + "resolution_url": { + "type": "string", + "minLength": 1, + "pattern": "^https://github\\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\\d*#(issuecomment-[1-9]\\d*|pullrequestreview-[1-9]\\d*)$" + } } } }, diff --git a/tests/test_node_status_atlas.mjs b/tests/test_node_status_atlas.mjs index f47959fa9..ae13857fe 100644 --- a/tests/test_node_status_atlas.mjs +++ b/tests/test_node_status_atlas.mjs @@ -85,6 +85,7 @@ test('published node-status records are real graph nodes with no owner and no ma assert.deepEqual(ownerObject.required, ['id', 'evidence_url']); assert.equal(ownerObject.properties.evidence_url.pattern, PATTERN); assert.equal(ownerObject.properties.released_url.pattern, PATTERN); + assert.equal(schema.properties.carried_forward.items.properties.resolution_url.pattern, PATTERN); assert.equal(m.NODE_STATUS_EVIDENCE_URL_PATTERN, PATTERN); const printed = python(['-c', 'import sys; sys.path.insert(0, "tools"); from validate_node_status import EVIDENCE_URL_PATTERN; from current_site_history import EVIDENCE_URL_PATTERN as shared; assert shared == EVIDENCE_URL_PATTERN; print(EVIDENCE_URL_PATTERN)']); assert.equal(printed.status, 0, printed.stderr); @@ -183,6 +184,44 @@ test('closed schema, evidence pattern, and primary source_rev are enforced', asy assert.doesNotMatch(joined.stderr, /source_rev must equal sources\[0\]\.rev/); }); +test('carried_forward resolution_url uses the shared comment pattern', async () => { + const base = (await records())['eng.hard-gate']; + const item = (resolution_url, status = 'resolved') => ({ + ...base, + carried_forward: [{ + id: 'pickup', + text: 'A carried comment.', + source_url: COMMENT, + status, + resolution_url, + }], + }); + const rejected = [ + 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-0', + 'https://github.com/d6g8k5htny-coder/main/issues/475#issuecomment-01', + 'https://github.com/d6g8k5htny-coder/main/pull/463#pullrequestreview-0', + 'https://github.com/d6g8k5htny-coder/Math-/pull/0463#issuecomment-6198606495', + 'https://github.com/d6g8k5htny-coder/meta-framework/issues/475#issuecomment-12', + 'https://github.com/other-owner/main/issues/475#issuecomment-12', + ]; + for (const url of rejected) { + const failed = await validate(item(url)); + assert.notEqual(failed.status, 0, url); + assert.match(failed.stderr, /carried_forward\[0\] resolution_url does not match the comment pattern/); + assert.doesNotMatch(failed.stderr, /Traceback/); + } + for (const url of [COMMENT, RELEASED]) { + const passed = await validate(item(url)); + assert.equal(passed.status, 0, `${url}\n${passed.stderr}`); + assert.equal(passed.saved.carried_forward[0].resolution_url, url); + } + const open = await validate({ + ...base, + carried_forward: [{id: 'pickup', text: 'Still open.', source_url: 'https://github.com/d6g8k5htny-coder/meta-framework/issues/475#issuecomment-0', status: 'open'}], + }); + assert.equal(open.status, 0, open.stderr); +}); + test('released_url keeps the owner object and the atlas says released', async () => { const fixture = JSON.parse(await readFile(new URL('./fixtures/node_status/math-463-released.json', import.meta.url), 'utf8')); assert.equal(fixture.owner.evidence_url, PICKUP); diff --git a/tools/validate_node_status.py b/tools/validate_node_status.py index 912ca87f1..1dbb55236 100644 --- a/tools/validate_node_status.py +++ b/tools/validate_node_status.py @@ -29,8 +29,9 @@ NODE_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*\Z") SHA = re.compile(r"[0-9a-f]{40}\Z") OBSERVED = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z") -# Canonical evidence_url and released_url pattern. The same characters are -# NODE_STATUS_EVIDENCE_URL_PATTERN in docs/site/current-model.mjs. +# Canonical comment URL: evidence_url, released_url, and carried_forward +# resolution_url. The same characters are NODE_STATUS_EVIDENCE_URL_PATTERN +# in docs/site/current-model.mjs. EVIDENCE_URL_PATTERN = r"^https://github\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\d*#(issuecomment-[1-9]\d*|pullrequestreview-[1-9]\d*)$" EVIDENCE_URL = re.compile(EVIDENCE_URL_PATTERN) PINNED_URL = re.compile( @@ -43,9 +44,6 @@ CARRIED_SOURCE_URL = re.compile( r"^https://github\.com/[\w.-]+/[\w.-]+/(?:issues|pull)/\d+" r"(?:#(?:issuecomment|pullrequestreview)-\d+)?$") -CARRIED_COMMENT_URL = re.compile( - r"^https://github\.com/[\w.-]+/[\w.-]+/(?:issues|pull)/\d+" - r"#(?:issuecomment|pullrequestreview)-\d+$") STATUSES = frozenset({"fresh", "stale", "unknown"}) MAX_FILES = 256 MAX_BYTES = 65536 @@ -142,6 +140,14 @@ def check_schema_file(root): problems.append("schema: released_url pattern drift") if "released_url" in owner_object.get("required", []): problems.append("schema: released_url must stay optional") + resolution = ( + props.get("carried_forward", {}) + .get("items", {}) + .get("properties", {}) + .get("resolution_url", {}) + ) + if resolution.get("pattern") != EVIDENCE_URL_PATTERN: + problems.append("schema: resolution_url pattern drift") return problems @@ -218,8 +224,8 @@ def check_carried(value, problems): problems.append(f"{label}.source_url must be a GitHub issue or comment URL") if item["status"] not in ("open", "resolved"): problems.append(f"{label}.status must be open or resolved") - elif item["status"] == "resolved" and not CARRIED_COMMENT_URL.fullmatch(str(item.get("resolution_url", ""))): - problems.append(f"{label} resolved without a resolution_url") + elif item["status"] == "resolved" and not EVIDENCE_URL.fullmatch(str(item.get("resolution_url", ""))): + problems.append(f"{label} resolution_url does not match the comment pattern") def check_source(source, index, problems): From 5995556b64121dd11c1e431de2e4624a2848b229 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 18:17:37 +0000 Subject: [PATCH 4/4] Compare pinned revs to the blob or tree segment A substring check accepted a sha that appeared later in the path. Parse the /blob// or /tree// segment and require equality. Published node-status tests follow each record's owner and timestamp. Co-authored-by: Dylan Roy --- tests/test_node_status_atlas.mjs | 109 +++++++++++++++++++++++++++---- tools/validate_node_status.py | 20 +++++- 2 files changed, 113 insertions(+), 16 deletions(-) diff --git a/tests/test_node_status_atlas.mjs b/tests/test_node_status_atlas.mjs index ae13857fe..2cc97cbee 100644 --- a/tests/test_node_status_atlas.mjs +++ b/tests/test_node_status_atlas.mjs @@ -66,7 +66,29 @@ function noteOf(view) { return view.operational.find(row => row.field === 'next_action').note; } -test('published node-status records are real graph nodes with no owner and no mathematical flags', async () => { +function expectedNote(record, now) { + const recorded = record.status; + const observed = record.observed_at; + let shown = recorded; + if (Number.isFinite(now)) { + const age = now - Date.parse(observed); + if (Number.isFinite(age) && age > DAY) shown = 'stale'; + } + return `Node-status view ${shown} as of ${observed}. Recorded status ${recorded}. Scientific effect NONE. Freshness is not scientific validity.`; +} + +function expectedOwner(record) { + const owner = record.owner; + if (owner === null) return {text: 'No owner recorded in this node-status view.'}; + if (owner && Object.hasOwn(owner, 'released_url')) { + if (m.NODE_STATUS_EVIDENCE_URL.test(owner.released_url)) return {text: 'released', url: owner.released_url}; + return {text: owner.released_url}; + } + if (owner && m.NODE_STATUS_EVIDENCE_URL.test(owner.evidence_url)) return {text: owner.id, url: owner.evidence_url}; + return {text: owner && owner.evidence_url}; +} + +test('published node-status records are real graph nodes with a valid owner and no mathematical flags', async () => { const view = await records(); const ids = Object.keys(view); assert.ok(ids.length >= 2 && ids.length <= 3, ids.join(',')); @@ -97,7 +119,10 @@ test('published node-status records are real graph nodes with no owner and no ma for (const [id, record] of Object.entries(view)) { assert.equal(record.node_id, id); assert.equal(record.schema_version, 0); - assert.equal(record.owner, null); + if (record.owner !== null) { + assert.equal(m.NODE_STATUS_EVIDENCE_URL.test(record.owner.evidence_url), true, id); + if (Object.hasOwn(record.owner, 'released_url')) assert.equal(m.NODE_STATUS_EVIDENCE_URL.test(record.owner.released_url), true, id); + } assert.equal(record.scientific_effect, 'NONE'); assert.ok(['fresh', 'stale', 'unknown'].includes(record.status)); assert.equal(record.source_rev, record.sources[0].rev); @@ -184,6 +209,27 @@ test('closed schema, evidence pattern, and primary source_rev are enforced', asy assert.doesNotMatch(joined.stderr, /source_rev must equal sources\[0\]\.rev/); }); +test('pinned rev equals the blob or tree commit segment', async () => { + const base = (await records())['eng.hard-gate']; + const pin = 'a'.repeat(40); + const later = 'b'.repeat(40); + const other = 'c'.repeat(40); + const pinned = (kind, sha, path) => `https://github.com/d6g8k5htny-coder/Math-/${kind}/${sha}/${path}`; + const withPin = (url, rev) => ({...base, sources: [{url, rev}], source_rev: rev}); + for (const kind of ['blob', 'tree']) { + const path = kind === 'blob' ? `receipts/${later}.json` : `receipts/${later}`; + const matched = await validate(withPin(pinned(kind, pin, path), pin)); + assert.equal(matched.status, 0, `${kind}\n${matched.stderr}`); + const inPath = await validate(withPin(pinned(kind, pin, path), later)); + assert.notEqual(inPath.status, 0, kind); + assert.match(inPath.stderr, /sources\[0\]\.rev must equal the blob or tree commit/); + assert.doesNotMatch(inPath.stderr, /Traceback/); + const mismatch = await validate(withPin(pinned(kind, pin, 'frontiers/downstream_gate_20260925/GRAPH.json'), other)); + assert.notEqual(mismatch.status, 0, kind); + assert.match(mismatch.stderr, /sources\[0\]\.rev must equal the blob or tree commit/); + } +}); + test('carried_forward resolution_url uses the shared comment pattern', async () => { const base = (await records())['eng.hard-gate']; const item = (resolution_url, status = 'resolved') => ({ @@ -264,10 +310,11 @@ test('resume projection shows a node-status view and refuses an owner without ev for (const id of Object.keys(view)) { const shown = m.resumeProjection(data, id, undefined, view); assert.equal(shown.operational.find(row => row.field === 'next_action').text, view[id].next_action); - assert.match(noteOf(shown), /Node-status view fresh as of 2026-10-22T17:00:35Z\. Recorded status fresh\./); + assert.equal(noteOf(shown), expectedNote(view[id])); assert.match(noteOf(shown), /Freshness is not scientific validity/); - assert.equal(ownerRow(shown).text, 'No owner recorded in this node-status view.'); - assert.equal(ownerRow(shown).url, undefined); + const want = expectedOwner(view[id]); + assert.equal(ownerRow(shown).text, want.text); + assert.equal(ownerRow(shown).url, want.url); assert.equal(shown.scientific_effect, undefined); assert.doesNotMatch(JSON.stringify(shown.operational), /merge now|not an owner/); assert.deepEqual(flags(shown), []); @@ -291,15 +338,21 @@ test('resume projection shows a node-status view and refuses an owner without ev assert.equal(ownerRow(plain).text, 'javascript:alert(1)'); assert.equal(m.resumeFieldHref({field: 'owner_evidence', text: 'javascript:alert(1)', url: 'javascript:alert(1)'}), null); assert.equal(m.nodeStatusEvidenceURL('javascript:alert(1)'), null); - const observed = view[id].observed_at; + const fresh = {...view[id], status: 'fresh', owner: null}; + const freshView = {...view, [id]: fresh}; + const unowned = m.resumeProjection(data, id, undefined, freshView); + assert.equal(ownerRow(unowned).text, 'No owner recorded in this node-status view.'); + assert.equal(ownerRow(unowned).url, undefined); + const observed = fresh.observed_at; const instant = Date.parse(observed); - const boundary = m.resumeProjection(data, id, instant + DAY, view); + const boundary = m.resumeProjection(data, id, instant + DAY, freshView); + assert.equal(noteOf(boundary), expectedNote(fresh, instant + DAY)); assert.match(noteOf(boundary), /Node-status view fresh as of /); assert.doesNotMatch(noteOf(boundary), /Node-status view stale/); - const past = m.resumeProjection(data, id, instant + DAY + 1, view); + const past = m.resumeProjection(data, id, instant + DAY + 1, freshView); assert.match(noteOf(past), /Node-status view stale as of /); assert.match(noteOf(past), /Recorded status fresh/); - const unknown = {[id]: {...view[id], status: 'unknown'}}; + const unknown = {[id]: {...fresh, status: 'unknown'}}; const overridden = m.resumeProjection(data, id, instant + DAY + 1, unknown); assert.match(noteOf(overridden), /Node-status view stale as of /); assert.match(noteOf(overridden), /Recorded status unknown/); @@ -308,10 +361,10 @@ test('resume projection shows a node-status view and refuses an owner without ev const ids = [...Object.keys(view), 'other']; const py = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids, records: view})}); assert.deepEqual(py, ids.map(node => m.resumeProjection(data, node, undefined, view).operational)); - const pyPast = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: view, now: instant + DAY + 1})}); - assert.deepEqual(pyPast, [m.resumeProjection(data, id, instant + DAY + 1, view).operational]); - const pyEdge = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: view, now: instant + DAY})}); - assert.deepEqual(pyEdge, [m.resumeProjection(data, id, instant + DAY, view).operational]); + const pyPast = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: freshView, now: instant + DAY + 1})}); + assert.deepEqual(pyPast, [m.resumeProjection(data, id, instant + DAY + 1, freshView).operational]); + const pyEdge = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: freshView, now: instant + DAY})}); + assert.deepEqual(pyEdge, [m.resumeProjection(data, id, instant + DAY, freshView).operational]); const pyScript = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: hostile})}); assert.deepEqual(pyScript, [plain.operational]); const pyOwner = pythonJson(['-c', script, tools], {input: JSON.stringify({data, ids: [id], records: ownerless})}); @@ -320,6 +373,36 @@ test('resume projection shows a node-status view and refuses an owner without ev assert.deepEqual(pyFlag, [m.resumeProjection(data, id, undefined, flagged).operational]); }); +test('a valid observed_at and source_rev refresh still validates and renders', async () => { + const view = await records(); + const used = new Set(Object.values(view).map(record => record.observed_at)); + const refreshedAt = used.has('2026-10-23T17:00:35Z') ? '2026-10-24T17:00:35Z' : '2026-10-23T17:00:35Z'; + const pin = 'e'.repeat(40); + const refreshed = Object.fromEntries(Object.entries(view).map(([id, record]) => { + const source = {...record.sources[0]}; + const current = source.url.match(/\/(?:blob|tree)\/([0-9a-f]{40})\//); + const nextPin = current && current[1] === pin ? 'f'.repeat(40) : pin; + const url = source.url.replace(/\/(blob|tree)\/[0-9a-f]{40}\//, `/$1/${nextPin}/`); + if (url !== source.url) { + source.url = url; + source.rev = nextPin; + } else source.rev = refreshedAt; + return [id, {...record, observed_at: refreshedAt, sources: [source, ...record.sources.slice(1)], source_rev: source.rev}]; + })); + for (const record of Object.values(refreshed)) { + const result = await validate(record); + assert.equal(result.status, 0, result.stderr); + assert.notEqual(record.observed_at, view[record.node_id].observed_at); + } + const id = Object.keys(refreshed)[0]; + const data = snapshot({[id]: {kind: 'candidate', classification: 'OPEN_ACTIVE'}}); + const now = Date.parse(refreshedAt); + const shown = m.resumeProjection(data, id, now, refreshed); + assert.equal(noteOf(shown), expectedNote(refreshed[id], now)); + assert.match(noteOf(shown), new RegExp(`as of ${refreshedAt}`)); + assert.equal(ownerRow(shown).text, expectedOwner(refreshed[id]).text); +}); + test('the atlas resume section passes the published node-status view into resumeProjection', async () => { const source = await readFile(new URL('../docs/site/atlas.mjs', import.meta.url), 'utf8'); assert.match(source, /fetch\('node-status\.json'/); diff --git a/tools/validate_node_status.py b/tools/validate_node_status.py index 1dbb55236..e9d44b762 100644 --- a/tools/validate_node_status.py +++ b/tools/validate_node_status.py @@ -37,6 +37,19 @@ PINNED_URL = re.compile( r"^https://github\.com/d6g8k5htny-coder/(main|Math-)/(blob|tree)/" r"[0-9a-f]{40}/[A-Za-z0-9_./-]+$") + + +def pinned_commit(url): + """Commit in the /blob// or /tree// segment, not elsewhere in the path.""" + if not isinstance(url, str) or PINNED_URL.fullmatch(url) is None: + return None + parts = url.split("/") + if len(parts) < 8 or parts[5] not in ("blob", "tree"): + return None + commit = parts[6] + if SHA.fullmatch(commit) is None: + return None + return commit GITHUB_OBJECT_URL = re.compile( r"^https://github\.com/d6g8k5htny-coder/(main|Math-)/(issues|pull)/[1-9]\d*" r"(#(issuecomment-[1-9]\d*|pullrequestreview-[1-9]\d*))?$") @@ -234,11 +247,12 @@ def check_source(source, index, problems): problems.append(f"{label} must have url and rev") return url, rev = source.get("url"), source.get("rev") - if isinstance(url, str) and PINNED_URL.fullmatch(url): + pin = pinned_commit(url) + if pin is not None: if not isinstance(rev, str) or not SHA.fullmatch(rev): problems.append(f"{label}.rev must be a 40-character commit") - elif rev not in url: - problems.append(f"{label}.rev must appear in its url") + elif rev != pin: + problems.append(f"{label}.rev must equal the blob or tree commit") return if isinstance(url, str) and GITHUB_OBJECT_URL.fullmatch(url): if not isinstance(rev, str) or not UTC_REV.fullmatch(rev):