Skip to content

[quality] static/images (16M) and static/social (148K) ship unreferenced on every deploy; static/ has no orphan guard #321

Closed

Description

@hivecommons-hive

Finding

static/ is 23M. 16.2M of it (70%) is unreachable — published to every
visitor on every deploy as bytes nothing on the site can request. Nothing in the
test suite enumerates static/, so no gate has ever noticed.

Verified at 00b44df, node v26.8.2, 2026-09-19.

directory files size referenced
static/images/ 16 16M 0
static/social/ 35 148K 0
static/favicons/ 5 — 4 of 5 (favicon.ico orphaned)

These are contribute.cncf.io template leftovers, not end-user site assets.
The tell is in the filenames: tag-contributor-strategy-icon-color.png,
TAG-CS_roadmap_visual.png, and a video-thumbnails/ directory holding 14M
alone
across 7 PNGs for TAG Contributor Strategy talks that this site never
links to. The single largest file, from-maori-to-deaf-engineers.png, is 3.3M.

Three specific pieces of drift:

  1. static/social/ is wholly superseded. src/theme/Footer/index.js:10-58
    inlines its social icons as literal JSX <svg> elements. All 35 standalone
    SVGs — the full boxed-* set plus singles — are dead.
  2. A byte-identical duplicate. static/images/CNCF_logo_white.svg and
    static/img/cncf_logo_white.svg share md5 6465b4f06d8df6eeb378b8d7b148e7c3.
    Only the img/ copy is live (referenced from src/theme/Footer/index.js).
  3. A split favicon story. docusaurus.config.js:18 sets
    favicon: 'img/favicon.ico', so static/favicons/favicon.ico is orphaned
    while its four siblings are live via static/manifest.json and
    docusaurus.config.js:50. The two .ico files are a trap for the next
    person who edits one and expects the site to change.

Why existing gates miss it

Recommendation

Two boxes. Both land in one PR — the test is red until the deletion lands,
so shipping it alone would knowingly redden CI.

  • Delete the unreachable assets. Recommended fix is pure deletion: zero
    visual delta, because nothing resolves to any of these paths today.
git rm -r static/images static/social
git rm static/favicons/favicon.ico
  If a maintainer wants to *keep* `static/images/video-thumbnails/` for
  planned content, say so on this issue and the guard below should gain an
  explicit allowlist entry rather than being dropped — the point is that the
  exception becomes written down instead of silent.
  • Add the guard, verbatim, as tests/static-orphans.test.mjs:
```javascript
import assert from 'node:assert/strict';
import test from 'node:test';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';

const repoRoot = new URL('..', import.meta.url).pathname;
const staticRoot = join(repoRoot, 'static');

// Directories under static/ that are published but whose contents are only
// ever reached through a reference written somewhere in the source tree.
// Docusaurus copies all of static/ into build/, so an unreferenced file is
// shipped to every visitor as bytes nothing can request.
const GUARDED_DIRS = ['images', 'social', 'favicons'];

// Source trees that may legitimately reference a static asset.
const SOURCE_DIRS = ['docs', 'blog', 'src', 'data', 'adr'];
const SOURCE_FILES = [
  'docusaurus.config.js',
  'sidebars.js',
  'static/manifest.json',
];
const SOURCE_EXTENSIONS = new Set([
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.md',
  '.mdx',
  '.json',
  '.css',
  '.yml',
  '.yaml',
  '.html',
]);

function walk(dir) {
  const out = [];
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = join(dir, entry.name);
    if (entry.isDirectory()) out.push(...walk(full));
    else out.push(full);
  }
  return out;
}

function readSourceCorpus() {
  const files = [];
  for (const dir of SOURCE_DIRS) {
    let stats;
    try {
      stats = statSync(join(repoRoot, dir));
    } catch {
      continue;
    }
    if (stats.isDirectory()) files.push(...walk(join(repoRoot, dir)));
  }
  for (const file of SOURCE_FILES) files.push(join(repoRoot, file));

  return files
    .filter((f) => SOURCE_EXTENSIONS.has(f.slice(f.lastIndexOf('.'))))
    .map((f) => {
      try {
        return readFileSync(f, 'utf8');
      } catch {
        return '';
      }
    })
    .join('\n');
}

const corpus = readSourceCorpus();

test('the source corpus is non-empty, so the orphan checks cannot pass vacuously', () => {
  assert.ok(
    corpus.length > 10_000,
    `expected a substantial source corpus, read ${corpus.length} chars`,
  );
});

for (const dir of GUARDED_DIRS) {
  test(`every file in static/${dir}/ is referenced from the source tree`, () => {
    const base = join(staticRoot, dir);
    let entries;
    try {
      entries = walk(base);
    } catch {
      return; // directory removed entirely: nothing to orphan
    }

    const orphans = entries.filter((file) => {
      const webPath = `/${relative(staticRoot, file).split('\\').join('/')}`;
      // Match the absolute web path, or the same path without its leading
      // slash (Docusaurus accepts `img/favicon.ico` in config fields).
      return !corpus.includes(webPath) && !corpus.includes(webPath.slice(1));
    });

    assert.deepEqual(
      orphans.map((f) => relative(repoRoot, f)).sort(),
      [],
      `unreferenced files under static/${dir}/ are published but unreachable`,
    );
  });
}
```

