Skip to main content

Track 2: Architecture Refactor — Import-by-Type + Platform-Only Findings

Context

Track 1 is complete: all 7 PRD requirements shipped, 141 connector tests + 179 platform tests passing, CI workflow running. The connector currently uses heavy ExecutionChain and Integration dataclasses that pre-link entities (BR→REST→OAuth→SP) before the transformer decomposes them back into individual NormalizedGraph nodes/edges. Detection logic lives in the connector (detectors.py) duplicating what the platform evaluator should handle.

Track 2 refactors this architecture:

  1. Import-by-type: Connector discovers entities independently, emits flat NormalizedGraph directly
  2. Platform-only findings: All detection/evaluation in platform evaluator, connector is pure discovery + classification
  3. Path materializer extension: Platform reconstructs automation→identity→resource chains via BFS

Key Insight

Automations already map to autonomous_identityidentity entity type in the platform. They already go through ingestion, diff engine, and versioning. But execution paths are empty for automations because the path materializer only follows HAS_ROLE → GRANTS → APPLIES_TO (+ one AUTHENTICATES_TO hop). Automations have RUNS_AS and EXECUTES_ON edges that the materializer doesn't traverse.

Fix the materializer first → existing evaluator rules (orphaned_ownership, dormant_authority, scope_drift, privilege_justification_gap) automatically work for automations. Then simplify the connector.

Graph Edge Direction Reference (Source of Truth)

From transformer.py — these are the actual edge directions emitted by the connector:

EdgeSourceTargetWhere
AUTHENTICATES_TOSPSN OAuth entitytransformer.py:287-292
AUTHENTICATES_VIAREST MessageOAuth entitytransformer.py:310
RUNS_ASAutomation (BR/SI/Job)SPtransformer.py:438-445
RUNS_ASFlowSP (inferred from endpoint chain)transformer.py:661-666
RUNS_ASFlowSN User (from run_as field)transformer.py:708-713
OWNED_BYSPAzure Ownertransformer.py:204-211
OWNED_BYAutomationSN Creatortransformer.py:897-902
CREATED_BYSN OAuth entitySN Creatortransformer.py:227-234
TRIGGERS_ONAutomationTable resourcetransformer.py:404-412
EXECUTES_ONAutomationREST Message / endpointtransformer.py:634-638

Critical: SP → AUTHENTICATES_TO → OAuth (NOT OAuth → AUTHENTICATES_TO → SP). The materializer follows AUTHENTICATES_TO from source identity, so it traverses: SP → OAuth. OAuth entities do NOT have outgoing AUTHENTICATES_TO edges.


Phase Map

T2-0: Bug fix — ownership_level property naming mismatch (Track 1 defect)
T2-1: Platform — Path materializer extension (unblocks everything)
T2-2: Platform — Evaluator gap analysis + new rules (REQUIRED before T2-4)
T2-3: Connector — Import-by-type refactor (replace ExecutionChain)
T2-4: Connector — Deprecate detectors.py (GATED on T2-2 rule parity)
T2-5: Docs — ADRs + interface spec updates
T2-6: QA — End-to-end validation

T2-0 is a Track 1 defect fix. T2-1 and T2-2 are platform-side. T2-3 and T2-4 are connector-side. T2-5 is docs. T2-6 is verification.

