| name | consistency-rules |
| description | Defines the rules used by /architect:validate-consistency to detect conflicts across project outputs |
Consistency Rules Skill
Defines the 23 rules used by /architect:validate-consistency to detect conflicts across project outputs. These rules are the "law of the land" for cross-command consistency.
Rule Catalog
The 23 rules are divided into three categories:
- State Rules (6) — Validate individual fields in
_state.json
- Output Rules (8) — Validate files generated by individual commands
- Cross-Command Rules (9) — Detect conflicts between outputs of different commands
State Rules (RULE-S-xxx)
Validate that fields in _state.json follow the canonical schema and contain sensible values.
RULE-S-001: Design colors must be valid hex
Rule: Every field under _state.json.design that ends with a color name (primary, secondary, accent, surface, text_primary, text_secondary, etc.) must be a valid hex color code in format #RRGGBB.
Check:
/^#[0-9a-fA-F]{6}$/.test(design_field)
Examples:
- ✅
#f97316 — valid
- ✅
#0EA5E9 — valid (case-insensitive)
- ❌
f97316 — missing #
- ❌
#f973 — too short
- ❌
rgb(249, 115, 22) — wrong format
Severity: Critical (breaks design token generation)
Auto-fixable: Yes (add # prefix or reject invalid)
RULE-S-002: Component ports must be unique and numeric
Rule: Every component in _state.json.components[] must have a unique port number. Ports must be integers in range 1024-65535. No two components can share the same port.
Check:
components.forEach(c => {
if (typeof c.port !== 'number') throw "PORT_NOT_NUMBER";
if (c.port < 1024 || c.port > 65535) throw "PORT_OUT_OF_RANGE";
});
if (new Set(components.map(c => c.port)).size !== components.length) {
throw "DUPLICATE_PORT";
}
Examples:
- ✅ Components: api-server (3000), web-app (3001), worker (5000) — all unique, all valid
- ❌ Two components on port 3000 — duplicate
- ❌ Component port "3000" (string) — not numeric
- ❌ Component port 999 — below 1024
Severity: Critical (port collisions block local dev)
Auto-fixable: Partial (can detect, requires user to reassign)
RULE-S-003: Entity names must match schema if schema exists
Rule: Every entity name in _state.json.entities[] must have a corresponding type definition in the data model schema. If schema exists (schema.prisma, etc.), all entities must be defined there.
Check:
const stateEntityNames = state.entities.map(e => e.name);
const schemaEntityNames = parseSchema(schema).types.map(t => t.name);
const missing = stateEntityNames.filter(n => !schemaEntityNames.includes(n));
if (missing.length > 0) throw "ENTITY_NOT_IN_SCHEMA: " + missing;
Examples:
- ✅ State has User, Product; schema has User, Product — match
- ❌ State has User, Product, Order; schema has User, Product — Order missing from schema
- ✅ No schema exists — skip this rule
Severity: Warning (incomplete schema, future code gen problems)
Auto-fixable: No (requires investigation: add entity to schema or remove from state)
RULE-S-004: Tech stack versions must be valid semver
Rule: All version strings in _state.json.tech_stack (e.g., "Node.js 18", "Next.js 14", "PostgreSQL 15") must parse as valid semantic versioning (MAJOR.MINOR.PATCH) or be underspecified (MAJOR or MAJOR.MINOR).
Check:
const versionRegex = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[a-zA-Z0-9]+)?$/;
tech_stack_values.forEach(v => {
if (!versionRegex.test(v)) throw "INVALID_VERSION: " + v;
});
Examples:
- ✅ "Node.js 18" (parsed as 18.0.0)
- ✅ "Node.js 18.2" (parsed as 18.2.0)
- ✅ "Node.js 18.2.1" (exact semver)
- ❌ "Node.js v18" — 'v' prefix not allowed
- ❌ "Node.js latest" — not a version
- ❌ "Node.js 18.2.1.5" — too many parts
Severity: Warning (doesn't break things immediately, but version pinning issues later)
Auto-fixable: Partial (can normalize "v18" → "18")
RULE-S-005: Persona and decision IDs must be unique
Rule: Every ID in _state.json.personas[] and _state.json.decisions[] must be unique within its array. IDs are user-facing keys for tracking decisions and personas.
Check:
const personaIds = state.personas.map(p => p.id);
if (new Set(personaIds).size !== personaIds.length) throw "DUPLICATE_PERSONA_ID";
const decisionIds = state.decisions.map(d => d.id);
if (new Set(decisionIds).size !== decisionIds.length) throw "DUPLICATE_DECISION_ID";
Examples:
- ✅ Personas: P-001, P-002, P-003 — all unique
- ❌ Personas: P-001, P-001 — duplicate ID
- ✅ Decisions: D-001, D-002, D-003 — all unique
- ❌ Decisions: D-001, D-002, D-001 — duplicate ID
Severity: Critical (breaks persona/decision tracking)
Auto-fixable: No (requires renumbering, which affects references)
RULE-S-006: All referenced fields must exist
Rule: If one field in _state.json references another field (e.g., an entity references a field name), that reference must resolve. Common referential integrity checks:
entities[].fields[] must be strings (field names)
risk_register[].mitigations[] must reference actual mitigation strategies
personas[].pain_points[] must be defined (no null/undefined)
Check:
entities.forEach(entity => {
entity.fields.forEach(field => {
if (typeof field !== 'string' || !field) throw "INVALID_FIELD_NAME: " + entity.name;
});
});
personas.forEach(p => {
p.skills?.forEach(skill => {
if (!availableSkills.includes(skill)) throw "SKILL_NOT_FOUND: " + skill;
});
});
Examples:
- ✅ Entity User has fields: [id, email, name, role] — all strings
- ❌ Entity User has fields: [id, 123, name] — 123 is not a string
- ✅ Persona references skill "procurement" and that skill is defined
- ❌ Persona references skill "unknown_skill" which doesn't exist
Severity: Warning (broken references cause issues downstream)
Auto-fixable: Partial (can remove broken references with warning)
Output Rules (RULE-O-xxx)
Validate that individual command outputs are internally consistent and match expectations.
RULE-O-001: Design tokens file must match _state.json colors
Rule: The design-system/design-tokens.json file must have the same color values as _state.json.design. These should be identical (no rounding, no color space conversion).
Check:
const state_colors = {
primary: state.design.primary,
secondary: state.design.secondary,
};
const token_colors = tokens_json.colors;
if (JSON.stringify(state_colors) !== JSON.stringify(token_colors)) {
throw "COLOR_MISMATCH";
}
Examples:
- ✅ _state.json.design.primary = "#f97316", tokens_json.primary = "#f97316" — match
- ❌ _state.json.design.primary = "#f97316", tokens_json.primary = "#0ea5e9" — mismatch
- ❌ _state.json.design.primary = "#f97316", tokens_json.primary = "#F97316" (case diff) — treated as mismatch (case-sensitive)
Severity: Critical (UI color inconsistency)
Auto-fixable: Partial (can prompt user which source is correct)
Root cause: Usually design-system command ran with old state, or state was updated after design-system ran.
RULE-O-002: Scaffold files must match component definitions
Rule: Every component defined in _state.json.components[] must have corresponding files in the scaffold directory. Component name must match folder/file naming (kebab-case).
Check:
const state_components = state.components.map(c => kebabCase(c.name));
const scaffold_dirs = fs.readdirSync('src/components').filter(d => fs.statSync(...).isDirectory());
const missing = state_components.filter(c => !scaffold_dirs.includes(c));
if (missing.length > 0) throw "COMPONENT_NOT_SCAFFOLDED: " + missing;
Examples:
- ✅ State has component "api-server", scaffold has
src/services/api-server/ directory
- ❌ State has component "AuthService", scaffold has no
auth-service/ directory (missing)
- ❌ State has component "web-app", but scaffold has
web_app/ (wrong naming)
Severity: Warning (incomplete scaffold, inconsistent naming)
Auto-fixable: Partial (can detect, requires investigation)
RULE-O-003: Cost estimate must be based on current component count
Rule: The cost estimate (if generated) must reference the current component count from _state.json. If state has N components but cost-estimate was run with M components (where M ≠ N), flag as stale.
Check:
const state_component_count = state.components.length;
const cost_estimate_component_count = cost_estimate.metadata.component_count;
const age_days = daysSince(cost_estimate.generated_at);
if (state_component_count !== cost_estimate_component_count || age_days > 14) {
throw "COST_ESTIMATE_STALE";
}
Examples:
- ✅ State has 8 components, cost-estimate.metadata.component_count = 8, generated 2 days ago — fresh
- ❌ State has 12 components, cost-estimate.metadata.component_count = 8 — undercounted by 33%
- ❌ cost-estimate generated 30 days ago — stale regardless of match
Severity: Warning (estimates outdated, budget planning impacts)
Auto-fixable: No (requires rerunning /architect:cost-estimate)
RULE-O-004: Test coverage percentage must be 0-100%
Rule: Any field in test outputs that reports coverage (test_suite.coverage%, test_suite.coverage_target%) must be a number between 0 and 100. No negative percentages, no >100%.
Check:
if (typeof coverage !== 'number' || coverage < 0 || coverage > 100) {
throw "INVALID_COVERAGE_PERCENT: " + coverage;
}
Examples:
- ✅ coverage = 75 — valid
- ✅ coverage = 0 — valid (no tests yet)
- ✅ coverage = 100 — valid (perfect)
- ❌ coverage = 120 — impossible
- ❌ coverage = -10 — impossible
Severity: Critical (invalid metrics break dashboards)
Auto-fixable: Yes (clamp to 0-100)
RULE-O-005: Compliance rules must reference existing entities or patterns
Rule: Each compliance control in a compliance report must reference real entities (from _state.json.entities[]) or architectural patterns that actually exist in the scaffold.
Check:
const state_entities = state.entities.map(e => e.name);
compliance.controls.forEach(control => {
if (control.applies_to_entity && !state_entities.includes(control.applies_to_entity)) {
throw "ENTITY_NOT_FOUND: " + control.applies_to_entity;
}
});
Examples:
- ✅ Compliance rule: "Data encryption for User entity" → User exists in _state.json
- ❌ Compliance rule: "Data encryption for UnknownEntity" → UnknownEntity doesn't exist
Severity: Warning (orphaned controls, incomplete scope)
Auto-fixable: No (requires investigation)
RULE-O-006: Monitoring metrics must align with tech stack
Rule: Monitoring outputs (dashboards, metrics, alerts) must reference services/frameworks that are actually in the tech stack. Can't monitor "Kafka" if tech stack doesn't include Kafka.
Check:
const tech_stack_items = state.tech_stack.backend.concat(state.tech_stack.integrations);
monitoring.metrics.forEach(metric => {
if (!tech_stack_items.some(t => metric.service.includes(t))) {
throw "SERVICE_NOT_IN_TECH_STACK: " + metric.service;
}
});
Examples:
- ✅ Tech stack has "Datadog" in integrations, monitoring references Datadog metrics — match
- ❌ Tech stack has "CloudWatch", monitoring references Datadog (wrong provider)
- ❌ Monitoring references "Kafka", but tech stack has "RabbitMQ"
Severity: Warning (monitoring won't work, missing instrumentation)
Auto-fixable: No (requires choosing a monitoring provider)
RULE-O-007: Load test scenarios must use valid endpoints
Rule: Each scenario in a load test plan must reference endpoints that actually exist in the project (from API contracts or scaffold routes).
Check:
const valid_endpoints = contracts.api.paths;
load_test.scenarios.forEach(scenario => {
scenario.requests.forEach(req => {
if (!valid_endpoints.includes(req.endpoint)) {
throw "ENDPOINT_NOT_FOUND: " + req.endpoint;
}
});
});
Examples:
- ✅ Load test scenario requests
POST /api/users and that endpoint exists in scaffold
- ❌ Load test scenario requests
GET /unknown/endpoint — endpoint doesn't exist
Severity: Warning (load test won't run correctly)
Auto-fixable: No (requires investigation)
RULE-O-008: Documentation must reference existing components
Rule: All generated documentation (API docs, guides, runbooks) must reference components, entities, or patterns that actually exist in the project.
Check:
const state_components = state.components.map(c => c.name);
docs.content.split('\n').forEach(line => {
const referenced_components = extractComponentReferences(line);
referenced_components.forEach(comp => {
if (!state_components.includes(comp)) {
throw "COMPONENT_NOT_FOUND_IN_DOCS: " + comp;
}
});
});
Examples:
- ✅ Docs reference "api-server" and that component exists in _state.json
- ❌ Docs reference "legacy-service" which no longer exists
Severity: Info (documentation outdated, confusing but not blocking)
Auto-fixable: No (requires updating docs)
Cross-Command Rules (RULE-X-xxx)
Detect conflicts between outputs of different commands. These are the most important rules for catching architectural drift.
RULE-X-001: No component should exist in both created and deleted lists
Rule: If a component was created by /architect:scaffold-component, it shouldn't appear in any "removed components" or "deprecated" list. A component can't exist and not exist.
Check:
const created = activity.filter(a => a.phase === 'scaffold-component').map(a => a.component);
const removed = state.deprecated_components || [];
const both = created.filter(c => removed.includes(c));
if (both.length > 0) throw "COMPONENT_IN_BOTH_CREATED_AND_REMOVED: " + both;
Examples:
- ✅ Scaffold created auth-service, remove list is empty — consistent
- ❌ Scaffold created auth-service, remove list contains auth-service — contradiction
Severity: Critical (architectural confusion)
Auto-fixable: No (requires deciding to keep or remove component)
RULE-X-002: Design personality must be consistent across scaffold and design-system
Rule: The design personality chosen in _state.json.design.personality must be used consistently across the scaffold. If state says "bold-commercial", scaffold component names and structure should reflect that personality (not "serene-health" or other personality).
Check:
const personality = state.design.personality;
const scaffold_personalities = extractPersonalitiesFromScaffold();
if (scaffold_personalities.length > 0 && !scaffold_personalities.includes(personality)) {
throw "PERSONALITY_MISMATCH: " + personality + " vs " + scaffold_personalities;
}
Examples:
- ✅ State personality = "bold-commercial", scaffold component naming is bold/strong — match
- ❌ State personality = "serene-health", scaffold has commercial/aggressive styling — mismatch
Severity: Warning (visual inconsistency, brand confusion)
Auto-fixable: No (requires aligning design direction)
RULE-X-003: Entity count must increase or stay same, never decrease
Rule: As the project evolves, the entity count in _state.json.entities[] should only increase or stay the same. It should never decrease (would indicate entities were deleted, which is a major architecture change that should be audited).
Check:
const current_count = state.entities.length;
const previous_count = previousState.entities.length;
if (current_count < previous_count) {
throw "ENTITY_COUNT_DECREASED: " + previous_count + " → " + current_count;
}
Examples:
- ✅ Entity count: 5 → 8 (grew, normal)
- ✅ Entity count: 8 → 8 (same, normal)
- ❌ Entity count: 8 → 5 (decreased, needs audit)
Severity: Critical (indicates major refactor that should be tracked)
Auto-fixable: No (requires investigation)
RULE-X-004: Blueprint architecture must match scaffold service layout
Rule: The services/components described in the blueprint must match the actual scaffolded components. If blueprint says "api-server, web-app, worker", scaffold must have those 3 components (not 2 or 4).
Check:
const blueprint_services = blueprint.services.map(s => s.name);
const scaffold_services = state.components.map(c => c.name);
if (JSON.stringify(blueprint_services.sort()) !== JSON.stringify(scaffold_services.sort())) {
throw "BLUEPRINT_SCAFFOLD_MISMATCH";
}
Examples:
- ✅ Blueprint describes api-server, web-app, worker; scaffold has all 3
- ❌ Blueprint describes api-server, web-app, worker; scaffold only has api-server and web-app (missing worker)
Severity: Warning (architectural mismatch, confusing for developers)
Auto-fixable: No (requires deciding which is correct)
RULE-X-005: Tech stack languages must match codebase
Rule: Languages listed in _state.json.tech_stack.backend and .frontend must match the actual programming languages found in the scaffolded source code.
Check:
const tech_languages = extractLanguages(state.tech_stack);
const codebase_languages = detectLanguagesInScaffold();
const missing_in_code = tech_languages.filter(l => !codebase_languages.includes(l));
const extra_in_code = codebase_languages.filter(l => !tech_languages.includes(l));
if (missing_in_code.length > 0 || extra_in_code.length > 0) {
throw "LANGUAGE_MISMATCH: " + missing_in_code + " missing, " + extra_in_code + " extra";
}
Examples:
- ✅ Tech stack says "Node.js", codebase has .js/.ts files — match
- ✅ Tech stack says "Python + Node.js", codebase has both .py and .js/.ts — match
- ❌ Tech stack says "Python", codebase has only .js files — mismatch
- ❌ Tech stack says "Go", codebase has .java files — mismatch
Severity: Warning (tech stack and codebase are out of sync)
Auto-fixable: No (requires investigation)
RULE-X-006: Monitoring provider must be listed in tech stack integrations
Rule: The observability provider chosen in monitoring setup (Datadog, New Relic, Prometheus, etc.) must be listed in _state.json.tech_stack.integrations[].
Check:
const monitoring_provider = monitoring.provider;
const integrations = state.tech_stack.integrations;
if (!integrations.includes(monitoring_provider)) {
throw "PROVIDER_NOT_IN_INTEGRATIONS: " + monitoring_provider;
}
Examples:
- ✅ Monitoring setup uses Datadog, tech_stack.integrations includes "Datadog" — match
- ❌ Monitoring setup uses Datadog, tech_stack.integrations doesn't list Datadog — mismatch
Severity: Medium (missing from tech stack list, but monitoring will still work)
Auto-fixable: Yes (add provider to integrations)
RULE-X-007: Compliance frameworks must be supported by tech stack
Rule: Each compliance framework in the compliance plan (GDPR, SOC 2, HIPAA, etc.) must be achievable with the chosen tech stack. Some tech stacks can't support certain compliance frameworks (e.g., free tier services can't do HIPAA).
Check:
const compliance_frameworks = compliance.frameworks;
const tech_stack = state.tech_stack;
compliance_frameworks.forEach(fw => {
const supported = isSupportedBy(fw, tech_stack);
if (!supported) throw "FRAMEWORK_NOT_SUPPORTED: " + fw + " by " + tech_stack;
});
Examples:
- ✅ Framework = SOC 2, tech_stack has enterprise services (AWS, Datadog) — achievable
- ❌ Framework = HIPAA, tech_stack uses free-tier only services — not achievable
- ❌ Framework = GDPR + data residency in EU, tech_stack only has US deployments — not achievable
Severity: Critical (compliance impossible with current tech stack)
Auto-fixable: No (requires changing tech stack or compliance requirements)
RULE-X-008: Load test target RPS must be realistic for tech stack
Rule: The target requests-per-second (RPS) in load testing must be achievable with the chosen tech stack. Don't target 10k RPS if using a single-threaded framework.
Check:
const target_rps = load_test.target_rps;
const tech_stack = state.tech_stack;
const achievable_rps = estimateRpsFor(tech_stack);
if (target_rps > achievable_rps * 1.5) {
throw "TARGET_RPS_UNREALISTIC: " + target_rps + " vs achievable " + achievable_rps;
}
Examples:
- ✅ Target RPS = 1000, tech_stack is Node.js + Postgres on standard infra → achievable
- ❌ Target RPS = 100k, tech_stack is single-threaded Python → unrealistic
Severity: Warning (load test goals are unachievable)
Auto-fixable: No (requires adjusting tech stack or RPS targets)
RULE-X-009: All referenced external services must exist in tech stack
Rule: Any external service referenced in blueprints, architecture diagrams, or data flow (Stripe, Auth0, SendGrid, etc.) must be listed in _state.json.tech_stack.integrations[].
Check:
const referenced_services = extractExternalServices(blueprint, architecture_diagram, dataflow);
const integrations = state.tech_stack.integrations;
const missing = referenced_services.filter(s => !integrations.includes(s));
if (missing.length > 0) throw "SERVICE_NOT_IN_INTEGRATIONS: " + missing;
Examples:
- ✅ Blueprint mentions Stripe for payments, tech_stack.integrations includes "Stripe" — match
- ❌ Blueprint mentions Auth0, tech_stack.integrations is empty or doesn't list Auth0 — missing
Severity: Medium (integration incomplete, but not blocking)
Auto-fixable: Yes (add to integrations)
Using These Rules
In /architect:validate-consistency
for each rule in [RULE-S-001 through RULE-X-009]:
result = apply_rule(rule, state, outputs)
if result === FAIL:
conflict = {
rule_id: rule.id,
severity: rule.severity,
description: rule.description,
remediation: rule.auto_fixable ? "can fix" : "manual fix needed"
}
add conflict to report
In error messages
When a user runs a command and hits a rule violation, show them:
- Which rule failed (RULE-X-009)
- What it means in plain English
- How to fix it
- Why it matters
Example:
❌ RULE-X-009 violation: "Stripe" is referenced in blueprint but not listed in tech stack integrations.
This means: Your blueprint includes Stripe for payments, but I can't find "Stripe" in the integrations list.
Fix: Add "Stripe" to _state.json.tech_stack.integrations
Why: Keeping integrations in sync helps with cost estimation, security scanning, and deployment configuration.
Rule Maintenance
When to add a new rule
Add a new rule when you find:
- A conflict that happened to 3+ projects (not a one-off)
- A conflict that's expensive to fix (worth preventing)
- A conflict that's systematic (not human error)
Process:
- Create rule with clear name, check logic, examples
- Add to appropriate category (State/Output/Cross-Command)
- Test on 2-3 projects to validate
- Document severity and auto-fix capability
- Add to consistency-rules/SKILL.md
When to deprecate a rule
Deprecate (don't delete) a rule when:
- It becomes irrelevant (technology changed)
- A different rule subsumes it
- It's too noisy (more false positives than real issues)
Process:
- Mark as
DEPRECATED in rule definition
- Continue to report violations but with low severity
- Document why deprecated and what replaces it
- Keep rule in doc for historical reference
Related Commands
/architect:validate-consistency — applies all these rules
/architect:check-state — validates state schema (overlaps with State rules)
/architect:next-steps — considers consistency when recommending commands
/architect:production-readiness — blocks launch if critical rules fail