Skip to content

v0.5.0 I4 slice 4: Topic/Subscription in Architecture Intelligence answers - #232

Merged
michaelegner merged 1 commit into
mainfrom
i4-slice-4-architecture-intelligence
Sep 24, 2026
Merged

michaelegner merged 1 commit into
mainfrom
i4-slice-4-architecture-intelligence

Conversation

@michaelegner

Copy link
Copy Markdown
Owner

This PR implements I4 slice 4 (spec §15), the Architecture Intelligence vertical slice. Declared Topic/Subscription topology now appears in get_service_dependencies, get_architecture_drift and get_evidence. The service, REST and MCP return identical answers. There are no contract, schema or answer-scenario changes, and HTTP/Queue claim ids are byte-identical.

Approved plan (verbatim)

Context

Slices 1–3 are merged, ending at 5958b4d (#228–#231). Topic and Subscription nodes are persisted: PUBLISHES_TO, SUBSCRIPTION_OF, RECEIVES_FROM→Subscription and Topic CARRIES Message. They are also in the snapshot (canonicalization v3) and runtime-qualified (observed PUBLISHES_TO and RECEIVES_FROM→Subscription evidence), and the public contract and v0.5 schemas already carry them. What is still missing is the public answer path. get_service_dependencies and get_architecture_drift read only CALLS→Operation and SENDS→Queue, the qualification kernel raises on PUBLISHES_TO, and coverage is Queue-typed. Slice 4 makes Pub/Sub routes into public dependency and drift claims, following §12.

Governing Specification

docs/specifications/0.5.0/i4-source-independent-pubsub-semantics.md, Draft 0.3 (#226 + #227 → f98e48b). The sections that apply:

  • §12.2–§12.6, the public contract, projection, claim id, drift and exposure table;
  • §15 Slice 4;
  • §11, only its line that evidence refs must resolve at the same snapshot.

Scope & Non-Goals

Scope:

  • Pub/Sub graph reads.
  • Topic/Subscription dependency projection, with DeliveryRef.subscription and the claim-id subscription_id.
  • Drift pass-through.
  • Qualification and coverage widening.
  • Evidence drill-down reachability.
  • The reference claim-id mirror.
  • Service/REST/MCP equivalence tests.

Non-goals:

  • Broker-semantic fixtures, new evaluation/architecture_answers scenarios, the full matrix and the two-run byte-identity check. These are slice 5.
  • Docs, completion record and ADR 0017 acceptance. These are slice 6.
  • Pub/Sub in the O1–O4 runtime analyses.
  • The legacy evaluation/model.py and projector.py harness.
  • The stale reference/snapshot.py, which is still at v1. Disclosed only.
  • Contract or schema changes. None are expected, because slice 1 already widened them.
  • New MCP tools or REST endpoints.

Decisions settled by the user for spec gaps (disclose in the PR; fold into the spec or completion record in slice 6)

  1. Qualification = publisher only (same as Queue).
    • A claim's qualification and evidence_refs come only from the subject's PUBLISHES_TO evidence.
    • A consumer's RECEIVES_FROM→Subscription evidence goes only into its own route's resolution_evidence_refs, so it never reaches a sibling Subscription.
  2. SUBSCRIPTION_OF.
    • A Subscription route is usable only when its SUBSCRIPTION_OF has accepted (non-dangling) evidence.
    • That evidence is added to the route's resolution_evidence_refs, which makes it reachable through get_evidence (§12.6).
  3. Coverage = one widened shared rule.
    • Observed PUBLISHES_TO→Topic and RECEIVES_FROM→Subscription now set messaging_observed in the shared telemetry_coverage.
    • PUBLISHES_TO joins the kernel's messaging relation set.
    • Known side effect: O5 and REST messagingObserved become true for services that only have Pub/Sub telemetry.

Affected Components

  • app/qualification/declared_observed.py:37: _MESSAGING_RELATION_TYPES gains PUBLISHES_TO. Update the docstring.
  • app/analysis/runtime.py:307-314: widen the two observed queries to -[r:SENDS|PUBLISHES_TO]->(t) WHERE t:Queue OR t:Topic and -[r:RECEIVES_FROM]->(t) WHERE t:Queue OR t:Subscription. Update the comments.
  • app/architecture_intelligence/repository.py:
    • Add _PUBLISHES_QUERY, which returns (Service {id})-[PUBLISHES_TO]->(Topic) with id, name, protocol, namespace and evidence_ids.
    • Add _SUBSCRIPTIONS_FOR_TOPICS_QUERY, which returns (Subscription)-[SUBSCRIPTION_OF]->(Topic) WHERE topic.id IN $ids with the sub's id, name, protocol and namespace plus evidence_ids.
    • Add _RECEIVES_FOR_SUBSCRIPTIONS_QUERY, for (Service)-[RECEIVES_FROM]->(Subscription).
    • read_service_dependency_rows gains the row keys publishes, subscriptions and subscription_receives. All their evidence ids are added to the _referenced_evidence_ids union.
    • Fix the stale "7 canonical relation kinds" comment at about line 486.
  • app/architecture_intelligence/dependency_projection.py:
    • compute_claim_id(..., subscription_id: str | None = None) adds the key only when it is not None. canonical_json_bytes does not drop None, so this keeps HTTP and Queue ids byte-identical.
    • _build_claim passes delivery.subscription.id when it is present.
    • Add _topic_ref and _subscription_ref, which follow the pattern of _queue_ref.
    • Add _resolve_pubsub_destinations(publish, subscriptions, receivers_by_sub, evidence_by_id), which reuses _group_evidenced_rows. Per §12.3:
      • no usable Subscription gives one claim with object=Topic, via=Topic, subscription=None, DIRECT_TARGET_FALLBACK;
      • for each usable Subscription, sorted by id:
        • if it has no evidenced consumer, object=Subscription, via=Topic, subscription=Sub, DIRECT_TARGET_FALLBACK, and resolution refs from SUBSCRIPTION_OF;
        • otherwise, one RESOLVED_SERVICE claim per distinct consumer, where resolution refs = SUBSCRIPTION_OF ∪ that consumer's RECEIVES_FROM evidence.
    • Add a publish loop to project_service_dependencies:
      • use _qualify(..., relation_type="PUBLISHES_TO");
      • a None result gives _insufficient_evidence_limitation("PUBLISHES_TO", …);
      • each fallback gets UNRESOLVED_IDENTITY.
    • _unresolved_identity_limitation takes the fallback object ref.
      • Its noun depends on the type: queue / topic / subscription.
      • The Queue and Operation message text stays byte-identical.
    • _merge_duplicate_claims stays as it is, since it is correct once the Subscription is in the claim id.
  • No changes expected in service.py, drift_projection.py, evidence_projection.py, the REST/MCP adapters, contracts.py or schemas/. Changing any of these triggers a stop (see below).
  • evaluation/architecture_answers/reference/identities.py: claim_id(..., subscription_id=None) uses the same "omit when None" rule.
    • reference/__main__.py: add an optional --subscription-id to the claim-id subcommand.

Requirement → Task Mapping

Spec Task
§12.2 DeliveryRef Pub/Sub row; the Subscription is SUBSCRIPTION_OF via in the same snapshot The projection builds subscription only from SUBSCRIPTION_OF rows read inside the same read_extra snapshot read
§12.3 Topic fallback / Subscription fallback / one RESOLVED_SERVICE per consumer / instances collapse / two Subscriptions → two claims _resolve_pubsub_destinations + publish loop
§12.3 UNRESOLVED_IDENTITY on fallbacks, INSUFFICIENT_EVIDENCE when no safe claim can be made Reuse the existing limitation builders
§12.4 Queue and HTTP ids byte-identical; optional subscription_id; via = Topic id compute_claim_id + reference mirror
§12.5 drift = the same claims filtered; NOT_OBSERVED_IN_WINDOW under the messaging-coverage rule; no sibling confirmation; no OBSERVED_ONLY minting Kernel/coverage widening; drift is unchanged; decision 1
§11 / §12.6 evidence refs resolve via service, REST and MCP at the same snapshot Evidence ids in claim refs; _SUPPORTING_RELATIONS_QUERY already covers the new relations
§13.4 service/REST/MCP equivalence; exactly 3 tools; zero graph writes Integration equivalence tests
§14 Queue non-regression Pinned Queue/HTTP claim-id literals; all 23 answer scenarios unchanged and byte-identical

Acceptance Criteria → Test/Evidence Mapping

  • New tests/unit/test_architecture_intelligence_dependency_projection_pubsub.py:
    • each §12.3 row;
    • two Subscriptions give distinct claim ids;
    • the same consumer on two Subscriptions gives two claims;
    • multi-row instances of one Service give one claim with unioned evidence;
    • a dangling SUBSCRIPTION_OF gives the Topic fallback;
    • a consumer with dangling evidence gives the Subscription fallback;
    • declared-only PUBLISHES_TO gives NOT_OBSERVED_IN_WINDOW, with coverage following messaging_observed;
    • no PUBLISHES_TO evidence gives INSUFFICIENT_EVIDENCE and no claim;
    • consumer evidence on Subscription A is absent from B's refs, and B's qualification is not changed by it;
    • shuffled row order gives an identical result;
    • every claim validates against the committed architecture-answer.schema.json.
  • Claim ids:
    • the existing Queue/HTTP pinned-id tests stay unchanged;
    • a new pinned Pub/Sub id;
    • a Topic-fallback claim has no subscription_id key.
  • Reference sync (tests/unit/test_architecture_answers_reference.py): the real and reference ids agree for a Queue case, a Topic case and a Subscription case, and the CLI flag works.
  • Kernel (tests/unit/test_qualification_declared_observed.py):
    • PUBLISHES_TO maps to the messaging signal;
    • the unknown-type raise still holds.
  • Existing unit fakes:
    • The fake rows in tests/unit/test_architecture_intelligence_service.py and tests/unit/test_architecture_intelligence_dependency_projection.py gain the three empty keys.
    • Use .get(..., []) in the projection only if it is really needed. Prefer explicit keys, matching the repository's contract.
  • New tests/integration/test_pubsub_architecture_intelligence.py, which reuses _pubsub_scene, _import and TOPIC_ID from test_pubsub_persistence.py and the span helpers from test_pubsub_runtime.py:
    • service:orders dependencies give two RESOLVED_SERVICE claims, one each for billing and shipping, with the correct subscription routes;
    • a Topic with no Subscriptions gives the Topic fallback;
    • a Subscription without a consumer gives the Subscription fallback;
    • after runtime publish and receive spans: CONFIRMED, the observed evidence sits in the correct route's refs, and coverage is SUFFICIENT for a service whose only telemetry is Pub/Sub;
    • declared-only drift gives NOT_OBSERVED_IN_WINDOW, and drift claims are exactly equal to the dependency claims they came from;
    • an unmatched consumer span creates no claim;
    • every PUBLISHES_TO, SUBSCRIPTION_OF and RECEIVES_FROM evidence ref resolves through the service at the claim's snapshot id;
    • REST and MCP answers equal the service answer for dependencies, drift and evidence (reusing the patterns in test_api_architecture_intelligence_equivalence.py and test_mcp_*_equivalence.py);
    • graph write counters are zero after the reads.
  • Queue regression: tests/integration/test_evaluation_architecture_answers.py, with all 23 scenarios unchanged, and test_qualification_consistency.py green.

Validation Commands

uv run ruff format . && uv run ruff check .
uv run pytest tests/unit
uv run pytest tests/integration   # test_mcp_demo_script is blocked locally by the user's demo stack ports; disclose, don't stop the stack
git diff --stat main -- schemas/ app/architecture_intelligence/contracts.py evaluation/architecture_answers/scenarios   # expect empty

Open Questions / Assumptions / Stop Conditions

  • Assumption: existing answer scenarios contain no Topics (grep confirmed), so they stay byte-identical.
  • Stop and return to the spec if any of these happens:
  • Delivery: one PR for slice 4 on branch i4-slice-4-architecture-intelligence.
    • The PR body carries this plan verbatim and the marker.
    • Afterwards: reconciliation, then update the memory entry. Never merge the PR.

Reconciliation against the plan

Completed As Planned

  • Pub/Sub graph reads (repository.py): _PUBLISHES_QUERY, _SUBSCRIPTIONS_FOR_TOPICS_QUERY and _RECEIVES_FOR_SUBSCRIPTIONS_QUERY. They run inside the same snapshot read_extra, and their evidence ids join the evidence union. The stale "7 relation kinds" comment is fixed.
  • Projection (dependency_projection.py) covers every §12.3 row:
    • Topic fallback;
    • Subscription fallback;
    • one RESOLVED_SERVICE claim per distinct consumer, each sharing its Subscription route;
    • collapsing multiple instances of one Service, with their evidence unioned;
    • two Subscriptions → two claims;
    • UNRESOLVED_IDENTITY on every fallback;
    • INSUFFICIENT_EVIDENCE when there is no safe claim.
  • Claim id: compute_claim_id adds subscription_id only when a claim has a Subscription route. The reference identities.claim_id and the CLI --subscription-id mirror it.
  • Qualification and coverage: PUBLISHES_TO is in the kernel's messaging set, and the shared telemetry_coverage counts observed PUBLISHES_TO→Topic and RECEIVES_FROM→Subscription.
  • No changes to contracts.py, schemas/, evaluation/architecture_answers/scenarios, service.py, drift_projection.py, evidence_projection.py or the REST/MCP adapters.

Deviations & Justification

  • Decision 2 is narrowed for the Subscription-fallback case. The frozen DependencyClaim invariant (contracts.py, mirrored in the committed schema) requires resolution_evidence_refs to be empty for every DIRECT_TARGET_FALLBACK claim.
    • Changing the contract was a plan stop condition, so implementation stopped and the question went back to the owner.
    • The owner chose to keep the contract. SUBSCRIPTION_OF evidence still decides whether a route is usable, but it is referenced only from RESOLVED_SERVICE claims.
    • A Subscription-fallback claim carries empty resolution refs, the same as Queue and Topic fallbacks.
  • _unresolved_identity_limitation takes the fallback claim's object rather than delivery.via. For Operation/Queue fallbacks these are identical, so that message text is unchanged.

Specification Questions Discovered

All of these were resolved by the owner during planning or implementation, and are to be folded into the spec or completion record in slice 6:

  1. §12.5 "publisher or consumer evidence confirms the route". Qualification is publisher-only, the same as Queue. Consumer evidence is attributed only to its own route's resolution_evidence_refs.
  2. Whether SUBSCRIPTION_OF counts. A route is usable only when SUBSCRIPTION_OF has accepted evidence. Its evidence goes into resolved claims' resolution refs, as limited above.
  3. §12.5 "existing messaging-coverage rule". One shared rule, widened to Pub/Sub. Side effect: O5 and REST messagingObserved become true for services whose only telemetry is Pub/Sub. O1–O4 stay Queue-only.

Deferred Work

  • Broker-semantic fixtures, new answer scenarios, the full matrix and the two byte-identical runs: slice 5.
  • Docs, completion record and ADR 0017 acceptance: slice 6.
  • Pub/Sub in O1–O4: not in I4 scope.
  • The stale evaluation/architecture_answers/reference/snapshot.py, which is still at v1: disclosed, not fixed.

Test/Qualification Evidence

  • uv run ruff format --check .: 351 files already formatted. uv run ruff check .: all checks passed.
  • uv run pytest tests/unit: 2132 passed.
  • uv run pytest tests/integration --deselect tests/integration/test_mcp_demo_script.py: 417 passed, 12 deselected.
    • test_mcp_demo_script is blocked locally because the maintainer's demo compose stack holds its ports. CI runs it.
  • Queue non-regression: all 23 evaluation/architecture_answers scenarios pass unchanged, and test_qualification_consistency.py is green.
    • New tests pin HTTP/Queue ids to the unchanged five-field payload.
    • git diff origin/main -- schemas/ app/architecture_intelligence/contracts.py evaluation/architecture_answers/scenarios examples is empty.
  • Mutation check: reverting the runtime.py coverage widening makes 2 of the 8 new integration tests fail. They are restored and passing.

Remaining Limitations

  • A Subscription with no consumer can't be produced from AsyncAPI, because a subscribe operation always declares its consumer Service. That §12.3 row is therefore covered at unit level only.
  • The claim sort key (object.id, kind, via.id, claim_id) has no Subscription component. Two routes to the same object and Topic are ordered by claim_id, which is deterministic but not grouped by route.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XvQdzYyycuPbtbvBh5H49p

…swers

Project declared PUBLISHES_TO routes into get_service_dependencies and
get_architecture_drift per spec §12.2-§12.5: Topic fallback, Subscription
fallback, and one RESOLVED_SERVICE claim per consumer, each carrying its
DeliveryRef.subscription route. The claim-id payload gains an optional
subscription_id that is omitted entirely for HTTP/Queue claims, so their ids
stay byte-identical. The reference claim_id and its CLI are mirrored.

Qualification is publisher-only (Queue parity). A route needs evidenced
SUBSCRIPTION_OF, and that evidence joins resolution_evidence_refs on resolved
claims only (frozen contract: fallback claims carry none). The shared
messaging-coverage rule now counts observed PUBLISHES_TO->Topic and
RECEIVES_FROM->Subscription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvQdzYyycuPbtbvBh5H49p
Copilot AI lite review requested due to automatic review settings September 24, 2026 06:05
@michaelegner

Copy link
Copy Markdown
Owner Author

Review of head bc98dff21c61ee5e3887ccd0a910bab15175e74a — approved; no blocking findings.

I reviewed the Slice 4 projection against v0.5.0 I4 Draft 0.3 §§12.2–12.6 and the documented owner decisions in the PR. The reads and projection preserve the Topic/Subscription route, scope consumer and SUBSCRIPTION_OF evidence to its own Subscription, retain the required fallbacks and limitations, and omit subscription_id from pre-I4 claim IDs. The shared qualification and coverage paths are widened without changing the REST/MCP adapters. The documented decision to use publisher evidence for claim qualification is implemented consistently; consumer evidence remains on its own route.

The 66 focused unit tests passed locally. All PR checks, including integration-core, demo-e2e, quality and CodeQL, are green. The PR's integration tests cover same-snapshot evidence resolution, drift/dependency consistency, service/REST/MCP equivalence and zero read-path writes.

Disposition: No changes required for approval.

Copilot AI left a comment

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.

Copilot review overview

🔵 Needs a closer look

It changes core dependency/drift/evidence behavior and Neo4j read logic across multiple subsystems, so final human review is warranted despite strong automated test coverage.

Review effort: Lite
Findings: None

What changed in this PR

Implements v0.5.0 I4 slice 4 by projecting declared Topic/Subscription topology into the public Architecture Intelligence answers (dependencies, drift, and evidence drill-down), while keeping HTTP/Queue claim-id payloads byte-identical and aligning service/REST/MCP outputs.

Changes:

  • Extend dependency projection and graph reads to include PUBLISHES_TO (Service→Topic), SUBSCRIPTION_OF (Subscription→Topic), and RECEIVES_FROM (Service→Subscription), including subscription_id in claim identity only when a Subscription route exists.
  • Widen shared messaging telemetry coverage to treat observed PUBLISHES_TO→Topic and RECEIVES_FROM→Subscription as messaging coverage signals.
  • Add unit/integration coverage for Pub/Sub projection rows, claim-id compatibility, reference CLI support, and service/REST/MCP equivalence with zero-write read assertions.
File Description
tests/​unit/​test_qualification_declared_observed.py Adds coverage-signal test ensuring PUBLISHES_TO is treated as a messaging relation for coverage.
tests/​unit/​test_architecture_intelligence_service.py Updates dependency-row test fixtures to include new Pub/Sub row keys.
tests/​unit/​test_architecture_intelligence_dependency_projection.py Updates projection test harness rows to include Pub/Sub keys.
tests/​unit/​test_architecture_intelligence_dependency_projection_pubsub.py New unit suite pinning Pub/Sub projection behaviors and claim-id compatibility rules.
tests/​unit/​test_architecture_answers_reference.py Adds reference-vs-real claim-id parity for Topic/Subscription shapes and exercises new CLI flag.
tests/​integration/​test_pubsub_architecture_intelligence.py New integration suite validating Pub/Sub dependencies/drift/evidence, equivalence across surfaces, and zero writes.
evaluation/​architecture_answers/​reference/​identities.py Mirrors production claim-id logic with optional subscription_id omitted when None.
evaluation/​architecture_answers/​reference/​__main__.py Extends reference CLI claim-id subcommand with optional --subscription-id.
app/​qualification/​declared_observed.py Adds PUBLISHES_TO to messaging relation types and updates coverage mapping docstring.
app/​architecture_intelligence/​repository.py Adds Pub/Sub Cypher reads and includes their evidence IDs in the referenced-evidence union.
app/​architecture_intelligence/​dependency_projection.py Adds Pub/Sub destination resolution, routed subscription_id claim identity, and fallback limitation noun handling.
app/​analysis/​runtime.py Widens messaging coverage queries to include observed Pub/Sub relations while keeping label/type gating explicit.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@michaelegner
michaelegner merged commit af45004 into main Sep 24, 2026
14 checks passed
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.

2 participants