LearnNewsExamplesServices
Frontmatter
id11406
titleAdd CI grep-fail check for retired-primitive imports
stateClosed
labels
enhancementaiarchitecturebuildmodel-experience
assigneesneo-opus-4-7
createdAtMay 15, 2026, 11:37 AM
updatedAtMay 17, 2026, 12:07 AM
githubUrlhttps://github.com/neomjs/neo/issues/11406
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 17, 2026, 12:07 AM

Add CI grep-fail check for retired-primitive imports

Closedenhancementaiarchitecturebuildmodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 15, 2026, 11:37 AM

Context

ADR 0004 (learn/agentos/decisions/0004-github-content-architecture.md) gained a three-section substrate-evolution-guard layer tonight (2026-05-15):

  • §1.3 — The Regeneratable-Cache Strategic Principle (via PR #11401)
  • §2.6 — The Clean-Cut Pattern (via PR #11398)
  • §5.6 — Deprecation-theater anti-pattern (via PR #11398)

The layer was authored explicitly to prevent the @deprecated theater regression operator caught on PR #11381 — preservation-tendency on retired primitives like chunkPath.mjs / archivePath.mjs after their call sites get rewired. The substrate-evolution-guard is discipline-only (pattern-recognition aid for agents reading merged ADR substrate before authoring).

Empirical recurrence proves it's necessary but not sufficient: at the very next PR after the §1.3/§2.6/§5.6 layer merged (PR #11403, Cycle-1 review at PRR_kwDODSospM8AAAABAB3NsQ), the same clean-cut regression recurred — chunkPath.mjs + archivePath.mjs + ArchivePath.spec.mjs were preserved as dead code despite zero callers after PR #11403's import-removals. The regression was caught at peer-review time (Cycle-1 CHANGES_REQUESTED), not at CI time. The empirical anchor — two same-pattern occurrences across two different families within ~8 hours, with the second occurring after the discipline-only guard layer landed — justifies a mechanical-enforcement layer.

The Problem

The §1.3/§2.6/§5.6 guards work as recognition aids when an agent CITES them at AC-authoring or review time. They do NOT block via automated CI check. This means:

  1. Agents authoring substrate-rewire PRs must pull-and-read merged ADR substrate before authoring — a discipline-layer responsibility that empirically broke twice in one session.
  2. Reviewers carry the load of catching the regression. The lookback distance between author-time and reviewer-time is the regression window.
  3. Future ADR §2.3 RETIRED additions will face the same risk profile: retired-primitive imports can linger in source even after call-site rewires, with no automated tripwire.

Pattern-recognition + mechanical-enforcement closes the gap. Pattern-recognition is necessary (without it, mechanical enforcement is brittle/ad-hoc); mechanical enforcement is necessary (without it, pattern-recognition relies on humans/agents staying maximally attentive across all PRs).

The Architectural Reality

  • learn/agentos/decisions/0004-github-content-architecture.md defines the canonical authority for retired primitives. §2.3 RETIRED naming + §2.6 Clean-Cut Pattern + §1.3 Regeneratable-Cache Strategic Principle together establish the discipline. Future retirements add to this surface.
  • ai/services/github-workflow/shared/ is the current locus of retired primitives (chunkPath.mjs, archivePath.mjs — both deleted in PR #11403 commit 79ac1f8c9).
  • .github/workflows/ carries existing CI surface — relevant precedent: skill-manifest-lint.yml (substrate-discipline check on .agents/skills/** shape).
  • buildScripts/util/ is the canonical home for small CI-invoked utility scripts. Convention: .mjs ES modules, runnable via node buildScripts/util/<name>.mjs, wired into CI via a new step in an existing workflow.

The mechanical-enforcement layer fits as: a new buildScripts/util/checkRetiredPrimitives.mjs script that scans for retired-primitive imports under ai/ (excluding spec files), exits non-zero if any are found; wired into the existing Tests workflow (.github/workflows/tests.yml) as a new pre-test job step OR into skill-manifest-lint.yml as a sibling step.

The Fix

Add a small CI script + workflow wiring:

1. New script: buildScripts/util/checkRetiredPrimitives.mjs

// buildScripts/util/checkRetiredPrimitives.mjs
import {execSync} from 'node:child_process';
import process     from 'node:process';

/**
 * Retired primitives that MUST NOT be imported by non-spec source.
 * Add new entries as ADR 0004 §2.3 RETIRED table grows.
 *
 * Format: array of regex-safe import-path fragments that appear inside `from '...'` strings.
 */
const RETIRED_PRIMITIVES = [
    'shared/chunkPath.mjs',
    'shared/archivePath.mjs'
];

const SEARCH_ROOT  = 'ai/';
const EXCLUDE_GLOB = ['*.spec.mjs', '*.test.mjs'];

function buildPattern() {
    return RETIRED_PRIMITIVES.map(p => `from.*['"\`].*${p.replace(/\./g, '\\.')}`).join('\\|');
}

function main() {
    const pattern = buildPattern();
    const excludes = EXCLUDE_GLOB.map(g => `--exclude="${g}"`).join(' ');
    let stdout;

    try {
        stdout = execSync(`grep -rn ${excludes} "${pattern}" ${SEARCH_ROOT}`, {encoding: 'utf8'});
    } catch (err) {
        // grep returns exit 1 on zero matches — the CLEAN case.
        if (err.status === 1) {
            console.log(`[checkRetiredPrimitives] PASS: no retired-primitive imports under ${SEARCH_ROOT}`);
            return;
        }
        throw err;
    }

    console.error(`[checkRetiredPrimitives] FAIL: found retired-primitive imports under ${SEARCH_ROOT}:`);
    console.error(stdout);
    console.error('\nRefer to ADR 0004 §2.6 Clean-Cut Pattern — retired primitives must be DELETED, not preserved as dead code after call-site migration.');
    process.exit(1);
}

main();

2. CI wiring

Add a new step to .github/workflows/tests.yml (or skill-manifest-lint.yml if grouping with substrate-discipline checks). Example:

- name: Check retired primitives
  run: node buildScripts/util/checkRetiredPrimitives.mjs

Wire as a fast pre-test gate (sub-second runtime — pure grep scan).

3. Optional: npm script alias

Add to package.json:

"check-retired-primitives": "node buildScripts/util/checkRetiredPrimitives.mjs"

So local dev workflow can run npm run check-retired-primitives before pushing.

Acceptance Criteria

  • AC1: buildScripts/util/checkRetiredPrimitives.mjs exists with the prescribed shape — RETIRED_PRIMITIVES constant array, grep-scan logic, exit-1-on-match behavior.
  • AC2: Script is wired into an existing CI workflow (tests.yml or skill-manifest-lint.yml) as a new step. Workflow runs on every PR to dev.
  • AC3: Running the script against the current dev head (post-PR-#11403-merge) exits 0 — substrate is in compliance after Lane B clean-cut.
  • AC4: Adding a deliberate test-import of chunkPath.mjs (e.g., temp branch with import './shared/chunkPath.mjs' in a non-spec file) makes the script exit non-zero, with output naming the offending file:line + the ADR 0004 §2.6 reference.
  • AC5: Script + workflow step is documented in ADR 0004 §2.6 (or a new §2.6.1) — mechanical-enforcement layer cross-references the strategic-principle / pattern layers.
  • AC6: Optional npm run check-retired-primitives alias added to package.json for local dev use.

Out of Scope

  • Generalizing to all ADR-§2.3-RETIRED tables across all ADRs: keep this scoped to ADR 0004's retired primitives initially. Future ADRs gaining §2.3 RETIRED tables can extend RETIRED_PRIMITIVES array additively. A fully-table-driven check (parse ADR markdown → extract RETIRED entries) is a future substrate ticket if this primitive proves successful.
  • Auto-deletion of retired primitive files: discipline-only — the check FAILS on import-presence, doesn't auto-remove files. Author still owns the deletion in the call-site-migration PR.
  • Linting other substrate-discipline rules (@deprecated annotations, etc.): scope of separate substrate tickets per pattern. This ticket is narrowly retired-primitive-imports.
  • CI for archived/historical retired primitives that have been fully purged: once all current retired primitives are deleted from dev, the script is still useful as a forward-defense against future regressions.

Avoided Traps

  • Hardcoding a single-occurrence check (only chunkPath.mjs): rejected — the array shape (RETIRED_PRIMITIVES) lets future ADR §2.3 additions extend without rewriting the script. Empirically: even at this filing time, two retired primitives are in scope.
  • Building a complex AST-based detector: rejected — grep on from.*['"].*` is sufficient. Imports in modern JS/MJS are statically declarable; AST parsing is over-engineering for the actual pattern.
  • Wiring as a hard pre-commit hook: rejected — pre-commit hooks bypass CI authority + complicate local dev. CI-step placement matches existing substrate-discipline-check pattern (skill-manifest-lint.yml precedent).
  • Coupling to ADR markdown parsing: rejected for this ticket — adds substrate-coupling between the check script and ADR substrate format. Future ticket may pursue this if the array maintenance becomes painful at >5-10 entries. For now, explicit array is simpler and equally correct.

Related

  • Authority artifact: ADR 0004 learn/agentos/decisions/0004-github-content-architecture.md — §1.3 + §2.3 + §2.6 + §5.6 substrate-evolution-guard layer
  • Empirical anchor for filing:
    • PR #11381 (Lane A foundation): operator caught @deprecated theater regression on merged code — friction → gold seed
    • PR #11397 → PR #11398 (Gemini, §2.6 + §5.6): pattern + anti-pattern layer
    • PR #11400 → PR #11401 (me, §1.3): strategic principle layer
    • PR #11403 (Lane B): Cycle-1 same-pattern recurrence (PRR_kwDODSospM8AAAABAB3NsQ) — empirical V-B-A that discipline-only guards are necessary-but-not-sufficient
    • PR #11403 Cycle-2 (PRR_kwDODSospM8AAAABAB-g-A): clean-cut closed; this ticket queued as follow-up
  • Precedent CI shape: .github/workflows/skill-manifest-lint.yml — substrate-discipline check for .agents/skills/** shape; lift its workflow-step pattern
  • Epic ancestor: Epic #11372 (ADR 0004 Phase 1) — this is substrate-meta improvement; not a direct sub-task

Origin Session

  • Origin Session ID: 656c0935-0b3e-4b06-9b14-548524275859
  • Cross-session anchor: ADR 0004 §1.3 authoring session b8a152e1-a41c-49aa-8196-8e5d2eba84ca (this session's earlier-arc continuation)

Retrieval Hint

Search for CI grep-fail retired primitives clean-cut pattern ADR 0004 mechanical enforcement substrate-evolution-guard.

Or commit-range anchor: PR #11403 Cycle-1 review at PRR_kwDODSospM8AAAABAB3NsQ is the empirical recurrence; PR #11403 Cycle-2 at PRR_kwDODSospM8AAAABAB-g-A is the queued-follow-up commitment.

tobiu referenced in commit 172d54d - "feat(ci): CI grep-fail check for retired ADR 0004 primitives (#11406) (#11490) on May 17, 2026, 12:07 AM
tobiu closed this issue on May 17, 2026, 12:07 AM