// @requirements REQ-PRODUCT-SERVICE-PROFILES /** @requirements REQ-PRODUCT-SERVICE-CATALOG REQ-PRODUCT-CF-ADMIN REQ-PRODUCT-CF-HISTORY */ import {renderCatalogProductProfile} from './catalog-profile.js'; import type {EvidenceRecord,JsonValue} from './types.js'; import type {RenderOptions} from './render.js'; import {escapeHtml,renderWorkspaceShell} from './render.js'; import {catalogReviewCanonical} from './catalog-release-policy.js'; const obj=(v:JsonValue|undefined):Record=>v&&typeof v==='object'&&!Array.isArray(v)?v:{}; const array=(v:JsonValue|undefined):JsonValue[]=>Array.isArray(v)?v:[]; const text=(v:JsonValue|undefined)=>typeof v==='string'?v:'Not recorded'; const e=(v:JsonValue|undefined)=>escapeHtml(text(v)); const url=(v:JsonValue|undefined)=>{try{const u=new URL(text(v));return u.protocol==='https:'&&!u.username&&!u.password?escapeHtml(u.href):undefined;}catch{return undefined;}}; function inspect(record:EvidenceRecord){ const data=obj(record.data),review=obj(data.catalogReview),entries=array(review.selections); if(data.kind!=='catalog-release-review-record'||data.artifactId!==record.id||! /^[a-f0-9]{64}$/u.test(record.id)||review.schemaVersion!==1||review.kind!=='catalog-release-review'||entries.length<1||entries.length>32)throw Error('Invalid catalog review record.'); const byId=new Map>();for(const raw of entries){const entry=obj(raw),id=obj(entry.service).id;if(typeof id!=='string'||byId.has(id))throw Error('Invalid selected service.');byId.set(id,entry);}return {data,byId}; } function facts(entry:Record|undefined,label:string){ if(!entry)return `

${label}

No selected catalog reference in this release.

`; const service=obj(entry.service),reference=obj(entry.reference),product=obj(array(reference.products)[0]),provider=obj(array(reference.providers)[0]); const terms:[string,JsonValue|undefined][]=[['Site provider',service.provider],['Location',service.location],['Catalog',reference.id],['Catalog revision',reference.revision],['Product',product.name],['Product revision',product.revision],['Reference provider',provider.name],['Provider revision',provider.revision]]; return `

${label}

${terms.map(([name,value])=>`
${name}
${e(value)}
`).join('')}

${e(product.description)}

Reference review: ${e(product.review)}. Operator confirmation records selected use; it does not verify provider practices.

${product.profile?renderCatalogProductProfile(reference,text(product.id),{headingLevel:4}):''}
Purpose facts used in this release${array(entry.purposes).map(raw=>{const p=obj(raw);return `

${e(p.name)}

${e(p.description)}

Purpose ID
${e(p.id)}
Declared basis
${e(p.legalBasis)}
Retention
${e(p.retention)}
`;}).join('')}
Source references${array(reference.sources).map(raw=>{const s=obj(raw),href=url(s.url);return `

${e(s.id)}

${href?`${e(s.url)}`:'No usable source URL.'}

Revision: ${e(s.revision)} · ${s.access==='read'?'Checked '+e(s.checkedOn):'Reference only; not checked'}

${e(s.note)}

`;}).join('')}
Complete selected reference facts
${escapeHtml(JSON.stringify(entry,null,2))}
`; } function changedFacts(before:Record,after:Record):string { const groups=(entry:Record)=>{ const reference=obj(entry.reference),result=new Map>([['Catalog',{id:reference.id!,revision:reference.revision!}],['Product',obj(array(reference.products)[0])],['Reference provider',obj(array(reference.providers)[0])],['Site use',obj(entry.service)]]); const product=obj(array(reference.products)[0]),profile=obj(product.profile);const productFacts={...product};delete productFacts.profile;result.set('Product',productFacts);if(product.profile)result.set('Product profile',{schemaVersion:profile.schemaVersion!}); for(const [kind,key] of [['Documentation note','facts'],['Document link','links'],['Review question','reviewPrompts']])for(const raw of array(profile[key!])){const item=obj(raw);result.set(kind+' '+text(item.id),item);} for(const [kind,items] of [['Source',array(reference.sources)],['Purpose',array(entry.purposes)]] as const)for(const raw of items){const item=obj(raw);result.set(kind+' '+text(item.id),item);}return result; }; const old=groups(before),now=groups(after),rows:string[]=[]; const label=(key:string)=>key.replace(/([a-z])([A-Z])/gu,'$1 $2').replaceAll('.',' / '); const value=(v:JsonValue|undefined)=>v===undefined?'Not present':typeof v==='string'?v:JSON.stringify(v,null,2); for(const group of new Set([...old.keys(),...now.keys()])){ const a=old.get(group)??{},b=now.get(group)??{}; for(const key of new Set([...Object.keys(a),...Object.keys(b)]))if(catalogReviewCanonical(a[key]??null)!==catalogReviewCanonical(b[key]??null))rows.push(`

${escapeHtml(group+' / '+label(key))}

Before
${escapeHtml(value(a[key]))}
After
${escapeHtml(value(b[key]))}
`); } return `
${rows.join('')}
`; } export function renderCatalogReview(before:EvidenceRecord,after:EvidenceRecord|undefined,options:RenderOptions):string { const previous=inspect(before),next=after?inspect(after):undefined;if(next&&previous.data.siteId!==next.data.siteId)throw Error('Compare catalog reviews for the same site.'); const current=after??before,ids=[...new Set([...previous.byId.keys(),...(next?.byId.keys()??[])])];let changed=0; const cards=ids.map(id=>{ const old=previous.byId.get(id),now=next?.byId.get(id),value=next?now??old:old; const changes=next?['reference','service','purposes'].filter(key=>catalogReviewCanonical(old?.[key]??null)!==catalogReviewCanonical(now?.[key]??null)):[];if(changes.length)changed++; const label=!next?'Selected reference':!old?'Added':!now?'Removed':changes.length?'Changed: '+changes.map(k=>k==='reference'?'catalog facts':k==='service'?'site use':'purpose facts').join(', '):'Unchanged'; const body=`

${label}

${e(obj(value?.service).name)}

Local service: ${escapeHtml(id)}

${next&&old&&now&&changes.length?changedFacts(old,now):''}${next?'
All facts before and after':''}
${facts(old,next?'Before':'Release')}${next?facts(now,'After'):''}
${next?'
':''}
`; return next&&!changes.length?`
${e(obj(value?.service).name)} · unchanged selected reference${body}
`:body; }).join(''); const form=after?'':`
`; return renderWorkspaceShell(`← Back to release

Retained service references

${after?'Catalog review changes':'Catalog review'}

Authorized scope: ${escapeHtml(options.scopeLabel)}

${next?`${changed} of ${ids.length} selected services changed between these releases.`:`${ids.length} selected service${ids.length===1?'':'s'} archived with this release.`} This count does not describe coverage of the full inventory.

These are the selected reference and site-use facts supplied at build time. Private justifications stay in the setup report. An archive does not establish publication, legal review or provider verification.

Release references

Before: ${escapeHtml(before.id)}

${after?`

After: ${escapeHtml(after.id)}

`:''}
${cards}${form}`,options); }