Skip to content

feat: programmatic architectural enforcement via ESLint boundaries#623

Closed
yanekyuk wants to merge 3 commits intoindexnetwork:devfrom
yanekyuk:feat/architectural-enforcement
Closed

feat: programmatic architectural enforcement via ESLint boundaries#623
yanekyuk wants to merge 3 commits intoindexnetwork:devfrom
yanekyuk:feat/architectural-enforcement

Conversation

@yanekyuk
Copy link
Copy Markdown
Contributor

@yanekyuk yanekyuk commented Apr 2, 2026

Summary

  • ESLint 9 upgrade: Protocol and frontend both upgraded to ESLint 9 flat config (typescript-eslint v8+)
  • eslint-plugin-boundaries: Enforces strict layering rules in protocol — controllers→services→adapters, protocol layer isolation, no cross-service imports
  • Layer violation fixes: Refactored lib/protocol/tools/index.ts (adapter injection), 6 controllers (routed through services), and chat.graph.ts (removed service import)
  • Adapter naming check: scripts/check-adapter-names.sh flags adapters named after technology instead of concept
  • Pre-commit hook: lint-staged runs ESLint on staged files before commit
  • CI workflow: GitHub Actions runs bun run lint + adapter naming check on PRs to dev

Enforcement Stack

Editor → red squiggles on violations (ESLint)
  ↓
git commit → lint-staged blocks commit (pre-commit hook)
  ↓
PR to dev → GitHub Actions blocks merge (CI)

Boundary Rules Enforced

From \ To controllers services adapters protocol queues events guards schemas types
controllers -
services
adapters
protocol
queues

Test files (*.spec.ts, *.test.ts, tests/) are excluded from boundary checks.

Known Pre-existing Issues

  • 357 pre-existing ESLint errors (no-explicit-any, no-unused-vars) — unrelated to this PR
  • 2 eslint-disable comments for known protocol→service violations in contact.tools.ts and integration.tools.ts (tracked for follow-up refactoring)

Test Plan

  • cd protocol && bun run lint — zero boundary violations
  • ./scripts/check-adapter-names.sh — passes
  • cd protocol && tsc --noEmit — zero type errors
  • Temporarily add import { x } from "../adapters/database.adapter" to a controller → ESLint error appears
  • cd protocol && bun test — all tests pass

Summary by CodeRabbit

  • Chores
    • Added automated linting validation in the continuous integration pipeline for pull requests
    • Enabled staged file linting during local development workflow
    • Introduced adapter naming convention enforcement checks
    • Updated linting tools and configuration with enhanced validation rules

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Introduces ESLint linting infrastructure with a new GitHub Actions workflow that validates boundary dependencies, adopts a flat config structure with the boundaries plugin to enforce architectural layering, integrates lint-staged for pre-commit validation, and adds adapter naming convention enforcement via a new shell script.

Changes

Cohort / File(s) Summary
GitHub Actions Workflow
.github/workflows/lint.yml
New workflow triggered on PRs to dev branch that runs ESLint in JSON format, filters for boundaries/dependencies violations, and executes adapter naming convention checks.
ESLint Configuration
protocol/eslint.config.mjs, protocol/package.json
Migrated to flat config format using typescript-eslint, replaced legacy @typescript-eslint packages with typescript-eslint and @eslint/js, added eslint-plugin-boundaries with detailed layer definitions, and updated lint script invocation.
Package Configuration
package.json
Added lint-staged devDependency and configured staged file linting for TypeScript files in protocol/src/ and frontend/src/ directories.
Pre-commit Hooks & Enforcement
scripts/hooks/pre-commit, scripts/check-adapter-names.sh
Enhanced pre-commit hook to run lint-staged and adapter naming validation; added new script to enforce adapter naming conventions by blocking technology-prefixed filenames (drizzle, redis, bullmq, s3, resend, composio, postgres, pgvector, ioredis).
Layering Violations (Suppressions)
protocol/src/controllers/chat.controller.ts, protocol/src/controllers/debug.controller.ts, protocol/src/controllers/integration.controller.ts, protocol/src/controllers/mcp.handler.ts, protocol/src/controllers/storage.controller.ts, protocol/src/controllers/tool.controller.ts, protocol/src/controllers/user.controller.ts, protocol/src/services/integration.service.ts, protocol/src/services/tool.service.ts, protocol/src/lib/protocol/graphs/opportunity.graph.ts
Added ESLint boundary-dependency suppressions (eslint-disable-next-line) with TODO comments on existing cross-layer imports that violate the new architectural boundaries; no functional logic changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A linter hops through code so clean,
With boundaries drawn, no mixed scene,
Adapters named for concepts true,
Not tech-stack labels through and through,
Pre-commit gates keep lint-staged fed,
Now layered code stays architecturally well-bred!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: programmatic architectural enforcement via ESLint boundaries' clearly and specifically describes the main change: adding ESLint boundaries to enforce architectural layering. It is concise, directly related to the core objective, and conveys the primary change a teammate would need to understand.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@yanekyuk yanekyuk force-pushed the feat/architectural-enforcement branch from 8c36278 to 0254d71 Compare April 2, 2026 20:23
yanekyuk added 3 commits April 3, 2026 00:41
…ement

