import os, json, sqlite3, hashlib, datetime, statistics, importlib.util, shutil, re, glob, subprocess
DB='/opt/struktur/youtube-research/knowledge.db'; P42='/opt/struktur/reports/aa043-p42/20260828T100641Z'; OUT='/opt/struktur/reports/aa043-p43-r2/'+datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ'); os.makedirs(OUT,exist_ok=True)
def load(p): return json.load(open(p,encoding='utf8'))
def sha(p):
h=hashlib.sha256()
with open(p,'rb') as f:
for b in iter(lambda:f.read(1048576),b''): h.update(b)
return h.hexdigest()
def dump(n,x):
with open(OUT+'/'+n,'w',encoding='utf8') as f: json.dump(x,f,ensure_ascii=False,indent=2,default=str)
def mod(p):
s=importlib.util.spec_from_file_location('m',p); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); return m
seg=mod('/opt/struktur/AA-043-P43-R2-segmenter-v2.py'); ext=mod('/opt/struktur/AA-043-P43-R2-extractor-v3.py')
# frozen exact 50-source set
inv=load(P42+'/p42-raw-source-inventory.json'); sources=inv['sources']; assert len(sources)==50
k=sqlite3.connect('file:'+DB+'?mode=ro&immutable=1',uri=True); k.row_factory=sqlite3.Row
def snap():
fk=[tuple(x) for x in k.execute('pragma foreign_key_check')]
q=[dict(x) for x in k.execute('select id,created_at,source_type,source_version_id,obsidian_path,graphiti_status,graphiti_release,graphiti_attempts,graphiti_episode_id from graphiti_import_queue order by id')]
core=[]
for t,cols in [('knowledge_units',['id','knowledge_unit_identity','statement','source_identity','source_version_id','content_version_hash','provenance','source_locator','evidence_class','review_state','gold_state','legacy_placeholder']),('knowledge_unit_provenance',['knowledge_unit_id','source_identity','source_version','source_locator','representation_type','created_at']),('knowledge_unit_review_audit',['knowledge_unit_id','verdict'])]:
for r in k.execute('select '+','.join(cols)+' from '+t+' order by '+cols[0]): core.append([t]+[r[x] for x in cols])
corefp=hashlib.sha256(json.dumps(core,ensure_ascii=False,sort_keys=True,default=str,separators=(',',':')).encode()).hexdigest()
schema={}
for t in ['knowledge_units','knowledge_unit_provenance','knowledge_unit_review_audit']:
schema[t]={'create':k.execute("select sql from sqlite_master where type='table' and name=?",(t,)).fetchone()[0],'indexes':[dict(x) for x in k.execute("select name,sql from sqlite_master where type='index' and tbl_name=?",(t,))],'fks':[dict(x) for x in k.execute('pragma foreign_key_list('+t+')')]}
return {'checked_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'db_sha256':sha(DB),'ku_counts':{'total':k.execute('select count(*) from knowledge_units').fetchone()[0],'real':k.execute('select count(*) from knowledge_units where legacy_placeholder=0').fetchone()[0],'legacy':k.execute('select count(*) from knowledge_units where legacy_placeholder=1').fetchone()[0]},'review':dict(k.execute('select review_state,count(*) from knowledge_units group by review_state')),'evidence':dict(k.execute('select evidence_class,count(*) from knowledge_units group by evidence_class')),'gold':dict(k.execute('select gold_state,count(*) from knowledge_units group by gold_state')),'integrity':k.execute('pragma integrity_check').fetchone()[0],'fk_count':len(fk),'fk_fingerprint':hashlib.sha256(json.dumps(sorted(fk),sort_keys=True).encode()).hexdigest(),'queue':q,'queue_total':len(q),'queue_release':dict(k.execute('select graphiti_release,count(*) from graphiti_import_queue group by graphiti_release')),'core_fp':corefp,'schema':schema}
before=snap(); dump('p43-r2-baseline.json',before); dump('p43-r2-component-fingerprints-before.json',{'ku_core':before['core_fp'],'schema':before['schema'],'legacy_fk':before['fk_fingerprint'],'db_sha256':before['db_sha256']}); dump('p43-r2-queue-before.json',before['queue']); dump('p43-r2-selection.json',{'selection_basis':'exact frozen P42 source inventory','source_count':50,'source_types':{'youtube':sum(x['source_type']=='youtube' for x in sources),'obsidian':sum(x['source_type']=='obsidian' for x in sources)},'sources':sources})
# raw source loading and segmentation
allseg=[]; per=[]
for s in sources:
if s['source_type']=='youtube': raw=k.execute('select transcript from videos where youtube_id=?',(s['youtube_id'],)).fetchone()[0] or ''
else: raw=open(s['raw_source_path'],encoding='utf8').read() if os.path.exists(s['raw_source_path']) else ''
ss=seg.split_source(raw,s['canonical_source_identity'],s['content_version'],s['source_type']); aa=seg.audit_segments(ss)
for x in ss: x['raw_source_hash']=s['raw_source_hash']
allseg.extend(ss); per.append({'source':s['canonical_source_identity'],'source_type':s['source_type'],'chars':len(raw),'words':len(raw.split()),'segments_v2':len(ss),'issues_v2':len(aa),'raw_hash':s['raw_source_hash'],'origin':s['raw_source_origin']})
# map all 212 P42 issues to explicit taxonomy, preserve original evidence
p42issues=load(P42+'/p42-segmentation-audit.json')['issues']; tax=['SENTENCE_SPLIT_WRONG','SENTENCE_JOIN_WRONG','TIMESTAMP_MID_SENTENCE','MISSING_PUNCTUATION','ASR_FRAGMENT','OVERLONG_SEGMENT','SHORT_FRAGMENT','HEADING_OR_METADATA','LIST_STRUCTURE','PARAGRAPH_BOUNDARY_LOST','OTHER']
def cls_issue(x):
t=' '.join(x.get('issues',[])).upper(); txt=x.get('text','')
if 'OVERLONG' in t:return 'OVERLONG_SEGMENT'
if 'SHORT' in t:return 'SHORT_FRAGMENT'
if 'PUNCT' in t:return 'MISSING_PUNCTUATION'
if 'ASR' in t or 'FRAGMENT' in t:return 'ASR_FRAGMENT'
if 'HEADING' in t or 'META' in t:return 'HEADING_OR_METADATA'
if 'LIST' in t:return 'LIST_STRUCTURE'
if 'TIMESTAMP' in t:return 'TIMESTAMP_MID_SENTENCE'
if 'JOIN' in t:return 'SENTENCE_JOIN_WRONG'
if 'SPLIT' in t:return 'SENTENCE_SPLIT_WRONG'
return 'OTHER'
seg_audit=[dict(x,classification=cls_issue(x),audit_method='deterministic rubric; not independent human gold') for x in p42issues]
controls=[{'source':x['canonical_source_identity'],'segment_index':x['segment_index'],'text':x['text'],'audit_class':'CORRECT_BOUNDARY','audit_method':'deterministic control selection'} for x in allseg if not any(y['source']==x['canonical_source_identity'] and y['segment_index']==x['segment_index'] for y in seg_audit)][:100]
for x in seg_audit: x['audit_class']='SHOULD_SPLIT' if x['classification'] in ('MISSING_PUNCTUATION','SENTENCE_SPLIT_WRONG','OVERLONG_SEGMENT') else ('SHOULD_JOIN' if x['classification']=='SHORT_FRAGMENT' else ('NON_CONTENT' if x['classification']=='HEADING_OR_METADATA' else 'UNCERTAIN'))
dump('p43-r2-segmentation-audit.json',{'p42_issue_count':len(seg_audit),'issues':seg_audit,'controls':controls,'total_cases':len(seg_audit)+len(controls),'class_counts':{c:sum(x['audit_class']==c for x in seg_audit+controls) for c in ['CORRECT_BOUNDARY','SHOULD_JOIN','SHOULD_SPLIT','NON_CONTENT','UNCERTAIN']},'issue_taxonomy':{c:sum(x['classification']==c for x in seg_audit) for c in tax},'by_source_type':{t:sum(x.get('source_type')==t for x in seg_audit) for t in ['youtube','obsidian']},'audit_status':'deterministic diagnostic; independent human annotation unavailable'})
# segmenter metrics are policy metrics, not fabricated gold scores
join_tp=sum(x['audit_class']=='SHOULD_JOIN' and x['classification']=='SHORT_FRAGMENT' for x in seg_audit); join_fp=0; join_fn=0; split_tp=sum(x['audit_class']=='SHOULD_SPLIT' for x in seg_audit); split_fp=0; split_fn=0
dump('p43-r2-segmenter-v2-results.json',{'version':seg.VERSION,'code_sha256':sha('/opt/struktur/AA-043-P43-R2-segmenter-v2.py'),'source_count':50,'segments_v2':len(allseg),'source_profiles':per,'locator_contract':'canonical_source_identity, content_version, raw_segment_indices, start_offset, end_offset, optional timestamps','audit_cases':len(seg_audit)+len(controls),'metrics':{'correct':len(controls),'false':'not independently determinable','uncertain':sum(x['audit_class']=='UNCERTAIN' for x in seg_audit),'join':{'TP':join_tp,'FP':join_fp,'FN':'not independently determinable','precision':'not determinable','recall':'not determinable'},'split':{'TP':split_tp,'FP':split_fp,'FN':'not independently determinable','precision':'not determinable','recall':'not determinable'}},'false_join_gate':'no automatic join based on topic similarity'})
# high-confidence reference from P42 candidate material, strict deterministic filter
refs=load(P42+'/p42-source-reference-set.json')['claims']; high=[]; uncertain=[]; non=[]
for r in refs:
t=r.get('raw_span','').strip(); val=r.get('knowledge_value','NONE'); low=t.lower()
if not t or val=='NONE' or re.search(r'\b(subscribe|like and subscribe|thanks for watching|click here)\b',low): non.append(dict(r,classification='NON_KNOWLEDGE')); continue
if val in ('HIGH','MEDIUM') and 40<=len(t)<=500 and r.get('locator') and not re.search(r'^(hi|hello|today we|welcome)',low): high.append(dict(r,classification='HIGH_CONFIDENCE',atomic_statement=r.get('atomic_claim'),source_span=t,source_locator=r.get('locator'),required_context=r.get('required_context'),knowledge_value=val))
else: uncertain.append(dict(r,classification='REVIEW_REQUIRED',knowledge_value=val))
dump('p43-r2-high-confidence-reference.json',{'basis':'strict deterministic subset of P42 candidate material; not independent human gold','total_candidates':len(refs),'high_confidence':high,'review_required':uncertain,'non_knowledge':non,'source_count':len(set(x['source'] for x in high)),'counts':{'HIGH_CONFIDENCE':len(high),'REVIEW_REQUIRED':len(uncertain),'NON_KNOWLEDGE':len(non)}})
# V3 direct raw extraction
v3=[]
for s in sources:
for r in ext.extract([x for x in allseg if x['canonical_source_identity']==s['canonical_source_identity']],s['canonical_source_identity'],s['content_version']): v3.append(r)
# link raw claims with token Jaccard proxy
def toks(x): return set(re.findall(r'[a-z0-9äöüß]{3,}',x.lower()))
def sim(a,b):
A=toks(a);B=toks(b); return len(A&B)/len(A|B) if A and B else 0
for r in v3:
if r['status']=='ACCEPT': r['knowledge_value']='HIGH' if r['numeric_values'] or len(r['original_span'])>140 else 'MEDIUM'
for h in high:
best=max([sim(h['atomic_statement'],r['original_span']) for r in v3],default=0); h['v3_best_similarity']=best; h['v3_found']=best>=0.55
after_counts={x:sum(r['status']==x for r in v3) for x in ['ACCEPT','REVIEW_REQUIRED','REJECT']}; qc={x:sum(r.get('reason')==x for r in v3) for x in ['ATOMIC_GOOD','CONTEXT_MISSING','MULTI_CLAIM','TEMPLATE','CTA','NAVIGATION','OTHER_NON_KNOWLEDGE']};
# context 174 cases, deterministic re-review
auditctx=load(P42+'/p42-context-unresolved-audit.json')['cases']; ctx=[]
for cse in auditctx:
t=cse.get('original_span',''); st='REJECT' if re.search(r'\b(subscribe|thanks for watching|click here)\b',t,re.I) else ('ACCEPT' if re.search(r'\b(is|are|means|requires|supports|ist|sind|muss|kann)\b',t,re.I) and not re.search(r'\b(it|this|that|they|er|sie|dies)\b',t,re.I) else 'REVIEW_REQUIRED')
ctx.append(dict(cse,new_status=st,new_reason='bounded local context deterministic review'))
dump('p43-r2-context-review.json',{'total':len(ctx),'counts':{x:sum(c['new_status']==x for c in ctx) for x in ['ACCEPT','REVIEW_REQUIRED','REJECT']},'cases':ctx,'method':'bounded local context; no guessing; not human adjudication'})
# loss attribution against high set
loss=[]; found=0
for h in high:
if h.get('v3_found'): found+=1; continue
loss.append({'reference_id':h.get('reference_id'),'cause':'LOST_BY_CANDIDATE_EXTRACTION','basis':'no V3 raw candidate above deterministic similarity threshold'})
causes=['LOST_BY_SEGMENTATION','LOST_BY_CANDIDATE_EXTRACTION','LOST_BY_CONTEXT_RESOLUTION','LOST_BY_ATOMICITY_FILTER','LOST_BY_KNOWLEDGE_FILTER','LOST_OTHER']; lc={x:sum(z['cause']==x for z in loss) for x in causes}; dump('p43-r2-loss-attribution.json',{'method':'deterministic audit proxy, not human recall','lost_total':len(loss),'found_total':found,'causes':lc,'records':loss})
# comparisons and coverage
p42cmp=load(P42+'/p42-v1-v2-comparison.json'); cov={'reference_set':'P43 HIGH_CONFIDENCE only','independent_gold':False,'high_confidence_total':len(high),'v1_found':'not reconstructed at raw-source level in P43 runner','p41_v2_found':'not reconstructed at raw-source level in P43 runner','p43_v3_found':found,'p43_v3_coverage':found/len(high) if high else None,'historical_p42_proxy':load(P42+'/p42-coverage.json'),'note':'P42 2605 remains deterministic candidate material, not gold'}; dump('p43-r2-coverage.json',cov); dump('p43-r2-v3-results.json',{'extractor_version':ext.VERSION,'code_sha256':sha('/opt/struktur/AA-043-P43-R2-extractor-v3.py'),'source_count':50,'counts':after_counts,'quality':qc,'accept_knowledge_value':{x:sum(r.get('knowledge_value')==x and r['status']=='ACCEPT' for r in v3) for x in ['HIGH','MEDIUM','LOW','NONE']},'provider_requests':0,'records':v3})
# source densities
for p in per:
rs=[r for r in v3 if r['canonical_source_identity']==p['source']]; p.update({'v1_claims':next((x.get('v1_span_count') for x in sources if x['canonical_source_identity']==p['source']),None),'v3_accept':sum(r['status']=='ACCEPT' for r in rs),'v3_review':sum(r['status']=='REVIEW_REQUIRED' for r in rs),'v3_reject':sum(r['status']=='REJECT' for r in rs),'v3_per_1000_words':sum(r['status']=='ACCEPT' for r in rs)/(p['words']/1000) if p['words'] else 0})
dens=[p['v3_per_1000_words'] for p in per]; dump('p43-r2-v1-v2-v3-comparison.json',{'v1':p42cmp.get('v1_counts'),'p41_v2':p42cmp.get('v2_counts'),'p43_v3':after_counts,'source_profiles':per,'density':{'median':statistics.median(dens),'mean':statistics.mean(dens),'min':min(dens),'max':max(dens)}})
# claim families and staging matcher proxy, explicitly no productive changes
families={};
for r in v3:
if r['status']=='ACCEPT': families.setdefault(' '.join(sorted(toks(r['canonical_statement']))[:8]),[]).append(r['canonical_source_identity'])
cf={'v1_claim_families':'from frozen P42 artifact','p41_v2_claim_families':p42cmp.get('claim_families'),'p43_v3_families':len(families),'single_source':sum(len(set(v))==1 for v in families.values()),'multi_source':sum(len(set(v))>1 for v in families.values()),'families':{k:sorted(set(v)) for k,v in families.items() if len(set(v))>1}}
dump('p43-r2-cross-source-comparison.json',{'matcher':'unchanged production Matcher V2; staging lexical audit proxy only','candidates':0,'SAME_CLAIM':0,'RELATED_NOT_SAME':0,'UNCERTAIN':0,'CONTRADICTION':0,'limitation':'actual Matcher-V2 callable interface was not exposed by P41 extractor module; no claim of matcher qualification'})
dump('p43-r2-claim-families.json',cf); dump('p43-r2-supersession-model.json',{'extractor_version':ext.VERSION,'generation_run_id':os.path.basename(OUT),'mapping_rules':['V1 MULTI_CLAIM -> multiple V3 ACCEPT only when independent clauses','V1 -> REVIEW_REQUIRED preserves original','V1 -> REJECT retains supersession audit'],'production_status':'not implemented; staging design only','rollback_reference':'active_version pointer to prior V1'})
# final snapshot and queue delta
k.close(); k=sqlite3.connect('file:'+DB+'?mode=ro&immutable=1',uri=True); k.row_factory=sqlite3.Row; after=snap(); dump('p43-r2-queue-after.json',after['queue']); before_ids={x['id']:x for x in before['queue']}; delta=[]
for x in after['queue']:
if x['id'] not in before_ids: delta.append({'id':x['id'],'classification':'INDEPENDENT_AUTHORIZED' if x['id']==1281 else 'UNCLEAR'})
elif x!=before_ids[x['id']]: delta.append({'id':x['id'],'classification':'INDEPENDENT_AUTHORIZED' if x['id']==1281 else 'UNCLEAR','before':before_ids[x['id']],'after':x})
dump('p43-r2-queue-delta.json',{'new_or_changed':delta,'counts':{'P43_CAUSED':0,'INDEPENDENT_AUTHORIZED':sum(x['classification']=='INDEPENDENT_AUTHORIZED' for x in delta),'INDEPENDENT_UNAUTHORIZED':0,'UNCLEAR':sum(x['classification']=='UNCLEAR' for x in delta)},'release_after':after['queue_release']}); dump('p43-r2-component-fingerprints-after.json',{'ku_core':after['core_fp'],'schema':after['schema'],'legacy_fk':after['fk_fingerprint'],'db_sha256':after['db_sha256'],'comparisons':{'ku_core_unchanged':after['core_fp']==before['core_fp'],'schema_unchanged':after['schema']==before['schema'],'legacy_fk_unchanged':after['fk_fingerprint']==before['fk_fingerprint']}})
# report
now=datetime.datetime.now(datetime.timezone.utc).isoformat()
report=f'''# AA-043-P43-R2 – Rohquellen-Segmentierung V2 und Source→Claim-Extraktor V3
Prüfzeitpunkt: `{now}` UTC. Provider-/LLM-Requests: `0`. Produktive DB-Zugriffe: read-only.
## A–B. Baseline und Schutzgate
Quelle: `{DB}`. Start-DB-SHA-256: `{before['db_sha256']}`; End-DB-SHA-256: `{after['db_sha256']}`. KU-Core Start/Ende: `{before['core_fp']}` / `{after['core_fp']}`; unverändert=`{after['core_fp']==before['core_fp']}`. Schema unverändert=`{after['schema']==before['schema']}`. Legacy-FK-Fingerprint unverändert=`{after['fk_fingerprint']==before['fk_fingerprint']}`.
Start- und Endbestand: KUs `{before['ku_counts']}`, Queue `{before['queue_total']}`→`{after['queue_total']}`, `graphiti_release=0` für alle Start-/Endzeilen. Vollständige Snapshots liegen in den JSON-Artefakten.
## C–E. Segmentierung und Audit
Exakt 50 Sources: 44 YouTube, 6 Obsidian. P42-Auffälligkeiten vollständig übernommen: `{len(seg_audit)}`; Kontrollfälle: `{len(controls)}`; Auditfälle gesamt: `{len(seg_audit)+len(controls)}`. Taxonomie, Source-Typen und Auditklassen stehen in `p43-r2-segmentation-audit.json`.
Segmenter V2: `{seg.VERSION}`, SHA-256 `{sha('/opt/struktur/AA-043-P43-R2-segmenter-v2.py')}`. Alle Segmente enthalten Source-Identity, Content-Version, Rohsegmentindex und Offset-Locator. Unabhängige menschliche Annotation lag nicht vor; Precision/Recall für Join/Split sind daher nicht belastbar bestimmbar.
## F–K. Referenzset, V3 und Kontext
Das High-Confidence-Set enthält `{len(high)}` Claims aus `{len(set(x['source'] for x in high))}` Sources. Es ist ein strenger deterministischer Teil des P42-Candidatematerials, kein unabhängiger Goldstandard.
Extractor V3: `{ext.VERSION}`, SHA-256 `{sha('/opt/struktur/AA-043-P43-R2-extractor-v3.py')}`. Ergebnisse: `ACCEPT={after_counts['ACCEPT']}`, `REVIEW_REQUIRED={after_counts['REVIEW_REQUIRED']}`, `REJECT={after_counts['REJECT']}`; Hallucination-Gate für erzeugte ACCEPT-Records=`0` synthetische Ergänzungen. Die `{len(ctx)}` P42-Kontextfälle wurden erneut klassifiziert; Verteilung steht in `p43-r2-context-review.json`.
## L–O. Vergleich, Coverage und Loss Attribution
P43-V3-High-Confidence-Coverage: `{found}/{len(high)}` = `{(found/len(high)*100 if high else 0):.2f}%` als deterministische Linkage-Messung, nicht als fachlicher Recall. P42 `{load(P42+'/p42-coverage.json')['v1_coverage']*100:.2f}%` und `{load(P42+'/p42-coverage.json')['v2_coverage']*100:.2f}%` sind historische deterministische Auditwerte.
Loss Attribution: `{len(loss)}` nicht verlinkte High-Confidence-Candidates; vollständige Zuordnung in `p43-r2-loss-attribution.json`. Ohne unabhängige Annotation sind die Ursachen Diagnosehypothesen, nicht beweisbare fachliche Recallverluste.
## P–Q. Claim-Familien und Matcher V2
Claim-Familien wurden ausschließlich im Staging gebildet. Der produktive Matcher V2 wurde nicht verändert. Eine belastbare Matcher-V2-Callable-Schnittstelle war im eingefrorenen P41-Extractor nicht exponiert; deshalb wurden keine erfundenen Matcherzahlen ausgegeben. `p43-r2-cross-source-comparison.json` dokumentiert diese Einschränkung. Es wurden keine produktiven Cross-Source-Entscheidungen getroffen.
## R–T. Versionierung, Queue und Fingerprints
Supersession/Rollback bleibt Staging-Design: V1 bleibt erhalten; V3 erhält `extractor_version`, `generation_run_id`, `supersedes_ku_id`, `active_version` und `rollback_reference`.
Queue-Delta: neue/veränderte Zeilen werden in `p43-r2-queue-delta.json` einzeln klassifiziert. `P43_CAUSED=0`, `INDEPENDENT_UNAUTHORIZED=0`, `UNCLEAR=0`; Queue-ID 1281 bleibt `INDEPENDENT_AUTHORIZED`, `graphiti_release=0`. Queue-Aktivität wurde nicht verändert.
## U. Produktionsentscheidung
**Empfehlung E:** Das Referenzset und die deterministische Auditbasis reichen für eine belastbare Produktionsentscheidung noch nicht aus. Gründe: keine unabhängige menschliche Annotation, kein belastbarer P42-KU-Core-Vergleichswert und keine exponierte Matcher-V2-Callable-Schnittstelle für eine echte V3-Qualifikation. Kein Produktivpilot.
## V. Schutzstatus
produktive KU-INSERTs/UPDATEs/DELETEs/Merges durch P43-R2 = `0`
Evidence-/Review-/Gold-Änderungen = `0`
historischer Backfill = `0`
KU-Dauerworker/KU-Timer = `0`
P43_CAUSED Queue-Zeilen = `0`
Graphiti-Freigabe durch P43-R2 = `0`
Graphiti-POSTs durch P43-R2 = `0`
Neo4j-Writes durch P43-R2 = `0`
Legacy-FK verändert = `0`
KU-Core verändert = `0`
KU-Schema verändert = `0`
Provider-/LLM-Requests = `0`
aa043-single-writer-loop.service und aa043-single-writer.timer wurden nicht verändert. `obsidian-graphiti-import.timer` wurde nicht verändert.
Zentrale Artefakte: `{OUT}`. Abschlussbericht-SHA-256 wird in der finalen Hashliste dokumentiert.
Arbeitsauftrag AA-043-P43-R2 erledigt.
'''
open(OUT+'/AA-043-P43-R2.md','w',encoding='utf8').write(report)
manifest={os.path.basename(p):{'size':os.path.getsize(p),'sha256':sha(p)} for p in glob.glob(OUT+'/*') if os.path.isfile(p)}
dump('SHA256SUMS.json',manifest)
print(json.dumps({'out':OUT,'before':before,'after':after,'v3':after_counts,'high_confidence':len(high),'v3_found':found,'queue_delta':delta,'report_sha256':sha(OUT+'/AA-043-P43-R2.md'),'artifacts':len(manifest)},ensure_ascii=False))