Supply API · source and documentation comparison
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Supply API · source and documentation comparison</title>
<style>
:root{color-scheme:light;--ink:#172b39;--muted:#465d6d;--line:#b9c8ce;--paper:#f6f5ef;--lime:#d5f525;--blue:#174f75}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:17px/1.65 system-ui,sans-serif}main{max-width:1120px;margin:auto;padding:30px 28px 48px}header{border-top:7px solid var(--ink);padding-top:20px}.eyebrow{font-weight:750;letter-spacing:.06em;color:var(--blue);margin:0 0 9px}h1{font-size:clamp(29px,5vw,44px);line-height:1.17;letter-spacing:-.035em;margin:0 0 16px;max-width:750px}h2{font-size:23px;line-height:1.3;margin:0 0 14px}h3{font-size:19px;margin:0 0 10px}p{margin:0 0 16px}.intro{max-width:850px}.notice{border-left:4px solid var(--blue);padding:12px 16px;background:#e8eff2}button,textarea{font:inherit}button{min-height:46px;border:1px solid var(--ink);border-radius:5px;padding:9px 14px;background:white;color:var(--ink);font-weight:650;cursor:pointer}button:hover{background:#e7edeb}button[aria-pressed="true"]{background:var(--ink);color:white}button:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid var(--blue);outline-offset:3px}.controls{display:flex;gap:9px;flex-wrap:wrap;margin:12px 0}.primary{background:var(--lime);color:#202600}.primary:hover{background:#c4e51a}.workbench{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:26px}.panel{min-width:0;border-top:2px solid var(--ink);padding-top:16px}label{display:block;font-weight:700;margin:12px 0 7px}textarea{display:block;width:100%;height:350px;resize:vertical;border:1px solid #8197a2;border-radius:6px;background:#fff;color:var(--ink);padding:14px;font-family:ui-monospace,Consolas,monospace;font-size:16px;line-height:1.5;tab-size:2}.subtle{color:var(--muted);font-size:16px}.tag{display:inline-block;background:#dde8ed;padding:2px 9px;border-radius:4px;font-weight:700}code{font:16px/1.6 ui-monospace,Consolas,monospace;overflow-wrap:anywhere}#comparison{margin-top:28px;padding:23px;border:1px solid var(--ink);background:white}#result{font-size:23px;font-weight:750;line-height:1.4;margin-bottom:8px}#runResult{padding:10px 12px;background:#edf1f0;margin:15px 0}.comparison-row{display:grid;grid-template-columns:1fr 1.7fr 1.7fr;gap:15px;border-top:1px solid var(--line);padding:16px 0}.comparison-row p{margin:0}.comparison-row strong{display:block}.value{white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,Consolas,monospace;font-size:16px}.caption{display:block;color:var(--muted);font:600 16px/1.6 system-ui,sans-serif;margin-bottom:5px}.badge{font-weight:750;color:#18553c}.different{color:#8c341c}.ledger{margin-top:32px;display:grid;grid-template-columns:1fr 1fr;gap:24px}.ledger section{border-top:1px solid var(--line);padding-top:18px}ul{padding-left:22px;margin:0}li{margin-bottom:9px}footer{border-top:1px solid var(--line);padding-top:18px;margin-top:30px}details{margin-top:18px}summary{cursor:pointer;font-weight:700}mark{background:var(--lime);color:#202600;padding:0 3px}@media(max-width:650px){main{padding:20px 18px 35px}.workbench,.ledger{grid-template-columns:1fr;gap:22px}textarea{height:290px}.comparison-row{grid-template-columns:1fr;gap:9px}#comparison{padding:17px}button{flex:1 1 auto}.controls{gap:8px}}
</style>
</head>
<body>
<main>
<header>
<p class="eyebrow">API DOCUMENTATION / REVISION REVIEW</p>
<h1>Did the field rename reach the reference?</h1>
<p class="intro">Compare an OpenAPI response with values recorded from a documentation page. Start with S1, then switch the source to S2 to expose the old <code>label</code> field.</p>
<p class="notice"><strong>Illustrative comparison.</strong> Compare pasted JSON with recorded documentation values. Presets are fictional; no build runs here.</p>
</header>
<div class="controls"><button type="button" id="sourceS1" aria-pressed="true">Use S1 source</button><button type="button" id="sourceS2" aria-pressed="false">Use S2 source</button><button type="button" class="primary" id="compare">Compare records</button><button type="button" id="reset">Reset to S1</button></div>
<section id="comparison" aria-labelledby="result">
<h2 id="result" role="status" aria-live="polite"></h2>
<p id="nextAction"></p>
<p id="runResult"></p>
<div id="rows"></div>
<p class="subtle">Counts cover four recorded comparisons only. Matching entries do not prove file provenance, native generation, rendered inspection, or live API behavior.</p>
</section>
<div class="workbench">
<section class="panel" aria-labelledby="sourceHeading">
<h2 id="sourceHeading">1. Maintained source</h2>
<p><span class="tag">GET /supplies</span> <code>200 · application/json</code></p>
<label for="sourceInput">OpenAPI 3.0.3 description (JSON)</label>
<textarea id="sourceInput" spellcheck="false" aria-describedby="sourceHint"></textarea>
<p id="sourceHint" class="subtle">S2 renames label in properties, required, and example. This focused tool accepts only the inline GET /supplies response shape shown here; it does not resolve $ref.</p>
</section>
<section class="panel" aria-labelledby="recordHeading">
<h2 id="recordHeading">2. Documentation observation</h2>
<p>Values a reviewer records from the rendered reference.</p>
<div class="controls"><button type="button" id="recordS1">Load S1 observation</button><button type="button" id="recordS2">Load S2 observation</button></div>
<label for="recordInput">Observation record (JSON)</label>
<textarea id="recordInput" spellcheck="false" aria-describedby="recordHint"></textarea>
<p id="recordHint" class="subtle">runStatus: pending, failed, generated, or inspected. Preset “inspected” is illustrative only. Replace sample values with your own observations.</p>
</section>
</div>
<div class="controls"><button type="button" id="failed">Load failed-run scenario</button></div>
<div class="ledger">
<section><h2>3. Attach the real build evidence</h2><ul><li>Source: revision, path, and exact byte fingerprint.</li><li>Environment: installed generator/runtime versions, configuration, and working directory.</li><li>Run: exact command, start/end times, and exit result.</li><li>Artifact: output path, separate byte fingerprint, and rendered inspection path.</li></ul><p class="subtle">Native source fingerprint, artifact fingerprint, versions, and execution: not recorded by this demo.</p></section>
<section><h2>Keep the outcomes separate</h2><p><mark>Lint → generate → inspect</mark> each answers a different question. A live API test needs an actual service; this fictional fixture has none.</p><p>Documented starting command:<br><code>redocly build-docs openapi.yaml</code></p><p>Default output: <code>redoc-static.html</code>. Check your installed command help and inspect output asset references before choosing a host.</p></section>
</div>
<details><summary>Comparison scope and input rules</summary><p>The tool checks title, item property names and types, required item names, and the complete media-type example. It requires an outer object with required items, an items array of objects with string fields, and examples that satisfy those required fields. It checks operationId listSupplies and the Supplies listed response description. Extra operations, external references, and other OpenAPI shapes are outside this tool's scope. Object key order and required-list order do not affect equality.</p></details>
<footer class="subtle">Keep the OpenAPI source authoritative. Correct it, rebuild with the recorded environment, then inspect the new reference. Editing a generated field alone breaks that chain.</footer>
</main>
<script>
(() => {
'use strict';
const find = selector => document.querySelector(selector);
const sourceInput = find('#sourceInput');
const recordInput = find('#recordInput');
function sourceFor(revised) {
const field = revised ? 'displayLabel' : 'label';
const itemSchema={type:'object',required:['id',field],properties:{id:{type:'string'},[field]:{type:'string'}}};
const schema={type:'object',required:['items'],properties:{items:{type:'array',items:itemSchema}}};
const media={schema,example:{items:[{id:'s1',[field]:'Brush'}]}};
const response={description:'Supplies listed',content:{'application/json':media}};
return {
openapi:'3.0.3',
info:{title:'Practice Supply API'+(revised?' revised':''),version:'1.0.0'},
paths:{'/supplies':{get:{operationId:'listSupplies',responses:{'200':response}}}}
};
}
function recordFor(revised) {
const field = revised ? 'displayLabel' : 'label';
return {title:'Practice Supply API'+(revised?' revised':''),fields:{id:'string',[field]:'string'},required:['id',field],example:{items:[{id:'s1',[field]:'Brush'}]},runStatus:'inspected'};
}
const pretty = value => JSON.stringify(value,null,2);
const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
function canonical(value) {
if (Array.isArray(value)) return value.map(canonical);
if (isObject(value)) return Object.fromEntries(Object.keys(value).sort().map(key => [key,canonical(value[key])]));
return value;
}
const equal = (left,right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
function requireCondition(condition,message) { if (!condition) throw new Error(message); }
function validateFields(fields,required,example,context) {
requireCondition(isObject(fields) && Object.keys(fields).length>0 && Object.values(fields).every(type=>type==='string'),context+': fields must be a nonempty object of string types.');
requireCondition(Array.isArray(required) && required.length>0 && required.every(name=>typeof name==='string' && Object.hasOwn(fields,name)) && new Set(required).size===required.length,context+': required must list unique declared field names.');
requireCondition(isObject(example) && Array.isArray(example.items),context+': example must contain an items array.');
requireCondition(example.items.length>0 && example.items.every(item=>isObject(item) && required.every(name=>typeof item[name]==='string') && Object.entries(item).every(([name,value])=>!Object.hasOwn(fields,name)||typeof value==='string')),context+': example items must satisfy required string fields.');
}
function extractSource(source) {
requireCondition(isObject(source) && source.openapi==='3.0.3' && typeof source.info?.title==='string' && source.info.title.trim(), 'Source: provide OpenAPI 3.0.3 and a nonempty info.title.');
requireCondition(isObject(source.paths) && equal(Object.keys(source.paths),['/supplies']) && equal(Object.keys(source.paths['/supplies']),['get']), 'Source: only GET /supplies is supported.');
const operation = source.paths['/supplies'].get;
requireCondition(operation?.operationId==='listSupplies','Source: operationId must be listSupplies.');
const response = operation.responses?.['200'];
requireCondition(response?.description==='Supplies listed','Source: the 200 response description must be Supplies listed.');
const media = response.content?.['application/json'];
const schema = media?.schema;
const arraySchema = schema?.properties?.items;
const itemSchema = arraySchema?.items;
requireCondition(schema?.type==='object' && Array.isArray(schema.required) && schema.required.includes('items') && arraySchema?.type==='array' && itemSchema?.type==='object' && isObject(itemSchema.properties), 'Source: expected an object requiring items, an array of inline objects. $ref is unsupported.');
const fields = Object.fromEntries(Object.entries(itemSchema.properties).map(([name,definition])=>[name,definition?.type]));
validateFields(fields,itemSchema.required,media.example,'Source');
return {title:source.info.title,fields,required:itemSchema.required,example:media.example};
}
function extractRecord(record) {
requireCondition(isObject(record) && typeof record.title==='string' && record.title.trim(), 'Observation: provide a nonempty title.');
requireCondition(['pending','failed','generated','inspected'].includes(record.runStatus),'Observation: runStatus must be pending, failed, generated, or inspected.');
validateFields(record.fields,record.required,record.example,'Observation');
return record;
}
function invalidate() {
find('#result').textContent='Inputs changed. Compare again.';
find('#nextAction').textContent='Previous comparison cleared.';
find('#runResult').textContent='Run status awaiting comparison.';
find('#rows').replaceChildren();
}
function setSource(revised) {
sourceInput.value=pretty(sourceFor(revised));
find('#sourceS1').setAttribute('aria-pressed',String(!revised));
find('#sourceS2').setAttribute('aria-pressed',String(revised));
invalidate();
}
function setRecord(revised) { recordInput.value=pretty(recordFor(revised)); invalidate(); }
function compare() {
const rows = find('#rows');
rows.replaceChildren();
try {
let source,record;
try { source=JSON.parse(sourceInput.value); } catch { throw new Error('Source: enter valid JSON before comparing.'); }
try { record=JSON.parse(recordInput.value); } catch { throw new Error('Observation: enter valid JSON before comparing.'); }
const expected=extractSource(source);
const observed=extractRecord(record);
const checks=[['Title',expected.title,observed.title],['Declared item fields',expected.fields,observed.fields],['Required item fields',[...expected.required].sort(),[...observed.required].sort()],['Response example',expected.example,observed.example]];
let differences=0;
for (const [label,left,right] of checks) {
const matches=equal(left,right);
if (!matches) differences++;
const row=document.createElement('div'); row.className='comparison-row';
const heading=document.createElement('p');
const name=document.createElement('strong');name.textContent=label;
const badge=document.createElement('span');badge.className=matches?'badge':'badge different';badge.textContent=matches?'Match':'Different';
heading.append(name,badge);row.append(heading);
for (const [caption,value] of [['Source expects',left],['Observation records',right]]) {
const cell=document.createElement('p');const captionNode=document.createElement('span');captionNode.className='caption';captionNode.textContent=caption;
const content=document.createElement('span');content.className='value';content.textContent=typeof value==='string'?value:pretty(value);cell.append(captionNode,content);row.append(cell);
}
rows.append(row);
}
find('#result').textContent=differences?differences+' of 4 checks differ':'4 of 4 recorded checks match';
find('#nextAction').textContent=differences?'Recheck the source revision and build record. The displayed fields may belong to an older artifact.':'Recorded fields agree. Verify provenance and inspect the actual rendered artifact before using it.';
const statuses={pending:'Build pending. No completed build is recorded.',failed:'Build failed. Retain the last-known artifact separately; output presence is not success.',generated:'Generated; rendered inspection pending. Field agreement does not complete the review.',inspected:'Record says inspected. Preset records are illustrative; no native build or inspection ran here.'};
find('#runResult').textContent=statuses[observed.runStatus];
} catch (error) {
find('#result').textContent='Cannot compare records';
find('#nextAction').textContent=error.message;
find('#runResult').textContent='No comparison result. Fix the indicated input and compare again.';
}
}
find('#sourceS1').addEventListener('click',()=>setSource(false));
find('#sourceS2').addEventListener('click',()=>setSource(true));
find('#recordS1').addEventListener('click',()=>setRecord(false));
find('#recordS2').addEventListener('click',()=>setRecord(true));
find('#compare').addEventListener('click',compare);
find('#failed').addEventListener('click',()=>{setSource(true);const record=recordFor(false);record.runStatus='failed';recordInput.value=pretty(record);compare();});
find('#reset').addEventListener('click',()=>{setSource(false);setRecord(false);compare();});
sourceInput.addEventListener('input',()=>{find('#sourceS1').setAttribute('aria-pressed','false');find('#sourceS2').setAttribute('aria-pressed','false');invalidate();});
recordInput.addEventListener('input',invalidate);
setSource(false);setRecord(false);compare();
})();
</script>
</body>
</html>