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.