AISA v2.0 / Technical documentation / validate_agent_map.py
validate_agent_map.py
Python · 280 lines · 31,000 bytes · wiki path 10 Architecture/ui/validate_agent_map.py · download the raw file · cited from Stage Agents and Tools · Decision register
Same folder: agent-catalog.json · agent_catalog.py · agent_map_svg.py · build_agent_map.py · build_case_content_docs.py · build_case_journeys.py · build_case_traces.py · build_operations_examples.py · case-content.json · case-journeys.json · case-runtime-traces.json · case-ui-specs.json · case_content.py · case_ui_model.py · frequent-operations.json · memory-capabilities.json · validate_case_content.py
"""Audit catalogue, graph, exact links and SVG text geometry. Not browser acceptance."""
from pathlib import Path
import json,re,xml.etree.ElementTree as ET
from _paths import ARCH,SITE,DOCS_DIRNAME,workspace_path
from agent_catalog import CATALOG,ENTITIES,PROFILE_IDS,anchor,page_rel,owner,entity_url
from agent_map_svg import build_svg,BOXES,SOURCES,SW,marked_stages,precipitates,marks_fit,gates_for,stage_title,visible_agents,cap_label,MODULE_ROOT,EDGE
from case_ui_model import MANIFESTS
count=0
def check(ok,message):
global count
count+=1
assert ok,message
check(len(ENTITIES)==len(CATALOG['entities']),'unique entity ids')
for key,e in ENTITIES.items():
check(bool(re.fullmatch(r'[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+',key)),'qualified id '+key)
check(not e['shared'] or key.startswith('common.'),'shared common namespace '+key)
for field in ('purpose','inputs','outputs','boundaries','sources','evidence_status'):
check(bool(e[field]),key+' '+field)
for field,kind in [('skills','skill'),('tools','tool')]:
for child in e.get(field,[]):check(child in ENTITIES and ENTITIES[child]['kind']==kind,key+' bound '+child)
for source in e['sources']:check(workspace_path(source['path']).is_file(),key+' source '+source['path'])
page=ARCH/page_rel(key)
check('id="'+anchor(key)+'"' in page.read_text(encoding='utf-8'),key+' exact documentation anchor')
for module,m in MANIFESTS.items():
for profile in m['profiles']:check(profile['key'] in PROFILE_IDS,'profile coverage '+profile['key'])
for stage in m['stages']:
ids=CATALOG['stages'][module+'/'+stage['id']]['agents']
check(PROFILE_IDS[stage['profile']] in ids,module+'/'+stage['id']+' primary covered')
check(all(k in ENTITIES for k in ids),module+'/'+stage['id']+' helpers resolve')
check(ENTITIES['configuration.source_analyst']['tools']==[] and not ENTITIES['configuration.source_analyst']['skills'][1:],'the independent source analyst stays source-semantic and DB-blind')
check(all(k.endswith('.builder') for k in ENTITIES if k.startswith('configuration.') and k.split('.')[-1] in ('planner','builder')),'a configuration domain profile plans and builds; nothing is left named planner')
check(all('configuration.'+d+'.builder' in CATALOG['stages']['configuration/'+s]['agents'] for d,s in [('abacus','S2'),('ipal','S3'),('offer','S4'),('serdica','S5')]),'each configuration domain stage is owned by its builder')
check(CATALOG['stages']['source/S3']['agents'][0]=='source.implementer' and next(s for s in MANIFESTS['source']['stages'] if s['id']=='S3')['profile']=='source.implementer','Implementation is owned by the generic implementer, not by one repository')
check(set(next(s for s in MANIFESTS['source']['stages'] if s['id']=='S3')['profile_variants'].values())>={'source.implementer.'+d for d in ('serdica-backend','serdica-ui','db-plsql','intentgpt')},'every declared source is a sibling variant')
check(ENTITIES['platform.intake']['kind']=='tool' and not ENTITIES['platform.intake']['skills'],'intake is poll/push, not an LLM agent')
check('common.skill.experience' in ENTITIES and 'common.tool.experience-search' not in ENTITIES,'experience retrieval is a strategy skill')
check(ENTITIES['common.skill.validation']['kind']=='skill' and 'common.validator' not in ENTITIES and all('common.skill.validation' in ENTITIES[k]['skills'] for k in ('support.validator','configuration.abacus.validator','configuration.integration.validator','source.validator')),'verification is a shared method bound by every domain validator')
check(sum(1 for c in ENTITIES.values() if c.get('consults'))>=15,'consults declared on the agents that ask other domains')
check(len(ENTITIES['source.test_runner']['tools'])==0,'test execution is runtime, not a fictional tool package')
applier=ENTITIES['common.effect-applier']
check(applier['kind']=='skill' and not applier['skills'] and not applier['tools'] and CATALOG['aliases']['common.skill.effect-application']=='common.effect-applier' and 'common.skill.effect-application' not in ENTITIES,'effect application is one published skill; the old procedure id is its alias (D200)')
check('common.write-auditor' not in ENTITIES and 'common.write-executor' not in ENTITIES and all(CATALOG['aliases'][k]=='common.effect-applier' for k in ('common.write-auditor','common.write-executor')),'the auditor and the executor are one component')
check(all('common.effect-applier' in ENTITIES[o]['skills'] for o in ('support.application','support.recovery','configuration.ipal.builder','configuration.deployer','source.deployer','support.root')),'the agent that owns an effectful stage runs the effect-application skill itself')
check(not any(k in s['agents'] for s in CATALOG['stages'].values() for k in ('common.db-prober','common.effect-applier')),'a skill is never listed among a stage\'s agents')
check(all(ENTITIES[k]['kind']=='skill' and not ENTITIES[k].get('runtime_profiles') and not ENTITIES[k].get('eval_set') for k in ('common.db-prober','common.effect-applier')),'measuring a database and applying a result are skills without a profile or an eval set (D200)')
check(all('common.db-prober' in ENTITIES[k]['skills'] for k in ('configuration.normalizer','configuration.ipal.builder','support.investigator','source.planner')),'the probe is bound by the agents that measure')
check('source.db_extractor' not in ENTITIES and ENTITIES['common.db-extractor']['shared'] and ENTITIES['common.db-extractor']['kind']=='agent' and 'common.db-extractor' in CATALOG['stages']['source/S1']['agents'] and CATALOG['aliases']['source.db_extractor']=='common.db-extractor','the stored-source extractor is a shared agent')
check(json.loads((ARCH/'contracts/connector-capability.schema.json').read_text(encoding='utf-8'))['properties']['application']['enum']==['endpoint','statement','publication','ui','person'],'the target declares the medium its effect is made through')
# five media, and the two the platform cannot cause are assigned and then measured
_conn=json.loads((ARCH/'contracts/connector-capability.schema.json').read_text(encoding='utf-8'))['properties']['application']['description']
check('caused' in _conn and 'assigned' in _conn and 'never on the actor' in _conn,'the medium fixes the behaviour, and an assigned action closes on a measurement')
_gate=(ARCH/'Gating.md').read_text(encoding='utf-8')
check(all(m in _gate for m in ('**endpoint**','**statement**','**publication**','**ui**','**person**')) and 'Two behaviours, one contract' in _gate,'every effect medium is stated with its evidence and its revert')
check(any("is an input to the check, not the check" in b for b in ENTITIES['common.effect-applier']['boundaries']),"a person's report is not proof that an effect landed")
for key,e in ENTITIES.items():
if e['kind']=='tool' and not e.get('runtime_profiles'):
check(bool(e.get('executable')),'real executable evidence '+key)
for field in ('source','binary'):
if e['executable'].get(field):check(workspace_path(e['executable'][field]).is_file(),'executable exists '+key)
if not key.startswith('common.'):
check('id="'+anchor(key)+'"' not in (ARCH/'50 Agents/Common Capabilities.md').read_text(encoding='utf-8'),'common catalogue does not duplicate '+key)
check('**[P] Function.**' not in (ARCH/'50 Agents/Agent Catalogue.md').read_text(encoding='utf-8'),'central page is references only')
PLATFORM=json.loads((ARCH/'contracts/platform-profiles.json').read_text(encoding='utf-8'))
declared_eval={s for m in MANIFESTS.values() for s in m['eval_sets']}|set(PLATFORM['eval_sets'])
kinds={p['key']:p['execution_kind'] for m in MANIFESTS.values() for p in m['profiles']}
kinds.update({p['key']:p['execution_kind'] for p in PLATFORM['profiles']})
for key,e in ENTITIES.items():
rps=e.get('runtime_profiles',[])
if not rps:continue
check(e['kind'] in ('agent','tool'),'only an agent or a tool carries a runtime profile '+key)
check(all(rp in kinds or rp in CATALOG.get('aliases',{}) for rp in rps),'runtime profile declared by a manifest or platform-profiles.json '+key)
if any(kinds.get(rp)=='llm' for rp in rps):check(e.get('eval_set') in declared_eval,'model profile names a declared eval set '+key)
else:check(not e.get('eval_set'),'deterministic handler has no eval set '+key)
for h in json.loads((ARCH/'ui/frequent-operations.json').read_text(encoding='utf-8'))['helper_consolidation']:
check(h['method'] in ENTITIES and ENTITIES[h['method']]['kind']=='skill','helper candidate binds to a catalogued method '+h['id'])
for seed in h['seeds']:check(workspace_path(seed).is_file(),h['id']+' seed exists '+seed)
operations=json.loads((ARCH/'ui/frequent-operations.json').read_text(encoding='utf-8'))['operations']
helper_ids={o['id'] for o in operations}
for h in json.loads((ARCH/'ui/frequent-operations.json').read_text(encoding='utf-8'))['helper_consolidation']:
for o in h['serves']:check(o in helper_ids,h['id']+' serves a catalogued operation '+o)
check(len({o['id'] for o in operations})==len(operations),'unique operation identities')
operation_page=(ARCH/'40 Support/Frequent Operations.md').read_text(encoding='utf-8')
for op in operations:
for field in ('trigger','evidence','outcome','profile','method','tool_opportunity','recurrence_basis','sources'):
check(bool(op[field]),op['id']+' '+field)
check('id="'+op['id'].replace('.','-')+'"' in operation_page,'exact operation link '+op['id'])
for source in op['sources']:check(workspace_path(source['path']).is_file(),op['id']+' cited source exists')
check(next(o for o in operations if o['id']=='op.desk.classification')['profile']=='support.root','operation inventory agrees with classification ownership')
svg=(SITE/'assets/agent-map.svg').read_text(encoding='utf-8')
check(svg==build_svg(),'SVG matches source')
check('Stage 2: Abacus' in svg and 'Stage 1: Triage, classification and route' in svg and 'Customer-specific development' in svg and 'Stage: Closure' in svg,'stage agents titled Stage X and the module renamed')
# the two nodes beside the root are identified by the agent they draw, not by their caption
check(all('data-entity="'+k+'"' in svg for k in ('support.triage','common.communicator')),'the classification adviser and the clarification agent flank Stage 1')
check(all(('data-stage=' in g) for g in re.findall(r'<g [^>]*class="am-node am-stage"[^>]*>',svg)),'every stage node names its stage')
boxes={x['id']:x for x in BOXES if x['kind']=='stage'}
check(set(boxes)==set(CATALOG['stages']),'all declared stage boxes represented')
check(all(boxes['support/S1'][k]==boxes['support/route'][k] for k in ('x','y','w','h')),'classification and route are one node')
check(boxes['support/closure']['y']>max(b['y'] for b in boxes.values() if b['id'] not in ('support/closure','data/S8')),'closure is the common end of every working lane')
check(boxes['data/S8']['y']>boxes['support/closure']['y'],'precipitation runs after the workflow, below closure')
envelopes={b['id']:b for b in BOXES if b['kind']=='envelope'}
for group,stages in [('configuration/delivery',['configuration/release']),('source/delivery',['source/S5','source/S6','source/S7','source/S8'])]:
env=envelopes[group]
for key in stages:
node=boxes[key]
check(env['x']<=node['x'] and node['x']+node['w']<=env['x']+env['w'] and env['y']<node['y'] and node['y']+node['h']<env['y']+env['h'],'stage belongs to its own envelope '+key)
check('data/learning' not in envelopes,'precipitation is no longer a Data-lane envelope')
# precipitation: declared on the stage, marked in the same place as a decision checkpoint, and one asynchronous workflow
marked=marked_stages()
check([(m,s) for m,s in marked]==[('data','S6'),('data','S7'),('configuration','joint'),('configuration','release'),('source','S4'),('source','S7'),('commercial','O4')],'the marked stages are the declared ones')
check(len({m for m,_ in marked})==4,'every working lane raises precipitation events')
check(not precipitates('data','S8'),'the precipitation stage does not mark itself')
check(svg.count('data-title="Precipitation event · asynchronous"')==len(marked)+1,'one mark per declared stage, plus the key inside the workflow')
for m,sid in marked:
owner=visible_agents(m,sid)[0]
on_title=marks_fit(stage_title(m,sid),gates_for(m,sid),SW,cap_label(owner),True)
check(on_title or (m,sid)==('configuration','release'),'the mark sits beside the title wherever it fits '+m+'/'+sid)
check('Decision checkpoints and precipitation' in svg,'the one node that cannot hold its marks beside the title keeps them on one labelled lane')
check(all(cap not in svg for cap in ('Shared execution boundary','Shared helpers and reviewed reuse','Next request ·')),'the three unread lanes are gone')
check('Precipitation · asynchronous · one workflow for every lane' in svg and 'Scheduled summarisation' in svg and 'Seed it for everyone' in svg,'the workflow reads scheduled summarisation, creation, human seeding')
check(svg.count('class="am-precip-flow"')==3,'the workflow runs horizontally, three steps of flow')
band=[b for b in BOXES if b['kind']=='stage' and b['id']=='data/S8'][0]
check(abs(band['y']-boxes['support/closure']['y'])>0 and band['x']==60+2*660,'the precipitation stage is the third step of the horizontal workflow')
check('sub-agents, skills and tools from v1 memory' in svg,'the memory-derived inventory moved with precipitation, not lost with the removed lanes')
check(next(e for e in MANIFESTS['data']['edges'] if e['from']=='S6' and e['to']=='S8')['kind']=='asynchronous' and precipitates('data','S6'),'the asynchronous S6 to S8 edge is drawn as the mark on S6')
check('Request needs assessment and an offer' not in svg,'no duplicate yellow offer branch')
check(MANIFESTS['support']['stages'][0]['profile']=='support.root','classification belongs to existing root')
check(next(s for s in MANIFESTS['data']['stages'] if s['id']=='S2')['profile']=='support.investigator','unknown domain does not default to transfers')
check('commercial.skill.prepare-offer' not in ENTITIES and ENTITIES['commercial.offer_preparer']['kind']=='agent','offer preparation belongs to an agent after assessment')
for sid,domain in [('S2','abacus'),('S3','ipal'),('S4','offer'),('S5','serdica')]:
ids=CATALOG['stages']['configuration/'+sid]['agents']
check('configuration.'+domain+'.builder' in ids and 'configuration.'+domain+'.validator' in ids,'domain build and independent validation '+sid)
check('configuration.integration.validator' not in ids,'joint validator is not substituted for domain validator '+sid)
check('No match, stale match, or contradicted experience' in svg and 'frequent-operations.html#operation-inventory' in svg,'fresh investigation and full operation inventory visible')
root=ET.fromstring(svg);ns={'s':'http://www.w3.org/2000/svg'}
for node in root.findall('.//*[@data-map-hint]'):
key=node.attrib.get('data-entity')
check('data-href' in node.attrib and '#' in node.attrib['data-href'],'each node has a technical target and anchor')
if key and 'data-stage' in node.attrib and 'data-variant' not in node.attrib:check('#stage-' in node.attrib['data-href'] and key in ENTITIES,'stage agent node links its stage page '+key)
elif key:check(node.attrib['data-href']==entity_url(key),'exact entity target '+key)
if 'data-variant' in node.attrib:
_m_,_s_=node.attrib['data-stage'].split('/');_decl=next(s for s in MANIFESTS[_m_]['stages'] if s['id']==_s_)
check(key in {PROFILE_IDS[p] for p in _decl['profile_variants'].values()} and key!=PROFILE_IDS[_decl['profile']],'a variant box is a declared profile variant of its stage, not its owner '+key)
gate_nodes=root.findall('.//s:g[@class="am-node am-gate"]',ns)
check(len(gate_nodes)>25,'gates are visible nodes')
check('Core Development is one offer type' in svg and 'Assessment & Offers' in svg,'Core belongs to assessment/offer flow')
check(all(name in svg for name,_,_,_ in SOURCES) and 'helpdesk' not in svg.lower(),'four declared sources feed intake; no channel is hard-wired')
check('Domain-agent consults' in svg and 'runtime protocol' in svg,'consult is a runtime protocol')
page=(SITE/'index.html').read_text(encoding='utf-8')
check('data-flow="core-development"' not in page,'no Core module card or pipeline')
check('id="agent-map-board"' in page and 'map-gate-legend' not in page and 'agent-map-tooltip' not in page,'the board replaces the floating brief and the gate legend row')
check('Executable helper:' not in svg and 'Case-selected domain method' not in svg,'methods and helpers are read in the board, not drawn inside nodes')
# the vocabulary: every actor says its class and how it runs, and only actors carry a profile id
from agent_catalog import node_class,runs_code
check(all(node_class(e) in ('agent','skill','tool') for e in ENTITIES.values()),'every entity is an agent, a skill or a tool')
check({node_class(ENTITIES[k]) for k in ('configuration.abacus.builder','common.skill.validation','common.tool.aisa-jira')}=={'agent','skill','tool'},'three classes, and each is distinct')
check(node_class(ENTITIES['common.effect-applier'])=='skill' and 'platform.effect_applier' not in kinds and 'configuration.estate_prober' not in kinds,'applying a result is a skill the owning stage agent runs; no runtime profile stands for it')
check({k for k,e in ENTITIES.items() if e.get('runtime_profiles') and runs_code(e)}=={'platform.intake','source.test_runner'},'the two tools the platform runs itself')
check(all(ENTITIES[k]['kind']=='tool' for k in ('platform.intake','source.test_runner')),'a profile the platform runs without a model is catalogued as a tool')
check(svg.count('agent · runs a model')>=25 and 'tool · code with a contract · no model' in svg,'each node states its class and what it runs')
check('what these mean' in page and 'agents.html#07-agent-skill-tool' in page,'the legend links to where the vocabulary is defined')
check('∞' not in svg and '∞' not in page,'no shared glyph: the common namespace already says it')
check('map-key skill' in page and 'map-key tool' in page and 'map-key root' in page and not any(k in page for k in ('map-key code','map-key method','map-key mechanism','map-key helper','map-key shared')),'the legend is module root, stage agent, utility agent, skill and tool')
_legend=re.search(r'<div class="map-legend"[^>]*>(.*?)</div>',page,re.S).group(1)
for _label in ('Orchestration agent','Stage agent','Utility agent','Skill','Tool','Decision gate','Precipitation event','flow','return or rework','may consult'):
check(_label in _legend,'legend names '+_label)
check('Module root' not in _legend and 'in the brief' not in _legend,'the legend says orchestration agent, and the skill key needs no qualifier: skills are drawn as miniatures')
_css=(SITE/'assets/agent-map.css').read_text(encoding='utf-8')
import re as _re
_keys={k:_re.search(r'\.map-key\.'+k+r'\{([^}]*)\}',_css).group(1) for k in ('root','stage','agent','skill','tool')}
# the legend is the renderer's own table: fill and border must be identical, key by key
from agent_map_svg import STYLE,TONE
for _k,(_f,_s) in [('root',TONE['root'][:2]),('stage',TONE['agent'][:2]),('agent',STYLE['agent'][:2]),('skill',STYLE['skill'][:2]),('tool',STYLE['tool'][:2])]:
check('background:'+_f+';' in _keys[_k]+';' and 'border-color:'+_s in _keys[_k],'legend key '+_k+' matches what the renderer draws')
check(len({_keys[k].split('border-radius')[0] for k in _keys})==5,'the five legend swatches differ in fill or border, not only in shape')
check('border-width:2px' in _keys['tool'] and svg.count('stroke="'+STYLE['tool'][1]+'" stroke-width="2"')==svg.count('stroke="'+STYLE['tool'][1]+'"'),'a tool box has a 2 px border on the map and in the legend: shape carries code, not colour alone')
# and every class the map draws has exactly one look; a note or frame never borrows one of them
_styles={};_notes=set()
for _m in _re.finditer(r'<g [^>]*class="am-node am-([\w-]+)"[^>]*>',svg):
_seg=svg[_m.end():svg.find('</g>',_m.end())];_r=_re.search(r'<rect [^>]*/>',_seg) # the node's own rect, never the next node's
if not _r:continue
_a=dict(_re.findall(r'([\w-]+)="([^"]*)"',_r.group(0)));_look=(_a.get('fill'),_a.get('stroke'),_a.get('rx'),_a.get('stroke-width'))
if _m.group(1)=='note':_notes.add(_look);continue
if _m.group(1) in ('gate','precip','module'):continue
_styles.setdefault(_m.group(1),set()).add(_look)
for _cls,_seen in _styles.items():
check(len(_seen)==1,'one style per class on the map: '+_cls+' has '+str(sorted(_seen)))
check(set(_styles)=={'agent','tool','stage','root'},'the map draws module roots, stage agents, utility agents and tools, and nothing unnamed')
check(not (_notes & set().union(*_styles.values())),'a note or frame never wears a capability look')
_precip_key=_re.search(r'\.map-key\.precip\{([^}]*)\}',_css).group(1)
check('border-radius:0 50% 50% 50%' in _precip_key and 'rotate(45deg)' in _precip_key,'the precipitation key is a drop with its point up, rotated like the mark on the map')
for _k,_pair in [('decision',('#fff5dc','#dbb464')),('precip',('#f8f3fe','#a98cc9'))]:
_v=_re.search(r'\.map-key\.'+_k+r'\{([^}]*)\}',_css).group(1)
check('background:'+_pair[0] in _v and 'border-color:'+_pair[1] in _v and _pair[0] in svg and _pair[1] in svg,'legend key '+_k+' matches the mark the renderer draws')
# the roles of the one class agent: the module roots and only they wear the root tone; every declared variant is drawn once, in the stage tone, inside its stage
_roots={_m.group(1) for _m in _re.finditer(r'<g [^>]*class="am-node am-root"[^>]*data-entity="([^"]+)"',svg)}
check(_roots==set(MODULE_ROOT.values()) and svg.count('class="am-node am-root"')==5,'the orchestration agents, and only they, wear the root tone: the four lane heads and Stage 1: '+str(sorted(_roots)))
check('data-stage="support/S1+route"' in svg[svg.find('class="am-node am-root"'):] and _re.search(r'class="am-node am-root"[^>]*data-entity="support.root" data-stage="support/S1\+route"',svg),'Stage 1 is owned by the Support root and wears the root tone')
_variants=_re.findall(r'<g [^>]*class="am-node am-agent"[^>]*data-entity="([^"]+)" data-stage="([^"]+)" data-variant="1"',svg)
_declared={(PROFILE_IDS[p],m+'/'+s['id']) for m in MANIFESTS for s in MANIFESTS[m]['stages'] for p in (s.get('profile_variants') or {}).values() if PROFILE_IDS[p]!=PROFILE_IDS[s['profile']]}
check(set(_variants)==_declared and len(_variants)==len(_declared)==10,'every declared profile variant is drawn once, white like any called agent, inside its stage')
check('Variants of the stage agent · one is selected by symptom domain' in svg and 'Variants of the stage agent · one per declared source' in svg and 'Selected investigator examples' not in svg,'a variant block says what selects the variant')
# the legend's miniatures on the nodes: a skill badge on every drawn agent that runs a skill, a tool badge on every one that reaches a tool
from agent_map_svg import tool_set
_drawn=[_m.group(2) for _m in _re.finditer(r'<g [^>]*class="am-node am-(agent|root|stage)"[^>]*data-entity="([^"]+)"',svg)]
check(svg.count('am-badge-skill')==sum(1 for k in _drawn if ENTITIES[k].get('skills')) and svg.count('am-badge-tool')==sum(1 for k in _drawn if tool_set(ENTITIES[k])),'every drawn agent carries the legend miniatures for the skills it runs and the tools it reaches')
check(svg.count('am-badge-skill')>=30 and svg.count('am-badge-tool')>=10,'skills and tools are visible on the map itself, not only in the brief')
check(' skills · ' not in svg and ' skill · ↔' not in svg and 'no skill or tool of its own' not in svg,'the runs line no longer spells the counts the badges carry')
check('Same commercial.assessor task continues' not in svg and 'One assessor task' in svg and svg.count('data-entity="commercial.assessor"')==1,'the assessor continuation is a note, not a second actor')
# marks and edges: one corner for every decision-gate chip; the legend edge keys carry the drawn strokes; no edge class that nothing draws
_gate_rx={_re.search(r'rx="(\d+)"',svg[_m.end():svg.find('</g>',_m.end())]).group(1) for _m in _re.finditer(r'<g [^>]*class="am-node am-gate"[^>]*>',svg)}
check(_gate_rx=={'7'},'every decision-gate chip has the same corner: '+str(_gate_rx))
for _k in ('flow','return'):
_v=_re.search(r'\.map-key\.'+_k+r'\{([^}]*)\}',_css).group(1)
check(EDGE[_k] in _v and 'stroke:'+EDGE[_k] in svg,'legend edge key '+_k+' carries the drawn stroke colour')
check('am-consult' not in svg,'consult is a line of node text, not an edge class nothing draws')
check(svg.count('data-mark="event"')==len(marked_stages())+1,'the precipitation mark is the only am-precip node that is an event')
# the retired words stay retired in the drawing and the brief: mechanism, method, helper are not class words here
for _w in ('Software mechanism','tested helper','method or helper','executable-helper','shared services','Also calls'):
check(_w not in svg,'retired wording stays out of the map: '+_w)
_js=(SITE/'assets/agent-map.js').read_text(encoding='utf-8')
check('Case-selected skill' in _js and 'Case-selected method' not in _js,'the brief says skill, never method')
check('source.test_runner' not in CATALOG['stages']['source/S4']['agents'] and 'source.test_runner' in CATALOG['stages']['source/S3']['agents'],'the producer runs the tests; the validator reads what ran')
check(svg.count('Shared (common.) it also calls · ')==sum(1 for k in CATALOG['stages'] if visible_agents(*k.split('/'))[3] and k not in ('support/closure','support/S1','support/route')),'every stage that calls a shared agent says so, and names the classes it calls')
check('id="07-agent-skill-tool"' in (SITE/DOCS_DIRNAME/'10-architecture/50-agents/agents.html').read_text(encoding='utf-8'),'that definition exists')
check('"urls"' in page,'the embedded catalogue carries entity URLs for the board')
CAPS=json.loads((ARCH/'ui/memory-capabilities.json').read_text(encoding='utf-8'))
cap_ids=[c['id'] for c in CAPS['capabilities']];check(len(cap_ids)==len(set(cap_ids)),'capability ids unique')
helper_ids_all={h['id'] for h in json.loads((ARCH/'ui/frequent-operations.json').read_text(encoding='utf-8'))['helper_consolidation']}
cap_page=(ARCH/'50 Agents/Memory Derived Capabilities.md').read_text(encoding='utf-8')
agent_caps={c['id'] for c in CAPS['capabilities'] if c['kind']=='agent'}
for c in CAPS['capabilities']:
check(c['kind'] in ('tool','skill','article','agent') and c['status'] in ('existing','candidate','article'),'capability kinds '+c['id'])
check((c['status']=='existing')==bool(c.get('existing_ref')),'existing rows name their catalogue id '+c['id'])
if c.get('existing_ref'):check(c['existing_ref'] in ENTITIES or c['existing_ref'] in helper_ids_all,'existing_ref resolves '+c['id'])
for seed in c['seeds']:check(workspace_path(seed).is_file(),c['id']+' seed exists '+seed)
check('id="mdc-'+re.sub(r'[^A-Za-z0-9]+','-',c['id'])+'"' in cap_page,'capability row anchored '+c['id'])
owner=c['owner'];check(owner in ENTITIES or owner in agent_caps or owner=='workspace' or '/' in owner or owner in ('support','configuration','source','platform','commercial'),'capability owner resolves '+c['id']+' '+owner)
import collections as _c
check(CAPS['census']['total']==len(cap_ids) and dict(_c.Counter(c['kind'] for c in CAPS['capabilities']))==CAPS['census']['by_kind'] and dict(_c.Counter(c['status'] for c in CAPS['capabilities']))==CAPS['census']['by_status'],'capability census matches the rows')
check('"capabilities"' in page,'the embedded catalogue carries the memory-derived candidates')
embedded=re.search(r'<!-- BEGIN DETAILED AGENT MAP -->\s*(.*?)\s*<!-- END DETAILED AGENT MAP -->',page,re.S).group(1)
check(embedded==svg.replace('href="../'+DOCS_DIRNAME+'/','href="'+DOCS_DIRNAME+'/'),'inline/export parity')
traces=json.loads((ARCH/'ui/case-runtime-traces.json').read_text(encoding='utf-8'))
for cid,trace in traces.items():
stages=trace['stage_traces']
owners=[next(i for i in stages[k]['agent_initializations'] if i['profile_key']=='support.root') for k in ('main/support/S1','main/support/route')]
check(owners[0]['task_id']==owners[1]['task_id'] and owners[0]['stage_id'] is None and owners[1]['stage_id'] is None,cid+' persistent root through classification and route')
check(not any(i['profile_key']=='support.triage' for i in trace['agent_initializations']),cid+' no mandatory classification adviser')
check('Create root and triage tasks' not in json.dumps(trace),cid+' startup prose uses one root')
for s in trace['journey']['paths']['first']:
if s['module']=='configuration' and s['stage'] in ('S2','S3','S4','S5'):
domain={'S2':'abacus','S3':'ipal','S4':'offer','S5':'serdica'}[s['stage']]
check(any(i['profile_key']=='configuration.'+domain+'.validator' for i in trace['agent_initializations']),cid+' prepared domain verification '+domain)
for cid in ('DV-09','DV-16'):
j=traces[cid]['journey']
check(j['ui']['working_module']=='commercial' and j['ui']['core_terms_pending'],cid+' pending assessed offer')
check([s['stage'] for s in j['paths']['first']]==['O1','O2','O3','O4'],cid+' complete proposal flow')
check(not any(s['module']=='source' for s in j['paths']['first']+j['auxiliary']+j['linked']),cid+' no implementation')
check(not any(i['profile_key'].startswith('source.') for i in traces[cid]['agent_initializations']),cid+' no Source agents')
stages=traces[cid]['stage_traces']
check(any(i['profile_key']=='commercial.assessor' for i in stages['main/commercial/O1']['agent_initializations']),cid+' one assessor task carries clarification and assessment')
initial={i['profile_key']:i for i in traces[cid]['agent_initializations']}
check(all(initial[k]['parent_task_id']==initial['commercial.coordinator']['task_id'] for k in ('commercial.assessor','commercial.offer_preparer')),cid+' commercial work returns to its actual immediate parent')
try:
from PIL import ImageFont
regular=Path('C:/Windows/Fonts/segoeui.ttf');bold=Path('C:/Windows/Fonts/seguisb.ttf')
if not regular.exists():raise ImportError
except ImportError:print('Font-width check unavailable on this host.')
else:
for node in root.findall('.//s:g',ns):
r=node.find('s:rect',ns)
if r is None:continue
x,y,w,h=[float(r.attrib[k]) for k in ('x','y','width','height')]
for t in node.findall('s:text',ns):
tx,ty=[float(t.attrib[k]) for k in ('x','y')]
font=ImageFont.truetype(str(bold if int(t.attrib['font-weight'])>=600 else regular),int(round(float(t.attrib['font-size']))))
value=''.join(t.itertext())
check(tx+font.getlength(value)<=x+w+2,'Text exceeds node width: '+value)
check(ty<=y+h-2,'Text exceeds node height: '+value)
print(str(count)+' catalogue/graph/link/geometry checks pass; not browser acceptance.')