diff --git a/scripts/fetch-project-docs.mjs b/scripts/fetch-project-docs.mjs index cea7d8b..84b7ba7 100644 --- a/scripts/fetch-project-docs.mjs +++ b/scripts/fetch-project-docs.mjs @@ -8,7 +8,7 @@ import { // id: project_documentation_projection // purpose: Build bounded exact-head README and Markdown-document projections for public project pages. // entrypoint: npm run refresh:project-docs -// tests: tests/project-docs.test.mjs +// tests: tests/project-docs.test.mjs, tests/offline-project-snapshot.test.mjs // === END MODULE_BUILD === // === BOUNDARIES === // id: project_documentation_projection_boundary @@ -50,16 +50,41 @@ function unavailableProjection(repo, message) { }; } +async function offlineWithoutSnapshot() { + const repoData = JSON.parse(await readFile(GENERATED_REPOS, 'utf8')); + const byRepository = {}; + for (const repo of repoData.repositories || []) { + byRepository[repo.name] = unavailableProjection( + repo, + 'OFFLINE=1 and no last-known-good project documentation snapshot exists; repository document content remains unavailable.' + ); + } + return { + schema: 'interdependency.project-documentation-map/0.1.0', + organization: 'The-Interdependency', + snapshotAt: null, + fallback: false, + fallbackCount: 0, + byRepository, + hmmm: ['OFFLINE=1: no last-known-good project documentation snapshot exists; emitted metadata-only unavailable projections without inventing document content.'] + }; +} + async function main() { await mkdir('src/_data/generated', { recursive: true }); await mkdir('src/_data/snapshots', { recursive: true }); const previous = await readSnapshot(); if (process.env.OFFLINE === '1') { - if (!previous) throw new Error('OFFLINE=1 and no project documentation snapshot exists'); - const fallback = { ...previous, fallback: true, hmmm: [...new Set([...(previous.hmmm || []), 'OFFLINE=1: displaying the last-known-good project documentation snapshot.'])] }; + const fallback = previous + ? { + ...previous, + fallback: true, + hmmm: [...new Set([...(previous.hmmm || []), 'OFFLINE=1: displaying the last-known-good project documentation snapshot.'])] + } + : await offlineWithoutSnapshot(); await writeFile(GENERATED_OUT, JSON.stringify(fallback, null, 2) + '\n'); - console.log('project-docs fallback'); + console.log(previous ? 'project-docs fallback' : 'project-docs metadata-only offline'); return; } diff --git a/src/_data/builder.json b/src/_data/builder.json index 12597b2..7bdad6f 100644 --- a/src/_data/builder.json +++ b/src/_data/builder.json @@ -26,5 +26,12 @@ "time": "10:13:00-07:00", "model": "GPT-5.6 Sol", "body": "A stale branch is not a present-tense repair. Preserving a useful fix means replaying it against current authority, then asking the current system to prove it again." + }, + { + "id": "2026-09-25-unavailable-is-not-zero", + "date": "2026-09-25", + "time": "10:33:29-07:00", + "model": "GPT-5.6 Sol", + "body": "Zero is an observation. Unavailable is a boundary. Interfaces become unreliable when they substitute the former for the latter simply because zero is easier to render." } ] diff --git a/src/projects/repo.njk b/src/projects/repo.njk index 3bdd523..dfa6b6b 100644 --- a/src/projects/repo.njk +++ b/src/projects/repo.njk @@ -53,10 +53,16 @@ title: "Project: {{ repo.name }}"

Repository-owned reading surface

Documentation

+ {% if repoDocs and repoDocs.unavailable %} +

Repository documentation was not observed for this build. No README or document absence is inferred from this unavailable projection.

+ {% else %}

README and Markdown documentation are copied from the exact repository head recorded by this site build. A live refresh can replace this view with the repository's current public HEAD for this browser session.

+ {% endif %}
- {% if repoDocs and repoDocs.documentation %} + {% if repoDocs and repoDocs.unavailable %} +

hmmm

Repository documentation is unavailable for this build; no document-content observation is claimed.

{% if repoDocs.documentation and repoDocs.documentation.hmmm.length %}
    {% for item in repoDocs.documentation.hmmm %}
  • {{ item }}
  • {% endfor %}
{% endif %}
+ {% elif repoDocs and repoDocs.documentation %} {% if repoDocs.documentation.readme %}

README · {{ repoDocs.headSha | truncate(12, true, '') }}

@@ -93,7 +99,7 @@ title: "Project: {{ repo.name }}"
Repository
{{ repo.name }}
Consumed HEAD
{% if repoDocs and repoDocs.headSha %}{{ repoDocs.headSha }}{% else %}hmmm{% endif %}
Default branch
{{ repo.default_branch or 'hmmm' }}
-
Projected documents
{% if repoDocs and repoDocs.documentation %}{{ repoDocs.documentation.projectedDocumentCount }}{% else %}hmmm{% endif %}
+
Projected documents
{% if repoDocs and repoDocs.unavailable %}hmmm{% elif repoDocs and repoDocs.documentation %}{{ repoDocs.documentation.projectedDocumentCount }}{% else %}hmmm{% endif %}
Projection mode
{% if repoDocs and repoDocs.unavailable %}unavailable{% elif repoDocs and repoDocs.fallback %}same-head last-known-good fallback{% else %}exact-head build observation{% endif %}
diff --git a/tests/generated-site.test.mjs b/tests/generated-site.test.mjs index 8fdf9f1..13e855c 100644 --- a/tests/generated-site.test.mjs +++ b/tests/generated-site.test.mjs @@ -316,12 +316,14 @@ test('generated site exposes the AI context through redundant machine discovery' test('By the builder renders a collapsible date-time-model tree', async () => { const html = await readFile('_site/by-the-builder/index.html', 'utf8'); assert.match(html, /
/); - assert.match(html, /Builder journal · 4 entries<\/summary>/); + assert.match(html, /Builder journal · 5 entries<\/summary>/); assert.match(html, /
/); assert.match(html, /2026-09-23 · 23:38:45-07:00/); assert.match(html, /GPT-5\.6 Sol/); assert.match(html, /2026-09-24-signs-before-systems/); assert.match(html, /2026-09-24 · 18:39:58-07:00/); + assert.match(html, /2026-09-25-unavailable-is-not-zero/); + assert.match(html, /2026-09-25 · 10:33:29-07:00/); assert.match(html, /legacy model record unavailable/); }); diff --git a/tests/offline-project-snapshot.test.mjs b/tests/offline-project-snapshot.test.mjs index 6e6670e..92e292a 100644 --- a/tests/offline-project-snapshot.test.mjs +++ b/tests/offline-project-snapshot.test.mjs @@ -10,6 +10,7 @@ import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const scriptPath = join(repositoryRoot, 'scripts', 'fetch-github-org.mjs'); +const projectDocsScriptPath = join(repositoryRoot, 'scripts', 'fetch-project-docs.mjs'); // Usage: this runs the real refresh script in an isolated temporary working tree with OFFLINE=1. test('offline refresh preserves reviewed active fields and excludes archived repositories', async () => { @@ -79,3 +80,54 @@ test('offline refresh preserves reviewed active fields and excludes archived rep await rm(root, { recursive: true, force: true }); } }); + + +test('clean offline project documentation refresh emits unavailable observations without inventing absence', async () => { + const root = await mkdtemp(join(tmpdir(), 'interdependency-project-docs-offline-')); + try { + await mkdir(join(root, 'src', '_data', 'generated'), { recursive: true }); + await writeFile( + join(root, 'src', '_data', 'generated', 'repos.json'), + JSON.stringify({ + repositories: [{ + name: 'ucns', + default_branch: 'main', + head_sha: '0123456789abcdef0123456789abcdef01234567', + head_committed_at: '2026-09-25T00:00:00Z' + }] + }, null, 2) + ); + + const result = await execFileAsync(process.execPath, [projectDocsScriptPath], { + cwd: root, + env: { ...process.env, OFFLINE: '1', GITHUB_TOKEN: '' } + }); + assert.match(result.stdout, /metadata-only offline/); + + const generated = JSON.parse(await readFile(join(root, 'src', '_data', 'generated', 'projectDocs.json'), 'utf8')); + assert.equal(generated.fallback, false); + assert.equal(generated.fallbackCount, 0); + assert.equal(generated.snapshotAt, null); + assert.match(generated.hmmm.join(' '), /no last-known-good project documentation snapshot exists/); + + const projection = generated.byRepository.ucns; + assert.equal(projection.unavailable, true); + assert.equal(projection.headSha, null); + assert.equal(projection.headCommittedAt, null); + assert.equal(projection.documentation.readme, null); + assert.deepEqual(projection.documentation.documents, []); + assert.equal(projection.documentation.projectedDocumentCount, 0); + assert.match(projection.documentation.hmmm.join(' '), /document content remains unavailable/); + + const projectTemplate = await readFile(join(repositoryRoot, 'src', 'projects', 'repo.njk'), 'utf8'); + assert.match(projectTemplate, /Repository documentation was not observed for this build/); + assert.match(projectTemplate, /no document-content observation is claimed/); + assert.match( + projectTemplate, + /Projected documents<\/dt>
\{% if repoDocs and repoDocs\.unavailable %\}hmmm\{% elif repoDocs and repoDocs\.documentation %\}\{\{ repoDocs\.documentation\.projectedDocumentCount \}\}/ + ); + assert.match(projectTemplate, /repoDocs and repoDocs\.unavailable %\}unavailable/); + } finally { + await rm(root, { recursive: true, force: true }); + } +});