diff --git a/.eleventy.js b/.eleventy.js index f019fb1..34a5356 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -20,6 +20,12 @@ import { installMathRenderer } from './scripts/markdown-math.mjs'; // then: the website publishes the generated presentation view without becoming a second source of repo canon // class: evidence // since: 2026-08-14 +// +// id: website_org_msdmd_has_one_public_projection_copy +// given: commit-pinned repo-owned msdmd collections are joined into the generated organization map +// then: the website publishes that generated evidence artifact unchanged and uses it for the visual map without becoming repository metadata authority +// class: evidence +// since: 2026-08-16 // === END CONTRACTS === // Usage: run `npm run build`; Eleventy emits the static site, copies root machine instructions, and preserves dependency-free public reading paths. @@ -30,6 +36,7 @@ export default function configureEleventy(eleventyConfig) { 'src/assets': 'assets', 'src/_data/gonol_relationship_display.json': 'assets/data/gonol-relationship-display-v1.json', 'src/_data/generated/sitrep.json': 'assets/data/sitrep.json', + 'src/_data/generated/orgMsdmd.json': 'assets/data/org-msdmd.json', 'CNAME': 'CNAME', 'llms.txt': 'llms.txt', 'artifacts/four-cuts-1.html': 'artifacts/four-cuts/index.html', diff --git a/package.json b/package.json index 5b3775b..fa4d117 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "type": "module", "scripts": { "dev": "npm run refresh:data && eleventy --serve", - "refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:sitrep && npm run refresh:textbook && npm run refresh:works", + "refresh:data": "npm run refresh:canon && npm run refresh:github && npm run refresh:msdmd && npm run refresh:sitrep && npm run refresh:textbook && npm run refresh:works", "refresh:canon": "node scripts/fetch-canon.mjs && node scripts/parse-canon.mjs", "refresh:github": "node scripts/fetch-github-org.mjs", + "refresh:msdmd": "node scripts/fetch-org-msdmd.mjs", "refresh:sitrep": "node scripts/fetch-sitrep.mjs", "refresh:textbook": "node scripts/fetch-textbook.mjs", "research:enrich": "node scripts/enrich-citations.mjs", @@ -20,7 +21,7 @@ "validate": "node scripts/validate-content.mjs && node scripts/verify-generated-routes.mjs && node scripts/verify-article-canon.mjs", "build": "npm run validate && eleventy && pagefind --site _site && node scripts/write-build-info.mjs", "pretest": "node scripts/prepare-tests.mjs", - "test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs tests/sitrep.test.mjs", + "test": "node --test tests/aicontext.test.mjs tests/edcm-mathematics.test.mjs tests/gonol-relationships.test.mjs tests/org-msdmd.test.mjs tests/post-merge-reconciliation.test.mjs tests/llms-build.test.mjs tests/canon-parser.test.mjs tests/canon-integrity.test.mjs tests/textbook-integrity.test.mjs tests/math-rendering.test.mjs tests/narratives.test.mjs tests/offline-project-snapshot.test.mjs tests/repo-coverage.test.mjs tests/research-ledger.test.mjs tests/works-registry.test.mjs tests/site-contract.test.mjs tests/sitrep.test.mjs", "test:generated": "node --test tests/generated-site.test.mjs tests/human-ui-generated.test.mjs tests/textbook-generated.test.mjs tests/math-generated.test.mjs && node tests/links.test.mjs", "test:browser": "playwright test", "test:e2e": "playwright test tests/site.spec.mjs", diff --git a/scripts/fetch-github-org.mjs b/scripts/fetch-github-org.mjs index 8e58400..508f302 100644 --- a/scripts/fetch-github-org.mjs +++ b/scripts/fetch-github-org.mjs @@ -4,19 +4,21 @@ import yaml from 'js-yaml'; // === MODULE_BUILD === // id: organization_project_map -// purpose: Build public project pages from GitHub facts plus reviewed repository manifests or central overrides. +// purpose: Build public project pages from GitHub facts plus reviewed repository manifests or central overrides, recording exact default-branch heads for reproducible downstream consumers. // entrypoint: npm run refresh:github // tests: tests/repo-coverage.test.mjs, tests/offline-project-snapshot.test.mjs // === END MODULE_BUILD === // === BOUNDARIES === // id: github_public_metadata -// network: reads only allowlisted HTTPS GitHub API endpoints; optional token raises rate limits +// network: reads only allowlisted HTTPS GitHub API and raw.githubusercontent.com endpoints; optional token raises API rate limits // storage: writes generated and last-known-good JSON snapshots // failure: preserves last-known-good data with fallback=true, including reviewed editorial fields // === END BOUNDARIES === +// Usage: run `npm run refresh:github`; the generated repo snapshot records each exact default-branch head so later collectors can fetch commit-pinned source without repeating GitHub API lookups. const org = 'The-Interdependency'; const githubApiOrigin = 'https://api.github.com'; +const rawGithubOrigin = 'https://raw.githubusercontent.com'; const headers = ['-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28']; if (process.env.GITHUB_TOKEN) headers.push('-H', `Authorization: Bearer ${process.env.GITHUB_TOKEN}`); @@ -26,6 +28,11 @@ function githubApiUrl(pathname, search = {}) { return url; } +function rawGithubUrl(repoName, commit, path) { + const parts = [org, repoName, commit, ...String(path).split('/')].map(encodeURIComponent); + return new URL(`/${parts.join('/')}`, rawGithubOrigin); +} + function getJson(target) { const url = target instanceof URL ? target : new URL(target); if (url.protocol !== 'https:' || url.origin !== githubApiOrigin) { @@ -38,19 +45,45 @@ function getJson(target) { )); } +function getText(target) { + const url = target instanceof URL ? target : new URL(target); + if (url.protocol !== 'https:' || url.origin !== rawGithubOrigin) { + throw new Error(`refusing non-raw-GitHub target: ${url.origin}`); + } + return execFileSync( + 'curl', + ['-fsSL', '--retry', '2', '--max-time', '20', url.href], + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } + ); +} + function normalizeRepoName(value) { const name = String(value || ''); if (!/^[A-Za-z0-9_.-]{1,100}$/.test(name)) throw new Error(`invalid GitHub repository name: ${name}`); return name; } -function getManifest(repoName) { +function getHead(repoName, defaultBranch) { try { + if (!defaultBranch) return { sha: null, committed_at: null }; const safeRepo = normalizeRepoName(repoName); const response = getJson(githubApiUrl( - `/repos/${encodeURIComponent(org)}/${encodeURIComponent(safeRepo)}/contents/.interdependency/project.yml` + `/repos/${encodeURIComponent(org)}/${encodeURIComponent(safeRepo)}/commits/${encodeURIComponent(defaultBranch)}` )); - return yaml.load(Buffer.from(response.content || '', 'base64').toString('utf8')) || null; + return { + sha: response.sha || null, + committed_at: response.commit?.committer?.date || response.commit?.author?.date || null + }; + } catch { + return { sha: null, committed_at: null }; + } +} + +function getManifest(repoName, headSha) { + try { + if (!headSha) return null; + const safeRepo = normalizeRepoName(repoName); + return yaml.load(getText(rawGithubUrl(safeRepo, headSha, '.interdependency/project.yml'))) || null; } catch { return null; } @@ -91,7 +124,10 @@ try { const repositories = rawRepos.map(repo => { const repoName = normalizeRepoName(repo.name); - const manifest = fallback ? null : getManifest(repoName); + const head = fallback + ? { sha: repo.head_sha || null, committed_at: repo.head_committed_at || null } + : getHead(repoName, repo.default_branch); + const manifest = fallback ? null : getManifest(repoName, head.sha); const inheritedEditorial = fallback ? { category: repo.category, status: repo.status, @@ -113,6 +149,9 @@ const repositories = rawRepos.map(repo => { if (!editorial.status && !hmmm.includes('Project maturity has not been explicitly declared.')) { hmmm.push('Project maturity has not been explicitly declared.'); } + if (!fallback && !head.sha) { + hmmm.push('Exact default-branch head was unavailable during this snapshot; commit-pinned downstream collection is suspended for this repository.'); + } return { name: repoName, slug: repoName.toLowerCase().replace(/[^a-z0-9]+/g, '-'), @@ -127,6 +166,8 @@ const repositories = rawRepos.map(repo => { archived: Boolean(repo.archived), fork: Boolean(repo.fork), default_branch: repo.default_branch || null, + head_sha: head.sha, + head_committed_at: head.committed_at, topics: Array.isArray(repo.topics) ? repo.topics : [], license: repo.license?.spdx_id || repo.license || null, language: repo.language || null, diff --git a/scripts/fetch-org-msdmd.mjs b/scripts/fetch-org-msdmd.mjs new file mode 100644 index 0000000..796bb6b --- /dev/null +++ b/scripts/fetch-org-msdmd.mjs @@ -0,0 +1,544 @@ +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { pathToFileURL } from 'node:url'; + +// === MODULE_BUILD === +// id: organization_msdmd_map_collector +// purpose: Join repo-owned msdmd collection points into one provenance-bearing organization map without transferring source authority to the website. +// entrypoint: npm run refresh:msdmd +// tests: tests/org-msdmd.test.mjs +// === END MODULE_BUILD === +// === BOUNDARIES === +// id: organization_msdmd_source_boundary +// network: reads only commit-pinned raw.githubusercontent.com collection files named _msdmd.ts; repository heads come from the prior GitHub metadata refresh +// storage: writes generated and last-known-good JSON snapshots only +// authority: repository collection points remain source authority; this module namespaces, resolves, aggregates, and displays their declared relations +// failure: missing, invalid, stale, ambiguous, and broken explicit inputs remain visible; ordinary owner/file/route/tool/external targets remain valid opaque relations rather than being mislabeled hmmm +// === END BOUNDARIES === +// === CONTRACTS === +// id: organization_msdmd_exact_input_identity +// given: a repository head and collection file are consumed +// then: repository, exact head SHA, collection path, collection SHA-256, declared source commit, and match status remain in the output receipt +// class: evidence +// +// id: organization_msdmd_target_kinds_remain_distinct +// given: an msdmd edge target is not a repository or declaration identity +// then: retain it as an opaque external target; only empty, ambiguous declaration, or broken explicit declaration references become unresolved hmmm +// class: safety +// +// id: organization_msdmd_reproducible_snapshot +// given: repository heads and collection bytes do not change +// then: the emitted JSON is byte-identical because no wall-clock build timestamp enters the artifact +// class: correctness +// === END CONTRACTS === +// Usage: run after `npm run refresh:github`. The generated artifact is consumed by /projects/map/ and copied to /assets/data/org-msdmd.json. + +const ORGANIZATION = 'The-Interdependency'; +const RAW_GITHUB_ORIGIN = 'https://raw.githubusercontent.com'; +const GENERATED_REPOS = 'src/_data/generated/repos.json'; +const GENERATED_OUT = 'src/_data/generated/orgMsdmd.json'; +const SNAPSHOT_OUT = 'src/_data/snapshots/org-msdmd.last-known-good.json'; +const COLLECTION_SUFFIX = '_msdmd.ts'; +const COLLECTION_MARKER = 'defineMsdmdCollection('; +const HMMM_RESOLUTIONS = new Set(['empty-target', 'ambiguous-declaration', 'broken-explicit-declaration']); + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} + +function stableJson(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function rawGithubUrl(repo, commit, path) { + const parts = [ORGANIZATION, repo, commit, ...String(path).split('/')].map(encodeURIComponent); + return new URL(`/${parts.join('/')}`, RAW_GITHUB_ORIGIN); +} + +function getText(target) { + const url = target instanceof URL ? target : new URL(target); + if (url.protocol !== 'https:' || url.origin !== RAW_GITHUB_ORIGIN) { + throw new Error(`refusing non-raw-GitHub target: ${url.origin}`); + } + return execFileSync( + 'curl', + ['-fsSL', '--retry', '2', '--max-time', '20', url.href], + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } + ); +} + +export function stripComments(text) { + let out = ''; + let quote = ''; + for (let i = 0; i < text.length;) { + const ch = text[i]; + if (quote) { + out += ch; + if (ch === '\\' && i + 1 < text.length) { + out += text[i + 1]; + i += 2; + continue; + } + if (ch === quote) quote = ''; + i += 1; + continue; + } + if (ch === '"' || ch === "'") { + quote = ch; + out += ch; + i += 1; + continue; + } + if (ch === '/' && text[i + 1] === '/') { + i += 2; + while (i < text.length && text[i] !== '\n') i += 1; + continue; + } + if (ch === '/' && text[i + 1] === '*') { + const end = text.indexOf('*/', i + 2); + i = end < 0 ? text.length : end + 2; + continue; + } + out += ch; + i += 1; + } + return out; +} + +export function extractCollectionPayload(text) { + const start = text.indexOf(COLLECTION_MARKER); + if (start < 0) throw new Error('collection marker not found'); + const payloadStart = start + COLLECTION_MARKER.length; + let depth = 1; + let quote = ''; + for (let i = payloadStart; i < text.length; i += 1) { + const ch = text[i]; + if (quote) { + if (ch === '\\') { + i += 1; + continue; + } + if (ch === quote) quote = ''; + continue; + } + if (ch === '"' || ch === "'") { + quote = ch; + continue; + } + if (ch === '(') depth += 1; + if (ch === ')') { + depth -= 1; + if (depth === 0) return text.slice(payloadStart, i); + } + } + throw new Error('unterminated collection call'); +} + +function readQuoted(text, start) { + const quote = text[start]; + let value = ''; + for (let i = start + 1; i < text.length; i += 1) { + const ch = text[i]; + if (ch === quote) return { json: JSON.stringify(value), next: i + 1 }; + if (ch !== '\\') { + value += ch; + continue; + } + if (i + 1 >= text.length) throw new Error('unterminated string escape'); + const next = text[++i]; + const escapes = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', '0': '\0' }; + if (next === 'u') { + const hex = text.slice(i + 1, i + 5); + if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new Error('invalid unicode escape'); + value += String.fromCharCode(Number.parseInt(hex, 16)); + i += 4; + } else if (next === 'x') { + const hex = text.slice(i + 1, i + 3); + if (!/^[0-9a-fA-F]{2}$/.test(hex)) throw new Error('invalid hex escape'); + value += String.fromCharCode(Number.parseInt(hex, 16)); + i += 2; + } else { + value += escapes[next] ?? next; + } + } + throw new Error('unterminated quoted string'); +} + +export function objectLiteralToJson(text) { + let out = ''; + for (let i = 0; i < text.length;) { + const ch = text[i]; + if (ch === '"' || ch === "'") { + const quoted = readQuoted(text, i); + out += quoted.json; + i = quoted.next; + continue; + } + const match = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(text.slice(i)); + if (match) { + const ident = match[0]; + let j = i + ident.length; + while (/\s/.test(text[j] || '')) j += 1; + out += text[j] === ':' ? JSON.stringify(ident) : ident; + i += ident.length; + continue; + } + out += ch; + i += 1; + } + return out.replace(/,\s*([}\]])/g, '$1'); +} + +export function parseCollectionText(text) { + const stripped = stripComments(String(text)).trim(); + if (stripped.startsWith('{')) return JSON.parse(stripped); + const payload = extractCollectionPayload(stripped).trim(); + try { + return JSON.parse(payload); + } catch { + return JSON.parse(objectLiteralToJson(payload)); + } +} + +function normalizeRepoName(value) { + const name = String(value || ''); + if (!/^[A-Za-z0-9_.-]{1,100}$/.test(name)) throw new Error(`invalid repository name: ${name}`); + return name; +} + +function collectionRepoMatches(value, repoName) { + if (!value) return true; + return value === repoName || value === `${ORGANIZATION}/${repoName}`; +} + +function normalizeDeclaration(repoName, declaration) { + const localId = String(declaration?.id || '').trim(); + if (!localId) return null; + return { + id: `${repoName}::${localId}`, + localId, + repo: repoName, + file: String(declaration?.file || 'hmmm'), + block: String(declaration?.block || 'hmmm'), + fields: declaration?.fields && typeof declaration.fields === 'object' ? declaration.fields : {} + }; +} + +function explicitRepoTarget(target, repoNames) { + if (repoNames.has(target)) return { repo: target, declaration: null }; + const escapedOrg = ORGANIZATION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const full = new RegExp(`^${escapedOrg}/([^@#:\\s/]+)(?:@[^#:\\s]+)?(?:#{1}|::)(.+)$`).exec(target); + if (full && repoNames.has(full[1])) return { repo: full[1], declaration: full[2].trim() || null }; + const fullRepo = new RegExp(`^${escapedOrg}/([^@#:\\s/]+)(?:@[^#:\\s]+)?$`).exec(target); + if (fullRepo && repoNames.has(fullRepo[1])) return { repo: fullRepo[1], declaration: null }; + const scoped = /^([^:\s]+)::(.+)$/.exec(target); + if (scoped && repoNames.has(scoped[1])) return { repo: scoped[1], declaration: scoped[2].trim() || null }; + return null; +} + +function resolveEdgeTarget(sourceRepo, target, context) { + const raw = String(target || '').trim(); + if (!raw) return { resolution: 'empty-target', targetId: null, targetRepo: null }; + + const local = `${sourceRepo}::${raw}`; + if (context.declarationIds.has(local)) { + return { resolution: 'local-declaration', targetId: local, targetRepo: sourceRepo }; + } + + const explicit = explicitRepoTarget(raw, context.repoNames); + if (explicit) { + if (!explicit.declaration) { + return { resolution: 'explicit-repository', targetId: `repo:${explicit.repo}`, targetRepo: explicit.repo }; + } + const explicitId = `${explicit.repo}::${explicit.declaration}`; + if (context.declarationIds.has(explicitId)) { + return { resolution: 'explicit-declaration', targetId: explicitId, targetRepo: explicit.repo }; + } + return { resolution: 'broken-explicit-declaration', targetId: null, targetRepo: explicit.repo }; + } + + const global = context.globalIds.get(raw) || []; + if (global.length === 1) { + const [targetId] = global; + return { resolution: 'unique-global-declaration', targetId, targetRepo: targetId.split('::')[0] }; + } + if (global.length > 1) { + return { resolution: 'ambiguous-declaration', targetId: null, targetRepo: null }; + } + return { resolution: 'external-target', targetId: null, targetRepo: null }; +} + +function isHmmmResolution(value) { + return HMMM_RESOLUTIONS.has(value); +} + +function aggregateRepositoryEdges(edges) { + const grouped = new Map(); + for (const edge of edges) { + if (!edge.targetRepo || edge.targetRepo === edge.sourceRepo || isHmmmResolution(edge.resolution)) continue; + const key = `${edge.sourceRepo}\u0000${edge.targetRepo}`; + const entry = grouped.get(key) || { + from: edge.sourceRepo, + to: edge.targetRepo, + count: 0, + kinds: new Set(), + sourceBlocks: new Set() + }; + entry.count += 1; + entry.kinds.add(edge.kind); + entry.sourceBlocks.add(edge.sourceBlock); + grouped.set(key, entry); + } + return [...grouped.values()] + .map(item => ({ + from: item.from, + to: item.to, + count: item.count, + kinds: [...item.kinds].sort(), + sourceBlocks: [...item.sourceBlocks].sort() + })) + .sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to)); +} + +export function buildOrgMap(repositoryInputs) { + const repoNames = new Set(repositoryInputs.map(item => item.name)); + const declarations = []; + const gaps = []; + const repositoryRows = []; + + for (const item of repositoryInputs) { + const hmmm = [...(item.hmmm || [])]; + const collection = item.collection || null; + const row = { + name: item.name, + slug: item.slug, + archived: Boolean(item.archived), + defaultBranch: item.defaultBranch || null, + headSha: item.headSha || null, + headCommittedAt: item.headCommittedAt || null, + collection: { + status: item.collectionStatus || 'missing', + path: item.collectionPath || `${item.name}${COLLECTION_SUFFIX}`, + sha256: item.collectionSha256 || null, + declaredRepo: collection?.repo || null, + declaredSourceCommit: collection?.source_commit || null, + sourceCommitMatchesHead: collection?.source_commit && item.headSha + ? collection.source_commit === item.headSha + : null, + error: item.collectionError || null + }, + counts: { + declarations: 0, + gaps: 0, + edges: 0, + resolvedEdges: 0, + externalTargets: 0, + unresolvedEdges: 0, + crossRepoEdges: 0 + }, + blockCounts: {}, + hmmm + }; + + if (!collection) { + if (item.collectionStatus === 'invalid') hmmm.push('The repository collection point was present but could not be parsed.'); + else hmmm.push('No consumable repo-level msdmd collection point was found at the recorded head.'); + repositoryRows.push(row); + continue; + } + if (!collectionRepoMatches(collection.repo, item.name)) { + hmmm.push(`Collection declares repo=${collection.repo}; consumed repository is ${item.name}.`); + } + if (row.collection.sourceCommitMatchesHead === false) { + hmmm.push('Collection-declared source_commit does not match the repository head consumed by this website build.'); + } + + for (const declaration of collection.declarations || []) { + const normalized = normalizeDeclaration(item.name, declaration); + if (!normalized) continue; + declarations.push(normalized); + row.counts.declarations += 1; + row.blockCounts[normalized.block] = (row.blockCounts[normalized.block] || 0) + 1; + } + for (const gap of collection.gaps || []) { + gaps.push({ + repo: item.name, + file: String(gap?.file || 'hmmm'), + missing: Array.isArray(gap?.missing) ? gap.missing.map(String) : [], + reason: gap?.reason ? String(gap.reason) : null + }); + row.counts.gaps += 1; + } + row._rawEdges = Array.isArray(collection.edges) ? collection.edges : []; + row.counts.edges = row._rawEdges.length; + repositoryRows.push(row); + } + + const declarationIds = new Set(declarations.map(item => item.id)); + const globalIds = new Map(); + for (const declaration of declarations) { + const values = globalIds.get(declaration.localId) || []; + values.push(declaration.id); + globalIds.set(declaration.localId, values); + } + const context = { repoNames, declarationIds, globalIds }; + const edges = []; + for (const row of repositoryRows) { + for (const edge of row._rawEdges || []) { + const sourceLocalId = String(edge?.from || edge?.source_id || 'hmmm'); + const sourceId = `${row.name}::${sourceLocalId}`; + const targetRaw = String(edge?.to || ''); + const resolved = resolveEdgeTarget(row.name, targetRaw, context); + const normalized = { + sourceRepo: row.name, + sourceId, + sourceLocalId, + targetRaw, + targetId: resolved.targetId, + targetRepo: resolved.targetRepo, + resolution: resolved.resolution, + kind: String(edge?.kind || 'relation'), + sourceBlock: String(edge?.source_block || 'hmmm'), + sourceDeclarationId: String(edge?.source_id || sourceLocalId) + }; + edges.push(normalized); + if (isHmmmResolution(normalized.resolution)) row.counts.unresolvedEdges += 1; + else if (normalized.resolution === 'external-target') row.counts.externalTargets += 1; + else row.counts.resolvedEdges += 1; + if (normalized.targetRepo && normalized.targetRepo !== row.name && !isHmmmResolution(normalized.resolution)) { + row.counts.crossRepoEdges += 1; + } + } + delete row._rawEdges; + } + + const repositoryEdges = aggregateRepositoryEdges(edges); + const unresolvedEdges = edges.filter(edge => isHmmmResolution(edge.resolution)); + const externalTargetCount = edges.filter(edge => edge.resolution === 'external-target').length; + const graphResolvedEdgeCount = edges.length - unresolvedEdges.length - externalTargetCount; + const collectionCount = repositoryRows.filter(repo => repo.collection.status === 'ok').length; + const invalidCollectionCount = repositoryRows.filter(repo => repo.collection.status === 'invalid').length; + const missingCollectionCount = repositoryRows.length - collectionCount - invalidCollectionCount; + const latestHeadCommittedAt = repositoryRows + .map(repo => repo.headCommittedAt) + .filter(Boolean) + .sort() + .at(-1) || null; + const stateMaterial = repositoryRows + .map(repo => ({ + repo: repo.name, + headSha: repo.headSha, + collectionStatus: repo.collection.status, + collectionSha256: repo.collection.sha256 + })) + .sort((a, b) => a.repo.localeCompare(b.repo)); + + return { + schema: 'interdependency.org-msdmd-map/0.1.0', + organization: ORGANIZATION, + fallback: false, + sourceSnapshot: { + policy: 'current default-branch heads recorded by refresh:github; collection bytes fetched at those exact SHAs', + latestHeadCommittedAt, + stateDigest: sha256(JSON.stringify(stateMaterial)) + }, + summary: { + repositoryCount: repositoryRows.length, + collectionCount, + missingCollectionCount, + invalidCollectionCount, + declarationCount: declarations.length, + gapCount: gaps.length, + edgeCount: edges.length, + resolvedEdgeCount: graphResolvedEdgeCount, + externalTargetCount, + crossRepoEdgeCount: repositoryEdges.reduce((sum, edge) => sum + edge.count, 0), + crossRepoPairCount: repositoryEdges.length, + unresolvedEdgeCount: unresolvedEdges.length + }, + repositories: repositoryRows.sort((a, b) => a.name.localeCompare(b.name)), + declarations: declarations.sort((a, b) => a.repo.localeCompare(b.repo) || a.file.localeCompare(b.file) || a.block.localeCompare(b.block) || a.localId.localeCompare(b.localId)), + gaps: gaps.sort((a, b) => a.repo.localeCompare(b.repo) || a.file.localeCompare(b.file)), + edges: edges.sort((a, b) => a.sourceRepo.localeCompare(b.sourceRepo) || a.sourceId.localeCompare(b.sourceId) || a.kind.localeCompare(b.kind) || a.targetRaw.localeCompare(b.targetRaw)), + repositoryEdges, + unresolvedEdges: unresolvedEdges.sort((a, b) => a.sourceRepo.localeCompare(b.sourceRepo) || a.sourceId.localeCompare(b.sourceId) || a.targetRaw.localeCompare(b.targetRaw)), + hmmm: [ + 'Only empty, ambiguous declaration, and broken explicit declaration references are unresolved. Ordinary owners, routes, files, tools, capability names, and external systems remain valid opaque targets.', + 'Missing collection points remain visible until the source repository publishes one.' + ] + }; +} + +async function fetchRepositoryInputs(repositories) { + const inputs = []; + for (const repo of repositories) { + const name = normalizeRepoName(repo.name); + const collectionPath = `${name}${COLLECTION_SUFFIX}`; + const base = { + name, + slug: repo.slug || name.toLowerCase().replace(/[^a-z0-9]+/g, '-'), + archived: repo.archived, + defaultBranch: repo.default_branch, + headSha: repo.head_sha || null, + headCommittedAt: repo.head_committed_at || null, + collectionPath, + hmmm: [] + }; + if (!base.headSha) { + inputs.push({ ...base, collectionStatus: 'missing', collectionError: 'exact default-branch head unavailable' }); + continue; + } + try { + const text = getText(rawGithubUrl(name, base.headSha, collectionPath)); + const collection = parseCollectionText(text); + inputs.push({ + ...base, + collectionStatus: 'ok', + collectionSha256: sha256(Buffer.from(text, 'utf8')), + collection + }); + } catch (error) { + const stderr = String(error?.stderr || ''); + const status = /404|not found/i.test(stderr) ? 'missing' : 'invalid'; + inputs.push({ + ...base, + collectionStatus: status, + collectionError: status === 'missing' ? 'collection point absent at consumed head' : String(error?.message || error) + }); + } + } + return inputs; +} + +async function main() { + await mkdir('src/_data/generated', { recursive: true }); + await mkdir('src/_data/snapshots', { recursive: true }); + + if (process.env.OFFLINE === '1') { + const fallback = JSON.parse(await readFile(SNAPSHOT_OUT, 'utf8')); + fallback.fallback = true; + fallback.hmmm = [...new Set([...(fallback.hmmm || []), 'OFFLINE=1: displaying the last-known-good org msdmd snapshot.'])]; + await writeFile(GENERATED_OUT, stableJson(fallback)); + console.log(`org-msdmd ${fallback.summary?.collectionCount || 0}/${fallback.summary?.repositoryCount || 0} fallback`); + return; + } + + try { + const repoData = JSON.parse(await readFile(GENERATED_REPOS, 'utf8')); + const inputs = await fetchRepositoryInputs(repoData.repositories || []); + const data = buildOrgMap(inputs); + await writeFile(GENERATED_OUT, stableJson(data)); + await writeFile(SNAPSHOT_OUT, stableJson(data)); + console.log(`org-msdmd ${data.summary.collectionCount}/${data.summary.repositoryCount} collections · ${data.summary.crossRepoPairCount} cross-repo pairs · ${data.summary.externalTargetCount} opaque targets · ${data.summary.unresolvedEdgeCount} hmmm edges`); + } catch (error) { + const fallback = JSON.parse(await readFile(SNAPSHOT_OUT, 'utf8')); + fallback.fallback = true; + fallback.hmmm = [...new Set([...(fallback.hmmm || []), `Refresh failed; displaying last-known-good snapshot: ${String(error?.message || error)}`])]; + await writeFile(GENERATED_OUT, stableJson(fallback)); + console.log(`org-msdmd ${fallback.summary?.collectionCount || 0}/${fallback.summary?.repositoryCount || 0} fallback`); + } +} + +const invokedAsScript = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; +if (invokedAsScript) await main(); diff --git a/src/_data/snapshots/org-msdmd.last-known-good.json b/src/_data/snapshots/org-msdmd.last-known-good.json new file mode 100644 index 0000000..c0c4bd3 --- /dev/null +++ b/src/_data/snapshots/org-msdmd.last-known-good.json @@ -0,0 +1,36 @@ +{ + "schema": "interdependency.org-msdmd-map/0.1.0", + "organization": "The-Interdependency", + "fallback": true, + "sourceSnapshot": { + "policy": "bootstrap fallback only; no repository collection evidence has been consumed in this checked-in snapshot", + "latestHeadCommittedAt": null, + "stateDigest": "hmmm-bootstrap-no-source-evidence" + }, + "summary": { + "repositoryCount": 0, + "collectionCount": 0, + "missingCollectionCount": 0, + "invalidCollectionCount": 0, + "declarationCount": 0, + "gapCount": 0, + "edgeCount": 0, + "resolvedEdgeCount": 0, + "externalTargetCount": 0, + "crossRepoEdgeCount": 0, + "crossRepoPairCount": 0, + "unresolvedEdgeCount": 0 + }, + "repositories": [], + "declarations": [], + "gaps": [], + "edges": [], + "repositoryEdges": [], + "unresolvedEdges": [], + "hmmm": [ + "Bootstrap fallback: this checked-in artifact contains no repository collection evidence and must not be mistaken for an organization-state measurement.", + "A successful online refresh replaces this file in the build workspace with a commit-pinned last-known-good snapshot.", + "Only empty, ambiguous declaration, and broken explicit declaration references are unresolved. Ordinary owners, routes, files, tools, capability names, and external systems remain valid opaque targets.", + "Missing collection points remain visible until the source repository publishes one." + ] +} diff --git a/src/_includes/layouts/base.njk b/src/_includes/layouts/base.njk index 788b5b9..7fed671 100644 --- a/src/_includes/layouts/base.njk +++ b/src/_includes/layouts/base.njk @@ -13,7 +13,9 @@ + + diff --git a/src/assets/css/org-map.css b/src/assets/css/org-map.css new file mode 100644 index 0000000..8e8fcab --- /dev/null +++ b/src/assets/css/org-map.css @@ -0,0 +1,39 @@ +/* Usage: loaded by the base layout; styles activate only on /projects/map/ markup and keep the static fallback readable without JavaScript. */ +.org-map-shell { margin: 2rem 0 3.5rem; } +.org-map-frame { position: relative; min-height: 34rem; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius); background: radial-gradient(circle at 50% 50%, rgba(155, 135, 245, .11), rgba(16, 24, 42, .9) 42%, rgba(9, 13, 24, .98) 78%); } +.org-map-frame svg { display: block; width: 100%; min-height: 34rem; } +.org-map-edge { stroke: rgba(174, 187, 210, .36); vector-effect: non-scaling-stroke; } +.org-map-edge[data-kind~="requires"] { stroke: var(--cyan); } +.org-map-edge[data-kind~="claims_proves"] { stroke: #9ce2b9; } +.org-map-node circle { fill: var(--night-raised); stroke: var(--violet); stroke-width: 2; vector-effect: non-scaling-stroke; } +.org-map-node[data-status="missing"] circle { stroke: var(--amber); stroke-dasharray: 5 4; } +.org-map-node[data-status="invalid"] circle { stroke: var(--scarlet); } +.org-map-node[data-archived="true"] { opacity: .55; } +.org-map-node text { fill: var(--starlight); font: 700 15px/1 ui-monospace, SFMono-Regular, Consolas, monospace; pointer-events: none; paint-order: stroke; stroke: var(--night); stroke-width: 4px; stroke-linejoin: round; } +.org-map-node:hover circle, .org-map-node:focus circle { fill: var(--night-soft); stroke: var(--cyan); stroke-width: 3; } +.org-map-detail { position: absolute; left: 1rem; bottom: 1rem; z-index: 2; max-width: min(31rem, calc(100% - 2rem)); padding: .8rem 1rem; border: 1px solid var(--line); border-radius: .75rem; background: rgba(9, 13, 24, .94); box-shadow: var(--shadow); } +.org-map-detail strong { display: block; color: var(--starlight); } +.org-map-detail span { color: var(--silver); font-size: .9rem; } +.org-map-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .75rem; margin: 1.25rem 0 2rem; } +.org-map-stat { padding: .85rem 1rem; border: 1px solid var(--line); border-radius: .75rem; background: rgba(16,24,42,.72); } +.org-map-stat strong { display: block; color: var(--starlight); font: 800 1.55rem/1.2 ui-monospace, monospace; } +.org-map-stat span { color: var(--silver); font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; } +.org-map-repo-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); } +.org-map-repo-card dl { display: grid; grid-template-columns: auto 1fr; gap: .2rem .7rem; margin: .8rem 0 0; font-size: .86rem; } +.org-map-repo-card dt { color: var(--silver); } +.org-map-repo-card dd { margin: 0; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; } +.org-map-table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); } +.org-map-table { width: 100%; border-collapse: collapse; min-width: 42rem; } +.org-map-table th, .org-map-table td { padding: .7rem .8rem; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; } +.org-map-table th { color: var(--cyan); font: 800 .74rem/1.3 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .06em; } +.org-map-table td { color: var(--silver); } +.org-map-table tr:last-child td { border-bottom: 0; } +.org-map-receipts { display: grid; gap: .6rem; padding: 0; list-style: none; } +.org-map-receipts li { padding: .75rem .9rem; border: 1px solid var(--line); border-radius: .7rem; background: rgba(16,24,42,.62); } +.org-map-receipts code { overflow-wrap: anywhere; } +.org-map-unresolved { display: grid; gap: .45rem; padding-left: 1.2rem; } +.org-map-unresolved code { overflow-wrap: anywhere; } +@media (max-width: 680px) { + .org-map-frame, .org-map-frame svg { min-height: 28rem; } + .org-map-detail { position: static; max-width: none; margin: 0; border-width: 1px 0 0; border-radius: 0; box-shadow: none; } +} diff --git a/src/assets/js/org-map.js b/src/assets/js/org-map.js new file mode 100644 index 0000000..781dcce --- /dev/null +++ b/src/assets/js/org-map.js @@ -0,0 +1,136 @@ +// === MODULE_BUILD === +// id: organization_msdmd_map_renderer +// purpose: Render the generated repository-level msdmd relation graph as an accessible SVG enhancement while preserving the complete static fallback below it. +// entrypoint: base layout deferred script; activates only when [data-org-map] exists +// tests: tests/org-msdmd.test.mjs, tests/generated-site.test.mjs +// === END MODULE_BUILD === +// === BOUNDARIES === +// id: organization_msdmd_map_renderer_boundary +// network: fetches only the same-origin generated /assets/data/org-msdmd.json artifact +// storage: none +// failure: leaves the complete server-rendered repository, relation, unresolved-edge, and provenance lists intact +// === END BOUNDARIES === +// Usage: no direct invocation is needed. The /projects/map/ page supplies data-source and an accessible static fallback; this script adds only the visual graph. + +const SVG_NS = 'http://www.w3.org/2000/svg'; + +function svgElement(name, attributes = {}) { + const element = document.createElementNS(SVG_NS, name); + for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value)); + return element; +} + +function shortName(value, limit = 18) { + const text = String(value || 'hmmm'); + return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`; +} + +function positionsFor(repositories) { + const rows = [...repositories].sort((a, b) => a.name.localeCompare(b.name)); + const centerX = 500; + const centerY = 350; + const split = rows.length > 18 ? Math.ceil(rows.length * 0.62) : rows.length; + return new Map(rows.map((repo, index) => { + const outer = index < split; + const ringRows = outer ? rows.slice(0, split) : rows.slice(split); + const ringIndex = outer ? index : index - split; + const radius = outer ? 285 : 175; + const angle = (-Math.PI / 2) + (2 * Math.PI * ringIndex / Math.max(1, ringRows.length)); + return [repo.name, { + x: centerX + Math.cos(angle) * radius, + y: centerY + Math.sin(angle) * radius + }]; + })); +} + +function updateDetail(detail, repo) { + detail.replaceChildren(); + const strong = document.createElement('strong'); + strong.textContent = repo.name; + const span = document.createElement('span'); + const collection = repo.collection?.status || 'hmmm'; + const counts = repo.counts || {}; + span.textContent = `${collection} collection · ${counts.declarations || 0} declarations · ${counts.edges || 0} edges · ${counts.gaps || 0} gaps`; + detail.append(strong, span); +} + +export function renderOrganizationMap(root, data) { + const frame = root.querySelector('[data-org-map-frame]'); + const detail = root.querySelector('[data-org-map-detail]'); + if (!frame || !detail || !Array.isArray(data?.repositories)) return; + + const positions = positionsFor(data.repositories); + const byName = new Map(data.repositories.map(repo => [repo.name, repo])); + const svg = svgElement('svg', { + viewBox: '0 0 1000 700', + role: 'img', + 'aria-label': 'Repository relationship map generated from msdmd declarations' + }); + + const edgeLayer = svgElement('g', { 'aria-hidden': 'true' }); + for (const edge of data.repositoryEdges || []) { + const from = positions.get(edge.from); + const to = positions.get(edge.to); + if (!from || !to) continue; + const line = svgElement('line', { + x1: from.x, + y1: from.y, + x2: to.x, + y2: to.y, + class: 'org-map-edge', + 'data-kind': (edge.kinds || []).join(' '), + 'stroke-width': Math.min(6, 1 + Math.log2((edge.count || 1) + 1)) + }); + edgeLayer.append(line); + } + svg.append(edgeLayer); + + const nodeLayer = svgElement('g'); + for (const repo of data.repositories) { + const point = positions.get(repo.name); + if (!point) continue; + const link = svgElement('a', { + href: `/projects/${repo.slug}/`, + class: 'org-map-node', + 'data-status': repo.collection?.status || 'hmmm', + 'data-archived': Boolean(repo.archived), + 'aria-label': `${repo.name}: ${repo.collection?.status || 'hmmm'} msdmd collection` + }); + const radius = Math.min(21, 8 + Math.sqrt(repo.counts?.declarations || 0)); + link.append(svgElement('circle', { cx: point.x, cy: point.y, r: radius })); + const label = svgElement('text', { + x: point.x, + y: point.y + radius + 19, + 'text-anchor': 'middle' + }); + label.textContent = shortName(repo.name); + link.append(label); + link.addEventListener('mouseenter', () => updateDetail(detail, repo)); + link.addEventListener('focus', () => updateDetail(detail, repo)); + nodeLayer.append(link); + } + svg.append(nodeLayer); + + frame.prepend(svg); + const first = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name))[0]; + if (first) updateDetail(detail, first); +} + +async function activate() { + const root = document.querySelector('[data-org-map]'); + if (!root) return; + const source = root.getAttribute('data-source'); + if (!source) return; + try { + const response = await fetch(source, { credentials: 'same-origin' }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + renderOrganizationMap(root, await response.json()); + } catch (error) { + const detail = root.querySelector('[data-org-map-detail]'); + if (detail) { + detail.textContent = `Visual enhancement unavailable; the complete static map remains below. ${String(error?.message || error)}`; + } + } +} + +activate(); diff --git a/src/projects/index.njk b/src/projects/index.njk index c457490..9393dc9 100644 --- a/src/projects/index.njk +++ b/src/projects/index.njk @@ -3,5 +3,5 @@ layout: layouts/base.njk title: Projects description: A generated map of every public repository in The Interdependency organization, grouped by function and status. --- -

