Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ normal review pipeline continues.
| Retrieval | Qdrant semantic search combined with deterministic metadata and exact-path lookup |
| Stored Context | Semantic source chunks plus exact-source, architecture, graph, plugin snapshot, and repository-detection points |
| Full Reindex | Builds a pending collection generation and atomically swaps the project alias only after successful completion |
| Resilient Writes | Quarantines malformed files and exact rejected points while retaining valid content; systemic Qdrant failures still prevent activation |
| Incremental Reindex | Applies one pinned commit change set and replaces changed semantic chunks together with affected graph and state groups |
| PR Context | Uses an immutable, commit-pinned PR overlay so changed files do not retrieve stale base-branch copies |
| Compatibility Guard | Returns HTTP 409 for stale or incompatible representation/plugin state before a review-model call; recovery is a full reindex with matching service images |
| Compatibility Guard | Source-build fingerprints are provenance only and never force a reindex; durable plugin descriptor, snapshot-integrity, and Qdrant vector-shape checks remain enforced |
| Prompt Budget | Plugin and RAG evidence share bounded context budgets; plugins cannot create an additional model stage |

The Vector Storage Explorer exposes the different point types and their
Expand Down
10 changes: 10 additions & 0 deletions analysis-plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,13 @@ applies it before full/incremental file loading and PR overlay construction; and
inference maps generated/excluded files to explicit non-reviewable hunk
dispositions before Stage 0. Empty registries keep the generic fallback, while a
failed policy contribution aborts instead of silently changing the disposition.

Magento classifies XML as module configuration only when it is directly below an
`etc` directory or directly below a known Magento area such as `etc/frontend`.
Custom descendants such as `etc/samples/*.xml` stay `full` project content and
are handled by the generic indexing path. DTDs and entities in those ordinary
documents are not resolved by the Magento repository analyzer. Actual Magento
configuration XML remains architecture input, but a malformed file or one with a
DTD/entity declaration is quarantined from the architecture snapshot instead of
failing the repository index. Other valid plugin inputs continue to contribute
deterministic context. Runtime/plugin contract failures remain fatal.
14 changes: 14 additions & 0 deletions analysis-plugins/contracts/python/codecrow_plugins/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,18 @@ class PluginDiagnostic:
code: str
message: str
plugin_id: str | None = None
path: str | None = None
recoverable: bool = False

def __post_init__(self) -> None:
_non_blank(self.code, "diagnostic code")
_non_blank(self.message, "diagnostic message")
if self.plugin_id is not None:
_plugin_id(self.plugin_id)
if self.path is not None:
normalized = normalize_path(self.path)
if normalized != self.path:
raise ValueError("diagnostic path must already be normalized")


T = TypeVar("T")
Expand Down Expand Up @@ -537,12 +543,20 @@ class RepositoryAnalysis:
packets: tuple[ArchitecturePacket, ...] = ()
snapshots: tuple[RepositorySnapshot, ...] = ()
contexts: tuple[RepositoryContext, ...] = ()
diagnostics: tuple[PluginDiagnostic, ...] = ()

def __post_init__(self) -> None:
_sorted_unique(self.symbols, "repository symbols")
_sorted_unique(self.packets, "architecture packets")
_sorted_unique(self.snapshots, "repository snapshots")
_sorted_unique(self.contexts, "repository contexts")
if any(
not isinstance(diagnostic, PluginDiagnostic)
for diagnostic in self.diagnostics
):
raise ValueError(
"repository diagnostics must contain PluginDiagnostic values"
)


@dataclass(frozen=True, order=True)
Expand Down
22 changes: 13 additions & 9 deletions analysis-plugins/contracts/python/codecrow_plugins/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,15 +401,18 @@ def ingest(self, artifacts: tuple[FileArtifact, ...]) -> None:
raise ValueError("repository artifacts must be path-sorted")
retained: list[tuple[str, object]] = []
for plugin_id, session in self._sessions:
try:
session.ingest(artifacts)
retained.append((plugin_id, session))
except Exception as exception:
self._diagnostics.append(PluginDiagnostic(
code="plugin-repository-ingest-exception",
message=f"{type(exception).__name__}: {exception}",
plugin_id=plugin_id,
))
for artifact in artifacts:
try:
session.ingest((artifact,))
except Exception as exception:
self._diagnostics.append(PluginDiagnostic(
code="plugin-repository-file-skipped",
message=f"{type(exception).__name__}: {exception}",
plugin_id=plugin_id,
path=artifact.path,
recoverable=True,
))
retained.append((plugin_id, session))
self._sessions = retained

