---
id: 11406
title: Add CI grep-fail check for retired-primitive imports
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
  - build
  - model-experience
assignees:
  - neo-opus-ada
createdAt: '2026-05-15T09:37:53Z'
updatedAt: '2026-06-07T17:12:01Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11406'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-16T22:07:40Z'
---
# Add CI grep-fail check for retired-primitive imports

## 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`

```javascript
// 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:

```yaml
- 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`:

```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.*['"`].*<path>` 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](https://github.com/neomjs/neo/pull/11403#pullrequestreview-4296920497)) — empirical V-B-A that discipline-only guards are necessary-but-not-sufficient
  - PR #11403 Cycle-2 ([PRR_kwDODSospM8AAAABAB-g-A](https://github.com/neomjs/neo/pull/11403#pullrequestreview-4297040120)): 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.

## Timeline

- 2026-05-15T09:37:55Z @neo-opus-ada added the `enhancement` label
- 2026-05-15T09:37:55Z @neo-opus-ada added the `ai` label
- 2026-05-15T09:37:55Z @neo-opus-ada added the `architecture` label
- 2026-05-15T09:37:55Z @neo-opus-ada added the `build` label
- 2026-05-15T09:37:55Z @neo-opus-ada added the `model-experience` label
- 2026-05-15T09:38:04Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-15T09:55:15Z @neo-gpt cross-referenced by #11408
- 2026-05-15T09:58:15Z @neo-opus-ada cross-referenced by PR #11407
- 2026-05-15T10:05:42Z @neo-gpt cross-referenced by PR #11409
- 2026-05-15T10:56:47Z @neo-opus-ada cross-referenced by #11411
- 2026-05-15T11:38:30Z @neo-opus-ada cross-referenced by #11417
- 2026-05-16T10:53:28Z @neo-opus-ada cross-referenced by #11187
- 2026-05-16T20:22:25Z @neo-opus-ada cross-referenced by PR #11490
- 2026-05-16T20:43:44Z @neo-opus-ada cross-referenced by #11491
- 2026-05-16T20:44:26Z @neo-opus-ada cross-referenced by #11492
- 2026-05-16T20:52:53Z @neo-opus-ada cross-referenced by PR #11494
- 2026-05-16T20:55:32Z @neo-opus-ada cross-referenced by #11495
- 2026-05-16T21:09:47Z @neo-opus-ada cross-referenced by PR #11497
- 2026-05-16T21:43:07Z @neo-opus-ada referenced in commit `ec44aac` - "fix(ci): full regex-escape for retired-primitive fragments (CodeQL js/incomplete-sanitization on #11406)

CodeQL alert on PR #11490 flagged `check-retired-primitives.mjs:48` — `js/incomplete-sanitization`
(high security). The prior `buildPattern()` only escaped literal `.` characters, leaving
other regex metacharacters (`* + ? ^ $ { } ( ) | [ ] \\`) unescaped. While none of the
current `RETIRED_PRIMITIVES` values contain those characters, future maintainers extending
the array could introduce regex-injection if a fragment contained a metacharacter.

Replaced the inline `.replace(/\./g, '\\.')` with a full-metachar `escapeRegex()` helper
matching MDN's canonical `escapeRegExp` shape. Pattern semantics for current values
unchanged — `shared/chunkPath.mjs` and `shared/archivePath.mjs` produce identical regex
output.

Verification:
- `node ai/scripts/check-retired-primitives.mjs` against current dev: exit 0 (PASS).
- `npm run test-unit -- test/playwright/unit/ai/scripts/checkRetiredPrimitives.spec.mjs`:
  4/4 pass in 784ms (clean-substrate PASS + 2 canary FAIL variants + spec-glob exclusion).

GPT flagged CodeQL state during routing coordination 2026-05-16T21:40Z (MESSAGE:04f1f758).
Refs #11406"
- 2026-05-16T22:07:40Z @tobiu referenced in commit `172d54d` - "feat(ci): CI grep-fail check for retired ADR 0004 primitives (#11406) (#11490)

* feat(ci): CI grep-fail check for retired ADR 0004 primitives (#11406)

Adds the mechanical-enforcement layer that complements ADR 0004's discipline-only
substrate-evolution-guards (§1.3/§2.6/§5.6). Empirical anchor justifying mechanical
enforcement: PR #11403 Cycle-1 review (`PRR_kwDODSospM8AAAABAB3NsQ`) — the same
clean-cut regression recurred at the very next PR after the §2.6 layer merged,
with `chunkPath.mjs` + `archivePath.mjs` preserved as dead code despite zero
callers. The regression was caught at peer-review time, not CI time. The lookback
distance between author-time and reviewer-time is the regression window this
mechanical layer closes.

Artifacts:

- `ai/scripts/check-retired-primitives.mjs` — grep-fail check using `execFileSync`
  (no shell-quoting hazards); RETIRED_PRIMITIVES array currently lists
  `shared/chunkPath.mjs` + `shared/archivePath.mjs` (deleted in PR #11403 commit
  79ac1f8c9); exits 1 with file:line + ADR 0004 §2.6 reference on hit.
- `.github/workflows/check-retired-primitives.yml` — runs on PR/push to dev when
  any `ai/**/*.mjs` changes; standalone workflow matching `substrate-size-guard.yml`
  / `skill-manifest-lint.yml` precedent.
- `package.json` adds `ai:check-retired-primitives` alias for local dev use.
- `test/playwright/unit/ai/scripts/checkRetiredPrimitives.spec.mjs` — 4 tests
  covering clean-substrate PASS, two canary-FAIL variants (chunkPath + archivePath),
  and spec-glob exclusion behavior. Uses `describe.serial` to isolate from
  Playwright fullyParallel worker contention (memory anchor
  `feedback_symmetric_spec_cleanup.md`).
- ADR 0004 §2.6.1 new subsection documenting the mechanical layer + extension
  protocol for future §2.3 RETIRED entries.

Verified locally:
- AC3: `node ai/scripts/check-retired-primitives.mjs` against current dev tip
  exits 0 with PASS narrative.
- AC4: planting a canary file with `import chunkPath from '../shared/chunkPath.mjs'`
  triggers exit 1 with file:line + ADR cross-reference.
- 4/4 spec tests pass in 781ms via `npm run test-unit`.

Authoring note: ticket #11406 prescribed `buildScripts/util/` for the script
location, but the codebase convention (verified via `check-substrate-size.mjs`,
`lint-skill-manifest.mjs`) is `ai/scripts/` for substrate-discipline CI checks.
Used the codebase convention.

Resolves #11406

* fix(ci): full regex-escape for retired-primitive fragments (CodeQL js/incomplete-sanitization on #11406)

CodeQL alert on PR #11490 flagged `check-retired-primitives.mjs:48` — `js/incomplete-sanitization`
(high security). The prior `buildPattern()` only escaped literal `.` characters, leaving
other regex metacharacters (`* + ? ^ $ { } ( ) | [ ] \\`) unescaped. While none of the
current `RETIRED_PRIMITIVES` values contain those characters, future maintainers extending
the array could introduce regex-injection if a fragment contained a metacharacter.

Replaced the inline `.replace(/\./g, '\\.')` with a full-metachar `escapeRegex()` helper
matching MDN's canonical `escapeRegExp` shape. Pattern semantics for current values
unchanged — `shared/chunkPath.mjs` and `shared/archivePath.mjs` produce identical regex
output.

Verification:
- `node ai/scripts/check-retired-primitives.mjs` against current dev: exit 0 (PASS).
- `npm run test-unit -- test/playwright/unit/ai/scripts/checkRetiredPrimitives.spec.mjs`:
  4/4 pass in 784ms (clean-substrate PASS + 2 canary FAIL variants + spec-glob exclusion).

GPT flagged CodeQL state during routing coordination 2026-05-16T21:40Z (MESSAGE:04f1f758).
Refs #11406

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-16T22:07:40Z @tobiu closed this issue
- 2026-05-16T23:48:38Z @neo-opus-ada cross-referenced by PR #11510
- 2026-05-25T01:29:38Z @neo-opus-ada cross-referenced by PR #11947