T2-0 should land immediately (it's a standalone bug fix). T2-1 must land before T2-2 (unblocks automation path evaluation). T2-2 must land before T2-4 (hard gate — cannot remove detectors until platform rules have proven parity). T2-3 can start after T2-1. T2-5 can run in parallel.


T2-0: Bug Fix — ownership_level Property Naming Mismatch

Status: Track 1 defect, fix immediately.

Problem

The connector emits OWNED_BY edge properties with camelCase ownershipLevel, but the platform reads snake_case ownership_level everywhere:

WhereProperty NameCode
Connector OWNED_BY (SP→Owner)ownershipLeveltransformer.py:209
Connector OWNED_BY (Automation→Creator)ownershipLeveltransformer.py:901
Evaluator orphaned-ownershipownership_levelorphaned-ownership.ts:27
Evidence sectionsownership_levelevidence/sections.ts:167
Evidence typesownership_levelevidence-packs/types.ts:71
MongoDB indexownership_levelmongo/schema.ts:169

Impact: The evaluator's getOwnershipLevel() always returns "primary" (default) because it can't find ownership_level — the actual value is stored under ownershipLevel. The ownership_degraded finding variant never fires.

Fix

Change the connector to emit ownership_level (snake_case) on OWNED_BY edge properties. This matches the platform's expectation and the graph-transformer passes edge properties through unchanged ({ ...edge.properties } at graph-transformer.ts:60).

Files:

  • sv0-connectors/integrations/entra-servicenow/transformer.py — 3 locations (line 209, line 599, line 901): "ownershipLevel""ownership_level"
  • Update tests that assert on the property name

T2-1: Path Materializer Extension

Goal: Automations get execution_paths computed via their RUNS_AS → identity chain.

File: sv0-platform/src/ingestion/path-materializer.ts

Current behavior (lines 98-188)

computePathsForIdentity() does:

  1. Get HAS_ROLE → fetch roles → get GRANTS → fetch permissions → get APPLIES_TO → fetch resources → emit ExecutionPath
  2. Follow AUTHENTICATES_TO edges when depth < MAX_AUTH_CHAIN_DEPTH (depth < 1, i.e. only at depth=0) → recurse at depth+1

What to add

Automations don't have HAS_ROLE. They have:

  • RUNS_AS → SP or User (the identity they execute as)
  • EXECUTES_ON → resource (REST Message, table, etc.)

The materializer should follow RUNS_AS edges to "borrow" the target identity's paths.

Critical: Depth budget design

Problem: Current MAX_AUTH_CHAIN_DEPTH = 1 means AUTHENTICATES_TO is only followed at depth=0. If RUNS_AS also increments depth, then the target identity's AUTHENTICATES_TO traversal is blocked (depth=1, and 1 < 1 is false).

Solution: RUNS_AS does NOT consume the auth depth budget. RUNS_AS is identity binding (which SP does this automation run as), not auth delegation. The target identity should get a fresh depth=0 start for its own AUTHENTICATES_TO traversal.

// New: Follow RUNS_AS edges (automation → identity it runs as)
// RUNS_AS is identity binding, not auth delegation — do NOT increment depth
const runsAsTargetIds = identity.relationships
.filter(r => r.type === "RUNS_AS")
.map(r => r.target_id);

for (const targetId of runsAsTargetIds) {
if (visited.has(targetId)) continue;
const targetIdentity = await storageAdapter.getEntity(tenantId, targetId);
if (!targetIdentity || targetIdentity.entity_type !== "identity") continue;

const targetPaths = await computePathsForIdentity(
targetIdentity, tenantId, storageAdapter,
depth, // <-- same depth, NOT depth+1 (RUNS_AS doesn't consume auth budget)
visited
);

for (const path of targetPaths) {
paths.push({
...path,
via_identity: targetIdentity._id
});
}
}

This goes inside computePathsForIdentity() BEFORE the existing AUTHENTICATES_TO block. The visited set prevents cycles. The RUNS_AS target identity then gets its own auth chain budget (can still follow AUTHENTICATES_TO at depth < 1).

Resulting traversal chains (with correct edge directions)

  1. Automation → SP (direct roles): automation → RUNS_AS → SP → HAS_ROLE → role → GRANTS → perm → APPLIES_TO → resource
  2. Automation → SP → OAuth (cross-system): automation → RUNS_AS → SP → AUTHENTICATES_TO → OAuth. OAuth typically has no HAS_ROLE so this path dead-ends. Valid for edge tracing but no execution_paths produced.
  3. Automation → User: automation → RUNS_AS → SN User → (user's own roles if any). SN Users may not have HAS_ROLE edges in this connector, so typically no paths. But the traversal is correct.

sync-ingestion verification

File: sv0-platform/src/workers/handlers/sync-ingestion.ts (line 132-134)

Currently only materializes paths for entity_type === "identity". Since automations ARE identities (mapped from autonomous_identityidentity), this already works. Verify with test.

Tests

File: sv0-platform/test/ingestion/path-materializer.test.ts

  1. automation_with_runs_as_to_sp_gets_paths — automation → RUNS_AS → SP → HAS_ROLE → role → GRANTS → perm → APPLIES_TO → resource. Automation gets SP's execution_paths. Verify via_identity is set to SP's ID.
  2. automation_without_runs_as_gets_empty_paths — automation with no RUNS_AS edge → empty execution_paths (but no error).
  3. automation_runs_as_sp_with_authenticates_to — automation → RUNS_AS → SP (depth=0) → SP follows AUTHENTICATES_TO → OAuth (depth=1). Verify SP's auth traversal is NOT blocked by RUNS_AS. (OAuth likely has no roles so paths stay the same as test 1, but the traversal must not error.)
  4. cycle_prevention — automation → RUNS_AS → identity → RUNS_AS → automation. Visited set prevents infinite loop.
  5. runs_as_preserves_auth_budget — automation → RUNS_AS → SP (depth stays 0) → SP can follow AUTHENTICATES_TO (depth < 1 = true). Explicitly assert that depth is NOT incremented by RUNS_AS.

T2-2: Evaluator Gap Analysis + New Rules

Goal: Verify existing platform rules fire correctly for automations. Add any missing rules. This phase must complete before T2-4 (detector deprecation).

Existing rules vs connector detectors mapping (corrected)

Connector DetectorPlatform RuleParity Status
CrossServiceDetector: NO_OWNERorphaned_ownershipWorks — checks OWNED_BY on any identity. Automations get OWNED_BY edges via _add_creator_ownership().
CrossServiceDetector: DISABLED_OWNERorphaned_ownershipWorks — checks target entity status (active/disabled).
CrossServiceDetector: INACTIVE_OWNERorphaned_ownershipGAP — evaluator checks entity status field, NOT sign-in dates. An owner with status: "active" but no recent sign-ins won't be caught. The connector detector may use different inactivity logic.
OwnershipDecayDetectorownership_degradedWorks after T2-0 — requires ownership_level property naming fix. Once fixed, correctly classifies primary/secondary/inherited decay.
ScopeDriftDetectorscope_driftWorks — compares HAS_ROLE over entity versions.
ExecutionChainDetector: dormant SPdormant_authorityWorks after T2-1 — needs execution_paths populated via RUNS_AS traversal.
ExecutionChainDetector: elevated accessprivilege_justification_gapWorks after T2-1 — needs execution_paths populated.
CrossServiceDetector: HIGH_PRIVILEGE_NO_OWNERorphaned_ownership + execution_paths blast radiusWorks after T2-1 — orphaned identity with populated execution_paths raises severity.
CrossServiceDetector: EXCESSIVE_DATA_ACCESSprivilege_justification_gapWorks — checks scope breadth.
ServiceNowDetector: SNOW_OAUTH_NO_OWNERorphaned_ownershipGAP — OAuth entities have CREATED_BY edges, NOT OWNED_BY. orphaned_ownership only checks OWNED_BY (line 48). SN OAuth entities without OWNED_BY will always be flagged as orphans. Need to decide: (a) add OWNED_BY edges to OAuth entities in connector, or (b) make evaluator also check CREATED_BY.
Shadow automation (unmatched client_id)GAP — no platform rule. See below.

Gap 1: Unresolved cross-system auth (REQUIRED for T2-4)

The connector's detectors check if an OAuth entity's client_id has no matching Azure SP (shadow automation). The platform evaluator doesn't currently detect this.

New rule: unresolved_cross_system_auth (REQUIRED — hard prerequisite for T2-4)

  • Trigger: identity has identity_binding_status: "unlinked" AND identitySubtype is a cross-system auth entity type (oauth_app or service_principal). Does NOT fire on automation subtypes (business_rule, system_execution, flow_designer_flow, scheduled_job) where "unlinked" means no deterministic execution-log join, not shadow auth.
  • Connector change: OAuth entity nodes now emit identity_binding_status: "bound" (integration path, matched to SP) or "unlinked" (execution chain path, no matching SP).
  • Severity: medium
  • Rationale: Without this rule, shadow automations (OAuth entities with unmatched client_id) will go undetected after detector removal

File: sv0-platform/src/evaluator/rules/unresolved-auth.ts (new) File: sv0-platform/src/evaluator/rules/index.ts (register rule)

Gap 2: INACTIVE_OWNER sign-in-based detection

Decision: Accepted gap — NOT in scope for Track 2.

The platform's isOwnerNonActive() only checks entity status field (disabled, suspended, etc.). An owner with status "active" but no sign-in for 6+ months would be missed. Document this as a known limitation. Can be addressed in a future Track if needed.

Gap 3: SN OAuth entity OWNED_BY coverage

OAuth entities in the NormalizedGraph get CREATED_BY edges (transformer.py:227-234) but NOT OWNED_BY edges. The orphaned_ownership rule only checks OWNED_BY (orphaned-ownership.ts:48).

Decision: Extend the evaluator to also check CREATED_BY edges.

Modify orphaned-ownership.ts to treat CREATED_BY as a fallback ownership relationship. If an identity has no OWNED_BY edges but has CREATED_BY edges, use the CREATED_BY targets as owners. This is the more general solution — applies to any entity type that uses CREATED_BY for ownership.

File: sv0-platform/src/evaluator/rules/orphaned-ownership.ts — add CREATED_BY fallback after OWNED_BY check (line 48)

Tests

Add test cases to sv0-platform/test/evaluator/ that:

  1. Automation identity with OWNED_BY edges (all disabled) → verify orphaned_ownership fires
  2. Automation with execution_paths (from T2-1) but no execution evidence → verify dormant_authority fires
  3. Automation with HAS_ROLE growth over versions → verify scope_drift fires
  4. Automation with identity_binding_status: "unlinked" → verify unresolved_cross_system_auth fires
  5. Automation with all primary owners non-active but active secondary → verify ownership_degraded fires (validates T2-0 fix)

T2-4 Gate Criteria

T2-4 (detector deprecation) MUST NOT proceed until:

  • unresolved_cross_system_auth rule is implemented and tested (with subtype guard against BR/SI false positives)
  • orphaned_ownership fires correctly for automations (validated with test 1, 5)
  • orphaned_ownership CREATED_BY fallback works for entities without OWNED_BY edges (Gap 3 — validated with CREATED_BY tests)
  • dormant_authority fires for automations with empty evidence (validated with test 2)
  • OAuth entity nodes emit identity_binding_status property (both integration and execution chain paths)
  • Property naming fix (T2-0) is landed and verified

T2-3: Connector Import-by-Type Refactor

Goal: Replace ExecutionChain + Integration dataclasses with flat entity discovery. Simplify correlator and transformer.

New data flow

Current:
SN client → raw chains dict → Correlator → ExecutionChain objects → Transformer → NormalizedGraph

Target:
SN client → entity dicts by type → EdgeResolver (client_id match) → Transformer → NormalizedGraph

Step 3a: New DiscoveredEntities container

File: sv0-connectors/integrations/entra-servicenow/correlator.py

Replace Integration and ExecutionChain with a simple container:

@dataclass
class DiscoveredEntities:
"""Flat collection of discovered entities by type — no pre-linked chains."""
business_rules: list[dict] # From SN: sys_id, name, table, script, etc.
script_includes: list[dict] # From SN: sys_id, name, api_name, script
scheduled_jobs: list[dict] # From SN: sys_id, name, run_as, script
flows: list[dict] # From SN: sys_id, name, triggers, actions
rest_messages: list[dict] # From SN: sys_id, name, endpoint, http_methods
oauth_entities: list[dict] # From SN: sys_id, name, client_id
azure_sps: list[dict] # From Azure: id, app_id, owners, roles, sign-ins
azure_users: list[dict] # From Azure: owner details
sn_users: list[dict] # From SN: creator details
execution_data: dict[str, dict] # Flow/job execution evidence

# Resolved edges (from EdgeResolver) — dicts, not tuples, to preserve provenance
auth_edges: list[dict] # {source_id, target_id, matching_field, matching_value, ...}
caller_edges: list[dict] # {source_id, target_id, evidence_refs, ...}

Important: Resolved edges are dicts (NOT tuples) to preserve provenance fields needed for evidence packs. The current AUTHENTICATES_TO edges carry evidenceReferences with matching field/value, issuing system/tenant, etc. (transformer.py:294-301). Losing these would break explainability.

Step 3b: EdgeResolver (replaces IntegrationCorrelator)

File: sv0-connectors/integrations/entra-servicenow/correlator.py

The correlator simplifies to:

@dataclass
class ResolvedEdge:
"""A resolved cross-entity edge with provenance."""
source_id: str # source entity sys_id / id
target_id: str # target entity sys_id / id
edge_type: str # e.g. AUTHENTICATES_TO, EXECUTES_ON
properties: dict # evidence references, matching fields, etc.

class EdgeResolver:
"""Resolve cross-entity relationships from discovery data."""

def resolve_auth_edges(self, oauth_entities, azure_sps) -> list[ResolvedEdge]:
"""Match SN OAuth entities to Azure SPs by client_id."""
sp_by_client = {sp["app_id"]: sp for sp in azure_sps}
edges = []
for oauth in oauth_entities:
matched_sp = sp_by_client.get(oauth.get("client_id"))
if matched_sp:
edges.append(ResolvedEdge(
source_id=matched_sp["id"], # SP is SOURCE (SP → OAuth)
target_id=oauth["sys_id"], # OAuth is TARGET
edge_type="AUTHENTICATES_TO",
properties={
"evidenceReferences": {
"matchingField": "client_id",
"matchingValue": oauth["client_id"],
"issuingSystemId": matched_sp.get("app_id", ""),
"targetSystemId": oauth.get("client_id", ""),
}
}
))
return edges

def resolve_caller_edges(self, automations, rest_messages) -> list[ResolvedEdge]:
"""Match automations to REST messages they call (from script references)."""
# Keep existing find_callers_of_rest_message logic but return ResolvedEdge
...

Note: AUTHENTICATES_TO edge direction is SP → OAuth (source=SP, target=OAuth), matching the current transformer behavior.

Step 3c: Simplified transformer

File: sv0-connectors/integrations/entra-servicenow/transformer.py

The transformer's transform() method changes signature:

# Current:
def transform(self, integrations, execution_chains, flows=None, execution_data=None) -> dict:

# New:
def transform(self, entities: DiscoveredEntities) -> dict:

Internally, it loops through each entity type and emits nodes:

  • Each business_rule → autonomous_identity node (identitySubtype: business_rule)
  • Each REST message → resource node
  • Each OAuth entity → autonomous_identity node (identitySubtype: oauth_app)
  • Each Azure SP → autonomous_identity node (identitySubtype: service_principal)
  • etc.

Edges come from the EdgeResolver's pre-computed edges plus per-entity relationships (TRIGGERS_ON from BR table field, RUNS_AS from flow run_as, etc.).

The enrichment pipeline (_enrich_automation_properties()) stays mostly the same — it still applies egress, origin, ownership, risk classification to automation nodes.

Step 3d: CLI pipeline update

File: sv0-connectors/integrations/entra-servicenow/cli.py

Replace:

# Old:
integrations = correlator.match_by_client_id(azure_sps, sn_apps)
chains = correlator.correlate_execution_chains(raw_chains, azure_sps)
graph = transformer.transform(integrations, chains, flows=flows, execution_data=execution_data)

# New:
entities = DiscoveredEntities(
business_rules=sn_client.get_business_rules(),
script_includes=sn_client.get_script_includes(),
...
)
edge_resolver = EdgeResolver()
entities.auth_edges = edge_resolver.resolve_auth_edges(entities.oauth_entities, entities.azure_sps)
entities.caller_edges = edge_resolver.resolve_caller_edges(...)
graph = transformer.transform(entities)

Migration strategy

Keep the old Integration/ExecutionChain dataclasses temporarily. Add DiscoveredEntities alongside. Update transformer to accept both (overloaded or flag). Once new path works end-to-end, remove old classes.

Tests

  • Update test_scenarios.py to produce DiscoveredEntities objects instead of (integrations, chains) tuples
  • Update all tests (test_transformer.py, test_prd_requirements.py, test_cli_smoke.py) to use new interface
  • All 141 tests must still pass with new data structures
  • PRD requirement assertions remain identical (same output properties)

T2-4: Deprecate detectors.py

Goal: Remove detection logic from connector. CLI reports become summary-only (no issue detection).

HARD GATE: This phase MUST NOT start until T2-2 gate criteria are met (see T2-2 section). Specifically:

  • unresolved_cross_system_auth platform rule must be implemented, tested, and passing
  • All T2-2 evaluator tests must pass (orphaned_ownership, dormant_authority for automations)
  • T2-0 property naming fix must be landed

What changes

  1. Remove detector imports from cli.py — no more CrossServiceDetector, ExecutionChainDetector calls
  2. CLI reports — chain_markdown_report and integration_markdown_report simplify to inventory reports (no "Detected Issues" sections)
  3. detectors.py — delete entirely (no "mark deprecated" half-measure)
  4. Exit codes — cli.py currently returns exit code 2 for critical issues found. With no detectors, exit code is 0 (success) or 1 (errors). Detection happens on platform side.

What stays in connector

  • Egress classifier (property on node)
  • Origin classifier (property on node)
  • Ownership validator (property on node)
  • Risk grouper (property on node)
  • These are CLASSIFICATIONS (node properties), not FINDINGS (platform evaluator)

Detection parity verification before merge

Before removing detectors, run both old (connector) and new (platform) detection side-by-side against all 5 test scenarios. Every finding the connector detectors produce must have a matching platform finding. Document any accepted differences.

Tests

  • Remove/update tests that assert detection output
  • CLI smoke tests that test report generation need updating (no more detection sections)
  • PRD tests focused on properties (not detections) stay unchanged

T2-5: ADRs + Docs

ADR: Import-by-Type Connector Architecture

File: sv0-documentation/docs/architecture/decisions/adr-004-import-by-type-connectors.md

Document:

  • Why: ExecutionChain pre-linking is redundant (transformer decomposes it back)
  • What: Connector discovers entities independently, emits flat NormalizedGraph
  • Trade-off: Slightly more work for platform (path materializer) but much simpler connector

ADR: Platform-Only Finding Generation

File: sv0-documentation/docs/architecture/decisions/adr-005-platform-only-findings.md

Document:

  • Why: Duplicate detection (connector detectors + platform evaluator)
  • What: Connector is pure discovery + classification. All findings from platform evaluator.
  • Trade-off: CLI loses issue detection capability (acceptable — platform is the source of truth)

Update connector interface spec

File: sv0-documentation/docs/architecture/05-connectors.md

  • Update connector output contract for import-by-type
  • Clarify that connectors emit entity properties (classifications) not findings
  • Document DiscoveredEntities → NormalizedGraph flow

Update data model

File: sv0-documentation/docs/architecture/01-data-model.md

  • Document automation execution path traversal pattern
  • Clarify RUNS_AS semantics for path materialization

T2-6: End-to-End Validation

Connector tests

cd sv0-connectors/integrations/entra-servicenow && pytest
# All tests pass with new DiscoveredEntities interface
# PRD requirement assertions unchanged
# CI report generates same NormalizedGraph structure

Platform tests

cd sv0-platform && npm test && npm run test:integration && npm run typecheck
# Path materializer tests include automation traversal
# Evaluator tests include automation entity evaluation
# Graph transformer tests include Track 1 properties

E2E smoke test

  1. Run python ci_report.py → generates NormalizedGraph from test scenarios
  2. Submit graph to local platform: python cli.py --all --submit --platform-url http://localhost:3000
  3. Verify:
    • Automation entities stored with all Track 1 properties
    • Automation entities have execution_paths (via RUNS_AS → SP paths)
    • Evaluator fires orphaned_ownership for automations with no active owner
    • Evaluator fires dormant_authority for automations with paths but no evidence
    • Evidence packs generated for automation findings

Execution Order

T2-0 (ownership_level fix) ─→ (land immediately, standalone)
T2-1 (path materializer) ────→ T2-2 (evaluator + new rules, REQUIRED)
→ T2-3 (connector refactor, can start after T2-1)
T2-2 (evaluator parity) ─────→ T2-4 (deprecate detectors, HARD GATE on T2-2)
T2-3 (import-by-type) ───────→ T2-4 (also depends on T2-3)
T2-5 (docs) ─────────────────→ (parallel with all)
T2-6 (E2E) ──────────────────→ (after T2-0 through T2-4)

Decisions Made

  1. INACTIVE_OWNER sign-in-based detection: Accepted gap — NOT in scope for Track 2. Platform checks entity status only.
  2. SN OAuth entity ownership: Extend evaluator to check CREATED_BY edges as fallback (more general than adding OWNED_BY in connector).

Files Summary

Platform (sv0-platform)

FileChangePhase
src/ingestion/path-materializer.tsAdd RUNS_AS traversal (no depth increment) to computePathsForIdentityT2-1
test/ingestion/path-materializer.test.tsAdd 5 automation chain testsT2-1
src/evaluator/rules/orphaned-ownership.tsAdd CREATED_BY fallback for ownership checkT2-2
src/evaluator/rules/unresolved-auth.tsNew rule (REQUIRED)T2-2
src/evaluator/rules/index.tsRegister unresolved_cross_system_authT2-2
test/evaluator/*.test.tsAdd 5+ automation evaluation testsT2-2

Connector (sv0-connectors/integrations/entra-servicenow)

FileChangePhase
transformer.pyFix ownershipLevelownership_level (3 locations) + add identity_binding_status to OAuth entity nodesT2-0
test_*.pyUpdate property name assertionsT2-0
correlator.pyAdd DiscoveredEntities + EdgeResolver (with ResolvedEdge, not tuples)T2-3
transformer.pyAccept DiscoveredEntities; keep old transform() temporarilyT2-3
cli.pyUpdate pipeline to use new data flowT2-3
test_scenarios.pyAdd DiscoveredEntities buildersT2-3
test_transformer.pyUpdate for new interfaceT2-3
test_prd_requirements.pyUpdate builders, keep assertionsT2-3
test_cli_smoke.pyUpdate for new pipelineT2-3
detectors.pyDelete entirely (after T2-2 gate passes)T2-4

Docs (sv0-documentation)

FileChangePhase
docs/architecture/decisions/adr-004-import-by-type-connectors.mdNew ADRT2-5
docs/architecture/decisions/adr-005-platform-only-findings.mdNew ADRT2-5
docs/architecture/05-connectors.mdUpdate interface specT2-5
docs/architecture/01-data-model.mdDocument automation path traversal + RUNS_AS semanticsT2-5