Skip to content

feat: add architectural boundary and threat model assessors (ADR B.1, B.2)#503

Merged
jwm4 merged 3 commits into
mainfrom
feat/adr-b1-b2-boundary-threat-model
Jun 18, 2026
Merged

feat: add architectural boundary and threat model assessors (ADR B.1, B.2)#503
jwm4 merged 3 commits into
mainfrom
feat/adr-b1-b2-boundary-threat-model

Conversation

@jwm4

@jwm4 jwm4 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add ArchitecturalBoundaryAssessor (B.1): binary pass/fail check for linter-enforced import boundaries across JS/TS (ESLint), Go (depguard/gomodguard), Python (import-linter/flake8-tidy-imports), and general (dependency-cruiser). Repos with <20 files return not_applicable.
  • Add ThreatModelAssessor (B.2): partial-credit scoring for THREAT_MODEL.md based on existence, content substance, 8-section schema coverage, and threat table structure. Falls back to SECURITY.md for partial credit.
  • Rebalance weights: test_execution 12%->11%, architecture_decisions 3%->2%, structured_logging 2%->1%, openapi_specs 3%->2%, freeing 4% for the two new attributes at 2% each (total remains 100%).
  • Update docs/attributes.md with full entries for both new Tier 3 attributes.

Closes #463

Test plan

  • 16 unit tests for ArchitecturalBoundaryAssessor (ESLint, Go, Python, dependency-cruiser configs, small repo N/A, malformed configs)
  • 15 unit tests for ThreatModelAssessor (full/partial sections, alternative filenames, SECURITY.md fallback, threat table detection)
  • Full test suite passes (1236 tests, 0 regressions)
  • agentready assess . runs end-to-end showing both new attributes
  • Linters pass (black, isort, ruff)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added assessment for a structured threat model document (e.g., THREAT_MODEL.md) with section and threats-table checks.
    • Added assessment for architectural boundary enforcement based on common linting/tooling configurations.
  • Updates
    • AgentReady now evaluates 27 total attributes with revised tier counts and updated tier weights (including two new Tier 3 attributes).
    • Refreshed the attributes reference documentation and implementation status.
  • Tests
    • Added unit test coverage for threat-model discovery/scoring and architectural boundary detection behavior.

… B.2)

Implement the final two assessors from the wg-agentic-sdlc alignment ADR:

- ArchitecturalBoundaryAssessor (B.1): binary pass/fail check for linter-enforced
  import boundaries (ESLint, depguard, gomodguard, import-linter, flake8-tidy-imports,
  dependency-cruiser). Repos with <20 files return not_applicable.

- ThreatModelAssessor (B.2): checks for THREAT_MODEL.md with partial credit scoring
  based on file existence (40 pts), content substance (10 pts), recognized sections
  from the 8-section schema (up to 48 pts), and threat table bonus (2 pts).
  Falls back to SECURITY.md threat model section for 25 pts partial credit.

Weight rebalancing: test_execution 12%->11%, architecture_decisions 3%->2%,
structured_logging 2%->1%, openapi_specs 3%->2%, freeing 4% for the two new
attributes at 2% each. Total remains 100%.

Closes #463

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Two new Tier 3 assessors are added: ThreatModelAssessor (scores structured THREAT_MODEL.md against an 8-section schema with content bonuses) and ArchitecturalBoundaryAssessor (detects import-boundary enforcement across ESLint, Go, Python, and JavaScript ecosystems). Both are registered in the factory, weighted in default-weights.yaml, and documented in docs/attributes.md, bringing the total to 27 assessed attributes. Tier weights are rebalanced from 59/27/12 to 58/27/13 by reducing existing weights.

Changes

New Tier 3 Assessors: Architectural Boundaries and Threat Model

