-
Validate inputs. Confirm scan_index_path exists and parses as JSON. Confirm all three related proposal paths exist and parse. Confirm kb_patterns_dir exists and contains at least one *.md file. Create output parent directories if missing. Structured failures: scan_index_missing, missing_related_proposal, ltm_resolution_failed.
-
Read related proposals. Load selected_capabilities[] from scope.yaml, the per-capability business rules and invariants from enriched-capabilities.yaml, and the domain → capability → feature tree from features.yaml. The component and bounded-context universe is strictly bounded by selected capabilities — structure that exists in the codebase but does not serve a selected capability is listed under unmapped_structure, never promoted to a first-class component.
-
Consume architecture-inference.yaml if present (stepping stone). When architecture_inference_path is supplied, load the artifact and treat:
module_structure[] as seed candidates for bounded contexts and components (role + boundaries lift into responsibility statements after tech-agnostic scrubbing).
design_patterns[] as component-typing hints (e.g., observed Repository pattern at src/orders/repo → comp-order-repository component of type persistence-adapter, stated without naming the ORM).
framework_conventions[] and lld_patterns{} are NOT consumed here — they belong to physical architecture and LLD, not logical.
-
Execute LTM Resolution Protocol (R3-focused) per core/components/memory/standards/rules/resolution.md. R3 = consult {kb_patterns_dir}/*.md (modular-monolith, microservices, event-driven, cqrs-event-sourcing, serverless, frontend-component-orchestration, evolutionary-scaling). Match canonical pattern indicators against scan-index signals to pick the system-level topology claim that best fits the observed code. R1/R2/R4 remain available. Write the trace to resolution_trace_path.
-
Derive bounded contexts. Scoring inputs:
| Signal (scan-index path) | Evidence rule |
|---|
repos[] | Every independent repo in a multi-repo layout → one candidate bounded context. |
trees[].children at the first level under src/, app/, packages/ | Each top-level module → candidate bounded context within its repo. |
patterns.framework_idioms[] | Idiomatic grouping (e.g., feature-folder, domain-per-service) → validates or collapses module-level candidates. |
git.co_change_top[] | High co-change across a proposed boundary = boundary mis-drawn. When two candidate contexts appear together in top co-change pairs, merge them into one bounded context and record the merge rationale in the decision manifest. When a candidate has near-zero co-change with every other candidate, keep it as its own context. This rule MUST be applied explicitly, not skipped. |
entry_points[] | Dedicated entry points per context anchor the boundary (e.g., a separate worker entry → its own bounded context). |
Each context records: id, name, capabilities_included (IDs from scope), repo (from repos[]) when multi-repo, source_modules (from trees), co_change_validated: true|false, rationale citing the scan-index path(s).
-
Enumerate components per bounded context. Scoring inputs:
| Signal | Evidence rule |
|---|
patterns.naming_suffix_counts | Repository → component type persistence-adapter; Service → application-service; Controller / Handler → interface-layer; Gateway / Client → integration-adapter; UseCase / Interactor → application-service; Mapper / Transformer → translation. Count >= 3 in a module → named component; count 1-2 → single component with a note. |
patterns.framework_idioms[] | Idiom match (e.g., auth-middleware → comp-auth-gateway of type interface-layer) → component role stated in tech-agnostic terms. |
entry_points[] | Each entry point → one interface-layer component (e.g., worker entry → comp-{context}-job-runner). |
architecture_inference_path when present | design_patterns[] observations upgrade medium-evidence components to high. |
Each component records: id, name, type, bounded_context, capabilities_served (intersected with selected capabilities), responsibilities (technology-agnostic — derive from module role, not framework name), depends_on, evidence, and rationale.
-
Build data_model. Entity sources, in priority order:
- Entity-named files:
*.entity.ts, *.entity.js, *.model.py, models.py, */models/*.go, *.schema.ts, domain/*.cs. Each distinct entity file → one entity.
- Schema-defining ADRs in
docs.adrs[] (titles matching data-model, schema, entity, domain-model) → promote declared entities.
- Business-rule nouns in
enriched-capabilities.yaml that are referenced but have no file counterpart → mark source: enriched_capabilities_only with coverage: implied (do not invent attributes).
Each entity records: id, name (PascalCase singular), owning_context, primary_identifier: natural-key | surrogate-key (infer from file content signals; when ambiguous leave null and surface a low-tier decision), pii_fields (populated from compliance flags in the project-profile proposal + enriched-capabilities business rules), invariants (lifted verbatim from enriched-capabilities business rules with citation), source (evidence path). Relationships are captured from import edges or explicit foreign-key-like field names, rendered as from → to with cardinality: one-to-one | one-to-many | many-to-many and a source citation. NO column types. NO storage engine names. NO nullable/unique DDL tokens. NO language type annotations.
-
Build api_surface. Capability-level operation groups from:
entry_points[] (each public entry → one surface group).
patterns.naming_suffix_counts for Controller, Handler, Resolver, Router inside each bounded context → group operations by the context they serve.
features.yaml feature names → align operation phrasing to the feature vocabulary.
Each surface group records: id, name, bounded_context, capabilities_covered, operations (verb-noun phrases such as initiate-checkout, cancel-order; NEVER POST /orders or createOrder(OrderDto)), consumers (other components or external-client), evidence.
-
Build integration_points. Sources:
manifests[].dependencies — third-party SDKs, payment processors, email providers, search engines, observability vendors. Each distinct external dependency of this class → one integration point.
config_files entries referencing external hostnames, webhook URLs, service account keys → one integration point per distinct external system.
- ADRs titled with integration vocabulary (
integration, webhook, vendor).
Each integration point records: id, name (the external system's generic purpose, NOT its product name — e.g., payment-processor not Stripe), purpose, capability, interaction_pattern (request-response | fire-and-forget | streaming | batch — chosen by the evidence, never by SDK-specific semantics), risk_category (vendor-lock-in | data-residency | availability-coupling | compliance-scope-expansion), boundary_isolation (the abstraction component that contains the dependency; if absent in code → risk: direct-call-no-isolation), evidence.
-
Build adr_log. Walk docs.adrs[] from the scan index. For each ADR entry, record: id (normalized to ADR-NNN), title (verbatim), status (if scan captured it), path (source file), summary (first paragraph only when adrs payload provides text). Do not fabricate ADR content not present in the scan. Mark the log derived_from: scan-index:docs.adrs so downstream /arch can re-anchor against physical-architecture.
-
Build component_capability_map. Every capability in scope.selected_capabilities MUST appear with at least one serving_components entry. Uncovered capabilities → add to coverage_gaps with recommended_action: user-review-at-codify-checkpoint. Structure present in code but not mapping to a selected capability → unmapped_structure[] with the module path and a one-line rationale, so the human reviewer can decide whether to expand scope or accept the orphan.
-
Apply tech-agnostic scrub (validate-abstraction-layer rules). Before writing, scan every string field across bounded_contexts, components, data_model, api_surface, integration_points, adr_log for deny-list tokens using validate-abstraction-layer semantics. Deny-list categories:
- Specific database engines (
PostgreSQL, MySQL, DynamoDB, Mongo, Redis).
- SDK or library method calls (
.save(), .publish(), boto3, stripe.Customer.create).
- Wire protocols and formats (
REST, HTTP, gRPC, GraphQL, WebSocket, JSON, Protobuf).
- Schema column types and DDL tokens (
VARCHAR, INTEGER, UUID, NOT NULL, PRIMARY KEY).
- Programming-language keywords and type annotations (
async, class, interface, struct, Promise<, List[).
- Specific product / framework identifiers (
Next.js, Spring, Rails, AWS, Kubernetes).
If any token is detected → halt, return structured failure abstraction_layer_violation with the offending field path and token.
-
Assemble the artifact. Write the meta block first, then the logical architecture body. meta.evidence lists every scan-index path consulted (deduplicated). meta.confidence is the min of per-section confidences; force low whenever scan_status: budget_exhausted.
-
Write decision manifest. One entry per inferred structural decision: bounded-context boundary, co-change merge, component-type inference, entity primary-identifier, PII tagging, integration risk classification, pattern claim from R3. Each entry: {decision_id, decision_type, tier, grounding_source, recommendation, alternatives_considered, confidence}. grounding_source is either scan-index:<json-path>, kb:{patterns-file}#{section}, or proposal:{file}#{field}. Low-confidence entries MUST list alternatives_considered with at least one alternative. Verbatim echoes from related proposals are NOT decisions — do not record those.