Build-time organization map

Projects

Every public repository receives a page. GitHub facts update automatically at build time; reviewed manifests supply purpose, maturity, relations, and primary artifacts. Missing editorial knowledge remains visible as hmmm.

{{ generated.repos.publicRepoCount }} public repos{% if generated.repos.fallback %}last-known-good snapshot{% endif %}
+

Build-time organization map

Projects

Every public repository receives a page. GitHub facts update automatically at build time; reviewed manifests supply purpose, maturity, relations, and primary artifacts. Missing editorial knowledge remains visible as hmmm.

{{ generated.repos.publicRepoCount }} public repos{% if generated.repos.fallback %}last-known-good snapshot{% endif %}
{% for category in generated.repos.categories %}

Project constellation

{{ category }}

{% for repo in generated.repos.repositories %}{% if repo.category == category %}
{{ repo.status }}{% if repo.archived %}archived{% endif %}

{{ repo.name }}

{{ repo.description or 'No public summary supplied.' }}

{% endif %}{% endfor %}
{% endfor %} diff --git a/src/projects/map/index.njk b/src/projects/map/index.njk new file mode 100644 index 0000000..8602802 --- /dev/null +++ b/src/projects/map/index.njk @@ -0,0 +1,108 @@ +--- +layout: layouts/base.njk +title: Organization map +description: A build-time, provenance-bearing relationship map generated from repository-owned msdmd collection points. +--- +{% set map = generated.orgMsdmd %} +
+

