Skip to content

fix(core): reject non-root module targets instead of dropping them silently - #15

Merged
TimCrooker merged 1 commit into
mainfrom
fix/module-targets-root-only
Jul 28, 2026
Merged

fix(core): reject non-root module targets instead of dropping them silently#15
TimCrooker merged 1 commit into
mainfrom
fix/module-targets-root-only

Conversation

@TimCrooker

@TimCrooker TimCrooker commented Jul 28, 2026

Copy link
Copy Markdown
Owner

The bug

modulesBody(target, modules) is called with the literal "root" at all three call sites in render.ts. Scoped outputs — apps/<pkg>/AGENTS.md, apps/<pkg>/CLAUDE.md, .claude/rules/*.md — are produced by buildScopedAgents/buildScopedClaude, which read only a scope's codexIncludes/claudeIncludes. Modules never feed them.

But loadModules validates a module's targets against manifest.targets, so targets: [mobile] passes as a "known target" and is then discarded at render time. ai-context build prints success and exits 0. The module's body reaches no generated file.

Found in a consuming repo: a mobile-interaction-guidance module had sat with targets: [mobile] since it was written. Its entire body — a surface-type decision matrix, sheet/tray rules, loading-state escalation thresholds — had never reached a single agent, and every build and verify run in that repo was green the whole time.

The fix

loadModules now throws AICTX_CONFIG_INVALID on a non-root target, naming the offending target and the route to take instead. The unknown-target error is kept ahead of it so a typo still reads as a typo rather than as a scoping mistake.

[AICTX_CONFIG_INVALID] Module '055-mobile-interaction-systems.md' targets 'mobile',
but modules only compose into the root output. Move this content to a rule file and
add it to the 'mobile' scope's includes in the scope manifest.

I chose to collapse the redundancy rather than implement composition into scoped outputs. Scope includes already fully own those files, and the target-to-scope mapping composition would need does not cleanly exist: buildScopedClaude is keyed by scope rather than target, a scope may have no codexTarget at all and write to paths absent from manifest.targets, and one scope can emit the same body to several codexAgents paths. Package-scoped content has a working route already — .ai/rules/ plus scope includes.

Docs

The meta-skill documented the behavior that was never implemented. authoring-modules.md listed apiapps/api/AGENTS.md in its targets table, which is what led the consuming repo into this. Corrected there, in manifest-schema.md, and in docs/configuration.md, plus a new section on routing package-scoped content through .ai/rules/ + scopes.

Verification

  • Two tests. The scoped-target test was confirmed to fail without the guard, not pass tautologically.
  • Full suite green: 223 tests across core/templates/cli. pnpm -r typecheck clean.
  • End-to-end against the real consuming repo with the offending module restored:
patched:          [AICTX_CONFIG_INVALID] Module '055-...' targets 'mobile', ...
published 1.3.0:  "Context outputs are up to date"     <- the silent drop

Changeset included: core minor, templates patch.

🤖 Generated with Claude Code

Note

Reject modules with non-root targets instead of silently dropping them

Previously, modules whose frontmatter named a non-root target were silently ignored at render time. Now loadModules in config.ts throws a AICTX_CONFIG_INVALID error immediately, instructing authors to move package-scoped content to a rule file included by a scope instead.

  • Documentation in configuration.md and authoring-modules.md is updated to reflect that root is the only legal module target.
  • Behavioral Change: builds that previously succeeded while silently dropping scoped module content will now fail with an explicit error.

Macroscope summarized e6a9103.

…lently

modulesBody() is only ever called with "root". Scoped outputs are composed
entirely from a scope's includes, so a module declaring targets: [mobile]
passed loadModules (mobile is a real manifest target key) and was then
discarded at render time — its body reached no generated file and the build
still exited 0.

loadModules now throws AICTX_CONFIG_INVALID naming the offending target and
the fix (move the content to a rule file, add it to that scope's includes).
The unknown-target error is kept ahead of it so a typo still reads as a typo.

The meta-skill docs promised the behavior that was never implemented — the
authoring-modules target table listed `api` → apps/api/AGENTS.md. Corrected
there and in manifest-schema.md and docs/configuration.md, plus a section on
routing package-scoped content through .ai/rules/ + scopes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 28, 2026 16:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@macroscopeapp

macroscopeapp Bot commented Jul 28, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved

This PR adds explicit validation to reject non-root module targets with a helpful error, instead of silently dropping their content. The change is small, well-tested, and improves fail-fast behavior. The author owns this codebase.

You can customize Macroscope's approvability policy. Learn more.

@TimCrooker
TimCrooker merged commit b3bd7c6 into main Jul 28, 2026
7 checks passed
@TimCrooker
TimCrooker deleted the fix/module-targets-root-only branch July 28, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants