Blocking validator for the six-artifact arch output contract (refined quality-profile, systems-inventory, logical-architecture, physical-architecture, tech-stack, technical-risks) plus their decision manifests. Enforces 22 checks derived from arch intent.yaml constraints and the artifact-verifiable failure conditions (F1-F12, F14-F16, F20-F23).
Installs into .claude/skills of the current project.
Are you the author of Validate Architecture Spec?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kapilvirenahuja-validate-architecture-spec)
---
name: validate-architecture-spec
description: Blocking validator for the six-artifact arch output contract (refined quality-profile, systems-inventory, logical-architecture, physical-architecture, tech-stack, technical-risks) plus their decision manifests. Enforces 22 checks derived from arch intent.yaml constraints and the artifact-verifiable failure conditions (F1-F12, F14-F16, F20-F23).
user-invocable: false
model: haiku
allowed-tools: Read, Write, Glob, Grep
deprecated: true
deprecated_note: '#434 ProductOS realignment — superseded by the command model; retained for Phase E reference, not installed'
---
# validate-architecture-spec
Called by `tech-architect` after every /arch derive skill completes. Blocking validator — any blocker violation causes `status: failed` and the calling play cycles back to the relevant stage.
This validator was rewritten for the model change in #403. The old shape enforced F1-F19 against five artifacts (logical, physical, nfr-spec, quality-vision, design-patterns). The new shape enforces against six artifacts (refined quality-profile, systems-inventory, logical, physical, tech-stack, technical-risks). The orchestrator-level failure conditions (F17 missing checkpoint, F18 whitelist bypass, F19 code emission, F24 hard-halt-on-missing-upstream) are NOT enforced here — they are play-orchestrator behaviors not artifact properties.
## Purpose
Enforce the /arch intent.yaml contract against the actual six-artifact output. Checks structural completeness, technology discipline, inventory grounding, layer model integrity, cycle absence, capability coverage, NFR delivery, pattern citation, risk shape, stage order, and decision-surfacing discipline across every artifact and every decision manifest.
## Input
Receive from the `tech-architect` agent via JSON contract. All paths resolve against `{product_base}` unless stated otherwise.
- `refined_qp_path` (path, required) — `{product_base}architecture/quality-profile.yaml`
- `inventory_dir` (path, required) — `{product_base}architecture/systems-inventory/`
- `logical_path` (path, required) — `{product_base}architecture/logical-architecture.yaml`
- `physical_path` (path, required) — `{product_base}architecture/physical-architecture.yaml`
- `tech_stack_path` (path, required) — `{product_base}architecture/tech-stack.yaml`
- `risks_path` (path, required) — `{product_base}architecture/technical-risks.yaml`
- `manifest_inventory_path` (path, required) — `{product_base}architecture/decision-manifest-derive-systems-inventory.yaml`
- `manifest_refine_qp_path` (path, required) — `{product_base}architecture/decision-manifest-refine-quality-profile.yaml`
- `manifest_logical_path` (path, required) — `{product_base}architecture/decision-manifest-derive-logical-architecture.yaml`
- `manifest_physical_path` (path, required) — `{product_base}architecture/decision-manifest-derive-physical-architecture.yaml`
- `manifest_tech_stack_path` (path, required) — `{product_base}architecture/decision-manifest-derive-tech-stack.yaml`
- `manifest_risks_path` (path, required) — `{product_base}architecture/decision-manifest-derive-technical-risks.yaml`
- `specify_qp_path` (path, required) — `{product_base}specification/quality-profile.yaml`. For delta_log integrity checks (V12).
- `scope_path` (path, required) — `{product_base}scope/scope.yaml`. For capability coverage (V4).
- `epics_dir` (path, required) — `{product_base}scope/epics/`.
- `project_profile_path` (path, required) — `{product_base}user-provided/project-profile.yaml`. For pin checks (V13).
- `kb_systems_dir` (path, required) — `{ltm_base}components/memory/knowledge/arch/systems/`. For KB-origin byte-for-byte check (V22).
- `kb_patterns_dir` (path, required) — `{ltm_base}components/memory/knowledge/arch/patterns/`. For pattern citation KB extension (V9).
- `output_path` (string, required) — `{product_base}architecture/validation-result.yaml`.
## Process
### Step 1 — Load all inputs
Parse each artifact and manifest. On any file missing or empty, record a V1 violation immediately but continue loading so the full violation set surfaces in one pass.
Load supporting context:
- `scope.selected_capabilities` — capability ID list.
- Parsed refined QP characteristics with relevance, target, delta_log.
- Parsed specify QP for delta_log diff (V12).
- All epic YAML files for failure_conditions and constraints.
- `project_profile.grounded_tools` map.
- Inventory map: glob `inventory_dir/*.md`, parse frontmatter for id, origin, capabilities_served, sub_systems[].
- KB systems catalog and KB patterns catalog: glob for indexing.
Collect all six manifests into a unified `all_decisions` list for cross-manifest checks (V14, V15, V16).
### Step 2 — V1: Artifact existence and non-emptiness (cites F1)
For each of the six canonical artifact paths:
1. Verify file exists (or, for inventory_dir, verify the directory has ≥ 1 .md file).
2. Verify primary section is non-empty:
- `refined_qp_path` → `characteristics` map with ≥ 1 entry with `relevance != not_applicable`.
- `inventory_dir` → ≥ 1 file with frontmatter `capabilities_served[]` non-empty.
- `logical_path` → `components[]` length ≥ 1 AND `layer_model.layers[]` length ≥ 1.
- `physical_path` → `components[]` length ≥ 1.
- `tech_stack_path` → `entries[]` length ≥ 1.
- `risks_path` → `risks[]` length ≥ 1.
Any missing or empty primary section is a **blocker**.
### Step 3 — V2: Logical purity scan (cites F2)
Scan every string in `logical_path` (component name, responsibilities, edge rationale, layer role text, capability_id text, system_ref display) against the tech-token deny-list (case-insensitive):
- Programming languages: JavaScript, TypeScript, Python, Go/Golang, Java, Kotlin, Ruby, PHP, C#, Rust, Swift, Dart, Elixir, Scala
- Runtimes: Node.js, Deno, Bun, JVM, CLR, V8, Wasm
- Frameworks/libraries: React, Vue, Angular, Svelte, Next.js, Nuxt, Express, Fastify, NestJS, FastAPI, Django, Flask, Spring, Rails, Laravel, .NET, ASP.NET
- Databases: PostgreSQL, MySQL, SQLite, MongoDB, DynamoDB, Cassandra, Redis, Memcached, Elasticsearch, Solr, Neo4j, Pinecone, Weaviate, MariaDB, Oracle
- Cloud products: AWS, Azure, GCP, Lambda, EC2, S3, RDS, ECS, EKS, GKE, AKS, Cloud Run, Cloud Functions, App Service, Cosmos DB, BigQuery, Firebase, Vercel, Netlify
- Messaging: Kafka, RabbitMQ, NATS, SQS, SNS, Pub/Sub, EventBridge, Kinesis
- Protocols: HTTPS, gRPC, GraphQL, REST, WebSocket, AMQP, MQTT, SOAP
Hit → V2 blocker with offending value and path.
### Step 4 — V3: Logical component shape (cites F3)
For every entry in `logical.components[]`:
- `system_ref` non-empty AND resolves to a file id in `inventory_dir`.
- `sub_system_ref` (when non-null) resolves to a `sub_systems[].id` inside that inventory file's frontmatter.
- `layer` non-empty AND matches a layer id in `logical.layer_model.layers[]`.
- `capability_ids[]` length ≥ 1.
Any missing/unresolved field → V3 blocker.
### Step 5 — V4: Capability coverage and end-to-end traceability (cites F4)
For every capability id in `scope.selected_capabilities`:
- At least one logical component has the capability id in `capability_ids[]`.
- At least one entry-layer component (component whose layer's `is_entry == true`) lists the capability.
- Graph traversal from each such entry-layer component along `outbound_edges[]` (ignoring `sync_mode: async` for graph purposes) reaches a component whose `system_ref` (or `sub_system_ref`) declares the capability in inventory's `capabilities_served[]`.
Orphans → V4 blocker per orphaned capability.
### Step 6 — V5: Cycle detection (cites F5)
Build directed graph from `logical.components.outbound_edges[]`, counting edges with `sync_mode: sync` and `sync_mode: hybrid`; excluding `sync_mode: async`. Run DFS, detect back-edges.
Build the same for `physical.components.comms[]`, with the same sync/hybrid-only rule.
Any cycle (logical or physical) → V5 blocker with cycle path.
### Step 7 — V6: Physical component shape (cites F6)
For every entry in `physical.components[]`:
- `logical_ref` non-empty AND matches a logical component id.
- `system_ref` non-empty AND equals the linked logical component's `system_ref` exactly.
- `layer` non-empty AND equals the linked logical component's `layer`.
- `deployment_target.kind` non-empty.
- `deployment_target.name` non-empty AND does NOT match the category-term deny-list (terms: "a database", "a relational database", "a queue", "a message queue", "an observability stack", "a frontend framework", "a CI system", "a search engine", "a cache", "a load balancer" — case-insensitive).
- `resources` block present.
Any failure → V6 blocker.
### Step 8 — V7: Mapping cardinality and layer match (cites F7)
For every `physical.components[]` entry:
- `logical_ref_cardinality` ∈ {one-to-one, many-to-one, one-to-many}.
- When `logical_ref_cardinality != one-to-one`, `cardinality_rationale` is non-empty.
- `layer` equals the linked logical component's `layer` (also checked in V6; repeated here for clarity in F7 mapping).
Any failure → V7 blocker.
### Step 9 — V8: NFR delivery coverage (cites F8)
For every characteristic in `refined_qp.characteristics` with `relevance != not_applicable`:
- At least one physical component's `nfr_delivery[]` has an entry with `nfr_characteristic` matching this characteristic, `target_reference` non-empty, `mechanism` non-empty, `rationale` non-empty.
Unmapped characteristic → V8 blocker per characteristic.
### Step 10 — V9: Pattern citation and system-level placement (cites F9)
For every `tech_stack.entries[]` with `category: pattern`:
- `pattern_citation.source` non-empty.
- `pattern_citation.reference` non-empty.
- Source matches the canonical allowlist (Gang of Four, PoEAA, DDD/Evans, Release It!/Nygard, Enterprise Integration Patterns/Hohpe-Woolf, Building Microservices/Newman, DDIA/Kleppmann, Cloud Native Patterns/Davis, microservices.io, IETF RFCs, OWASP, NIST, W3C, ISO/IEC) OR a KB pattern file exists at `{kb_patterns_dir}{pattern-id-or-name}.md` claiming the pattern.
- Source is NOT on the fabricated deny-list ("internal", "team standard", "{product_name}-pattern", "{team_name}-pattern", empty).
Verify every system-level decision (monolith, microservice, modular monolith, serverless — match by `name` against this set, case-insensitive) appears in tech-stack with `category: pattern`. If any of these names appears in a different artifact's primary content, that's a misplacement violation.
Any failure → V9 blocker.
### Step 11 — V10: Tech-stack entry shape (cites F10)
For every `tech_stack.entries[]`:
- `id` non-empty.
- `scope.kind` ∈ {components, components_in_layer, components_with_system_ref, global}.
- When `scope.kind != global`, `scope.targets[]` non-empty.
- `category` ∈ {language, runtime, framework, library, tool, pattern}.
- `name` non-empty.
- `source_type` non-empty.
- `rationale` non-empty.
Any failure → V10 blocker.
### Step 12 — V11: Technical risks shape and stage order (cites F11)
For every `risks.risks[]` entry:
- `id` non-empty.
- `risk_statement` non-empty.
- `trigger_conditions[]` length ≥ 1.
- `business_cost.kind`, `business_cost.magnitude`, `business_cost.cost_estimate`, `business_cost.rationale` all non-empty.
- `likelihood.level` ∈ {low, medium, high}; `likelihood.rationale` non-empty.
- `mitigation.action`, `mitigation.owner_role`, `mitigation.when`, `mitigation.delivery_cost` all non-empty.
- `residual_risk` non-empty AND NOT on the zero-residual deny-list (case-insensitive: empty, "none", "no residual", "fully mitigated", "eliminated", "n/a").
- `driver_refs[]` length ≥ 1; every entry has both `artifact` and `reference` non-empty AND `reference` resolves to a real path or in-artifact id.
- `discovered_by.scan` matches one of the eight defined scan kinds.
Stage order: confirm the five prior artifacts (`refined_qp_path`, `inventory_dir`, `logical_path`, `physical_path`, `tech_stack_path`) all exist before `risks_path` was written. (File mtime check: risks file's mtime must be ≥ each prior file's mtime.)
Any failure → V11 blocker.
### Step 13 — V12: Refined QP delta_log integrity (cites F12)
Read both `specify_qp_path` and `refined_qp_path`. Compute the diff per characteristic:
- For every characteristic where `target` or other content differs between specify and refined:
- There must be a `delta_log[]` entry in refined QP citing this characteristic, with field, before, after, direction, driver.kind, driver.reference, rationale all non-empty.
- `driver.reference` must resolve to a real artifact (inventory file, project_profile slot, epic id, regulation cite).
- For every characteristic where security target was loosened (refined target weaker than specify target) → V12 blocker (security ratchet violation).
Any failure → V12 blocker.
### Step 14 — V13: Source-type discipline across all manifests (cites F13)
Walk every entry in `all_decisions` (union of all six manifests):
- Every entry has a non-empty `grounding_source.kind`.
- No entry has `grounding_source.kind: agent_default_unilateral`.
- No entry's recommendation conflicts with a `project_profile.grounded_tools[]` pin for the same slot (cross-check: for any pin in project_profile, the corresponding decision must use the pinned value).
Any failure → V13 blocker.
### Step 15 — V14: Multi-candidate discipline (cites F14)
Walk every entry in `all_decisions`:
- When `grounding_source.kind: kb_catalog_multi_candidate_user_approved`, the citation must reference a `Q-arch-NNN` ID in `{product_base}user-provided/grounding-questions.md` OR a checkpoint ID where the user picked.
- When the citation is `Q-arch-NNN`, verify the question exists in grounding-questions.md.
Missing question or checkpoint citation → V14 blocker.
### Step 16 — V15: Decision surfacing (cites F15)
Walk every entry in `all_decisions`:
- `user_response` ∈ {accept, override, orbit, pending}.
- If `user_response: pending`, the validation result records a WARNING (not blocker) — the play orchestrator surfaces these at the next checkpoint. Validator's job is to flag them, not to block.
- If `user_response: override` or `orbit`, ensure `user_response_detail` is non-empty.
Override/orbit without detail → V15 blocker.
### Step 17 — V16: Manifest completeness (cites F16)
For each of the six manifests:
- File exists and parses cleanly.
- For each decision entry: `decision_id`, `decision_type`, `tier`, `grounding_source.kind`, `grounding_source.citation`, `recommendation`, `alternatives_considered[]` (may be empty but field present) all present.
Any missing field → V16 blocker.
### Step 18 — V20: Stage order broader check (cites F20)
File mtime ordering: refined_qp and inventory may be in either order; logical_path mtime ≥ both; physical_path ≥ logical_path; tech_stack_path ≥ physical_path; risks_path ≥ tech_stack_path.
Out-of-order → V20 blocker.
### Step 19 — V21: Layer model integrity (cites F21)
In `logical.layer_model`:
- `source` ∈ {project_profile_pin, kb_blueprint, user_authored}.
- `layers[]` length ≥ 2.
- Exactly one layer has `is_entry: true`.
- `layers[].order` values are unique positive integers.
- All layer `id` values are kebab-case (regex: `^[a-z][a-z0-9-]*$`).
In `logical.components[]`:
- Every `layer` field matches a layer id in `layer_model.layers[]`.
In `physical.components[]`:
- Every `layer` field matches the linked logical component's layer (also V6/V7 but rechecked here).
Any failure → V21 blocker.
### Step 20 — V22: Inventory provenance (cites F22)
For every file in `inventory_dir/*.md`:
- Frontmatter has `id`, `origin`, `provenance_summary`, `capabilities_served[]` non-empty.
- When `origin: kb`:
- `kb_path`, `kb_version_sha`, `copied_at`, `editable: false` all present.
- `kb_path` resolves to a real file under `kb_systems_dir`.
- Computed SHA-256 of the KB file content at `kb_path` matches `kb_version_sha`.
- The body of the inventory file (everything after the inventory frontmatter) matches the KB master byte-for-byte (after stripping the KB file's own frontmatter if any).
- When `origin: stm_research`:
- `editable: true`.
- All 7 required Markdown sections present (`When to Use`, `When to Avoid`, `Scale Profile`, `Capabilities Served`, `Sub-Systems`, `Tradeoffs`, `Anti-Patterns`) with non-empty body under each.
Any failure → V22 blocker.
### Step 21 — V23: Inventory grounding (cites F23)
For every `logical.components[].system_ref`:
- Resolves to a file id in inventory_dir.
- When `sub_system_ref` is non-null, the inventory file's frontmatter `sub_systems[]` contains a matching id.
For every `physical.components[].system_ref`:
- Equals the linked logical component's `system_ref` exactly.
- Resolves to inventory.
Unresolved → V23 blocker per offending component.
### Step 22 — Write result
Write `{output_path}`:
```yaml
status: passed | failed
violations:
- id: V1 | V2 | ... | V23
failure_bound: F1 | F2 | ...
severity: blocker | warning
artifact: {path}
detail: |
{prose description of the violation}
offending_value: {string, when applicable}
warnings:
- kind: pending_decision
decision_id: ...
detail: ...
summary:
checks_run: 22
blockers_count: <int>
warnings_count: <int>
artifacts_validated:
- {path}
```
If `blockers_count == 0` → `status: passed`. Otherwise `status: failed`.
## Output Contract
On success (passed):
```json
{
"status": "success",
"skill": "validate-architecture-spec",
"validation_status": "passed",
"result_path": "{product_base}architecture/validation-result.yaml",
"checks_run": 22,
"warnings_count": <int>
}
```
On validation failure (validator ran cleanly but artifacts have blockers):
```json
{
"status": "success",
"skill": "validate-architecture-spec",
"validation_status": "failed",
"result_path": "{product_base}architecture/validation-result.yaml",
"checks_run": 22,
"blockers_count": <int>,
"warnings_count": <int>,
"first_offending_failure_bound": "F1 | F2 | ..."
}
```
On skill failure (validator itself broke):
```json
{
"status": "failure",
"skill": "validate-architecture-spec",
"what_failed": "input_missing | input_unparseable | output_write_failed",
"details": "..."
}
```
## Evals
| Eval | Check |
|------|-------|
| V1-1 | Validation result includes every blocker found, with id, failure_bound, severity, artifact path, and detail populated. |
| V1-2 | `status: passed` is only emitted when blockers_count == 0. |
| V1-3 | When any artifact is missing OR contains zero primary entries, V1 fires. |
| V1-4 | When logical contains a tech token from the deny-list, V2 fires with the offending value. |
| V1-5 | When physical's `deployment_target.name` matches the category-term deny-list, V6 fires. |
| V1-6 | When a refined-QP characteristic with `relevance != not_applicable` has no matching `nfr_delivery[]`, V8 fires per orphaned characteristic. |
| V1-7 | When any `category: pattern` entry has no `pattern_citation` or fabricated source, V9 fires. |
| V1-8 | When any inventory file lacks provenance fields OR a KB-origin file's body diverges from the KB master, V22 fires. |
| V1-9 | When any logical or physical `system_ref` does not resolve to inventory, V23 fires. |
| V1-10 | Stage-order mtime check fires when risks_path mtime < any prior artifact's mtime (V20). |
## Constraints
- Writes ONLY to `{output_path}`. Read-only on every input including KB.
- Continues past first failure so the full violation set is reported in one pass.
- A `pending` user_response is a WARNING, never a blocker — the play orchestrator surfaces them at checkpoints.
- The validator does NOT check orchestrator-level F17 (missing checkpoint), F18 (whitelist bypass), F19 (code emission), or F24 (hard-halt-on-missing-upstream) — those are play behaviors, not artifact properties.
## Failure modes (skill-level)
- `input_missing` — a required input path is absent. (Distinct from V1 which records the violation in the result file; this is when the validator can't even start.)
- `input_unparseable` — an artifact file cannot be parsed as valid YAML.
- `output_write_failed` — the validation result cannot be written.