Repository-owned msdmd → organization-scale integration

+

Organization map

+

Repositories describe themselves. This website joins those commit-pinned descriptions and displays the resulting relations without becoming a second source of repository canon.

+
+ {{ map.summary.collectionCount }}/{{ map.summary.repositoryCount }} collections + {{ map.summary.declarationCount }} declarations + {{ map.summary.crossRepoPairCount }} cross-repo pairs + {% if map.summary.unresolvedEdgeCount %}{{ map.summary.unresolvedEdgeCount }} hmmm edges{% endif %} + {% if map.fallback %}last-known-good snapshot{% endif %} +
+
+ +
+

Generated view

+

Relations currently visible

+

The graphic shows repository-level relations only when an msdmd edge resolves to a repository or declaration identity. Owners, routes, files, tools, capability names, and external systems remain valid opaque targets; only genuinely broken or ambiguous graph references become hmmm.

+
+
{{ map.summary.repositoryCount }}repositories
+
{{ map.summary.collectionCount }}collections
+
{{ map.summary.edgeCount }}declared edges
+
{{ map.summary.resolvedEdgeCount }}graph-resolved
+
{{ map.summary.externalTargetCount or 0 }}opaque targets
+
{{ map.summary.gapCount }}coverage gaps
+
{{ map.summary.unresolvedEdgeCount }}hmmm edges
+
+
+
+
JavaScript adds the visual graph here. The complete static evidence remains below.
+
+
+
+ +
+

Static fallback · complete repository surface

+

Repository evidence

+ +
+ +
+

Exact cross-repository resolution

+

Cross-repo relations

+ {% if map.repositoryEdges.length %} +
+ + + + {% for edge in map.repositoryEdges %}{% endfor %} + +
FromToRelationsDeclarations
{{ edge.from }}{{ edge.to }}{{ edge.kinds | join(', ') }}{{ edge.count }}
+
+ {% else %} +

hmmm

No current msdmd edge resolves exactly across repository boundaries. The repositories remain mapped individually; the website will not invent cross-repo edges from naming similarity or editorial familiarity.

+ {% endif %} +
+ +
+

Resolution boundary

+

hmmm edges

+ {% if map.unresolvedEdges.length %} +
+ {{ map.unresolvedEdges.length }} declared graph targets remain genuinely unresolved +
    + {% for edge in map.unresolvedEdges %}
  1. {{ edge.sourceId }} —{{ edge.kind }}→ {{ edge.targetRaw or 'hmmm' }} · {{ edge.resolution }}
  2. {% endfor %} +
+
+ {% else %}

No broken or ambiguous graph targets in this snapshot. Opaque non-graph targets remain represented in the machine artifact.

{% endif %} +
+ +
+

Build receipt

+

Consumed source identities