Layer / File(s) Summary
ThreatModelAssessor
src/agentready/assessors/security.py, tests/unit/test_assessors_security.py
New assessor searches root and configured subdirectories (docs/, docs/security/) for THREAT_MODEL.md variants, scores recognized 8-section headings plus threat-table detection and content-length bonuses, falls back to SECURITY.md pattern matching, and produces Finding with remediation when score falls below pass threshold. Tests cover full/partial section scoring, filename variants, path precedence, fallback logic, malformed-file resilience, and metadata validation.
ArchitecturalBoundaryAssessor
src/agentready/assessors/structure.py, tests/unit/test_assessors_structure.py
New assessor gates on total_files >= 20, then runs per-ecosystem checks (ESLint no-restricted-imports/no-restricted-modules, Go depguard/gomodguard, Python .importlinter/flake8-tidy-imports, JavaScript .dependency-cruiser.*). Returns score 100 on first match; otherwise score 0 with remediation and configuration examples. Tests cover all config sources, malformed-file handling, and metadata assertions.
Factory registration and weight rebalancing
src/agentready/assessors/__init__.py, src/agentready/data/default-weights.yaml
create_all_assessors() imports and appends both new assessors to the Tier 3 block. default-weights.yaml adds architectural_boundaries: 0.02 and threat_model: 0.02, funded by reducing test_execution (0.12→0.11), architecture_decisions (0.03→0.02), openapi_specs (0.03→0.02), and structured_logging (0.02→0.01). Tier 1 comment updates to 58%, Tier 3 to 13% and 7 attributes.
Attribute reference docs
docs/attributes.md
Adds full architectural_boundaries and threat_model attribute sections (scoring rules, accepted locations, remediation templates). Updates Tier System table to 58%/27%/13% and associated attribute counts (9/9/7). Updates Implementation Status to mark all 27 assessors as fully implemented.

Possibly related PRs

  • ambient-code/agentready#407: Directly overlaps on default-weights.yaml Tier 3 weight adjustments for structured_logging, openapi_specs, and architecture_decisions—the same fields redistributed here to fund the two new assessors.
  • ambient-code/agentready#464: Modifies the same create_all_assessors() factory in src/agentready/assessors/__init__.py, removing and retiering assessors that provide context for this PR's new registrations.

Suggested labels

released

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format with feat type and scope, clearly describing the two new assessors (architectural boundary and threat model) being added.
Linked Issues check ✅ Passed PR fully implements all objectives from #463: both assessors (B.1, B.2) are Tier 3 at 2% each, include comprehensive boundary/threat detection logic, are registered in factory/weights/docs, and have 31 unit tests with real-world validation.
Out of Scope Changes check ✅ Passed All changes directly support the two new assessors: security.py and structure.py add ThreatModelAssessor and ArchitecturalBoundaryAssessor, init.py registers them, weights and docs are updated, and tests validate both assessors comprehensively.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adr-b1-b2-boundary-threat-model
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/adr-b1-b2-boundary-threat-model

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.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📈 Test Coverage Report

Branch Coverage
This PR 75.1%
Main 74.6%
Diff ✅ +0.5%

Coverage calculated from unit tests only