Upgrade ESLint to v9 flat config and add eslint-plugin-boundaries
to enforce strict layering rules:
- Controllers can only import services, guards, types, schemas
- Services cannot import other services or controllers
- Adapters cannot import protocol, services, or controllers
- Protocol layer can only import itself, types, schemas
- Test files are excluded from boundary checks

Pre-existing violations suppressed with eslint-disable + TODO comments.
…ng check

- lint-staged runs ESLint on staged files before commit
- Pre-commit hook blocks commits with lint violations
- GitHub Actions workflow checks boundaries on PRs to dev
- scripts/check-adapter-names.sh flags tech-named adapters
@yanekyuk yanekyuk force-pushed the feat/architectural-enforcement branch from 0254d71 to 566c3d2 Compare April 2, 2026 21:43
@yanekyuk yanekyuk marked this pull request as ready for review April 3, 2026 04:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
protocol/src/controllers/storage.controller.ts (1)

8-10: Direct adapter usage requires follow-up refactoring.

Unlike the other controllers that import only types or error classes from protocol, this controller directly instantiates and uses S3StorageAdapter throughout (lines 100, 136, 212, etc.). The eslint-disable and TODO appropriately track this, but this is a higher-priority violation to address since it involves actual adapter method calls, not just type imports. Consider creating a StorageService to wrap the adapter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/src/controllers/storage.controller.ts` around lines 8 - 10, The
controller is violating layering by importing and calling S3StorageAdapter
directly (S3StorageAdapter usage at spots like lines around 100/136/212);
refactor by introducing a StorageService abstraction (e.g., IStorageService
interface and a concrete S3StorageService) that wraps all adapter calls, replace
direct new/usage of S3StorageAdapter inside the StorageController with an
injected StorageService, update methods in StorageController to call the
StorageService (preserving method names used now), and remove the eslint-disable
and direct adapter import so the controller only depends on the service
interface/type.
scripts/check-adapter-names.sh (1)

12-19: Handle case when no adapter files exist or directory is missing.

With set -euo pipefail, the glob "$ADAPTER_DIR"/*.ts will expand literally (or error) if no .ts files exist, causing the loop to process a non-existent file. Add a guard to handle this edge case gracefully.

🛡️ Proposed defensive check
 TECH_NAMES="drizzle|redis|bullmq|s3|resend|composio|postgres|pgvector|ioredis"

+if [ ! -d "$ADAPTER_DIR" ]; then
+  echo "Adapter directory not found: $ADAPTER_DIR"
+  exit 1
+fi
+
+shopt -s nullglob
 for file in "$ADAPTER_DIR"/*.ts; do
+  [ -e "$file" ] || continue
   basename=$(basename "$file")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-adapter-names.sh` around lines 12 - 19, Add a guard before the
for-loop that checks the ADAPTER_DIR exists and contains at least one .ts file
so the glob won't be left unexpanded under set -euo pipefail; e.g., test [ -d
"$ADAPTER_DIR" ] && ls "$ADAPTER_DIR"/*.ts >/dev/null 2>&1 before executing the
loop over "$ADAPTER_DIR"/*.ts, and skip the loop (no change to VIOLATIONS) if
the test fails; keep existing symbols ADAPTER_DIR, TECH_NAMES, basename, and
VIOLATIONS unchanged so the rest of the script logic (the grep check and error
counting) runs only when files are present.
protocol/src/services/tool.service.ts (1)

33-37: Move tool collaborator wiring out of ToolService.

This file now opts out of the service boundary for both contactService and IntegrationService. Since ToolService is already assembling ToolDeps, it would be cleaner to receive those collaborators from a bootstrap/helper instead of importing sibling services directly, otherwise every new tool dependency will need another boundaries/dependencies suppression.

As per coding guidelines, "Services must not import other services. Use events, queues, or shared lib/graphs instead for inter-service communication."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/src/services/tool.service.ts` around lines 33 - 37, ToolService
currently violates layering by importing contactService and IntegrationService
directly; remove those imports and instead accept the collaborators via the
existing ToolDeps assembly so ToolService doesn't reference sibling services.
Update the ToolDeps type (and the ToolService constructor/factory) to include
fields for contactService and integrationService, remove the
boundaries/dependencies eslint disables in this file, and wire the actual
contactService and IntegrationService instances in the bootstrap/helper that
builds ToolDeps (e.g., the module that constructs ToolDeps), ensuring all
callers instantiate ToolService with the new dependencies rather than importing
the services inside ToolService.
protocol/src/controllers/mcp.handler.ts (1)

12-52: Treat MCP runtime creation as bootstrap, not controller logic.

This import block pulls the composition root, graph factories, agents, and MCP server bootstrap straight into the HTTP handler, so the controller is doing more than transport/auth and the boundaries rule has to be suppressed almost line-by-line. Moving the graph/server assembly next to protocol-init.ts would keep mcpHandler() focused and avoid drift with the similar wiring in protocol/src/services/tool.service.ts.

As per coding guidelines, "Controllers: HTTP only with guards (e.g., auth) and Zod validation; delegate to services or graphs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/src/controllers/mcp.handler.ts` around lines 12 - 52, Summary:
mcpHandler currently performs bootstrap/composition instead of being an
HTTP-only controller. Move the runtime/graph/server assembly out of the
controller by creating (or reusing) a bootstrap factory in protocol-init.ts that
composes createDefaultProtocolDeps, createMcpServer, the graph factories
(IntentGraphFactory, ProfileGraphFactory, OpportunityGraphFactory,
HydeGraphFactory, IndexGraphFactory, IndexMembershipGraphFactory,
IntentIndexGraphFactory, NegotiationGraphFactory), and agents (HydeGenerator,
LensInferrer, NegotiationProposer, NegotiationResponder) into a single exported
function (e.g., createProtocolRuntime or createMcpRuntime) that returns the
scoped deps/HydeGraphDatabase/McpAuthResolver/ToolDeps needed; then update
mcpHandler/mcp.handler.ts to remove those imports and instead call the new
bootstrap function (or accept injected runtime) so mcpHandler only handles
auth/validation and delegates work to the composed service, and remove the
per-line boundaries disable comments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/lint.yml:
- Around line 34-52: The current filter only captures messages where m.ruleId
=== "boundaries/dependencies" and misses parser/fatal errors (m.ruleId ===
null); update the processing of results in the "Check architectural boundaries"
step to also treat messages with m.fatal === true as violations (e.g., include
m.fatal in the filter alongside the existing ruleId check), ensure those fatal
messages are collected into the violations array and printed with filePath, line
and message, and keep the existing process.exit(1) when any violations
(including fatal/parser errors) are present.

In `@protocol/eslint.config.mjs`:
- Around line 69-84: Remove "init" from the services allowlist in the ESLint
rule and refactor chat.service.ts to eliminate its lazy require of
protocol-init/createDefaultProtocolDeps: change chat.service.ts to accept the
protocol dependencies (the object returned by createDefaultProtocolDeps) via
constructor or factory injection (e.g., add a ProtocolDeps parameter to the
ChatService constructor or to its factory function), update all call sites to
pass those deps from the composition root (where createDefaultProtocolDeps is
used), and remove any require/import of "protocol-init" from the service so the
protocol layer remains self-contained.

In `@protocol/package.json`:
- Around line 70-82: Update the `@eslint/js` dependency in package.json so it
matches the ESLint 9 major version: replace the current "@eslint/js": "^10.0.1"
entry with a 9.x range (e.g. "@eslint/js": "^9.0.0") to align with the existing
"eslint": "^9.0.0" entry and avoid the version mismatch.

In `@protocol/src/lib/protocol/graphs/opportunity.graph.ts`:
- Around line 2659-2663: Remove the layering-violating dynamic import fallback
and make the notifier explicit: delete the import(...) fallback in
opportunity.graph.ts so the code only uses the injected this.queueNotification
(typed as QueueOpportunityNotificationFn | undefined), update the
OpportunityGraph/constructor signature (or factory) to accept a
QueueOpportunityNotificationFn via constructor injection for consumers, and then
update all call sites (protocol/src/lib/protocol/tools/index.ts,
protocol/src/services/tool.service.ts,
protocol/src/services/opportunity.service.ts and any other creators) to pass the
concrete queueOpportunityNotification implementation (from
src/queues/notification.queue) into the graph's constructor; ensure interfaces
in src/lib/protocol/interfaces/ expose the notifier type if not already so the
protocol layer has no parent-directory imports.

---

Nitpick comments:
In `@protocol/src/controllers/mcp.handler.ts`:
- Around line 12-52: Summary: mcpHandler currently performs
bootstrap/composition instead of being an HTTP-only controller. Move the
runtime/graph/server assembly out of the controller by creating (or reusing) a
bootstrap factory in protocol-init.ts that composes createDefaultProtocolDeps,
createMcpServer, the graph factories (IntentGraphFactory, ProfileGraphFactory,
OpportunityGraphFactory, HydeGraphFactory, IndexGraphFactory,
IndexMembershipGraphFactory, IntentIndexGraphFactory, NegotiationGraphFactory),
and agents (HydeGenerator, LensInferrer, NegotiationProposer,
NegotiationResponder) into a single exported function (e.g.,
createProtocolRuntime or createMcpRuntime) that returns the scoped
deps/HydeGraphDatabase/McpAuthResolver/ToolDeps needed; then update
mcpHandler/mcp.handler.ts to remove those imports and instead call the new
bootstrap function (or accept injected runtime) so mcpHandler only handles
auth/validation and delegates work to the composed service, and remove the
per-line boundaries disable comments.

In `@protocol/src/controllers/storage.controller.ts`:
- Around line 8-10: The controller is violating layering by importing and
calling S3StorageAdapter directly (S3StorageAdapter usage at spots like lines
around 100/136/212); refactor by introducing a StorageService abstraction (e.g.,
IStorageService interface and a concrete S3StorageService) that wraps all
adapter calls, replace direct new/usage of S3StorageAdapter inside the
StorageController with an injected StorageService, update methods in
StorageController to call the StorageService (preserving method names used now),
and remove the eslint-disable and direct adapter import so the controller only
depends on the service interface/type.

In `@protocol/src/services/tool.service.ts`:
- Around line 33-37: ToolService currently violates layering by importing
contactService and IntegrationService directly; remove those imports and instead
accept the collaborators via the existing ToolDeps assembly so ToolService
doesn't reference sibling services. Update the ToolDeps type (and the
ToolService constructor/factory) to include fields for contactService and
integrationService, remove the boundaries/dependencies eslint disables in this
file, and wire the actual contactService and IntegrationService instances in the
bootstrap/helper that builds ToolDeps (e.g., the module that constructs
ToolDeps), ensuring all callers instantiate ToolService with the new
dependencies rather than importing the services inside ToolService.

In `@scripts/check-adapter-names.sh`:
- Around line 12-19: Add a guard before the for-loop that checks the ADAPTER_DIR
exists and contains at least one .ts file so the glob won't be left unexpanded
under set -euo pipefail; e.g., test [ -d "$ADAPTER_DIR" ] && ls
"$ADAPTER_DIR"/*.ts >/dev/null 2>&1 before executing the loop over
"$ADAPTER_DIR"/*.ts, and skip the loop (no change to VIOLATIONS) if the test
fails; keep existing symbols ADAPTER_DIR, TECH_NAMES, basename, and VIOLATIONS
unchanged so the rest of the script logic (the grep check and error counting)
runs only when files are present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c978cd8f-07d7-4010-aff1-ba0d4b75da00

📥 Commits

Reviewing files that changed from the base of the PR and between 833d731 and 566c3d2.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • protocol/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • .github/workflows/lint.yml
  • package.json
  • protocol/eslint.config.mjs
  • protocol/package.json
  • protocol/src/controllers/chat.controller.ts
  • protocol/src/controllers/debug.controller.ts
  • protocol/src/controllers/integration.controller.ts
  • protocol/src/controllers/mcp.handler.ts
  • protocol/src/controllers/storage.controller.ts
  • protocol/src/controllers/tool.controller.ts
  • protocol/src/controllers/user.controller.ts
  • protocol/src/lib/protocol/graphs/opportunity.graph.ts
  • protocol/src/services/integration.service.ts
  • protocol/src/services/tool.service.ts
  • scripts/check-adapter-names.sh
  • scripts/hooks/pre-commit

Comment on lines +34 to +52
- name: Check architectural boundaries
run: |
cd protocol
bunx eslint src/ --format json 2>/dev/null | bun -e '
const results = JSON.parse(await Bun.stdin.text());
const violations = results
.flatMap(r => r.messages.filter(m => m.ruleId === "boundaries/dependencies"))
.map((m, i, _, r = results.find(r2 => r2.messages.includes(m))) =>
`${r?.filePath}:${m.line} - ${m.message}`
);
if (violations.length > 0) {
console.log("❌ Architectural boundary violations found:\n");
violations.forEach(v => console.log(` ${v}`));
console.log(`\n${violations.length} violation(s) total.`);
process.exit(1);
} else {
console.log("✅ No architectural boundary violations.");
}
'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat <<'JSON' | bun -e '
const results = JSON.parse(await Bun.stdin.text());
const violations = results
  .flatMap(r => r.messages.filter(m => m.ruleId === "boundaries/dependencies"))
  .map((m, i, _, r = results.find(r2 => r2.messages.includes(m))) =>
    `${r?.filePath}:${m.line} - ${m.message}`
  );
if (violations.length > 0) process.exit(1);
console.log("workflow snippet exits 0");
'
[
  {
    "filePath": "src/broken.ts",
    "messages": [
      { "ruleId": null, "fatal": true, "line": 1, "message": "Parsing error: Unexpected token" }
    ]
  }
]
JSON

Repository: indexnetwork/index

Length of output: 105


🏁 Script executed:

cat -n .github/workflows/lint.yml | sed -n '30,55p'

Repository: indexnetwork/index

Length of output: 1308


🏁 Script executed:

# Search for any tests or documentation about the workflow behavior
fd -t f '*.ts' -o '*.js' -o '*.md' | xargs rg -l 'eslint|lint.yml' | head -20

Repository: indexnetwork/index

Length of output: 1058


🏁 Script executed:

# Look for ESLint configuration in the protocol directory
fd -t f '.eslintrc*' 'eslint.config*' -o '*.json' | rg -B 1 -A 1 'eslint|boundaries'

Repository: indexnetwork/index

Length of output: 193


🏁 Script executed:

# Check if there's eslint configuration in protocol directory
ls -la protocol/ | head -20

Repository: indexnetwork/index

Length of output: 1248


🏁 Script executed:

# Look for ESLint rules configuration, especially boundaries rule
fd -t f . protocol | rg -l 'eslint.*config|boundaries' | head -10

Repository: indexnetwork/index

Length of output: 69


🏁 Script executed:

cat -n protocol/eslint.config.mjs | head -100

Repository: indexnetwork/index

Length of output: 4165


🏁 Script executed:

# Search for boundaries rule configuration
rg -A 5 -B 5 'boundaries' protocol/eslint.config.mjs

Repository: indexnetwork/index

Length of output: 1652


🏁 Script executed:

# Look for any test files that might validate the workflow
fd -t f '.spec.ts' 'test.ts' | xargs rg -l 'fatal|parser.*error' 2>/dev/null | head -10

Repository: indexnetwork/index

Length of output: 610


🏁 Script executed:

# Look for any other workflow files or scripts that might handle eslint output
fd -t f 'workflow' '.yml' '.yaml' | xargs rg -l 'eslint' 2>/dev/null

Repository: indexnetwork/index

Length of output: 1258


🏁 Script executed:

# Search for how eslint fatal errors are typically handled
rg -i 'fatal|parser.*error' protocol/ | head -20

Repository: indexnetwork/index

Length of output: 2439


🏁 Script executed:

# Check if there's any documentation about the architecture check
cat docs/superpowers/specs/2026-04-02-architectural-enforcement-design.md | head -150

Repository: indexnetwork/index

Length of output: 4968


Fail this job on ESLint fatal/parser errors.

The step filters ESLint JSON output to only catch ruleId === "boundaries/dependencies" violations. Parser/fatal errors have ruleId: null and are not caught by this filter, allowing malformed TypeScript to pass the check.

Suggested guard
           const results = JSON.parse(await Bun.stdin.text());
+          const fatals = results
+            .flatMap(r => r.messages
+              .filter(m => m.fatal)
+              .map(m => `${r.filePath}:${m.line} - ${m.message}`));
           const violations = results
             .flatMap(r => r.messages.filter(m => m.ruleId === "boundaries/dependencies"))
             .map((m, i, _, r = results.find(r2 => r2.messages.includes(m))) =>
               `${r?.filePath}:${m.line} - ${m.message}`
             );
+          if (fatals.length > 0) {
+            console.log("❌ ESLint fatal errors found:\n");
+            fatals.forEach(v => console.log(`  ${v}`));
+            process.exit(1);
+          }
           if (violations.length > 0) {
🧰 Tools
🪛 actionlint (1.7.11)

[error] 35-35: shellcheck reported issue in this script: SC2016:info:2:53: Expressions don't expand in single quotes, use double quotes for that

(shellcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/lint.yml around lines 34 - 52, The current filter only
captures messages where m.ruleId === "boundaries/dependencies" and misses
parser/fatal errors (m.ruleId === null); update the processing of results in the
"Check architectural boundaries" step to also treat messages with m.fatal ===
true as violations (e.g., include m.fatal in the filter alongside the existing
ruleId check), ensure those fatal messages are collected into the violations
array and printed with filePath, line and message, and keep the existing
process.exit(1) when any violations (including fatal/parser errors) are present.

Comment on lines +69 to +84
// Services → adapters, protocol, init, events, queues, schemas, types
// Service-to-service NOT allowed — use events/queues
{
from: { type: "services" },
allow: {
to: {
type: [
"adapters",
"protocol",
"init",
"events",
"queues",
"schemas",
"types",
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 --type ts 'createDefaultProtocolDeps|protocol-init' protocol/src/services

Repository: indexnetwork/index

Length of output: 748


Remove "init" from the services allowlist and refactor chat.service.ts to break the circular dependency.

The ESLint rule currently permits services → protocol-init, which violates the architecture principle that the protocol layer be fully self-contained. This isn't hypothetical: chat.service.ts (lines 55–57) already imports createDefaultProtocolDeps from protocol-init via lazy require to work around a circular dependency. Remove "init" from the allowlist, then refactor chat.service.ts to receive protocol dependencies via constructor injection rather than pulling the composition root directly into the service layer.

ESLint config change
                   type: [
                     "adapters",
                     "protocol",
-                    "init",
                     "events",
                     "queues",
                     "schemas",
                     "types",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Services → adapters, protocol, init, events, queues, schemas, types
// Service-to-service NOT allowed — use events/queues
{
from: { type: "services" },
allow: {
to: {
type: [
"adapters",
"protocol",
"init",
"events",
"queues",
"schemas",
"types",
],
},
// Services → adapters, protocol, init, events, queues, schemas, types
// Service-to-service NOT allowed — use events/queues
{
from: { type: "services" },
allow: {
to: {
type: [
"adapters",
"protocol",
"events",
"queues",
"schemas",
"types",
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/eslint.config.mjs` around lines 69 - 84, Remove "init" from the
services allowlist in the ESLint rule and refactor chat.service.ts to eliminate
its lazy require of protocol-init/createDefaultProtocolDeps: change
chat.service.ts to accept the protocol dependencies (the object returned by
createDefaultProtocolDeps) via constructor or factory injection (e.g., add a
ProtocolDeps parameter to the ChatService constructor or to its factory
function), update all call sites to pass those deps from the composition root
(where createDefaultProtocolDeps is used), and remove any require/import of
"protocol-init" from the service so the protocol layer remains self-contained.

Comment on lines +70 to +82
"@eslint/js": "^10.0.1",
"@langchain/langgraph-cli": "^0.0.38",
"@types/bun": "^1.3.5",
"@types/busboy": "^1.5.4",
"@types/node": "^20.10.5",
"@types/node-cron": "^3.0.11",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"drizzle-kit": "^0.31.9",
"eslint": "^8.56.0",
"eslint": "^9.0.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-boundaries": "^6.0.2",
"typescript": "^5.3.3",
"typescript-eslint": "^8.58.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What version of @eslint/js is compatible with eslint 9?

💡 Result:

Any version of @eslint/js in the 9.x series (such as ^9.0.0 or the latest 9.x release) is compatible with ESLint 9. @eslint/js versions are kept in lockstep with ESLint releases, as evidenced by ESLint v9 release notes consistently upgrading @eslint/js to matching 9.x versions (e.g., v9.0.0 with @eslint/js@9.0.0, v9.39.3 with @eslint/js@9.39.3). The official migration guide for ESLint v9 instructs installing @eslint/js without specifying a particular minor/patch version, implying semver ^9.0.0 compatibility. @eslint/js 10.x is for ESLint 10.

Citations:


Fix @eslint/js version to match ESLint 9 major version.

@eslint/js@^10.0.1 is incompatible with eslint@^9.0.0. The @eslint/js package is released in lockstep with ESLint major versions: @eslint/js@9.x for ESLint 9.x and @eslint/js@10.x for ESLint 10.x. Change to @eslint/js@^9.0.0 to resolve the mismatch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/package.json` around lines 70 - 82, Update the `@eslint/js` dependency
in package.json so it matches the ESLint 9 major version: replace the current
"@eslint/js": "^10.0.1" entry with a 9.x range (e.g. "@eslint/js": "^9.0.0") to
align with the existing "eslint": "^9.0.0" entry and avoid the version mismatch.

Comment on lines +2659 to 2663
// TODO: fix layering violation — protocol layer should not import queues; pass notifier via constructor injection
const notifier: QueueOpportunityNotificationFn | undefined =
this.queueNotification ??
// eslint-disable-next-line boundaries/dependencies
(await import('../../../queues/notification.queue').then((m) => m.queueOpportunityNotification));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Make queueNotification explicit instead of falling back to a parent-layer import.

The provided call sites in protocol/src/lib/protocol/tools/index.ts, protocol/src/services/tool.service.ts, and protocol/src/services/opportunity.service.ts still omit queueNotification, so this suppressed dynamic import remains the normal production path. That means src/lib/protocol is still reaching into src/queues; the architecture is being bypassed, not enforced.

Direction of change
-    private queueNotification?: QueueOpportunityNotificationFn,
+    private queueNotification: QueueOpportunityNotificationFn,
...
-          const notifier: QueueOpportunityNotificationFn | undefined =
-            this.queueNotification ??
-            (await import('../../../queues/notification.queue').then((m) => m.queueOpportunityNotification));
-          if (!notifier) throw new Error('Opportunity notification not configured');
+          const notifier = this.queueNotification;

Call sites outside this range will also need to inject queueNotification.

As per coding guidelines, "Protocol layer must be fully self-contained with zero imports from parent directories. Receive adapters via constructor injection through interfaces in src/lib/protocol/interfaces/."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/src/lib/protocol/graphs/opportunity.graph.ts` around lines 2659 -
2663, Remove the layering-violating dynamic import fallback and make the
notifier explicit: delete the import(...) fallback in opportunity.graph.ts so
the code only uses the injected this.queueNotification (typed as
QueueOpportunityNotificationFn | undefined), update the
OpportunityGraph/constructor signature (or factory) to accept a
QueueOpportunityNotificationFn via constructor injection for consumers, and then
update all call sites (protocol/src/lib/protocol/tools/index.ts,
protocol/src/services/tool.service.ts,
protocol/src/services/opportunity.service.ts and any other creators) to pass the
concrete queueOpportunityNotification implementation (from
src/queues/notification.queue) into the graph's constructor; ensure interfaces
in src/lib/protocol/interfaces/ expose the notifier type if not already so the
protocol layer has no parent-directory imports.

@yanekyuk yanekyuk closed this Apr 3, 2026
@yanekyuk yanekyuk deleted the feat/architectural-enforcement branch April 3, 2026 05:01
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.

1 participant