Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion docs/content-model.md
Original file line number Diff line number Diff line change
@@ -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.
77 changes: 44 additions & 33 deletions scripts/canon-parser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ 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
// user_data_boundary: none
// 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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update parent IDs after duplicate heading renames

If a refreshed canon repeats a heading slug within the same section and either copy has lower-level children, this stores the pre-deduplicated id in headingStack; the duplicate pass later suffixes unit.id values but never rewrites existing parentIds. Those children then get dangling heading-parent edges and the new unit page parent lookup renders an empty “Parent heading” despite the parent unit existing under its suffixed ID.

Useful? React with 👍 / 👎.

}
finish(lines.length);

Expand Down Expand Up @@ -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' }))
]
};
}
28 changes: 23 additions & 5 deletions scripts/validate-content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`);
6 changes: 6 additions & 0 deletions src/assets/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand All @@ -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; }
}
Expand Down
6 changes: 3 additions & 3 deletions src/way/index.njk
Original file line number Diff line number Diff line change
@@ -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.
---
<header class="page-head"><p class="eyebrow">Canon-derived map</p><h1>Explore The Way</h1><p class="lede">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.</p></header>
<header class="page-head"><p class="eyebrow">Canon-derived map</p><h1>Explore The Way</h1><p class="lede">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.</p></header>
{% for section in generated.canon.sections %}
<section class="category"><p class="eyebrow">{{ section.id }}</p><h2>{{ section.title }}</h2><ul class="index-list">{% for unit in generated.canon.units %}{% if unit.section == section.id %}<li><a href="/way/{{ unit.routeSlug }}/"><small>lines {{ unit.startLine }}–{{ unit.endLine }}</small><strong>{{ unit.title }}</strong><span class="status status-canon">canon-derived</span></a></li>{% endif %}{% endfor %}</ul></section>
<section class="category"><p class="eyebrow">{{ section.id }}</p><h2>{{ section.title }}</h2><ul class="index-list canon-outline">{% for unit in generated.canon.units %}{% if unit.section == section.id %}<li class="unit-level-{{ unit.level }}" data-parent-id="{{ unit.parentId or '' }}"><a href="/way/{{ unit.routeSlug }}/"><small>lines {{ unit.startLine }}–{{ unit.endLine }} · {% if unit.level <= 2 %}section{% elif unit.level == 3 %}subheading{% else %}nested heading{% endif %}</small><strong>{{ unit.title }}</strong><span class="status status-canon">canon-derived</span></a></li>{% endif %}{% endfor %}</ul></section>
{% endfor %}
4 changes: 2 additions & 2 deletions src/way/unit.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ pagination:
permalink: "/way/{{ unit.routeSlug }}/"
title: "{{ unit.title }}"
---
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="/">Home</a> / <a href="/way/">The Way</a> / {{ unit.title }}</nav>
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="/home/">Home</a> / <a href="/way/">The Way</a> / {{ unit.title }}</nav>
<header class="page-head"><p class="eyebrow">Canon-derived companion · {{ unit.section }}</p><h1>{{ unit.title }}</h1><div class="status-row"><span class="status status-canon">canon-derived</span><span class="status status-interpretation">orientation pending review</span></div><p class="lede">This page preserves the unit’s identity and location while offering paths into interpretation and exact source. It does not rewrite the canonical text.</p></header>
<section class="panel"><h2>Place in the document</h2><dl class="meta"><dt>Unit ID</dt><dd><code>{{ unit.id }}</code></dd><dt>Stable route</dt><dd><code>{{ unit.routeSlug }}</code></dd><dt>Source lines</dt><dd>{{ unit.startLine }}–{{ unit.endLine }}</dd><dt>Detected notes</dt><dd>{{ unit.notes.length }}</dd><dt>Unit digest</dt><dd><code>{{ unit.hash }}</code></dd></dl></section>
<section class="panel"><h2>Place in the document</h2><dl class="meta"><dt>Unit ID</dt><dd><code>{{ unit.id }}</code></dd><dt>Heading level</dt><dd>{{ unit.level }}</dd>{% if unit.parentId %}<dt>Parent heading</dt><dd>{% for parent in generated.canon.units %}{% if parent.id == unit.parentId %}<a href="/way/{{ parent.routeSlug }}/">{{ parent.title }}</a>{% endif %}{% endfor %}</dd>{% endif %}<dt>Stable route</dt><dd><code>{{ unit.routeSlug }}</code></dd><dt>Source lines</dt><dd>{{ unit.startLine }}–{{ unit.endLine }}</dd><dt>Detected notes</dt><dd>{{ unit.notes.length }}</dd><dt>Unit digest</dt><dd><code>{{ unit.hash }}</code></dd></dl></section>
<section class="hmmm"><h2>Companion reading</h2><p>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.</p></section>
<div class="actions"><a class="button" href="/lab/{{ unit.routeSlug }}/">Enter this unit’s Lab</a><a class="button secondary" href="/source/{{ unit.routeSlug }}/">Read exact source</a></div>
12 changes: 11 additions & 1 deletion tests/canon-integrity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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));
});
Loading
Loading