Stage 3 skill of /arch. Builds the layered logical architecture for a product by selecting components from the systems inventory, placing each in a layer of the product-locked layer model, wiring edges with sync/async intent, detecting cycles, and validating end-to-end traceability of every selected capability from the user-facing entry layer to a serving component. Logical names systems by their inventory ID and role — never by product, runtime, language, protocol, or schema.
Installs into .claude/skills of the current project.
Are you the author of Derive Logical Architecture?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kapilvirenahuja-derive-logical-architecture)
---
name: derive-logical-architecture
description: Stage 3 skill of /arch. Builds the layered logical architecture for a product by selecting components from the systems inventory, placing each in a layer of the product-locked layer model, wiring edges with sync/async intent, detecting cycles, and validating end-to-end traceability of every selected capability from the user-facing entry layer to a serving component. Logical names systems by their inventory ID and role — never by product, runtime, language, protocol, or schema.
version: 1.0.0
user-invocable: false
deprecated: true
deprecated_note: '#434 ProductOS realignment — superseded by the command model; retained for Phase E reference, not installed'
---
# derive-logical-architecture
> **Decision Surfacing Discipline:** This skill emits a `decision-manifest-derive-logical-architecture.yaml` alongside its primary artifact. Every inferred decision — layer model selection (when not pinned), component placement, edge sync-mode, capability-to-component assignment — is recorded with tier, grounding source, recommendation, and alternatives. The orchestrator drives the tiered surfacing flow after this skill completes.
Called by `tech-architect` during `arch` Stage 3. Produces `logical-architecture.yaml` at `{product_base}architecture/logical-architecture.yaml` plus a decision manifest.
## Purpose
Build the layered, tech-agnostic structural view of the product. Every component IS a system or sub-system from the systems inventory written by Stage 1 — components are SELECTED, not invented. Every component lives in exactly one layer of the layer model that the play locks at Stage 3 opening. Cycles between components are forbidden. Every capability in scope has at least one end-to-end path through layers from the user-facing entry layer to a component whose system_ref serves it.
This artifact is the anchor every later stage references. Physical components reference component IDs from here, tech-stack scopes pick to component IDs from here, and technical risks cite components and their edges. Logical is structure; tech and runtime detail belong elsewhere.
**Differences from the prior version of this skill (model change in #403):**
- Components are now SELECTED from inventory, not invented at logical-time. Every component has `system_ref` (and optional `sub_system_ref`) resolving to an inventory entry.
- Every component now carries a `layer` field from the locked layer model.
- The bounded-contexts primitive is removed — layers + system grounding cover the same need.
- Edges carry sync_mode (sync | async | hybrid) as design intent.
- Cycle detection is a hard validation gate.
- End-to-end traceability is a hard validation gate.
- ADRs are no longer embedded in an `adr_log` block — decision-manifest entries flagged non-obvious are the trail.
## Input
Receive from the `tech-architect` agent. All paths resolve against `{product_base}` and `{ltm_base}` supplied by the play via JSON contract.
- `inventory_dir` (path, required) — `{product_base}architecture/systems-inventory/`. The set of systems and sub-systems components MUST be selected from.
- `refined_qp_path` (path, required) — `{product_base}architecture/quality-profile.yaml`. Stage 2 output. May influence component splits when a QP target demands independent failure domains or scaling units.
- `scope_path` (path, required) — `{product_base}scope/scope.yaml`. When missing under C1 soft pre-flight, read `{product_base}specification/capabilities-stand-in.yaml` instead.
- `enriched_capabilities_path` (path, optional) — `{product_base}scope/garura:enriched-capabilities.yaml`.
- `epics_dir` (path, required) — `{product_base}scope/epics/`.
- `design_spec_path` (path, optional) — `{product_base}experience/design-spec.md`. Flow context for edge wiring.
- `flows_dir` (path, optional) — `{product_base}experience/flows/`. User flows describe cross-component journeys.
- `personas_path` (path, optional) — `{product_base}experience/personas.md`.
- `project_profile_path` (path, required) — `{product_base}user-provided/project-profile.yaml`. `layer_model` pin authoritative; `grounded_tools.components[]` pins authoritative.
- `kb_layer_models_dir` (path, required) — `{ltm_base}components/memory/knowledge/arch/layer-models/`. Blueprint layer models surfaced when no pin.
- `output_path` (string, required) — `{product_base}architecture/logical-architecture.yaml`.
- `decision_manifest_path` (string, required) — `{product_base}architecture/decision-manifest-derive-logical-architecture.yaml`.
- `grounding_questions_path` (string, required) — `{product_base}user-provided/grounding-questions.md`. Append-only for multi-candidate halts.
## Output
### Logical architecture
YAML at `{output_path}`.
```yaml
layer_model:
source: project_profile_pin | kb_blueprint | user_authored
source_citation: {profile slot | kb file path | checkpoint id}
layers:
- id: experience # kebab-case
name: {Human-readable name}
role: |
{one or two lines describing what kind of components live in this layer}
order: 1 # ascending from entry layer; integer
is_entry: true # exactly ONE layer in the model has is_entry: true
- id: process
name: ...
role: ...
order: 2
is_entry: false
- id: systems
...
components:
- id: order-orchestrator # kebab-case, unique within components[]
name: Order Orchestrator
layer: process # MUST be one of layer_model.layers[].id
system_ref: shopify-functions # MUST resolve to an inventory file id at {inventory_dir}{system_ref}.md
sub_system_ref: null # when set, MUST resolve to a sub_systems[].id inside that inventory file
responsibilities:
- {one bullet per primary responsibility}
capability_ids:
- {capability_id from scope.selected_capabilities — at least one}
inbound_edges:
- from: web-experience # another component id; the playload-bearing trace
outbound_edges:
- to: inventory-api
sync_mode: sync | async | hybrid
rationale: {one line — why this mode}
driver_epics:
- {epic id citing this component}
driver_capabilities:
- {capability id citing this component}
```
**Validation rules baked into the schema:**
- Exactly one layer in `layer_model.layers[]` has `is_entry: true`.
- `layer_model.layers[].order` values are unique positive integers.
- Every component's `layer` matches a layer id in `layer_model.layers[]`.
- Every component's `system_ref` matches a file id under `inventory_dir`. When `sub_system_ref` is non-null, it matches a `sub_systems[].id` inside that inventory file's frontmatter.
- Every component has at least one `capability_id`.
- Every inbound_edges.from references a real component id in this file.
- Every outbound_edges.to references a real component id in this file. Self-edges are forbidden.
### Decision manifest
One YAML file at `{decision_manifest_path}` with entries for:
- Layer model selection (when not pinned) — one entry, decision_type: `layer_model_selection`.
- Each component placement — one entry per component, decision_type: `component_placement`.
- Each outbound edge with sync_mode — one entry per edge, decision_type: `edge_sync_mode`.
```yaml
manifest:
skill: derive-logical-architecture
written_at: {ISO 8601 timestamp}
decisions:
- decision_id: LA-001
decision_type: layer_model_selection | component_placement | edge_sync_mode
tier: high | mid | low
grounding_source:
kind: project_profile_pin | kb_blueprint | upstream_artifact | user_direct_answer | agent_default_with_user_approval
citation: {profile slot | kb file path | upstream path | checkpoint id | grounding-question id}
recommendation: {summarized decision}
alternatives_considered:
- option: {alt}
why_rejected: {one line}
agent_reasoning_summary: |
{2-4 line prose}
non_obvious: true | false # true when alternatives existed AND the choice narrows future paths (treats this entry as an ADR)
user_response: accept | override | orbit | pending
user_response_detail: {free text — null when pending}
```
Entries with `non_obvious: true` are the architectural decision record for downstream consumers. No separate `adr_log` block is emitted.
## Process
### 1. Read inputs
- Glob `inventory_dir/*.md` → parse each frontmatter into a system catalog with id, capabilities_served, sub_systems[]. Reject if `inventory_dir` is empty (Stage 1 must run first).
- Parse `refined_qp_path` → characteristic targets that may force component splits.
- Parse `scope_path` (or stand-in) → selected_capabilities[].
- Parse `enriched_capabilities_path` if present, every file under `epics_dir`, `design_spec_path` if present, every file under `flows_dir` if present, and `personas_path` if present.
- Parse `project_profile_path` → check `layer_model` pin and `grounded_tools.components[]` pins.
- Glob `kb_layer_models_dir/*.md` → blueprint layer models for surfacing if no pin.
### 2. Validate pre-conditions
- `inventory_dir` is non-empty and at least one inventory file claims a capability in `selected_capabilities`. Otherwise → `what_failed: empty_inventory_for_scope`.
- Required inputs exist OR documented stand-in exists.
### 3. Establish the layer model
In order, take the first path that resolves:
1. **Pin path.** If `project_profile.layer_model` carries an inline layers definition or a `kb_blueprint_ref`, use it. When a blueprint ref, copy the blueprint into `layer_model.layers[]` and tag `source: project_profile_pin`.
2. **Single blueprint path.** When no pin but only one blueprint exists in `kb_layer_models_dir`, use it. Tag `source: kb_blueprint`, citation = blueprint file path.
3. **Multi blueprint path.** When multiple blueprints exist and no pin, halt this slot. Append `Q-arch-NNN` to `grounding_questions_path` listing each blueprint's name and one-line summary. Tag a decision manifest entry `tier: mid`, `grounding_source.kind: kb_catalog_multi_candidate_user_approved`, `user_response: pending`. The orchestrator surfaces at Stage 3 checkpoint.
4. **No blueprint path.** When no pin and no blueprints, halt with `what_failed: no_layer_model_source`. The user must author one or add a blueprint to KB — the skill does NOT fabricate a layer model.
Once the model is in hand, validate: exactly one `is_entry: true`, unique `order` integers, all `id` values kebab-case.
### 4. Walk capabilities and select components
For each capability in `selected_capabilities`, in order:
1. **Pin check.** If `project_profile.grounded_tools.components[]` carries an entry for this capability with a component id and system_ref, use it. Skip the search.
2. **Inventory match.** Find every inventory entry (system or sub-system) whose `capabilities_served` includes this capability id. For each match:
- Decide whether the match represents one component or several. A single system serving multiple capabilities through one cohesive surface = one component. A system whose sub_systems[] declare capability-distinct surfaces = one component per sub_system_ref (when those sub-systems serve different capabilities or carry materially different responsibilities).
- Assign each chosen component to a layer per role:
- Components whose system serves user-facing surfaces (web, mobile, channel) → entry layer.
- Components whose system is a system-of-record (ERP, CRM, identity, payment gateway) → systems-of-record layer (whichever is the bottom-most layer in the model).
- Components whose system orchestrates or composes other components → process / orchestration layer (middle of the model).
- Components whose system delivers cross-cutting concerns (auth, observability, search) → AOP or cross-cutting layer per the model.
3. Record each placement as a decision-manifest entry. Tier resolution:
- `tier: high` when grounding is a pin or a single inventory match AND the role-to-layer mapping is unambiguous per the blueprint.
- `tier: mid` when role-to-layer mapping required agent reasoning (e.g., a system could plausibly sit in two layers).
- `tier: low` when multiple inventory entries serve the same capability and the agent had to pick AND the user hasn't been asked.
### 5. Wire edges with sync_mode
For each pair (caller-component, callee-component) implied by the design flows OR by the responsibility graph (caller depends on callee to fulfill its capability):
- Add the caller's id to callee's `inbound_edges[]`.
- Add the callee's id to caller's `outbound_edges[]` with `sync_mode` and `rationale`.
- Sync_mode resolution:
- `sync` is the default for request-response within a single user-perceived transaction.
- `async` when the design flow shows fire-and-forget intent, when the callee is a write-side projection / event-sourced store, or when a QP target (independent failure domain, latency budget) demands decoupling.
- `hybrid` when the design flow has both a sync acknowledgement and an async follow-on (e.g., accept-then-process).
- Record one decision-manifest entry per edge with sync_mode. Tier resolution:
- `tier: high` when the design flow explicitly indicates the mode.
- `tier: mid` when QP-derived (e.g., reliability target forces async).
- `tier: low` when neither — agent default; user must approve.
### 6. Cycle detection
Build a directed graph from outbound_edges[]. Run DFS from each component, tracking the recursion stack. Any back-edge is a cycle.
When a cycle is detected:
- Identify the cycle's component set and the offending edges.
- Halt with `what_failed: cycle_detected` and structured details (cycle path, candidate breaks: remove edge A→B, split component X, convert edge A→B to async to break the synchronous-cycle restriction).
- Per F5 (recovery REC5 is human-handoff), the orchestrator surfaces to the user with the candidate breaks — the skill does NOT pick the break unilaterally.
Note: an async edge in a cycle is NOT a cycle for runtime purposes — physical can deliver an async edge such that the cycle is broken at runtime. The skill encodes this by treating any cycle that has at least one `sync_mode: async` edge in the path as acceptable at the logical level (it still counts as a logical edge, but the cycle alert is suppressed). Synchronous-only cycles halt.
### 7. End-to-end traceability
For each capability in `selected_capabilities`, run a graph traversal from every entry-layer component (components whose `layer.is_entry == true` and which lists the capability in `capability_ids[]`) along outbound_edges[] toward components that serve the capability. If no path reaches a component whose `system_ref` (or `sub_system_ref`) serves the capability, the capability is orphaned.
A capability with no entry-layer component listing it is ALSO orphaned (no entry into the system for that capability).
Orphans halt with `what_failed: capability_orphan` and a structured list per orphaned capability id.
### 8. Write outputs
Write `{output_path}` with `layer_model`, `components[]`, and the embedded edges. Write `{decision_manifest_path}` with one entry per decision recorded in Steps 3-5. All `user_response` start as `pending`.
### 9. Validate output
Before returning:
- Schema rules from the Output section all hold (layer model, component fields, edge references).
- No component name, responsibility, or rationale contains tech tokens. Run a deny-list check against: programming language names (JavaScript, Python, Go, etc.), product names (PostgreSQL, Redis, Kafka, etc.), protocol identifiers (REST, gRPC, MQTT, etc.), wire format identifiers (JSON, Protobuf, XML, etc.), and SDK / library names. Any hit → `what_failed: tech_token_in_logical`.
- Decision manifest is complete (one entry per layer-model selection / component placement / edge sync_mode), every entry has all required fields.
## Output Contract
On success:
```json
{
"status": "success",
"skill": "derive-logical-architecture",
"outputs": {
"logical_path": "{product_base}architecture/logical-architecture.yaml",
"decision_manifest_path": "{product_base}architecture/decision-manifest-derive-logical-architecture.yaml",
"layer_model": { "source": "...", "layer_count": <int> },
"component_count": <int>,
"edge_count": <int>,
"decisions_count": <int>,
"halted_slots": [
{ "kind": "layer_model | capability_component | edge_sync_mode", "reference": "...", "grounding_question_id": "Q-arch-NNN" }
]
}
}
```
On failure:
```json
{
"status": "failure",
"skill": "derive-logical-architecture",
"what_failed": "missing_input | empty_inventory_for_scope | no_layer_model_source | cycle_detected | capability_orphan | tech_token_in_logical | unresolved_system_ref | manifest_incomplete",
"details": "..."
}
```
## Evals
| Eval | Failure Bound | Check |
|------|---------------|-------|
| LA-1 | F3 (component missing system_ref/layer/capability_ids) | Every component has non-null `system_ref`, `layer` matching a layer in `layer_model.layers[]`, and at least one `capability_id`. |
| LA-2 | F23 (system_ref not in inventory) | Every component's `system_ref` resolves to an inventory file id; non-null `sub_system_ref` resolves to a `sub_systems[].id` inside that file. |
| LA-3 | F2 (logical contains tech tokens) | No component name, responsibility, edge rationale, or layer role text matches the tech-token deny-list. |
| LA-4 | F5 (cycle in logical) | The graph of components and `outbound_edges[]` (counting any edge with `sync_mode: sync` and `sync_mode: hybrid` for graph purposes; ignoring `sync_mode: async` edges) is acyclic. |
| LA-5 | F4 (capability without component or no E2E path) | Every selected capability has at least one entry-layer component listing it AND at least one graph path from that entry-layer component to a serving component. |
| LA-6 | F21 (layer model issues) | `layer_model.layers[]` has exactly one `is_entry: true`, unique `order` values, kebab-case ids; every component's `layer` field matches a layer id. |
| LA-7 | F16 (manifest missing/malformed) | Decision manifest exists with one entry per inferred decision (layer model selection when not pinned, component placement, edge sync_mode); every entry has decision_id, decision_type, tier, grounding_source, recommendation, alternatives_considered, non_obvious. |
| LA-8 | F13 (source_type discipline) | No decision has `grounding_source.kind: agent_default_unilateral`. |
## Constraints
- Writes ONLY to `{output_path}`, `{decision_manifest_path}`, and (for multi-candidate halts) appends to `{grounding_questions_path}`.
- Read-only on `inventory_dir`, `refined_qp_path`, every scope/epics/design input, `project_profile_path`, and `kb_layer_models_dir`.
- NEVER names a product, runtime, protocol, wire format, programming language, library, or schema column. Tech-token deny-list is enforced at Step 9.
- NEVER invents a component. Every component MUST resolve to an inventory entry. If no inventory entry serves a needed capability, halt — return to Stage 1.
- NEVER picks a cycle break unilaterally. Cycle detection halts with candidate breaks for user decision (F5 / REC5 human handoff).
- NEVER picks a layer model unilaterally. No pin + no single blueprint = halt with candidate set for user pick (F21 / REC21 human handoff).
- Respects C18/C19 surfacing tiers and C20 multi-candidate halt.
## Failure modes
- `missing_input` — required input path absent and no stand-in.
- `empty_inventory_for_scope` — `inventory_dir` is empty or no inventory entry serves any selected capability.
- `no_layer_model_source` — no pin and no blueprints in `kb_layer_models_dir`.
- `cycle_detected` — synchronous-only cycle in the component graph.
- `capability_orphan` — a selected capability has no entry-layer component OR no graph path from entry to a serving component.
- `tech_token_in_logical` — component name, responsibility, edge rationale, or layer role text leaked a tech identifier.
- `unresolved_system_ref` — a component's system_ref or sub_system_ref does not resolve to an inventory entry.
- `manifest_incomplete` — decision manifest missing or any entry lacks required fields.