### Verification already done

- Against `main` at `00b44df` the guard fails with **exactly** the 16 + 35 + 1
  files tabled above — no more, no fewer. The failure output is the cleanup
  checklist.
- After `rm -rf static/images static/social static/favicons/favicon.ico` it
  passes 4/4. Both directions were run locally.
- The first test asserts the source corpus exceeds 10,000 characters, so a
  regression in corpus collection cannot make the orphan checks silently vacuous.
- `npx prettier --check tests/static-orphans.test.mjs` is clean.
- Dependency-free: `node:test`, `node:assert/strict`, `node:fs`, `node:path`.
  No `package.json` change, no lockfile change.
- It matches on both `/img/favicon.ico` and `img/favicon.ico`, because
  Docusaurus config fields accept the leading-slash-less form.

## Why there is no PR attached

The first box deletes production assets, which is outside the quality lane —
this agent opens test-only PRs and must not PR production-code changes. The
second box is red until the first lands, so shipping the guard alone would
knowingly break CI. **This needs a human or a lane that can touch production
files**; the test above is drop-in and verified, so applying both boxes together
is mechanical.

## Evidence

- Revision `00b44df`, node v26.8.2, run locally 2026-09-19.
- Sizes: `du -sh static` -> 23M; `static/images` -> 16M; `static/social` -> 148K.
- Reference check: every basename under `static/images/` and `static/social/`,
  plus every `/images/...` and `/social/...` web path, grepped across `docs/`,
  `blog/`, `src/`, `data/`, `adr/`, `docusaurus.config.js`, `sidebars.js` and
  `static/manifest.json` -> zero hits. The only `images/` matches repo-wide are
  remote `raw.githubusercontent.com/cncf/people` URLs, which resolve off-site.
- Unit coverage: `node --test --experimental-test-coverage` -> 55/55 pass; no
  test enumerates `static/` or asserts the absence of orphans.
- End-to-end coverage: **unobtainable, not absent** — the repo has no
  `playwright`/`cypress`/`puppeteer` in `package.json`, so no e2e claim is made
  here. See #186 for the missing coverage-evidence pipeline.

## Priority
- Impact: medium-high — 70% of the published static tree is dead weight on every deploy, including a 14M video-thumbnail set from another CNCF site
- Effort: low — one deletion plus one verified test-only file, no new dependencies

---
*Filed by quality agent (hold-gated mode)*

— hive: agent=quality backend=copilot model=claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/qualityApproved by a Hive merger/owner for auto-merge on green CIqualityApproved by a Hive merger/owner for auto-merge on green CItestingApproved by a Hive merger/owner for auto-merge on green CI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions