Skip to content

Commit c16a584

Browse files
fix(kb): level pages no longer duplicate id="what-it-is"
kb.long always opens with "## What it is", so the wrapping section and its own first heading both slugged to "what-it-is" — invalid HTML, and any in-page anchor to it resolved to the section instead of the heading. The wrapper no longer carries its own id; the heading's id (already in the page's TOC via headingsOf) is the only one, so anchors now land where they say they will. kb-generator.test.js gains a no-duplicate-id assertion across every generated page so this class of bug fails the suite next time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 4d45741 commit c16a584

7 files changed

Lines changed: 28 additions & 8 deletions

File tree

.development/scripts/generate-kb.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,11 @@ function levelPage(def, ctx) {
676676
};
677677

678678
// 1 — the level's own story, under its class in words and its short form.
679-
out.push(' <section class="kb-section" id="what-it-is">');
679+
// No id on the section itself: kb.long always opens with "## What it is",
680+
// whose rendered heading already gets that id (headingId: slug below) —
681+
// giving the wrapper the same id would duplicate it (invalid HTML) and make
682+
// the anchor resolve to the section instead of the heading it names.
683+
out.push(' <section class="kb-section">');
680684
out.push(` <p class="kb-lede">${escapeHtml(shortOf(def))}</p>`);
681685
if (def.kb.long) out.push(indent(md(def.kb.long, `${where}: kb.long`), 4));
682686
out.push(' </section>');

kb/raid0.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ <h1 class="kb-title">RAID 0</h1>
156156
<a class="kb-nav-item" href="../index.html">Sandbox</a>
157157
</nav>
158158

159-
<section class="kb-section" id="what-it-is">
159+
<section class="kb-section">
160160
<p class="kb-lede">Data dealt out in chunks across every disk and nothing else: no copy, no parity. Every disk works on every large request, so it is the fastest layout there is and uses all the space; and any one disk failing loses everything on all of them.</p>
161161
<h2 id="what-it-is">What it is</h2>
162162
<p>RAID 0 is the pair <em>striped + no redundancy</em>: the data is cut into chunks and dealt across the members in turn (<a href="striping.html">striping</a>), and that is all. The 1988 paper that named RAID defines five levels, from mirroring upward; a stripe with no redundancy is not one of them, and the name came later, for exactly the case the paper opens with: an array whose time to failure is one disk&#39;s divided by the number of disks (<a href="redundancy.html">redundancy</a>).</p>

kb/raid1.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ <h1 class="kb-title">RAID 1</h1>
156156
<a class="kb-nav-item" href="../index.html">Sandbox</a>
157157
</nav>
158158

159-
<section class="kb-section" id="what-it-is">
159+
<section class="kb-section">
160160
<p class="kb-lede">Every disk holds a full copy of the data, written in step. A read is served by any one copy, a write goes to all of them, and the array survives as long as one copy is left. It keeps one disk&#39;s worth of space however many disks it has, and it is the simplest redundancy there is.</p>
161161
<h2 id="what-it-is">What it is</h2>
162162
<p>RAID 1 is the pair <em>linear + mirror</em>: nothing is cut into chunks, and every member holds the whole (<a href="mirroring.html">mirroring</a>). It is the first level of the 1988 paper, &quot;the most expensive option we consider since all disks are duplicated&quot;, and every other level is a cheaper answer to the same problem. There is no layout to choose and no chunk size, because there is nothing to place: <code>mdadm</code> does not accept a chunk size for it.</p>

kb/raid10.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ <h1 class="kb-title">RAID 10</h1>
156156
<a class="kb-nav-item" href="../index.html">Sandbox</a>
157157
</nav>
158158

159-
<section class="kb-section" id="what-it-is">
159+
<section class="kb-section">
160160
<p class="kb-lede">Striping and mirroring in one flat array: every chunk is stored on two disks, and the chunks are dealt across all of them. It reads like a stripe, writes at a mirror&#39;s cost with no parity to compute, keeps half the space, and is guaranteed to survive one failure, more if the failures miss each other.</p>
161161
<h2 id="what-it-is">What it is</h2>
162162
<p>RAID 10 is the pair <em>striped + mirror</em>: the data is cut into chunks and dealt across the members (<a href="striping.html">striping</a>), and each chunk is stored twice, on two different disks (<a href="mirroring.html">mirroring</a>). In Linux <code>md</code> it is one level, not a nesting: &quot;every datablock is duplicated some number of times, and the resulting collection of datablocks are distributed over multiple drives&quot;. Where the second copy lands is the placement <a href="algorithm.html">algorithm</a>, <em>near</em>, <em>far</em> or <em>offset</em>; near, the default, puts the two copies side by side in the same stripe, which is the same arrangement a hardware controller reaches in a different way, by striping over mirror pairs. That nested form, RAID 1+0, has its own page; with the near layout the two put every block in the same place, and the difference is which engine built it and which other layouts it can offer. RAID 0+1, a mirror of two stripes, uses the same disks in a worse arrangement, and its page says why.</p>

