From 7825d8abc5d1b528a9d70665229cd9c26ded4336 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:36:52 -0700 Subject: [PATCH 1/9] Preserve Interdefinables heading hierarchy --- scripts/canon-parser.mjs | 77 +++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/scripts/canon-parser.mjs b/scripts/canon-parser.mjs index eb01343..c48e92d 100644 --- a/scripts/canon-parser.mjs +++ b/scripts/canon-parser.mjs @@ -5,10 +5,10 @@ import slugify from 'slugify'; // id: canon_parser_core // module_name: canon-parser // module_kind: engine -// summary: Parses canonical or recovery text into stable sections, units, notes, routes, and provenance-bearing hashes. +// summary: Parses canonical or recovery text into stable sections, nested heading units, notes, routes, and provenance-bearing hashes. // owner: Erin Spencer // public_surface: parseCanon, detectHeading, extractNotes -// internal_surface: slug, boundedRouteSlug, parseDefinitionLine, extractNoteMarkers +// internal_surface: slug, boundedRouteSlug, canonicalHeadingLevel, parseDefinitionLine, extractNoteMarkers // auth_boundary: none // storage_boundary: none // network_boundary: none @@ -16,12 +16,12 @@ import slugify from 'slugify'; // admin_only: false // tests: tests/canon-parser.test.mjs, tests/canon-integrity.test.mjs // rollout: imported by scripts/parse-canon.mjs during every canon refresh -// rollback: restore the prior inline parser and remove this import +// rollback: restore the prior parser version and remove nested heading-parent edges // === END MODULE_BUILD === -// Usage: import parseCanon(text, provenance); run `node --test tests/canon-parser.test.mjs` for recovery and note fixtures. -// Limits: heading recognition is canon-specific; unknown headings remain body text and must surface as hmmm during editorial review. +// Usage: import parseCanon(text, provenance); run `node --test tests/canon-parser.test.mjs` for hierarchy, recovery, and note fixtures. +// Limits: heading recognition is canon-specific; unknown headings retain normalized source levels and must surface as hmmm during editorial review. -export const parserVersion = '0.5.0'; +export const parserVersion = '0.6.0'; function slug(value) { return slugify(value, { lower: true, strict: true }) || 'unit'; @@ -34,39 +34,40 @@ function boundedRouteSlug(id) { return `${candidate.slice(0, 84).replace(/-+$/, '')}-${suffix}`; } +function canonicalHeadingLevel(value) { + const title = value.trim().replace(/:\s*$/, ''); + if (/^The Interdependent Way$/i.test(title)) return 1; + if (/^(Awakening|The Interdefinables|Preamble|Etiquette of the Body Politic)$/i.test(title)) return 2; + if (/^Rights.+of The Way[⁰¹²³⁴⁵⁶⁷⁸⁹]*$/i.test(title)) return 2; + if (/^Addendum:\s+.+$/i.test(value.trim())) return 2; + if (/^Human consciousness emerges from$/i.test(title)) return 3; + if (/^Article\s+(One|Two|Three|Four|Five|Six|Seven|Eight)(?:\s+\([^)]+\))?$/i.test(title)) return 3; + if (/^Binary essences meaningfully.*rooted[.]?$/i.test(title)) return 4; + if (/^Trinary perceptual focal (?:states|constructs) of complex system spirals/i.test(title)) return 4; + if (/^Trinary (?:states of social perception|social perception focal states)/i.test(title)) return 4; + if (/^(?:Five dominant )?Archetype passions of possession/i.test(title)) return 4; + if (/^(Summary|One-sentence takeaway \(exactly as previously given\))$/i.test(title)) return 3; + return null; +} + export function detectHeading(line) { const markdown = /^(#{1,6})\s+(.+?)\s*$/.exec(line); if (markdown) { const sourceLevel = markdown[1].length; - // The recovery mirror uses H3 for the same major divisions that the plain-text - // canon expresses as level 2, and H4 for article units. Normalize those levels - // before assigning parents so offline and remote builds have the same structure. - const level = sourceLevel >= 3 ? sourceLevel - 1 : sourceLevel; - return { - level, - sourceLevel, - title: markdown[2].replace(/#+$/, '').trim(), - syntax: 'markdown' - }; + const title = markdown[2].replace(/#+$/, '').trim(); + const declaredLevel = canonicalHeadingLevel(title); + // Recovery Markdown historically used H3 for both major sections and the + // Human-consciousness subheading. Canonical title semantics take priority; + // unknown headings retain the established H3→2 / H4→3 normalization. + const level = declaredLevel ?? (sourceLevel >= 3 ? sourceLevel - 1 : sourceLevel); + return { level, sourceLevel, title, syntax: 'markdown' }; } const title = line.trim(); if (!title) return null; - if (title === 'The Interdependent Way') return { level: 1, sourceLevel: 1, title, syntax: 'plain' }; - if (/^(Awakening|The Interdefinables|Human consciousness emerges from|Preamble|Etiquette of the Body Politic)$/i.test(title)) { - return { level: 2, sourceLevel: 2, title, syntax: 'plain' }; - } - if (/^Rights[\w\s’'&⁰¹²³⁴⁵⁶⁷⁸⁹-]+of The Way[⁰¹²³⁴⁵⁶⁷⁸⁹]*$/i.test(title)) { - return { level: 2, sourceLevel: 2, title, syntax: 'plain' }; - } - if (/^Addendum:\s+.+$/i.test(title)) return { level: 2, sourceLevel: 2, title, syntax: 'plain' }; - if (/^Article\s+(One|Two|Three|Four|Five|Six|Seven|Eight)(?:\s+\([^)]+\))?$/i.test(title)) { - return { level: 3, sourceLevel: 3, title, syntax: 'plain' }; - } - if (/^(Binary essences meaningfully, divided; then, rooted\.|Trinary perceptual focal states of complex system spirals:.+|Trinary states of social perception:|Archetype passions of possession\..+|Summary|One-sentence takeaway \(exactly as previously given\))$/i.test(title)) { - return { level: 3, sourceLevel: 3, title, syntax: 'plain' }; - } - return null; + const level = canonicalHeadingLevel(title); + if (!level) return null; + return { level, sourceLevel: level, title, syntax: 'plain' }; } function parseDefinitionLine(line) { @@ -111,6 +112,7 @@ export function parseCanon(text, provenance = {}) { const documentHash = createHash('sha256').update(text).digest('hex'); const units = []; const sections = []; + const headingStack = []; let current = null; let sectionId = 'source'; @@ -140,16 +142,22 @@ export function parseCanon(text, provenance = {}) { } } + const parent = [...headingStack].reverse().find(entry => entry.level < level) ?? null; + const id = `${sectionId}.${slug(title)}`; current = { - id: `${sectionId}.${slug(title)}`, + id, title, section: sectionId, + parentId: parent?.id ?? null, level, sourceLevel, syntax, startLine: index + 1, lines: [lines[index]] }; + + while (headingStack.length && headingStack.at(-1).level >= level) headingStack.pop(); + headingStack.push({ id, level }); } finish(lines.length); @@ -178,6 +186,9 @@ export function parseCanon(text, provenance = {}) { sections, units: units.map(({ lines: ignored, ...unit }) => unit), notes, - edges: units.map(unit => ({ from: unit.id, to: unit.section, type: 'unit-parent' })) + edges: [ + ...units.map(unit => ({ from: unit.id, to: unit.section, type: 'unit-parent' })), + ...units.filter(unit => unit.parentId).map(unit => ({ from: unit.id, to: unit.parentId, type: 'heading-parent' })) + ] }; } From 0cf6fc97c8114b271a6f20b89474611443679fde Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:37:16 -0700 Subject: [PATCH 2/9] Test Interdefinables nested hierarchy --- tests/canon-parser.test.mjs | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/canon-parser.test.mjs b/tests/canon-parser.test.mjs index 46c9254..ad03589 100644 --- a/tests/canon-parser.test.mjs +++ b/tests/canon-parser.test.mjs @@ -14,6 +14,49 @@ test('Markdown recovery headings normalize to plain-canon parent levels', () => assert.ok(data.sections.some(section => section.id === 'rights-and-definitions-of-the-way' && section.level === 2)); }); +test('Interdefinables owns the Human consciousness hierarchy until Preamble', () => { + const data = parseCanon(`The Interdependent Way\n\nAwakening\nOpening.\n\nThe Interdefinables\nDefinitions.\n\nHuman consciousness emerges from\nBinary essences meaningfully, divided; then, rooted.\nBody.\nTrinary perceptual focal states of complex system spirals: mind (body) soul\nBody.\nPreamble\nCivic claim.`); + const interdefinables = data.units.find(unit => unit.title === 'The Interdefinables'); + const human = data.units.find(unit => unit.title === 'Human consciousness emerges from'); + const binary = data.units.find(unit => unit.title.startsWith('Binary essences')); + const trinary = data.units.find(unit => unit.title.startsWith('Trinary perceptual')); + const preamble = data.units.find(unit => unit.title === 'Preamble'); + + assert.equal(interdefinables.level, 2); + assert.equal(human.level, 3); + assert.equal(human.section, 'interdefinables'); + assert.equal(human.parentId, interdefinables.id); + assert.equal(binary.level, 4); + assert.equal(binary.section, 'interdefinables'); + assert.equal(binary.parentId, human.id); + assert.equal(trinary.level, 4); + assert.equal(trinary.parentId, human.id); + assert.equal(preamble.level, 2); + assert.equal(preamble.section, 'preamble'); + assert.equal(data.sections.some(section => /^Human consciousness/.test(section.title)), false); + assert.ok(data.sections.findIndex(section => section.title === 'The Interdefinables') < data.sections.findIndex(section => section.title === 'Preamble')); + assert.ok(data.edges.some(edge => edge.type === 'heading-parent' && edge.from === human.id && edge.to === interdefinables.id)); +}); + +test('legacy Markdown title levels cannot promote Human consciousness to a peer section', () => { + const data = parseCanon(`# The Interdependent Way\n### The Interdefinables\nDefinitions.\n### Human consciousness emerges from:\n#### Binary essences meaningfully divided, then rooted:\nBody.\n### Preamble\nCivic claim.`); + const interdefinables = data.units.find(unit => unit.title === 'The Interdefinables'); + const human = data.units.find(unit => /^Human consciousness/.test(unit.title)); + const binary = data.units.find(unit => unit.title.startsWith('Binary essences')); + const preamble = data.units.find(unit => unit.title === 'Preamble'); + + assert.equal(human.sourceLevel, 3); + assert.equal(human.level, 3); + assert.equal(human.section, 'interdefinables'); + assert.equal(human.parentId, interdefinables.id); + assert.equal(binary.sourceLevel, 4); + assert.equal(binary.level, 4); + assert.equal(binary.parentId, human.id); + assert.equal(preamble.sourceLevel, 3); + assert.equal(preamble.level, 2); + assert.equal(preamble.section, 'preamble'); +}); + test('multiple superscript note definitions on one physical line remain distinct', () => { assert.deepEqual(extractNotes('>¹ first tension ² second tension ³ third tension'), [ { marker: '¹', text: 'first tension' }, From 16964ed8c5c70fe46fc5450e7516a361613a7e92 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:37:35 -0700 Subject: [PATCH 3/9] Render nested canon heading levels --- src/way/index.njk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/way/index.njk b/src/way/index.njk index 720c52d..69198e1 100644 --- a/src/way/index.njk +++ b/src/way/index.njk @@ -1,9 +1,9 @@ --- layout: layouts/base.njk title: Explore The Way -description: A sectioned companion map of The Interdependent Way, with exact source available one deliberate layer deeper. +description: A sectioned and nested companion map of The Interdependent Way, with exact source available one deliberate layer deeper. --- -