def finish(self) -> tuple[RepositoryAnalysis, tuple[PluginDiagnostic, ...]]:
Expand Down Expand Up @@ -444,6 +447,7 @@ def finish(self) -> tuple[RepositoryAnalysis, tuple[PluginDiagnostic, ...]]:
plugin_id=plugin_id,
))
continue
self._diagnostics.extend(contribution.diagnostics)
symbols.update(contribution.symbols)
if len(symbols) > self._runtime.MAX_REPOSITORY_SYMBOLS:
self._diagnostics.append(PluginDiagnostic(
Expand Down
52 changes: 50 additions & 2 deletions analysis-plugins/contracts/python/tests/test_builtin_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ def test_magento_emits_effective_di_from_repository_state_only():
)


def test_magento_rejects_xml_entities_without_resolution():
def test_magento_quarantines_unsafe_config_without_failing_repository_analysis():
catalog = PluginCatalog.discover(PLUGINS_ROOT)
runtime = PluginRuntime(catalog)
capabilities = ProjectSelector(catalog.registry).select(_facts())
Expand All @@ -685,8 +685,56 @@ def test_magento_rejects_xml_entities_without_resolution():
), key=lambda value: value.path)))
analysis, diagnostics = handle.finish()

assert analysis.packets == ()
assert analysis.snapshots
assert all(
artifact.path not in packet.paths
for packet in analysis.packets
)
assert [diagnostic.code for diagnostic in diagnostics] == ["magento-unsafe-xml"]
assert diagnostics[0].recoverable is True
assert diagnostics[0].path == artifact.path


def test_magento_keeps_custom_entity_bearing_xml_outside_architecture_analysis():
catalog = PluginCatalog.discover(PLUGINS_ROOT)
runtime = PluginRuntime(catalog)
capabilities = ProjectSelector(catalog.registry).select(_facts())
path = "app/code/Punchout/Gateway/etc/samples/cxml_inv_po.xml"
artifact = FileArtifact(
path,
"""<?xml version="1.0"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.014/InvoiceDetail.dtd">
<cXML payloadID="sample"><Response /></cXML>
""",
)

assert runtime.file_disposition(
path,
capabilities,
) is FileDisposition.FULL

handle = runtime.start_repository_analysis(
capabilities,
"0123456789abcdef",
)
handle.ingest(tuple(sorted((
artifact,
FileArtifact(
"app/code/Punchout/Gateway/etc/module.xml",
'<config><module name="Punchout_Gateway" /></config>',
),
FileArtifact(
"app/etc/config.php",
"<?php return ['modules' => ['Punchout_Gateway' => 1]];",
),
), key=lambda value: value.path)))
analysis, diagnostics = handle.finish()

assert diagnostics == ()
assert all(
path not in packet.paths
for packet in analysis.packets
)


def test_magento_file_policy_keeps_architecture_without_vendor_test_vectors():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2046,7 +2046,7 @@ def test_magento_di_emits_effective_virtual_and_php_inherited_object_arguments()
} <= child_packet_paths


def test_magento_di_virtual_type_argument_cycle_fails_closed():
def test_magento_di_virtual_type_argument_cycle_is_quarantined():
catalog = PluginCatalog.discover(PLUGINS_ROOT)
plugin = catalog.implementation("magento")
started = plugin.start_repository_analysis("virtual-cycle")
Expand All @@ -2071,10 +2071,11 @@ def test_magento_di_virtual_type_argument_cycle_fails_closed():

outcome = started.value.finish(RepositoryAnalysis())

assert outcome.status is OutcomeStatus.FAILED
assert outcome.diagnostic.code == (
assert outcome.status is OutcomeStatus.HANDLED
assert [diagnostic.code for diagnostic in outcome.value.diagnostics] == [
"magento-di-argument-inheritance-cycle"
)
]
assert outcome.value.diagnostics[0].recoverable is True


def test_magento_base_view_configuration_is_related_to_area_variant():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from types import SimpleNamespace

from codecrow_plugins import (
FileArtifact,
PluginDiagnostic,
PluginOutcome,
RepositoryAnalysis,
)
from codecrow_plugins.runtime import RepositoryAnalysisHandle


class _FileIsolatingSession:
def __init__(self):
self.ingested = []

def ingest(self, artifacts):
artifact = artifacts[0]
if artifact.path == "bad.xml":
raise ValueError("invalid project file")
self.ingested.append(artifact.path)

def finish(self, _dependencies):
return PluginOutcome.handled(RepositoryAnalysis(
diagnostics=(PluginDiagnostic(
code="project-warning",
message="recoverable repository diagnostic",
plugin_id="test-plugin",
path="warning.xml",
recoverable=True,
),),
))


def test_repository_runtime_quarantines_ingest_failure_and_keeps_session():
session = _FileIsolatingSession()
runtime = SimpleNamespace(
MAX_REPOSITORY_SYMBOLS=10,
MAX_ARCHITECTURE_PACKETS=10,
)
handle = RepositoryAnalysisHandle(
runtime,
[("test-plugin", session)],
[],
)

handle.ingest((
FileArtifact("bad.xml", "<invalid>"),
FileArtifact("good.xml", "<valid />"),
))
_analysis, diagnostics = handle.finish()

assert session.ingested == ["good.xml"]
assert [
(diagnostic.code, diagnostic.path, diagnostic.recoverable)
for diagnostic in diagnostics
] == [
("plugin-repository-file-skipped", "bad.xml", True),
("project-warning", "warning.xml", True),
]
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ValidationResult,
)