@jwm4 jwm4 marked this pull request as ready for review June 17, 2026 13:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/attributes.md`:
- Line 29: The document contains inconsistent attribute counts: line 29 states
"27 attributes" but an earlier overview sentence near the top still references
"25 attributes". Locate the overview sentence in the document that mentions 25
attributes and update it to 27 to align with the count stated on line 29,
ensuring the document is self-consistent throughout.

In `@src/agentready/assessors/security.py`:
- Around line 447-459: The exception handler in the try-except block that
catches OSError and UnicodeDecodeError when reading threat_model_path is
incorrectly returning a Finding with status="pass". Change the status from
"pass" to "fail" to properly indicate that the threat model file is unreadable
or malformed, and adjust the score accordingly to reflect the failure.
Additionally, update the error_message field from None to include the actual
exception details to provide debugging information, and update the evidence list
to clearly indicate that the threat model file could not be read rather than
just passing with partial credit.

In `@src/agentready/assessors/structure.py`:
- Around line 1533-1537: The _check_python_import_linter() method currently only
checks for the [importlinter] section in setup.cfg, missing flake8-tidy-imports
and banned-api configurations. Extend the setup.cfg check to also look for the
[flake8] section in addition to [importlinter], and when [flake8] is found,
append appropriate evidence indicating flake8-tidy-imports or banned-api is
configured. This will ensure repositories using these alternative import
boundary enforcement tools are correctly recognized and scored.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 190f5433-57e7-424e-9f5c-d7be4b9a8f57

📥 Commits

Reviewing files that changed from the base of the PR and between 6264bb0 and b920bb2.

📒 Files selected for processing (7)
  • docs/attributes.md
  • src/agentready/assessors/__init__.py
  • src/agentready/assessors/security.py
  • src/agentready/assessors/structure.py
  • src/agentready/data/default-weights.yaml
  • tests/unit/test_assessors_security.py
  • tests/unit/test_assessors_structure.py

Comment thread docs/attributes.md
Comment thread src/agentready/assessors/security.py
Comment thread src/agentready/assessors/structure.py
@jwm4

jwm4 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Review from Bill Murdock with assistance from Claude Code

Overall this is a solid PR. Both assessors follow established patterns well, tests are thorough (31 new, all passing), weights sum to 1.00, and the full suite (1373 tests) shows zero regressions. A few items to address before merging.

Bug

__init__.py:94: Comment says # Tier 3 Important — 14% total (7 attributes) but the actual weight sum is 13% (2+2+2+1+2+2+2). The default-weights.yaml header and docs/attributes.md both correctly say 13%. Looks like this comment was written before structured_logging was reduced from 2% to 1%.

Issues to fix

  1. Unreadable threat model files return status="pass" (security.py, around line 459): When THREAT_MODEL.md exists but can't be read (encoding error, etc.), the exception handler returns status="pass" with score 40. But 40 is below the pass threshold of 50, so this should be status="fail". The test test_malformed_file_no_crash would need updating to match. Also worth changing path.exists() to path.is_file() in _find_threat_model_file() to avoid matching directories. (CodeRabbit flagged this too.)

  2. setup.cfg missing flake8-tidy-imports check (structure.py, around line 1537): _check_python_import_linter() checks setup.cfg for [importlinter] but not for flake8-tidy-imports/banned-api, even though the pyproject.toml path does check for both. Repos using flake8-tidy-imports via setup.cfg would be missed. (CodeRabbit flagged this too.)

  3. docs/attributes.md attribute count inconsistency: The overview sentence near the top still says "25 attributes" but should say 27. The table and implementation status sections were updated but this line was missed. (CodeRabbit flagged this too.)

Cosmetic (non-blocking)

  • default-weights.yaml: The architectural_boundaries line uses fewer spaces before the comment than other entries.
  • ThreatModelAssessor uses os.path.join("docs", "security") at class level while the rest of the file uses pathlib.Path. Works fine, just inconsistent.

- Fix __init__.py T3 comment: 14% -> 13% (matches actual weights)
- Fix ThreatModelAssessor: unreadable files return status="fail" (score 40
  is below pass threshold of 50)
- Use path.is_file() instead of path.exists() in threat model file search
  to avoid matching directories
- Add flake8-tidy-imports/banned-api check for setup.cfg in
  ArchitecturalBoundaryAssessor (was only checked in pyproject.toml)
- Fix docs/attributes.md overview: 25 -> 27 attributes
- Align default-weights.yaml comment spacing for architectural_boundaries
- Replace os.path.join with pathlib.Path in ThreatModelAssessor, remove
  unused os import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jwm4

jwm4 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up from real-world testing (Bill Murdock with assistance from Claude Code)

Tested both new assessors against apache/ranger, which has an extensive PMC-reviewed THREAT_MODEL.md. Two issues surfaced.

1. ThreatModelAssessor: section matching too rigid for real-world threat models

Ranger's THREAT_MODEL.md is arguably a gold standard (334 lines, 14 detailed sections, PMC-reviewed with maintainer sign-off), but scores only 56/100 with 1/8 sections matched. Two root causes:

Section prefix handling: The regex ^##\s+(?:\d+\.\s*)?(.+)$ strips numeric prefixes like "1." but not "§1" (section symbol + digit). Ranger uses ## §7 Adversary model, so the captured text includes "§7" and the word-match against canonical names fails.

Vocabulary gap: The word-matching requires all canonical words to appear in the heading. Real threat models use different terminology for the same concepts:

Canonical Ranger heading Why it fails
threats §7 Adversary model "threats" not in heading
entry points §4 Trust boundaries and data flow "entry", "points" not in heading
deprioritized §3 Out of scope "deprioritized" not in heading
assets covered in §6 Assumptions about inputs "assets" not in heading
system context §2 Scope and intended use "system", "context" not in heading
provenance §1 Header (contains provenance metadata) "provenance" not in heading
recommended mitigations §10 Downstream responsibilities "recommended", "mitigations" not in heading

Only "open questions" (§14) matched. Consider adding synonym/alias support or a more flexible matching strategy for a follow-up.

2. ArchitecturalBoundaryAssessor: should return not_applicable for unsupported languages

The assessor currently gates applicability only on file count (>=20), not on language. It checks tools for JS/TS (ESLint), Go (golangci), and Python (import-linter/flake8-tidy-imports), plus dependency-cruiser. Ranger is a Java project, so none of those tools are relevant, but it scores fail (0%) instead of not_applicable.

Java repos could use ArchUnit or Checkstyle import controls for boundary enforcement, but the assessor doesn't check for those. Until Java (and Rust, Ruby, C#, etc.) tools are supported, repos in those languages should get not_applicable rather than being penalized for not having tools from other ecosystems.

…uage gating

ThreatModelAssessor: real-world testing against apache/ranger showed the
section matching was too rigid, matching only 1/8 sections on a gold-standard
threat model.

- Regex now strips section symbol prefixes (e.g., ## §7 Adversary model)
- Added synonym/alias support for all 8 canonical sections so common
  heading variations are recognized (e.g., "Adversary model" matches
  "threats", "Out of scope" matches "deprioritized", "Trust boundaries"
  matches "entry points")
- Threat table detection also matches "adversary model" headings

ArchitecturalBoundaryAssessor: Java repos scored fail(0%) despite having no
supported boundary tools to check.

- Added language gating: repos whose detected languages have no overlap
  with supported languages (Python, JS, TS, Go) now return not_applicable
  instead of fail
- Repos with empty language detection remain applicable (graceful fallback)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/agentready/assessors/structure.py (1)

1481-1497: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Boundary “enforcement” can be falsely detected from plain string presence.

Current checks treat any occurrence of rule/tool names as configured enforcement. This will pass on disabled rules ("no-restricted-imports": "off"), comments, or unrelated text, producing incorrect 100% scores.

This needs structured parsing/validation per config type (at least JSON/TOML where parseable) and an enabled-state check for ESLint rules.

Also applies to: 1508-1552

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agentready/assessors/structure.py` around lines 1481 - 1497, The current
implementation checks for boundary rule string presence in configuration files
without validating whether those rules are actually enabled. You need to parse
configuration files structurally (JSON for package.json and eslintConfig
sections) and verify each matched rule has an enabled state (not "off" or 0). In
the loop checking eslint_configs, after finding matched rules from
boundary_rules, parse the config content to check the actual configuration value
for each matched rule rather than just confirming string presence. Similarly, in
the package.json eslintConfig section where you build the matched list, parse
the eslintConfig JSON object and validate that each matched rule is not disabled
before appending to tools_found and evidence. This ensures only actively
enforced boundary rules are counted toward the assessment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agentready/assessors/security.py`:
- Around line 566-575: The _match_canonical_section method checks canonical
sections before alias matches, causing headings like "Non-threats" to be
incorrectly classified as "threats" (substring match) instead of reaching the
intended "deprioritized" alias. Reverse the matching order by moving the loop
that iterates through self.SECTION_ALIASES and checks aliases to execute before
the loop that checks self.CANONICAL_SECTIONS with word matching. This ensures
more specific alias matches are found before generic substring matches in
canonical sections.

In `@src/agentready/assessors/structure.py`:
- Around line 1418-1423: The supported language check using
`_has_supported_language(repository)` at the beginning of the method returns
`not_applicable` before the dependency-cruiser detection logic can run, which
incorrectly blocks valid cross-language boundary tool detection. Reorder the
logic so that language-agnostic tool checks (like the dependency-cruiser check
that should occur around line 1431) are performed before the supported language
validation. This allows repositories with dependency-cruiser configuration to
pass detection regardless of their primary programming language, while still
applying language checks to language-specific boundary tools.

In `@tests/unit/test_assessors_security.py`:
- Around line 1113-1138: The test_synonym_matching method does not explicitly
verify the Non-threats to deprioritized alias mapping. Replace the current "##
Out of scope" section in the content string with "## Non-threats" to create a
regression test for this specific mapping, then add an assertion to verify that
"non-threats" appears in the matched evidence string to ensure this
collision-prone heading is properly recognized and prevents future scoring
regressions.

In `@tests/unit/test_assessors_structure.py`:
- Around line 1515-1534: Add a new test method after the existing test cases in
the test class that creates a Java-only repository (using _make_repo with
languages set to {"Java": 100} or similar) but also includes a
.dependency-cruiser.cjs configuration file in the repo directory. Instantiate
the ArchitecturalBoundaryAssessor and call assess on the repo, then assert that
the finding status is not "not_applicable" to verify that the presence of
dependency-cruiser configuration makes the repo applicable even when the primary
language is unsupported (Java). This test prevents regressions in the
cross-language boundary-tool applicability logic.

---

Outside diff comments:
In `@src/agentready/assessors/structure.py`:
- Around line 1481-1497: The current implementation checks for boundary rule
string presence in configuration files without validating whether those rules
are actually enabled. You need to parse configuration files structurally (JSON
for package.json and eslintConfig sections) and verify each matched rule has an
enabled state (not "off" or 0). In the loop checking eslint_configs, after
finding matched rules from boundary_rules, parse the config content to check the
actual configuration value for each matched rule rather than just confirming
string presence. Similarly, in the package.json eslintConfig section where you
build the matched list, parse the eslintConfig JSON object and validate that
each matched rule is not disabled before appending to tools_found and evidence.
This ensures only actively enforced boundary rules are counted toward the
assessment.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: a5232be4-d4cc-47c5-b50b-0399dddba39b

📥 Commits

Reviewing files that changed from the base of the PR and between 9eb6eaa and 8cdca9d.

📒 Files selected for processing (5)
  • docs/attributes.md
  • src/agentready/assessors/security.py
  • src/agentready/assessors/structure.py
  • tests/unit/test_assessors_security.py
  • tests/unit/test_assessors_structure.py

Comment on lines +566 to +575
def _match_canonical_section(self, heading_lower: str) -> str | None:
for canonical in self.CANONICAL_SECTIONS:
canonical_words = canonical.split()
if all(word in heading_lower for word in canonical_words):
return canonical
for canonical, aliases in self.SECTION_ALIASES.items():
for alias in aliases:
if alias in heading_lower:
return canonical
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Alias-specific headings can be misclassified due to match order.

On Line 569, canonical substring matching runs before aliases. A heading like Non-threats gets classified as threats (because "threats" is a substring), so the intended deprioritized alias on Line 403 is never reached. This skews section scoring.

Proposed fix
     def _match_canonical_section(self, heading_lower: str) -> str | None:
-        for canonical in self.CANONICAL_SECTIONS:
-            canonical_words = canonical.split()
-            if all(word in heading_lower for word in canonical_words):
-                return canonical
         for canonical, aliases in self.SECTION_ALIASES.items():
             for alias in aliases:
                 if alias in heading_lower:
                     return canonical
+        for canonical in self.CANONICAL_SECTIONS:
+            canonical_words = canonical.split()
+            if all(word in heading_lower for word in canonical_words):
+                return canonical
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agentready/assessors/security.py` around lines 566 - 575, The
_match_canonical_section method checks canonical sections before alias matches,
causing headings like "Non-threats" to be incorrectly classified as "threats"
(substring match) instead of reaching the intended "deprioritized" alias.
Reverse the matching order by moving the loop that iterates through
self.SECTION_ALIASES and checks aliases to execute before the loop that checks
self.CANONICAL_SECTIONS with word matching. This ensures more specific alias
matches are found before generic substring matches in canonical sections.

Comment on lines +1418 to +1423
if not self._has_supported_language(repository):
langs = ", ".join(sorted(repository.languages.keys()))
return Finding.not_applicable(
self.attribute,
reason=f"No supported languages detected ({langs}); boundary checks cover Python, JavaScript, TypeScript, Go",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unsupported-language short-circuit blocks valid dependency-cruiser detection.

On Line 1418, not_applicable is returned before Line 1431 runs. That means repos in unsupported languages can never pass even if .dependency-cruiser.* is present, which conflicts with the assessor’s own “any language / cross-language” boundary-tool path.

Proposed fix
     def assess(self, repository: Repository) -> Finding:
         if repository.total_files < self.FILE_THRESHOLD:
             return Finding.not_applicable(
                 self.attribute,
                 reason=f"Repository has {repository.total_files} files (boundary rules relevant for >={self.FILE_THRESHOLD})",
             )

+        # Cross-language boundary tool: allow pass even when language set is unsupported
+        tools_found = []
+        evidence = []
+        self._check_dependency_cruiser(repository, tools_found, evidence)
+        if tools_found:
+            return Finding(
+                attribute=self.attribute,
+                status="pass",
+                score=100.0,
+                measured_value=f"boundary tools: {', '.join(tools_found)}",
+                threshold="at least one import boundary tool configured",
+                evidence=evidence,
+                remediation=None,
+                error_message=None,
+            )
+
         if not self._has_supported_language(repository):
             langs = ", ".join(sorted(repository.languages.keys()))
             return Finding.not_applicable(
                 self.attribute,
                 reason=f"No supported languages detected ({langs}); boundary checks cover Python, JavaScript, TypeScript, Go",
             )

-        tools_found = []
-        evidence = []
-
         self._check_eslint(repository, tools_found, evidence)
         self._check_go_boundary_tools(repository, tools_found, evidence)
         self._check_python_import_linter(repository, tools_found, evidence)
-        self._check_dependency_cruiser(repository, tools_found, evidence)

Also applies to: 1431-1431, 1554-1569

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agentready/assessors/structure.py` around lines 1418 - 1423, The
supported language check using `_has_supported_language(repository)` at the
beginning of the method returns `not_applicable` before the dependency-cruiser
detection logic can run, which incorrectly blocks valid cross-language boundary
tool detection. Reorder the logic so that language-agnostic tool checks (like
the dependency-cruiser check that should occur around line 1431) are performed
before the supported language validation. This allows repositories with
dependency-cruiser configuration to pass detection regardless of their primary
programming language, while still applying language checks to language-specific
boundary tools.

Comment on lines +1113 to +1138
def test_synonym_matching(self, tmp_path):
"""Alias headings match their canonical equivalents."""
content = (
"# Threat Model\n\n"
"## Scope and intended use\n"
"A REST API.\n\n"
"## Trust boundaries and data flow\n"
"External users access via /api.\n\n"
"## Out of scope\n"
"Local attacks.\n\n"
"## Adversary model\n"
"Remote unauthenticated attackers.\n"
)
(tmp_path / "THREAT_MODEL.md").write_text(content)
repo = self._make_repo(tmp_path)
assessor = ThreatModelAssessor()
finding = assessor.assess(repo)
sections_evidence = [
e for e in finding.evidence if "recognized sections" in e.lower()
]
assert len(sections_evidence) == 1
matched = sections_evidence[0]
assert "system context" in matched
assert "entry points" in matched
assert "deprioritized" in matched
assert "threats" in matched

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a regression test for Non-threatsdeprioritized mapping.

The synonym tests are good, but they don’t cover the non-threats alias explicitly. Adding that case would lock behavior for a known collision-prone heading and prevent scoring regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_assessors_security.py` around lines 1113 - 1138, The
test_synonym_matching method does not explicitly verify the Non-threats to
deprioritized alias mapping. Replace the current "## Out of scope" section in
the content string with "## Non-threats" to create a regression test for this
specific mapping, then add an assertion to verify that "non-threats" appears in
the matched evidence string to ensure this collision-prone heading is properly
recognized and prevents future scoring regressions.

Comment on lines +1515 to +1534
def test_java_repo_not_applicable(self, tmp_path):
"""Java-only repo gets not_applicable (unsupported language)."""
repo = self._make_repo(tmp_path, languages={"Java": 90, "XML": 10})
assessor = ArchitecturalBoundaryAssessor()
finding = assessor.assess(repo)
assert finding.status == "not_applicable"

def test_mixed_language_with_supported(self, tmp_path):
"""Repo with Java and Python is still applicable."""
repo = self._make_repo(tmp_path, languages={"Java": 60, "Python": 40})
assessor = ArchitecturalBoundaryAssessor()
finding = assessor.assess(repo)
assert finding.status != "not_applicable"

def test_no_languages_defaults_applicable(self, tmp_path):
"""Repo with empty languages dict remains applicable."""
repo = self._make_repo(tmp_path, languages={})
assessor = ArchitecturalBoundaryAssessor()
finding = assessor.assess(repo)
assert finding.status != "not_applicable"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add an edge-case test: Java-only repo with .dependency-cruiser.cjs should not be not_applicable.

The new gating tests cover unsupported languages, but they miss the cross-language boundary-tool path. A targeted case here would prevent regressions in applicability logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_assessors_structure.py` around lines 1515 - 1534, Add a new
test method after the existing test cases in the test class that creates a
Java-only repository (using _make_repo with languages set to {"Java": 100} or
similar) but also includes a .dependency-cruiser.cjs configuration file in the
repo directory. Instantiate the ArchitecturalBoundaryAssessor and call assess on
the repo, then assert that the finding status is not "not_applicable" to verify
that the presence of dependency-cruiser configuration makes the repo applicable
even when the primary language is unsupported (Java). This test prevents
regressions in the cross-language boundary-tool applicability logic.

@jwm4

jwm4 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Status update from Bill Murdock with assistance from Claude Code

All review feedback has been addressed across two fix commits (9eb6eaa, 8cdca9d). This PR is ready to merge.

Issues resolved

Round 1 (9eb6eaa fix: address PR review feedback):

  • __init__.py Tier 3 comment corrected from 14% to 13%
  • Unreadable THREAT_MODEL.md now returns status="fail" (was incorrectly "pass" with score 40, below the 50 pass threshold)
  • _find_threat_model_file() uses path.is_file() instead of path.exists() to avoid matching directories
  • setup.cfg now checked for flake8-tidy-imports/banned-api (was only checked in pyproject.toml)
  • docs/attributes.md overview sentence updated from "25 attributes" to "27"
  • YAML alignment fixed for architectural_boundaries line
  • os.path.join replaced with Path in ThreatModelAssessor, unused import os removed

Round 2 (8cdca9d fix: improve threat model section matching and boundary assessor language gating):

  • ThreatModelAssessor regex updated to handle §-prefixed section headings
  • Added synonym/alias matching for canonical sections (e.g., "Adversary model" matches "threats", "Out of scope" matches "deprioritized")
  • _has_threat_table regex updated to match "adversary model" heading in addition to "threats"
  • ArchitecturalBoundaryAssessor now returns not_applicable for repos whose languages are all unsupported (currently supports Python, JS, TS, Go)
  • docs/attributes.md updated to document both improvements

Testing performed

Unit tests: Full suite passes, 1380 passed, 21 skipped, 0 failures. The three commits added a total of 38 new tests covering section symbol prefixes, synonym matching, Ranger-style threat models, adversary model table detection, Java-only repo language gating, mixed-language applicability, and empty-language fallback.

Weight verification: Confirmed programmatically that all weights sum to exactly 1.00 (T1: 58%, T2: 27%, T3: 13%, T4: 2%, 27 attributes total).

Real-world testing against apache/ranger:

  • ThreatModelAssessor: Ranger has a 334-line PMC-reviewed THREAT_MODEL.md with 14 §-numbered sections. Before the Round 2 fix, the assessor matched only 1/8 sections (score 56). After the fix, it matches 6/8 sections (score 86). The two unmatched sections ("assets" and "provenance") have their content embedded in differently-named sections rather than standalone headings.
  • ArchitecturalBoundaryAssessor: Ranger (Java/Shell/XML) correctly returns not_applicable after the language gating fix. Previously returned fail with score 0.

Real-world testing against yjs/yjs:

  • ThreatModelAssessor: yjs has a THREAT_MODEL.md that scored 76/100 with 4/8 recognized sections (assets, threats, entry points, deprioritized) plus the threat table bonus. Status: pass.
  • ArchitecturalBoundaryAssessor: yjs (JavaScript) correctly returns fail with score 0 (supported language, but no ESLint boundary rules configured). This is the expected result for a JS repo without import restriction rules.

Follow-up

Filed #505 to audit remediation instructions across all assessors for alignment with scoring logic (not blocking this PR).

@jwm4 jwm4 merged commit c32325d into main Jun 18, 2026
18 checks passed
github-actions Bot pushed a commit that referenced this pull request Jun 18, 2026
# [2.48.0](v2.47.0...v2.48.0) (2026-06-18)

### Features

* add architectural boundary and threat model assessors (ADR B.1, B.2) ([#503](#503)) ([c32325d](c32325d)), closes [#463](#463)
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.48.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New assessors for architectural boundaries and threat models (ADR B.1, B.2)

1 participant