Canon-derived map

Explore The Way

This layer identifies the shape and relations of the source. It does not replace the canon. Open a unit for orientation, then choose its Lab conversation or exact source.

+

Canon-derived map

Explore The Way

This layer identifies the shape and relations of the source. Major sections, subheadings, and nested headings remain visibly distinct. It does not replace the canon. Open a unit for orientation, then choose its Lab conversation or exact source.

{% for section in generated.canon.sections %} -

{{ section.id }}

{{ section.title }}

+

{{ section.id }}

{{ section.title }}

{% endfor %} From c010acf76635dcda60665faebdec35fef1b88177 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:38:33 -0700 Subject: [PATCH 4/9] Style nested canon outline levels --- src/assets/css/site.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/assets/css/site.css b/src/assets/css/site.css index 9adc07a..58b52ac 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -80,6 +80,10 @@ h3 { font-size: clamp(1.2rem, 2vw, 1.55rem); } .index-list a { display: grid; grid-template-columns: 7rem 1fr auto; gap: 1rem; align-items: baseline; padding: .9rem 1rem; border: 1px solid var(--line); border-radius: .75rem; background: rgba(16,24,42,.75); color: inherit; text-decoration: none; } .index-list a:hover { border-color: var(--violet); } .index-list small { color: var(--silver); } +.canon-outline .unit-level-3 { margin-left: clamp(1rem, 3vw, 2.5rem); } +.canon-outline .unit-level-4 { margin-left: clamp(2rem, 6vw, 5rem); } +.canon-outline .unit-level-3 a { border-left: .25rem solid var(--violet); } +.canon-outline .unit-level-4 a { border-left: .25rem solid var(--cyan); background: rgba(9, 13, 24, .76); } .source-block { white-space: pre-wrap; overflow-wrap: anywhere; padding: 1rem; border: 1px solid var(--line); border-radius: .8rem; background: #070a12; color: #dce6fa; font: .92rem/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; } dl.meta { display: grid; grid-template-columns: minmax(9rem, .35fr) 1fr; gap: .45rem 1rem; } dl.meta dt { color: var(--silver); } @@ -104,6 +108,8 @@ summary { cursor: pointer; font-weight: 800; } .hero { grid-template-columns: 1fr; min-height: auto; } .hero-field { max-width: 22rem; width: 78%; margin: 1rem auto; } .index-list a { grid-template-columns: 1fr; gap: .15rem; } + .canon-outline .unit-level-3 { margin-left: .65rem; } + .canon-outline .unit-level-4 { margin-left: 1.3rem; } .site-footer { grid-template-columns: 1fr; } .hmmm-boundary { grid-column: auto; grid-template-columns: 1fr; } } From 2515f4de876238f8f050fa56a1080a9c9958581d Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:39:23 -0700 Subject: [PATCH 5/9] Gate canonical heading hierarchy --- scripts/validate-content.mjs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index 3b75702..fc796bd 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -5,18 +5,18 @@ import { access, readFile } from 'node:fs/promises'; // id: generated_content_gate // module_name: validate-content // module_kind: instrument -// summary: Refuses deployment when canon identity, snapshot integrity, generated route coverage, or recovery artifacts drift. +// summary: Refuses deployment when canon identity, heading hierarchy, snapshot integrity, generated route coverage, or recovery artifacts drift. // owner: Erin Spencer // public_surface: npm run validate -// internal_surface: canon snapshot digest and repository-route assertions +// internal_surface: canon snapshot digest, heading hierarchy, and repository-route assertions // auth_boundary: none // storage_boundary: read // network_boundary: none // user_data_boundary: none // admin_only: false -// tests: tests/canon-integrity.test.mjs, tests/repo-coverage.test.mjs, tests/site-contract.test.mjs +// tests: tests/canon-parser.test.mjs, tests/canon-integrity.test.mjs, tests/repo-coverage.test.mjs, tests/site-contract.test.mjs // rollout: required by npm run build and npm run check -// rollback: remove the gate only with an explicit replacement preserving provenance and route checks +// rollback: remove the gate only with an explicit replacement preserving provenance, hierarchy, and route checks // === END MODULE_BUILD === // Usage: run `npm run validate`; it refreshes data first and exits nonzero on any integrity mismatch. // Limits: validates repository artifacts, not GitHub Pages settings or public DNS. @@ -47,8 +47,26 @@ if (!canon.source.contentSha256 || canon.source.contentSha256.length !== 64) thr if (canon.source.contentSha256 !== snapshotHash) throw new Error('generated canon digest does not match selected snapshot'); if (!canon.source.fallback && (!canon.source.commit || !canon.source.blob)) throw new Error('remote canon provenance missing commit or blob SHA'); if (!canon.units.length || canon.units.some(unit => !unit.hash || !unit.id)) throw new Error('canon units missing identity or hash'); + +const interdefinables = canon.units.find(unit => unit.title === 'The Interdefinables'); +const humanConsciousness = canon.units.find(unit => /^Human consciousness emerges from:?$/i.test(unit.title)); +const preamble = canon.units.find(unit => unit.title === 'Preamble'); +if (!interdefinables || !humanConsciousness || !preamble) throw new Error('canon missing Interdefinables, Human consciousness, or Preamble hierarchy unit'); +if (humanConsciousness.level !== 3) throw new Error('Human consciousness emerges from must be a level-3 subheading'); +if (humanConsciousness.section !== interdefinables.section) throw new Error('Human consciousness emerges from escaped The Interdefinables section'); +if (humanConsciousness.parentId !== interdefinables.id) throw new Error('Human consciousness emerges from must be parented by The Interdefinables'); +if (canon.sections.some(section => /^Human consciousness emerges from:?$/i.test(section.title))) throw new Error('Human consciousness emerges from must not be promoted to a peer section'); +if (preamble.level !== 2 || preamble.section !== 'preamble') throw new Error('Preamble must remain the next major section boundary'); +if (!canon.source.fallback) { + const interdefinablesIndex = canon.sections.findIndex(section => section.title === 'The Interdefinables'); + const preambleIndex = canon.sections.findIndex(section => section.title === 'Preamble'); + if (interdefinablesIndex < 0 || preambleIndex !== interdefinablesIndex + 1) { + throw new Error('Preamble must be the next major section after The Interdefinables'); + } +} + if (repos.publicRepoCount !== repos.generatedRouteCount) throw new Error('repo route mismatch'); if (new Set(repos.repositories.map(repo => repo.slug)).size !== repos.repositories.length) throw new Error('duplicate project slug'); await access('fallback/index.html'); await access('artifacts/four-cuts-1.html'); -console.log(`validated ${canon.units.length} canon units, ${canon.notes.length} notes, and ${repos.publicRepoCount} repositories`); +console.log(`validated ${canon.units.length} canon units, ${canon.notes.length} notes, canonical heading hierarchy, and ${repos.publicRepoCount} repositories`); From 9abd22c9d2a30977059ed4ee5b6fb0be010b35f6 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:39:39 -0700 Subject: [PATCH 6/9] Assert generated canon heading parents --- tests/canon-integrity.test.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/canon-integrity.test.mjs b/tests/canon-integrity.test.mjs index 3165fa3..4ceaa8d 100644 --- a/tests/canon-integrity.test.mjs +++ b/tests/canon-integrity.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; -test('canon data preserves Wayseer identity, provenance, and stable unit evidence', async () => { +test('canon data preserves Wayseer identity, provenance, stable unit evidence, and nested heading parents', async () => { const canon = JSON.parse(await readFile('src/_data/generated/canon.json', 'utf8')); assert.equal(canon.source.repository, 'wayseer00/main'); assert.equal(canon.source.path, 'canon/INTERDEPENDENT_WAY.txt'); @@ -23,4 +23,14 @@ test('canon data preserves Wayseer identity, provenance, and stable unit evidenc assert.equal(routes.has(unit.routeSlug), false); routes.add(unit.routeSlug); } + + const interdefinables = canon.units.find(unit => unit.title === 'The Interdefinables'); + const human = canon.units.find(unit => /^Human consciousness emerges from:?$/i.test(unit.title)); + assert.ok(interdefinables); + assert.ok(human); + assert.equal(human.level, 3); + assert.equal(human.section, interdefinables.section); + assert.equal(human.parentId, interdefinables.id); + assert.equal(canon.sections.some(section => /^Human consciousness emerges from:?$/i.test(section.title)), false); + assert.ok(canon.edges.some(edge => edge.type === 'heading-parent' && edge.from === human.id && edge.to === interdefinables.id)); }); From 445b8a86f617448ac554bc6295e89aac54901d5d Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:40:07 -0700 Subject: [PATCH 7/9] Verify rendered Interdefinables hierarchy --- tests/generated-site.test.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/generated-site.test.mjs b/tests/generated-site.test.mjs index 29a67f3..7aef9f0 100644 --- a/tests/generated-site.test.mjs +++ b/tests/generated-site.test.mjs @@ -46,6 +46,22 @@ test('generated deployment artifact contains the unified routes', async () => { ]) assert.match(articles, new RegExp(title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); }); +test('Way map renders Human consciousness beneath Interdefinables and before Preamble', async () => { + const way = await readFile('_site/way/index.html', 'utf8'); + const interdefinablesSection = way.indexOf('

The Interdefinables

'); + const humanHeading = way.indexOf('Human consciousness emerges from'); + const binaryHeading = way.indexOf('Binary essences meaningfully'); + const preambleSection = way.indexOf('

Preamble

'); + + assert.ok(interdefinablesSection >= 0, 'Interdefinables section missing'); + assert.ok(humanHeading > interdefinablesSection, 'Human consciousness must appear inside Interdefinables'); + assert.ok(binaryHeading > humanHeading, 'Human consciousness child headings must follow their parent'); + assert.ok(preambleSection > binaryHeading, 'Preamble must be the next major section after Interdefinables'); + assert.match(way, /class="unit-level-3"[^>]*>[\s\S]*?Human consciousness emerges from/); + assert.match(way, /class="unit-level-4"[^>]*>[\s\S]*?Binary essences meaningfully/); + assert.doesNotMatch(way, /

Human consciousness emerges from:?<\/h2>/); +}); + test('generated deployment artifact contains all rights article vertical slices', async () => { const pages = [ ['article-one', /Contribution without contempt/, /From each as they will/], From 7c72aab634cc49cf8592164b364e73c27fbecdf2 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:40:28 -0700 Subject: [PATCH 8/9] Document canonical heading hierarchy --- docs/content-model.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/content-model.md b/docs/content-model.md index 3b2e588..8b22bc2 100644 --- a/docs/content-model.md +++ b/docs/content-model.md @@ -1,5 +1,30 @@ # Content model -- Canon units: parsed headings with stable IDs, line ranges, and SHA-256 hashes. +## Canon hierarchy + +Canon units are parsed headings with stable IDs, line ranges, SHA-256 hashes, major-section membership, and immediate heading-parent relationships. + +The opening hierarchy is load-bearing: + +```text +Awakening +The Interdefinables + Human consciousness emerges from + Binary essences... + Trinary perceptual focal states... + Trinary states of social perception... + Archetype passions of possession... +Preamble +Rights and Definitions of The Way +``` + +`Human consciousness emerges from` is not a peer section. It is a subheading beneath `The Interdefinables`; its binary, trinary, and archetype headings are nested beneath it. `Preamble` is the next major section after `The Interdefinables`. + +## Other records + - Project records: public GitHub facts plus visible `hmmm` when editorial metadata is missing. - Lab records: generated per canon unit, with reviewed research gaps explicitly shown. + +## Usage guidance + +Run `npm run validate` after a canon refresh. The validation gate rejects a build that promotes `Human consciousness emerges from` to a section, assigns it outside `The Interdefinables`, loses its immediate parent, or places another major section between `The Interdefinables` and `Preamble` in the controlling remote canon. From 8fe9938682a48084cd124f847150700367ba0774 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 21 Jul 2026 09:40:49 -0700 Subject: [PATCH 9/9] Expose immediate canon heading parent --- src/way/unit.njk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/way/unit.njk b/src/way/unit.njk index 12fef2b..1f62733 100644 --- a/src/way/unit.njk +++ b/src/way/unit.njk @@ -7,8 +7,8 @@ pagination: permalink: "/way/{{ unit.routeSlug }}/" title: "{{ unit.title }}" --- - +

Canon-derived companion · {{ unit.section }}

{{ unit.title }}

canon-derivedorientation pending review

This page preserves the unit’s identity and location while offering paths into interpretation and exact source. It does not rewrite the canonical text.

-

Place in the document

Unit ID
{{ unit.id }}
Stable route
{{ unit.routeSlug }}
Source lines
{{ unit.startLine }}–{{ unit.endLine }}
Detected notes
{{ unit.notes.length }}
Unit digest
{{ unit.hash }}
+

Place in the document

Unit ID
{{ unit.id }}
Heading level
{{ unit.level }}
{% if unit.parentId %}
Parent heading
{% for parent in generated.canon.units %}{% if parent.id == unit.parentId %}{{ parent.title }}{% endif %}{% endfor %}
{% endif %}
Stable route
{{ unit.routeSlug }}
Source lines
{{ unit.startLine }}–{{ unit.endLine }}
Detected notes
{{ unit.notes.length }}
Unit digest
{{ unit.hash }}

Companion reading

A reviewed plain-language reading has not yet been admitted for this unit. The source operators, conditions, exceptions, and obligations must remain intact before this field is populated.