kb/raid5.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ <h1 class="kb-title">RAID 5</h1>
156156
<a class="kb-nav-item" href="../index.html">Sandbox</a>
157157
</nav>
158158

159-
<section class="kb-section" id="what-it-is">
159+
<section class="kb-section">
160160
<p class="kb-lede">Data striped across every disk, with one parity block per stripe that moves from disk to disk. Any one disk can fail and everything on it is recomputed from the rest by XOR. It keeps all but one disk&#39;s worth of space, reads nearly as fast as a plain stripe, and pays for small writes with four disk operations each.</p>
161161
<h2 id="what-it-is">What it is</h2>
162162
<p>RAID 5 is the pair <em>striped + single parity</em>: the data is dealt out in chunks across all the members (<a href="striping.html">striping</a>), and in every stripe one chunk holds the XOR of the others (<a href="parity.html">parity</a>). It is the fifth level of the 1988 paper, and the one the paper builds up to: the fourth level had put all the parity on one disk, and &quot;the check disk is the bottleneck&quot;; the fifth &quot;distributes the data and check information across all the disks&quot;. Which disk holds the parity in which stripe is the placement <a href="algorithm.html">algorithm</a>, almost always left-symmetric.</p>

kb/raid6.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ <h1 class="kb-title">RAID 6</h1>
156156
<a class="kb-nav-item" href="../index.html">Sandbox</a>
157157
</nav>
158158

159-
<section class="kb-section" id="what-it-is">
159+
<section class="kb-section">
160160
<p class="kb-lede">RAID 5 with a second parity block per stripe, computed a different way, so that any two disks can fail at once. It keeps all but two disks&#39; worth of space, survives a second failure during a rebuild, and pays for each small write with six disk operations.</p>
161161
<h2 id="what-it-is">What it is</h2>
162162
<p>RAID 6 is the pair <em>striped + double parity</em>: the data is dealt across the members (<a href="striping.html">striping</a>), and every stripe carries two check blocks, <em>P</em>, the XOR of the data as in RAID 5, and <em>Q</em>, a Reed-Solomon code over the same blocks (<a href="parity.html">parity</a>). Two blocks give two equations, and two equations recover two unknowns: &quot;RAID-6 supports losing any two drives&quot;, in the words of the paper behind the Linux implementation. Which disks hold <em>P</em> and <em>Q</em> in each stripe is the placement <a href="algorithm.html">algorithm</a>, the same four names as RAID 5.</p>

tests/kb-generator.test.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* the pages are compared against; it runs the generator as a subprocess, so the
66
* vendored parser stays inside that process and this suite keeps no dependency.)
77
*
8-
* Four properties, each one a way a generated page has gone wrong before:
8+
* Five properties, each one a way a generated page has gone wrong before:
99
*
1010
* 1. DETERMINISM. Two runs on the same input produce the same bytes. Without
1111
* it every commit carries a diff nobody wrote, and the pre-commit hook
@@ -22,7 +22,10 @@
2222
* phone gets). The JSON-LD block (data, not code) and the Cookiebot /
2323
* Consent Mode / gtag.js analytics block — byte-identical on every page,
2424
* index.html is its source of truth — are the only <script> tags a page
25-
* may carry; nothing else runs.
25+
* may carry; nothing else runs;
26+
* 5. NO DUPLICATE ID. Every id in a page is unique. The knowledge base's
27+
* in-page anchors and "On this page" column both link to `#id`s the
28+
* generator assigns, and a collision means the wrong element wins.
2629
*/
2730

2831
const fs = require('fs');
@@ -294,5 +297,18 @@ for (const [name, html] of pages) {
294297
});
295298
}
296299

300+
// ---------------------------------------------------------------------------
301+
console.log('\n[7] no id is duplicated on a page');
302+
303+
for (const [name, html] of pages) {
304+
test(`${name}: every id is unique`, () => {
305+
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
306+
const seen = new Set();
307+
const dupes = new Set();
308+
for (const id of ids) (seen.has(id) ? dupes : seen).add(id);
309+
assert(dupes.size === 0, `${name}: id "${[...dupes][0]}" is used more than once`);
310+
});
311+
}
312+
297313
fs.rmSync(tmp, { recursive: true, force: true });
298314
finish();

0 commit comments

Comments
 (0)