diff --git a/docs/superpowers/plans/2026-05-21-ci-scope-thin-shim.md b/docs/superpowers/plans/2026-05-21-ci-scope-thin-shim.md
index 7c654fac..5f5e7e8e 100644
--- a/docs/superpowers/plans/2026-05-21-ci-scope-thin-shim.md
+++ b/docs/superpowers/plans/2026-05-21-ci-scope-thin-shim.md
@@ -1,10 +1,12 @@
-# ci-scope thin shim + implicitDependencies — Implementation Plan
+# ci-scope thin shim + namedInputs — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-**Goal:** Replace the 340-LOC `scripts/ci-scope.mjs` classifier with a ~50-LOC thin shim that delegates to `nx affected` for project ownership and reads `scope:*` tags off each project to decide scope booleans. Move non-project fallback path rules into project.json `implicitDependencies`.
+> **PR 1 shipped:** [#503](https://github.com/cacheplane/angular-agent-framework/pull/503) added `scope:*` tags to 87 project.json files. The original spec planned to also add `implicitDependencies: ["//path"]` for fallback files; Nx 22.5.1 rejected that syntax (it validates implicitDependencies as project names, not file paths). The implicitDependencies portion was reverted within PR #503; only tags remain on main. PR 2 (below) takes the Nx-native `namedInputs` approach instead. See the spec's header revision note for full context.
-**Architecture:** Sequenced as 3 PRs. PR 1 adds metadata (tags + implicitDependencies) inertly — no behavior change. PR 2 replaces ci-scope.mjs + migrates tests. PR 3 adds a drift-guard assertion. Each PR ships independently; PR 2 must be merged + verified before PR 3. PR 1 is reversible (pure metadata add).
+**Goal:** Replace the 340-LOC `scripts/ci-scope.mjs` classifier with a ~50-LOC thin shim that delegates to `nx affected` for project ownership and reads `scope:*` tags off each project to decide scope booleans. Move non-project fallback path rules into per-project `namedInputs` referenced by target `inputs`.
+
+**Architecture:** Sequenced as 3 PRs. **PR 1 (tags) is SHIPPED.** PR 2 adds `namedInputs` + replaces ci-scope.mjs + migrates tests. PR 3 adds a drift-guard assertion. Each PR ships independently; PR 2 must be merged + verified before PR 3.
**Tech Stack:** Node ESM, `nx show projects --affected --json`, vitest (test runner for ci-scope.spec.mjs).
@@ -14,320 +16,167 @@
**Modified across all 3 PRs:**
-- **PR 1**: `scripts/add-scope-tags.mjs` (throwaway helper script), ~93 `project.json` files (tag/implicitDeps add only).
-- **PR 2**: `scripts/ci-scope.mjs` (rewrite ~340 → ~80 LOC), `scripts/ci-scope.spec.mjs` (test fixture migration).
+- **PR 1 (shipped)**: `scripts/add-scope-tags.mjs` (throwaway helper, deleted before merge), 87 `project.json` files (tags only — implicitDependencies reverted).
+- **PR 2**: `apps/cockpit/project.json` + `apps/website/project.json` (add `namedInputs.deploymentConfig` + reference in target `inputs`), `scripts/ci-scope.mjs` (rewrite ~340 → ~80 LOC), `scripts/ci-scope.spec.mjs` (test fixture migration).
- **PR 3**: `apps/cockpit/cockpit-e2e-wiring.spec.ts` (extend with tag drift-guard).
-No new long-lived files. The throwaway `scripts/add-scope-tags.mjs` is deleted at the end of PR 1.
-
---
-# PR 1 — metadata add (no behavior change)
-
-### Task 1: Categorize projects + write a one-shot tag/implicit-deps script
-
-**Files:**
-- Create: `scripts/add-scope-tags.mjs` (throwaway — deleted in Step 5 of this task)
-
-- [ ] **Step 1: Write the tag-application script**
-
-Create `/tmp/ci-scope-hybrid/scripts/add-scope-tags.mjs`:
-
-```javascript
-#!/usr/bin/env node
-// SPDX-License-Identifier: MIT
-// Throwaway: one-shot tag/implicitDependencies populator for PR 1
-// of the ci-scope thin-shim migration. Delete after PR 1 merges.
-
-import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
-import path from 'node:path';
-
-const REPO_ROOT = path.resolve(process.cwd());
-const PROJECT_SKIP = new Set(['.git', '.next', '.nx', 'coverage', 'dist', 'node_modules']);
-
-const PUBLISHABLE_LIB_BROADCAST = [
- 'scope:library', 'scope:website', 'scope:website-e2e',
- 'scope:cockpit', 'scope:cockpit-examples', 'scope:cockpit-smoke',
- 'scope:cockpit-secret', 'scope:cockpit-deploy-smoke',
- 'scope:cockpit-e2e', 'scope:examples-chat',
-];
-const COCKPIT_INTERNAL_LIB = [
- 'scope:cockpit', 'scope:cockpit-examples',
- 'scope:cockpit-deploy-smoke', 'scope:cockpit-e2e',
-];
-
-function loadPublishable() {
- const nx = JSON.parse(readFileSync('nx.json', 'utf8'));
- return new Set(nx.release?.groups?.publishable?.projects ?? []);
-}
-
-function walk(dir, out = []) {
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
- if (PROJECT_SKIP.has(entry.name)) continue;
- const p = path.join(dir, entry.name);
- if (entry.isDirectory()) walk(p, out);
- else if (entry.name === 'project.json') out.push(p);
- }
- return out;
-}
+# PR 1 — tags only ✅ SHIPPED ([#503](https://github.com/cacheplane/angular-agent-framework/pull/503))
-function tagsFor(project, projectRoot, publishable) {
- const name = project.name ?? '';
- const root = projectRoot.replaceAll(path.sep, '/');
- const targets = project.targets ?? {};
+### Task 1: ✅ Shipped
- // Publishable libs broadcast to everything (matches today's
- // `if (publishableProjects.has(name))` block in ci-scope.mjs).
- if (publishable.has(name)) return PUBLISHABLE_LIB_BROADCAST;
+What landed:
+- Added `scope:*` tags to 87 project.json files via a throwaway `scripts/add-scope-tags.mjs` (deleted before merge).
+- Original spec also added `implicitDependencies: ["//path"]` for fallback files. **This broke 29 CI jobs** because Nx 22.5.1 rejected the `//path` syntax (error: "implicitDependencies point to non-existent project(s)").
+- The implicitDependencies portion was reverted within PR #503; only tags remain on main.
- // Cockpit cap python projects: trigger smoke + cap angular's e2e/examples
- if (root.startsWith('cockpit/') && root.endsWith('/python')) {
- const tags = ['scope:cockpit-examples', 'scope:cockpit-e2e'];
- if (targets.smoke) tags.push('scope:cockpit-smoke');
- return tags;
- }
+PR 2 (below) takes the Nx-native `namedInputs` approach to handle file-level affecting-deps.
- // Cockpit cap angular projects: trigger examples + e2e
- if (root.startsWith('cockpit/') && root.endsWith('/angular')) {
- const tags = ['scope:cockpit-examples', 'scope:cockpit-e2e'];
- if (targets.integration) tags.push('scope:cockpit-secret');
- return tags;
- }
+Original Task 1 script (for archival reference)
- // Cockpit internal libs (non-publishable)
- if (
- root.startsWith('libs/cockpit-') ||
- root === 'libs/design-tokens' ||
- root === 'libs/ui-react' ||
- root === 'libs/example-layouts' ||
- root === 'libs/e2e-harness'
- ) return COCKPIT_INTERNAL_LIB;
-
- // Website app
- if (name === 'website' || root === 'apps/website') return ['scope:website', 'scope:website-e2e'];
-
- // Cockpit app
- if (name === 'cockpit' || root === 'apps/cockpit') {
- return ['scope:cockpit', 'scope:cockpit-examples', 'scope:cockpit-deploy-smoke', 'scope:cockpit-e2e'];
- }
+The throwaway `scripts/add-scope-tags.mjs` had a `tagsFor()` function that mapped projects to tags based on root path + targets, and an `implicitDepsFor()` function that returned `//path`-style entries. The `tagsFor` portion was correct and shipped; `implicitDepsFor` was abandoned.
- // Examples chat
- if (root === 'examples/chat' || root.startsWith('examples/chat/')) return ['scope:examples-chat'];
+If you need to re-apply the tag-categorization logic to a new project, the rules are:
+- **Publishable libs** (chat, langgraph, ag-ui, render, a2ui, licensing, telemetry): broadcast to library + website + website-e2e + cockpit + cockpit-examples + cockpit-smoke + cockpit-secret + cockpit-deploy-smoke + cockpit-e2e + examples-chat.
+- **Cockpit cap python**: cockpit-examples + cockpit-e2e + (cockpit-smoke if `targets.smoke` exists).
+- **Cockpit cap angular**: cockpit-examples + cockpit-e2e + (cockpit-secret if `targets.integration` exists).
+- **Cockpit internal libs** (`libs/cockpit-*`, `libs/design-tokens`, `libs/ui-react`, `libs/example-layouts`, `libs/e2e-harness`): cockpit + cockpit-examples + cockpit-deploy-smoke + cockpit-e2e.
+- **`apps/website`**: website + website-e2e.
+- **`apps/cockpit`**: cockpit + cockpit-examples + cockpit-deploy-smoke + cockpit-e2e.
+- **`examples/chat/*`**: examples-chat.
+- **`tools/posthog`**: posthog.
- // PostHog tools
- if (name === 'posthog-tools' || root === 'tools/posthog') return ['scope:posthog'];
+
- // No CI gating for: marketing/*, minting-service, db, etc.
- return null;
-}
-function implicitDepsFor(name) {
- // Map each non-project fallback file to the project that should be
- // considered affected when it changes. Mirrors applyFallbackPathScope.
- switch (name) {
- case 'website':
- return ['//vercel.json'];
- case 'cockpit':
- return [
- '//vercel.cockpit.json', '//vercel.examples.json', '//vercel.demo.json',
- '//scripts/assemble-demo.ts', '//scripts/assemble-examples.ts',
- '//scripts/demo-middleware.ts', '//scripts/langgraph-proxy.ts',
- '//scripts/rate-limit.ts', '//scripts/deploy-smoke.ts',
- '//apps/cockpit/scripts/deploy-smoke.ts',
- '//scripts/generate-shared-deployment-config.ts',
- '//apps/cockpit/scripts/capability-registry.ts',
- ];
- default:
- return null;
- }
-}
+---
-function uniqueSorted(arr) {
- return [...new Set(arr)].sort();
-}
+# PR 2 — shim rewrite + test migration
-function main() {
- const publishable = loadPublishable();
- const projectJsonPaths = walk(REPO_ROOT)
- .filter((p) => !p.includes('/node_modules/'))
- .map((p) => path.relative(REPO_ROOT, p));
-
- let modified = 0;
- for (const relPath of projectJsonPaths) {
- const projectRoot = path.dirname(relPath);
- if (projectRoot === '.') continue; // skip top-level project.json
- const text = readFileSync(relPath, 'utf8');
- const project = JSON.parse(text);
-
- const newTags = tagsFor(project, projectRoot, publishable);
- const newImplicitDeps = implicitDepsFor(project.name);
-
- let changed = false;
-
- if (newTags) {
- const existing = project.tags ?? [];
- const merged = uniqueSorted([...existing, ...newTags]);
- if (JSON.stringify(merged) !== JSON.stringify(existing)) {
- project.tags = merged;
- changed = true;
- }
- }
+### Task 2: Branch + sync from PR 1's merge
- if (newImplicitDeps) {
- const existing = project.implicitDependencies ?? [];
- const merged = uniqueSorted([...existing, ...newImplicitDeps]);
- if (JSON.stringify(merged) !== JSON.stringify(existing)) {
- project.implicitDependencies = merged;
- changed = true;
- }
- }
+**Files:** none changed.
- if (changed) {
- writeFileSync(relPath, JSON.stringify(project, null, 2) + '\n');
- console.log(`updated ${relPath}`);
- modified++;
- }
- }
- console.log(`\n${modified} project.json files modified.`);
-}
+- [ ] **Step 1: After PR 1 merges, sync + new branch**
-main();
+```bash
+cd /Users/blove/repos/angular-agent-framework && git fetch origin main && git worktree add /tmp/ci-scope-shim-rewrite -b claude/ci-scope-shim-rewrite origin/main
```
-- [ ] **Step 2: Run the script + inspect a representative diff**
+- [ ] **Step 2: Verify the metadata from PR 1 is on main**
```bash
-cd /tmp/ci-scope-hybrid && node scripts/add-scope-tags.mjs 2>&1 | tail -10
+cd /tmp/ci-scope-shim-rewrite && grep -c '"scope:' libs/chat/project.json
```
-Expected: `~50 project.json files modified.` (cockpit caps, libs, apps, examples-chat). Marketing/minting-service/db are skipped (no CI gating).
+Expected: `10` (the 10 broadcast tags). If 0, PR 1 wasn't merged yet — stop.
-Spot-check three files for correctness:
+---
-```bash
-cd /tmp/ci-scope-hybrid && \
- echo "=== libs/chat (publishable, broadcast) ===" && \
- grep -A3 '"tags"' libs/chat/project.json | head -15 && \
- echo "=== cockpit/chat/messages/angular (cap angular) ===" && \
- grep -A3 '"tags"' cockpit/chat/messages/angular/project.json | head -10 && \
- echo "=== apps/cockpit (implicitDependencies) ===" && \
- grep -A20 '"implicitDependencies"' apps/cockpit/project.json | head -25
-```
+### Task 2.5: Add namedInputs for fallback paths
-Expected: `libs/chat` has all 10 broadcast scope tags; `cockpit/chat/messages/angular` has `scope:cockpit-e2e` + `scope:cockpit-examples`; `apps/cockpit` has the 12-entry implicit-deps list.
+**Files:**
+- Modify: `apps/cockpit/project.json`
+- Modify: `apps/website/project.json`
+
+This task gives Nx affected the project graph edges it needs so changes to vercel.*.json, deploy scripts, capability-registry.ts, etc. correctly mark `apps/cockpit` and `apps/website` as affected. Without this, ci-scope's new shim would underfire on those file changes.
-- [ ] **Step 3: Verify nx graph still loads**
+- [ ] **Step 1: Read current `apps/cockpit/project.json`**
```bash
-cd /tmp/ci-scope-hybrid && npx nx graph --file=/tmp/nx-graph-check.json 2>&1 | tail -5
+cd /tmp/ci-scope-shim-rewrite && cat apps/cockpit/project.json | python3 -m json.tool | head -40
```
-Expected: writes the graph file without errors. Validates all `implicitDependencies: ["//path"]` strings reference files nx can resolve.
+Note whether `namedInputs` or target `inputs` already exist (most likely not on `apps/cockpit`).
-If nx errors on a missing file: fix the implicit-deps entry (probably a typo or moved file).
+- [ ] **Step 2: Add `namedInputs.deploymentConfig` to `apps/cockpit/project.json`**
-- [ ] **Step 4: Verify implicit-dep files actually exist**
+Run:
```bash
-cd /tmp/ci-scope-hybrid && python3 -c "
+cd /tmp/ci-scope-shim-rewrite && python3 -c "
import json
-deps = json.load(open('apps/cockpit/project.json')).get('implicitDependencies', [])
-import os
-missing = [d for d in deps if d.startswith('//') and not os.path.exists(d[2:])]
-print('MISSING:', missing) if missing else print('all implicit-dep files exist')
+p = 'apps/cockpit/project.json'
+d = json.load(open(p))
+d.setdefault('namedInputs', {})['deploymentConfig'] = [
+ '{workspaceRoot}/vercel.cockpit.json',
+ '{workspaceRoot}/vercel.examples.json',
+ '{workspaceRoot}/vercel.demo.json',
+ '{workspaceRoot}/scripts/assemble-demo.ts',
+ '{workspaceRoot}/scripts/assemble-examples.ts',
+ '{workspaceRoot}/scripts/demo-middleware.ts',
+ '{workspaceRoot}/scripts/langgraph-proxy.ts',
+ '{workspaceRoot}/scripts/rate-limit.ts',
+ '{workspaceRoot}/apps/cockpit/scripts/deploy-smoke.ts',
+ '{workspaceRoot}/scripts/generate-shared-deployment-config.ts',
+ '{workspaceRoot}/apps/cockpit/scripts/capability-registry.ts',
+]
+# Reference in build target inputs
+build = d.setdefault('targets', {}).setdefault('build', {})
+inputs = build.get('inputs', ['default', '^default'])
+if 'deploymentConfig' not in inputs:
+ inputs.insert(1, 'deploymentConfig')
+build['inputs'] = inputs
+with open(p, 'w') as f: json.dump(d, f, indent=2, ensure_ascii=False); f.write('\n')
+print('apps/cockpit/project.json updated')
"
```
-Expected: `all implicit-dep files exist`. If any missing, remove from project.json (the spec may reference a file that was deleted/moved since PR-#432 era).
-
-- [ ] **Step 5: Delete the throwaway script + commit**
+- [ ] **Step 3: Add `namedInputs.deploymentConfig` to `apps/website/project.json`**
```bash
-cd /tmp/ci-scope-hybrid && git rm scripts/add-scope-tags.mjs && git add -A
-git diff --cached --stat | tail -5
-```
-
-Expected stat: ~50 files changed (only the tag/implicit-deps additions). No source code.
-
-```bash
-cd /tmp/ci-scope-hybrid && git commit -m "$(cat <<'EOF'
-chore(ci-scope): add scope:* tags + implicitDependencies (no behavior change)
-
-PR 1 of 3 for the ci-scope thin-shim migration. Adds metadata only:
-
-1. scope:* tags on every CI-participating project, replacing the
- hand-maintained applyProjectScope rules in scripts/ci-scope.mjs
- with data declarations next to each project.
-
-2. implicitDependencies on apps/cockpit and apps/website pointing at
- the non-project files (vercel.*.json, scripts/*.ts, capability-
- registry.ts) that currently live in applyFallbackPathScope.
-
-ci-scope.mjs unchanged in this PR — still drives gating via the old
-rules. The new metadata is inert. PR 2 will rewrite the shim to read
-from this metadata; PR 3 will add a drift-guard assertion.
-
-See docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md.
-
-Co-Authored-By: Claude Opus 4.7 (1M context)
-EOF
-)"
+cd /tmp/ci-scope-shim-rewrite && python3 -c "
+import json
+p = 'apps/website/project.json'
+d = json.load(open(p))
+d.setdefault('namedInputs', {})['deploymentConfig'] = [
+ '{workspaceRoot}/vercel.json',
+]
+build = d.setdefault('targets', {}).setdefault('build', {})
+inputs = build.get('inputs', ['default', '^default'])
+if 'deploymentConfig' not in inputs:
+ inputs.insert(1, 'deploymentConfig')
+build['inputs'] = inputs
+with open(p, 'w') as f: json.dump(d, f, indent=2, ensure_ascii=False); f.write('\n')
+print('apps/website/project.json updated')
+"
```
-- [ ] **Step 6: Push + open PR 1**
-
-```bash
-cd /tmp/ci-scope-hybrid && git push -u origin claude/ci-scope-hybrid 2>&1 | tail -3
-```
+- [ ] **Step 4: Verify nx accepts the namedInputs (no parse errors)**
```bash
-gh pr create --title "chore(ci-scope): add scope:* tags + implicitDependencies (PR 1/3)" --body "$(cat <<'EOF'
-## Summary
-PR 1 of 3 in the ci-scope thin-shim migration. **Metadata only — no behavior change.**
-
-- Adds \`scope:*\` tags to every CI-participating project.
-- Adds \`implicitDependencies\` to \`apps/cockpit\` and \`apps/website\` for non-project fallback files (vercel.*.json, deploy scripts, capability-registry.ts).
-
-\`scripts/ci-scope.mjs\` is untouched; it still drives gating via the old \`applyProjectScope\`/\`applyFallbackPathScope\` rules. The new metadata is inert until PR 2 rewrites the shim to read from it.
-
-## Verification
-- [ ] CI passes (no scope booleans should emit differently).
-- [ ] \`npx nx graph\` succeeds (validates implicit-dep file references).
-
-## Follow-ups
-- PR 2: rewrite ci-scope.mjs as a thin shim + migrate tests.
-- PR 3: drift-guard assertion + cleanup.
-
-See spec: \`docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md\`.
-
-🤖 Generated with [Claude Code](https://claude.com/claude-code)
-EOF
-)" 2>&1 | tail -3
+cd /tmp/ci-scope-shim-rewrite && npx nx show project cockpit --json 2>&1 | python3 -c "
+import json, sys
+d = json.load(sys.stdin)
+ni = d.get('namedInputs', {})
+print('deploymentConfig entries:', len(ni.get('deploymentConfig', [])))
+print('build inputs:', d.get('targets', {}).get('build', {}).get('inputs'))
+"
```
-**STOP after PR 1 opens. Wait for it to be reviewed + merged before continuing.** Subsequent tasks branch from PR 1's merge commit.
+Expected: `deploymentConfig entries: 11`, `build inputs: ['default', 'deploymentConfig', '^default']`.
----
-
-# PR 2 — shim rewrite + test migration
+If nx errors here, the namedInputs syntax is wrong — STOP and surface to the orchestrator.
-### Task 2: Branch + sync from PR 1's merge
-
-**Files:** none changed.
-
-- [ ] **Step 1: After PR 1 merges, sync + new branch**
+- [ ] **Step 5: Smoke-test that `nx show projects --affected` picks up vercel.cockpit.json changes**
```bash
-cd /Users/blove/repos/angular-agent-framework && git fetch origin main && git worktree add /tmp/ci-scope-shim-rewrite -b claude/ci-scope-shim-rewrite origin/main
+cd /tmp/ci-scope-shim-rewrite && \
+ echo '{}' > vercel.cockpit.json.bak && cp vercel.cockpit.json vercel.cockpit.json.bak && \
+ echo '{"version": 2, "test-edit": true}' >> vercel.cockpit.json && \
+ git add vercel.cockpit.json && \
+ npx nx show projects --affected --base origin/main --head HEAD --json 2>&1 | head -5
```
-- [ ] **Step 2: Verify the metadata from PR 1 is on main**
+Expected: output is a JSON array containing `"cockpit"` (the apps/cockpit project name).
+Cleanup:
```bash
-cd /tmp/ci-scope-shim-rewrite && grep -c '"scope:' libs/chat/project.json
+cd /tmp/ci-scope-shim-rewrite && git checkout vercel.cockpit.json && rm vercel.cockpit.json.bak
```
-Expected: `10` (the 10 broadcast tags). If 0, PR 1 wasn't merged yet — stop.
+If `cockpit` does NOT appear in the affected list, the namedInputs aren't taking effect — investigate before proceeding to Task 3.
---
diff --git a/docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md b/docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md
index 518227ef..65332286 100644
--- a/docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md
+++ b/docs/superpowers/specs/2026-05-21-ci-scope-thin-shim-design.md
@@ -1,13 +1,15 @@
-# ci-scope thin shim + implicitDependencies — design
+# ci-scope thin shim + namedInputs — design
> **Place in the larger plan.** Task #16 in the post-Task-#4 cleanup arc. Final item from the e2e audit. Replaces the hand-maintained 340-LOC `scripts/ci-scope.mjs` classifier with a ~50-LOC shim that delegates to `nx affected` for project ownership and reads `scope:*` tags off each project to decide which CI scopes to emit.
+>
+> **Revision (post-PR-#503):** The original spec used `implicitDependencies: ["//path"]` for file-level deps. PR #503 surfaced that Nx 22.5.1 rejects this syntax — `implicitDependencies` entries are validated strictly as project names, with error: "The following implicitDependencies point to non-existent project(s)". The correct Nx-native mechanism is per-project `namedInputs` referenced by target `inputs`. Spec updated below.
## Goal
Shrink `scripts/ci-scope.mjs` from 340 LOC to ~50 LOC by replacing two pieces of hand-maintained logic with data:
-1. **`applyProjectScope` rules** (~80 LOC) → become `tags: ["scope:*"]` on each `project.json`. The shim reads tags off projects nx reports as affected and unions them into scope booleans.
-2. **`applyFallbackPathScope` rules** (~50 LOC) → become `implicitDependencies` entries on the projects that should be affected when non-project files (vercel.json, deploy scripts, capability-registry.ts, etc.) change. Nx then considers those projects affected via the implicit dep edge.
+1. **`applyProjectScope` rules** (~80 LOC) → become `tags: ["scope:*"]` on each `project.json`. The shim reads tags off projects nx reports as affected and unions them into scope booleans. **Shipped in PR #503.**
+2. **`applyFallbackPathScope` rules** (~50 LOC) → become per-project `namedInputs` referenced by target `inputs`. When any file in the named input changes, nx considers the project affected. The shim picks up the project as affected via `nx show projects --affected` + reads its `scope:*` tags.
Preserve all existing CI gate semantics: every scope boolean still emits correctly for the same set of file changes; jobs still skip cleanly (no "fast pass" cost regression).
@@ -215,64 +217,88 @@ Every project that participates in CI gating gets one or more `scope:*` tags. Th
Each project's tags array is the **single declaration** of which workflow gates its changes trigger. Reviewers grep `scope:*` to audit.
-## implicitDependencies — fallback paths become first-class
+## namedInputs — fallback paths become first-class
-The current `applyFallbackPathScope` has ~12 file-specific rules. Each becomes an entry in the relevant project's `implicitDependencies`:
+The current `applyFallbackPathScope` has ~12 file-specific rules. Each fallback file becomes part of a per-project `namedInputs` entry, referenced by that project's target `inputs` array. When any file in the named input changes, nx considers the project affected — and the shim picks it up via `nx show projects --affected`.
-| File | Owner project | Rationale |
+**Why `namedInputs` instead of `implicitDependencies`:** Nx 22.5.1's `implicitDependencies` is validated strictly as a list of project names; file paths produce "implicitDependencies point to non-existent project(s)" at graph-load time. The `namedInputs` + target `inputs` pattern is the Nx-native mechanism for declaring file-level affecting-deps. (Surfaced by PR #503's first attempt; details in the spec header revision note.)
+
+### Per-project namedInputs
+
+| File | Owner project | Added to namedInput |
|---|---|---|
-| `vercel.json` | `apps/website` | Site-level Vercel config; affects website deploy. |
-| `vercel.cockpit.json` | `apps/cockpit` | Cockpit deploy config. |
-| `vercel.examples.json` | `apps/cockpit` | Examples assembly affects cockpit. |
-| `vercel.demo.json` | `apps/cockpit` | Demo-mode wrapper config. |
-| `scripts/assemble-demo.ts` | `apps/cockpit` | Builds cockpit demo bundle. |
-| `scripts/assemble-examples.ts` | `apps/cockpit` | Assembles per-cap examples. |
-| `scripts/demo-middleware.ts` | `apps/cockpit` | Demo runtime middleware. |
-| `scripts/langgraph-proxy.ts` | `apps/cockpit` | Demo LangGraph proxy. |
-| `scripts/rate-limit.ts` | `apps/cockpit` | Demo rate limiter. |
-| `scripts/deploy-smoke.ts` | `apps/cockpit` | Cockpit deploy-smoke driver. |
-| `apps/cockpit/scripts/deploy-smoke.ts` | `apps/cockpit` | Same; in-cockpit path. |
-| `scripts/generate-shared-deployment-config.ts` | `apps/cockpit` | Drives LangSmith deployment manifest. |
-| `apps/cockpit/scripts/capability-registry.ts` | `apps/cockpit` | Source of truth for all cap metadata. |
-| `tools/posthog/**` | `tools/posthog` (already a project) | Auto-owned via project root match. |
-
-After this migration, `applyFallbackPathScope` and `isGlobalCiFile`'s file-list disappear (except for the `GLOBAL_CI_FILES` short-circuit set, which stays in the shim because some workflow-config changes need to trigger every gate).
-
-Project.json edit example:
+| `vercel.json` | `apps/website` | `deploymentConfig` |
+| `vercel.cockpit.json` | `apps/cockpit` | `deploymentConfig` |
+| `vercel.examples.json` | `apps/cockpit` | `deploymentConfig` |
+| `vercel.demo.json` | `apps/cockpit` | `deploymentConfig` |
+| `scripts/assemble-demo.ts` | `apps/cockpit` | `deploymentConfig` |
+| `scripts/assemble-examples.ts` | `apps/cockpit` | `deploymentConfig` |
+| `scripts/demo-middleware.ts` | `apps/cockpit` | `deploymentConfig` |
+| `scripts/langgraph-proxy.ts` | `apps/cockpit` | `deploymentConfig` |
+| `scripts/rate-limit.ts` | `apps/cockpit` | `deploymentConfig` |
+| `apps/cockpit/scripts/deploy-smoke.ts` | `apps/cockpit` | `deploymentConfig` |
+| `scripts/generate-shared-deployment-config.ts` | `apps/cockpit` | `deploymentConfig` |
+| `apps/cockpit/scripts/capability-registry.ts` | `apps/cockpit` | `deploymentConfig` |
+| `tools/posthog/**` | `tools/posthog` (already a project) | (auto-owned via project root match) |
+
+After this migration, `applyFallbackPathScope` disappears entirely. `isGlobalCiFile`'s short-circuit set stays in the shim (workflow-config changes correctly trigger every gate via the full-scope return).
+
+### Project.json edit example
```json
-// apps/website/project.json
+// apps/cockpit/project.json
{
- "name": "website",
- "tags": ["scope:website", "scope:website-e2e"],
- "implicitDependencies": ["//vercel.json"],
- ...
+ "name": "cockpit",
+ "tags": [
+ "scope:cockpit", "scope:cockpit-deploy-smoke",
+ "scope:cockpit-e2e", "scope:cockpit-examples"
+ ],
+ "namedInputs": {
+ "deploymentConfig": [
+ "{workspaceRoot}/vercel.cockpit.json",
+ "{workspaceRoot}/vercel.examples.json",
+ "{workspaceRoot}/vercel.demo.json",
+ "{workspaceRoot}/scripts/assemble-demo.ts",
+ "{workspaceRoot}/scripts/assemble-examples.ts",
+ "{workspaceRoot}/scripts/demo-middleware.ts",
+ "{workspaceRoot}/scripts/langgraph-proxy.ts",
+ "{workspaceRoot}/scripts/rate-limit.ts",
+ "{workspaceRoot}/apps/cockpit/scripts/deploy-smoke.ts",
+ "{workspaceRoot}/scripts/generate-shared-deployment-config.ts",
+ "{workspaceRoot}/apps/cockpit/scripts/capability-registry.ts"
+ ]
+ },
+ "targets": {
+ "build": {
+ "inputs": ["default", "deploymentConfig", "^default"]
+ }
+ }
}
```
-The `//` prefix tells nx these are workspace file paths, not project names.
+The `{workspaceRoot}/...` syntax is the Nx-native token for repo-relative paths. The `inputs` reference in the `build` target tells nx that any change to `deploymentConfig` files invalidates the build's cache AND marks this project as affected.
+
+For `apps/cockpit`, the `inputs` need to land on whichever target nx affected uses to determine project affectedness — typically `build` (and any other long-running targets like `test`). The plan covers exact target placement during implementation.
## Migration sequencing
Split into **3 PRs** for safe rollout:
-### PR 1 — metadata add (no behavior change)
+### PR 1 — tags only (no behavior change) — **SHIPPED in PR #503**
-- Add `tags: ["scope:*"]` to every project that should participate in CI gating (~50 project.json files).
-- Add `implicitDependencies` for the ~13 fallback files to their target projects (~5-10 project.json files; some overlap).
-- ci-scope.mjs unchanged. The old `applyProjectScope`/`applyFallbackPathScope` still drive gating. Tags + implicitDependencies are inert.
-- **Verification**: `npx nx show projects --affected --base origin/main --head HEAD` returns expected projects when test files are touched (manual spot-check).
+- Added `tags: ["scope:*"]` to 87 project.json files.
+- Original spec also added `implicitDependencies: ["//path"]` for fallback files; this broke 29 CI jobs because Nx 22.5.1 rejects file-path syntax in implicitDependencies. Reverted within PR #503.
+- ci-scope.mjs unchanged. Tags inert until PR 2.
-### PR 2 — shim rewrite + test migration
+### PR 2 — namedInputs + shim rewrite + test migration
-- Replace `scripts/ci-scope.mjs` with the ~50-LOC shim above.
+- Add `namedInputs.deploymentConfig` to `apps/cockpit` and `apps/website` project.json. Reference it in target `inputs` (typically `build`, `test`).
+- Replace `scripts/ci-scope.mjs` with the ~50-LOC shim above (reads tags from `nx show projects --affected --json`).
- Migrate `scripts/ci-scope.spec.mjs`: tests now inject synthetic `affectedProjects` arrays (with `tags`) and assert scope output. Old `workspace` fixtures go away.
-- Run dual-mode in CI: keep the old code as `ci-scope-legacy.mjs`; run both, assert outputs match in a smoke test. (Optional safety net; the migration is the whole point so dual-mode is short-lived.)
-- **Verification**: CI on PR 2 itself must classify correctly — the PR's own gates must fire as expected.
+- **Verification**: CI on PR 2 itself must classify correctly — the PR's own gates fire as expected. The namedInputs change is the critical part — must confirm via `nx show projects --affected` that changing `vercel.cockpit.json` correctly marks `apps/cockpit` as affected.
-### PR 3 — cleanup
+### PR 3 — drift guard
-- Remove dual-mode + legacy script.
- Add a `cockpit-e2e-wiring.spec.ts`-style assertion: every cap project has the expected `scope:cockpit-e2e` + `scope:cockpit-examples` tags (drift guard).
## Test strategy
@@ -307,19 +333,19 @@ The `loadAffectedProjects` and `changedFilesBetween` Nx-shell-out helpers don't
- **Tag drift**: a contributor adds a new project but forgets `scope:*` tags → that project's changes silently don't trigger CI. Mitigation: PR 3 adds an assertion that every project in `cockpit/` has at least `scope:cockpit-examples` + `scope:cockpit-e2e`.
- **Nx version coupling**: `npx nx show projects --affected --json` output format could change across Nx major versions. Mitigation: pin Nx version (already pinned via package-lock); fail fast if output isn't a JSON array of strings.
-- **`implicitDependencies` for files that don't exist**: nx will warn but not fail. Mitigation: in PR 1, validate every implicit-dep file path exists at write time.
-- **PR 2's own gating**: the shim rewrite runs against the PR's own changes. If something's wrong, PR 2's gates fire wrong, and we may merge incorrectly. Mitigation: optional dual-mode parallel-run in PR 2 (assert old & new agree before cutting over).
+- **namedInputs target-binding fragility**: the named input must be referenced by a target's `inputs` array for nx to consider it affecting. If a contributor adds a new target without that reference, file changes in the named input won't mark the project affected through that target. Mitigation: bind `deploymentConfig` to a stable target (`build`) that exists on every relevant project; document the pattern in the project.json comment.
+- **PR 2's own gating**: the shim rewrite runs against the PR's own changes. If something's wrong, PR 2's gates fire wrong, and we may merge incorrectly. Mitigation: pre-flight verification — run `nx show projects --affected` locally against a fixture change in `vercel.cockpit.json` BEFORE pushing PR 2; verify `apps/cockpit` appears.
- **`nx affected` cold startup**: 2-5s overhead on every CI run. Already analyzed; acceptable trade-off for the simpler classifier + dependency-graph correctness.
## Acceptance criteria
- `scripts/ci-scope.mjs` is ≤80 LOC (down from 340).
-- Every CI-participating project has `scope:*` tags declaring its scope membership.
-- Every fallback file (vercel.*.json, scripts/*.ts, apps/cockpit/scripts/capability-registry.ts) is reachable via `implicitDependencies` on the correct project.
+- Every CI-participating project has `scope:*` tags declaring its scope membership. **(Shipped in PR #503.)**
+- Every fallback file (vercel.*.json, scripts/*.ts, apps/cockpit/scripts/capability-registry.ts) is reachable via per-project `namedInputs` referenced by a target's `inputs` on the correct project.
- All existing `scripts/ci-scope.spec.mjs` scenarios pass under the new shape (with synthetic-affected-project test fixtures).
- A PR that changes `vercel.json` triggers `website` + `website_e2e` gates (no other gates). (Smoke test on PR 2.)
- A PR that changes `cockpit/chat/messages/python/src/graph.py` triggers `cockpit_e2e` + `cockpit_smoke` + `cockpit_examples` gates. (Smoke test on PR 2.)
- A PR that changes `.github/workflows/ci.yml` triggers all gates (full scope short-circuit). (Smoke test on PR 2.)
- No regression: every gate that fires for a file today fires the same way after migration.
-**End state**: ci-scope.mjs is a ~50-LOC shim; project.json files declare their CI participation via `scope:*` tags; fallback paths live as `implicitDependencies` next to the project they affect. New contributors discover scope membership by reading the project they're touching, not a centralized 340-LOC classifier.
+**End state**: ci-scope.mjs is a ~50-LOC shim; project.json files declare their CI participation via `scope:*` tags; fallback paths live as per-project `namedInputs` referenced by target `inputs` — Nx-native, not a centralized 340-LOC classifier.