+

The snapshot identity is {{ map.sourceSnapshot.stateDigest }}. Its source time is the latest consumed repository head time, {{ map.sourceSnapshot.latestHeadCommittedAt or 'hmmm' }}. Rebuilding against unchanged heads and collection bytes produces the same JSON.

+ +
+ +
+

hmmm

+ {% for item in map.hmmm %}

{{ item }}

{% endfor %} +
diff --git a/tests/org-msdmd.test.mjs b/tests/org-msdmd.test.mjs new file mode 100644 index 0000000..c093902 --- /dev/null +++ b/tests/org-msdmd.test.mjs @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { buildOrgMap, parseCollectionText } from '../scripts/fetch-org-msdmd.mjs'; + +// Usage: `node --test tests/org-msdmd.test.mjs`; all fixtures are local and perform no network requests. + +test('parses generated and hand-authored msdmd collection points without evaluation', () => { + const generated = ` + import { defineMsdmdCollection } from './collection'; + export default defineMsdmdCollection({"repo":"alpha","declarations":[],"gaps":[],"edges":[]}); + // ratios: ignored + `; + assert.deepEqual(parseCollectionText(generated), { + repo: 'alpha', declarations: [], gaps: [], edges: [] + }); + + const handAuthored = ` + export default defineMsdmdCollection({ + repo: 'alpha', + declarations: [{ file: 'src/a.py', block: 'DEPENDENCIES', id: 'alpha_dep', fields: { requires: 'beta::beta_cap', }, }], + gaps: [], + edges: [{ from: 'alpha_dep', to: 'beta::beta_cap', kind: 'requires', source_block: 'DEPENDENCIES', source_id: 'alpha_dep', }], + }); + // ratios: trailing source seal + `; + const parsed = parseCollectionText(handAuthored); + assert.equal(parsed.repo, 'alpha'); + assert.equal(parsed.declarations[0].fields.requires, 'beta::beta_cap'); + assert.equal(parsed.edges[0].kind, 'requires'); +}); + +test('resolves exact graph targets, preserves opaque targets, and isolates true hmmm edges', () => { + const inputs = [ + { + name: 'alpha', slug: 'alpha', defaultBranch: 'main', headSha: 'a'.repeat(40), headCommittedAt: '2026-08-15T00:00:00Z', + collectionStatus: 'ok', collectionPath: 'alpha_msdmd.ts', collectionSha256: '1'.repeat(64), + collection: { + repo: 'alpha', source_commit: 'a'.repeat(40), gaps: [], + declarations: [{ file: 'src/a.py', block: 'DEPENDENCIES', id: 'alpha_dep', fields: {} }], + edges: [ + { from: 'alpha_dep', to: 'beta::beta_cap', kind: 'requires', source_block: 'DEPENDENCIES', source_id: 'alpha_dep' }, + { from: 'alpha_dep', to: 'node', kind: 'requires', source_block: 'DEPENDENCIES', source_id: 'alpha_dep' }, + { from: 'alpha_dep', to: 'The-Interdependency/beta#missing_cap', kind: 'requires', source_block: 'DEPENDENCIES', source_id: 'alpha_dep' } + ] + } + }, + { + name: 'beta', slug: 'beta', defaultBranch: 'main', headSha: 'b'.repeat(40), headCommittedAt: '2026-08-16T00:00:00Z', + collectionStatus: 'ok', collectionPath: 'beta_msdmd.ts', collectionSha256: '2'.repeat(64), + collection: { + repo: 'The-Interdependency/beta', source_commit: 'b'.repeat(40), gaps: [], edges: [], + declarations: [{ file: 'src/b.py', block: 'CAPABILITIES', id: 'beta_cap', fields: {} }] + } + }, + { + name: 'gamma', slug: 'gamma', defaultBranch: 'main', headSha: 'c'.repeat(40), headCommittedAt: '2026-08-14T00:00:00Z', + collectionStatus: 'missing', collectionPath: 'gamma_msdmd.ts', collectionError: 'collection point absent at consumed head' + } + ]; + + const first = buildOrgMap(inputs); + const second = buildOrgMap(inputs); + assert.equal(JSON.stringify(first), JSON.stringify(second), 'same inputs must emit byte-equivalent data'); + assert.equal(first.summary.repositoryCount, 3); + assert.equal(first.summary.collectionCount, 2); + assert.equal(first.summary.missingCollectionCount, 1); + assert.equal(first.summary.crossRepoPairCount, 1); + assert.equal(first.summary.externalTargetCount, 1); + assert.equal(first.summary.unresolvedEdgeCount, 1); + assert.deepEqual(first.repositoryEdges[0], { + from: 'alpha', to: 'beta', count: 1, kinds: ['requires'], sourceBlocks: ['DEPENDENCIES'] + }); + const opaque = first.edges.find(edge => edge.targetRaw === 'node'); + assert.equal(opaque.resolution, 'external-target'); + assert.equal(first.unresolvedEdges[0].targetRaw, 'The-Interdependency/beta#missing_cap'); + assert.equal(first.unresolvedEdges[0].resolution, 'broken-explicit-declaration'); + assert.match(first.repositories.find(repo => repo.name === 'gamma').hmmm[0], /No consumable repo-level msdmd collection point/); + assert.equal(first.repositories.find(repo => repo.name === 'beta').hmmm.length, 0, 'full-name repo identity is accepted'); +}); + +test('flags collection source commit drift without rejecting the source bytes', () => { + const data = buildOrgMap([{ + name: 'alpha', slug: 'alpha', defaultBranch: 'main', headSha: 'a'.repeat(40), headCommittedAt: '2026-08-16T00:00:00Z', + collectionStatus: 'ok', collectionPath: 'alpha_msdmd.ts', collectionSha256: '3'.repeat(64), + collection: { repo: 'alpha', source_commit: 'b'.repeat(40), declarations: [], gaps: [], edges: [] } + }]); + const repo = data.repositories[0]; + assert.equal(repo.collection.sourceCommitMatchesHead, false); + assert.ok(repo.hmmm.some(item => /does not match/.test(item))); +});