from .architecture import is_magento_config_xml
from .repository import MagentoRepositorySession


Expand Down Expand Up @@ -101,7 +102,7 @@ def file_disposition(self, path: str):
):
return PluginOutcome.handled(FileDisposition.ARCHITECTURE_ONLY)
if normalized.endswith(".xml") and (
"/etc/" in normalized
is_magento_config_xml(path)
or "/layout/" in normalized
or "/ui_component/" in normalized
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@
)


def is_magento_config_xml(path: str) -> bool:
"""Return whether an XML path participates in Magento config merging.

A module configuration file is either directly below ``etc`` or below one
known Magento area. Deeper/custom directories such as ``etc/samples`` are
ordinary project content, even though their path contains an ``etc``
segment. Treating every descendant as Magento configuration makes valid
payload formats such as cXML part of repository architecture analysis.
"""
normalized = path.replace("\\", "/").strip("/").casefold()
if not normalized.endswith(".xml"):
return False
marker = "/etc/"
candidate = f"/{normalized}"
if marker not in candidate:
return False
tail = candidate.split(marker, 1)[1].split("/")
return (
len(tail) == 1
or (len(tail) == 2 and tail[0] in MAGENTO_AREAS)
)


def tag(element: ET.Element) -> str:
return element.tag.rsplit("}", 1)[-1]

Expand All @@ -32,8 +55,11 @@ def safe_xml(plugin_id: str, path: str, content: str):
if "<!doctype" in lowered or "<!entity" in lowered:
return None, PluginDiagnostic(
"magento-unsafe-xml",
f"XML declarations/entities are forbidden in {path}",
"DTD/entity declarations are forbidden in Magento "
f"configuration {path}",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
plugin_id,
path=path,
recoverable=True,
)
try:
return ET.fromstring(content), None
Expand All @@ -42,6 +68,8 @@ def safe_xml(plugin_id: str, path: str, content: str):
"magento-invalid-xml",
f"Cannot parse {path}: {exception}",
plugin_id,
path=path,
recoverable=True,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
PacketGraph,
attrs,
config_area,
is_magento_config_xml,
line,
safe_xml,
tag,
Expand Down Expand Up @@ -209,6 +210,7 @@ def __init__(
self.graph = PacketGraph(plugin_id)
self._roots: dict[str, object] = {}
self._diagnostics: list[PluginDiagnostic] = []
self.invalid_paths: set[str] = set()
self._effective_di_arguments: dict[str, dict[str, dict]] = {}
self._template_layout_sources: dict[
str,
Expand Down Expand Up @@ -301,6 +303,7 @@ def _xml(self, path: str):
root, diagnostic = safe_xml(self.plugin_id, path, self.artifacts[path])
if diagnostic:
self._diagnostics.append(diagnostic)
self.invalid_paths.add(path)
self._roots[path] = root
return root

Expand Down Expand Up @@ -6485,7 +6488,7 @@ def _generic_config_references(self, modules: tuple[ModuleRecord, ...]) -> None:
for fact in packet.facts
}
for path in sorted(self.artifacts):
if not path.endswith(".xml") or "/etc/" not in f"/{path}":
if not is_magento_config_xml(path):
continue
module = self._module_for_path(path, modules)
is_application_config = path.startswith("app/etc/")
Expand Down Expand Up @@ -6916,7 +6919,7 @@ def ingest(self, artifacts: tuple[FileArtifact, ...]) -> None:
self.artifacts.pop(path, None)
continue
filename = PurePosixPath(path).name
is_config = path.endswith(".xml") and "/etc/" in f"/{path}"
is_config = is_magento_config_xml(path)
is_schema_whitelist = (
filename == "db_schema_whitelist.json"
and "/etc/" in f"/{path}"
Expand Down Expand Up @@ -6960,7 +6963,16 @@ def finish(self, dependencies: RepositoryAnalysis):
resolver = MagentoRepositoryResolver(self.plugin_id, self.artifacts, dependencies.symbols)
analysis, diagnostics = resolver.resolve()
if diagnostics:
return PluginOutcome.failed(diagnostics[0])
for diagnostic in diagnostics:
logger.warning(
"Skipping invalid Magento repository input "
"(code=%s path=%s): %s",
diagnostic.code,
diagnostic.path or "<repository>",
diagnostic.message,
)
for path in resolver.invalid_paths:
self.artifacts.pop(path, None)
related_paths = {
path for packet in analysis.packets for path in packet.paths
}
Expand Down Expand Up @@ -6996,4 +7008,14 @@ def finish(self, dependencies: RepositoryAnalysis):
packets=analysis.packets,
snapshots=(snapshot,),
contexts=contexts,
diagnostics=tuple(
PluginDiagnostic(
code=diagnostic.code,
message=diagnostic.message,
plugin_id=diagnostic.plugin_id,
path=diagnostic.path,
recoverable=True,
)
for diagnostic in diagnostics
),
))
Loading