Technical debt is the additional cost of future work caused by a current technical decision.
Consider an order service that writes directly to the inventory database. During a migration, that may be the fastest way to deliver a feature. Later, every inventory schema change requires coordination with the order team, duplicated integration tests, and a coordinated release. The direct dependency is now increasing the cost of planned work.
The dependency is observable in source. Its cost depends on what the organisation needs to change next.
Technical-debt research describes the term as a metaphor for technical compromises that can provide short-term benefit and create longer-term harm. It also documents that the term is used ambiguously across code, architecture, tests, requirements, and documentation. Li, Avgeriou, and Liang's systematic mapping study identified ten technical-debt types and called for clearer definitions and stronger empirical evidence around management approaches.
For engineering work, two distinctions matter.
A structural condition is a fact about the system.
Technical debt is the avoidable cost that condition creates for future work.
Debt needs a counterfactual
Every system requires effort to change. That effort is not technical debt by default.
A dependency becomes debt when a reasonable alternative would make expected future work cheaper.
The direct database dependency is a useful example. An inventory API may add latency, operational work, and another interface to maintain. It is not automatically better. If the database schema is stable and the services are owned by one team, the direct dependency may be a sensible choice.
The decision changes when the inventory model is being actively developed by a separate team. The alternative now reduces coordination and migration costs. The direct dependency has become an expensive way to continue working.
This is why source analysis cannot tell a team how much technical debt it has. It cannot see the roadmap, team boundaries, incident history, or the cost of changing a particular relationship. It can identify relationships that need a decision.
Martin Fowler's explanation of the debt metaphor uses the same economic logic: refactoring is worth doing when the cost of leaving an implementation in place exceeds the cost of improving it for the work ahead. Technical Debt.
Structural facts belong in the pull request
A pull request creates or modifies structural relationships:
- one package imports another;
- one service calls another;
- one route gains or loses a client;
- one event producer has no corresponding contract;
- one module becomes reachable from an area the change was not expected to affect.
These changes are easy to miss when they are spread across files or repositories. They are also cheap to examine before merge, while the author still has the task context.
The pull-request question is therefore specific:
Did this change introduce a relationship that breaks an architectural contract?
This is different from asking whether an existing condition should be refactored.
A legacy dependency may be accepted until the migration that makes it expensive. A newly introduced dependency has no such history. The author can usually choose a different implementation before other code starts relying on it.
An architectural contract has two parts
A contract contains a declared rule and a measured relationship.
The declared rule comes from the team:
layers:
- {name: delivery, paths: ["web/**", "notify/**"]}
- {name: api, paths: ["api/**"]}
- {name: storage, paths: ["storage/**"]}
The measured relationship comes from source analysis.
If code in storage imports notify, enola. can compare the branch with a pinned baseline and report the new edge:
FAIL — 1 structural regression introduced.
Regressions (fail):
- [layers] 1.00 — Layer violation: storage -> delivery
import of notify
Policy: fail on new findings from [layers] at confidence >= 1.00.
The output identifies the rule, the relationship, and the source of the relationship.
A reviewer can act on this result. The author can remove the import, move the responsibility, or agree that the layer order no longer reflects the system and change the contract deliberately.
Not every architectural signal is a contract. Enola separates declared rules, measured findings, and scope gates:
| Mechanism | Team decision or claim | What enola measures |
|---|---|---|
| Declared intent | Services use only declared cross-repository seams. | A measured seam is undeclared, or a declared seam is absent. |
| Coverage finding | Message producers correspond to loaded AsyncAPI operations. | A call site has no matching AsyncAPI operation, or an operation has no detected code. |
| Scope gate | The change is limited to the area named with --target. |
Modified packages fall outside the predicted dependency radius. |
| Layer or dependency constraint | Domain code does not depend on delivery code. | A new import or call crosses the boundary. |
Declared intent, layers, and constraints evaluate team-owned rules. Coverage findings require interpretation. Scope gates compare the change with its stated purpose.
The enola README documents these checks, including layer contracts, cross-repository intent, route coverage, messaging coverage, and scope spillover.
Constraints turn decisions into rules
Some decisions are more specific than a layer order: every route needs a loaded consumer, jobs must be enqueued somewhere, or a legacy subsystem may remain but gain no new dependants.
Enola represents these as constraints over named components and measured relationships. A decided breach is reported at confidence 1.00 and can be enforced explicitly:
enola check --fail-on=constraints
A constraint can also ratchet an existing condition: accept the baseline while preventing new work from making it worse. Its verdict remains bounded by the source and repositories Enola measured. Constraints documentation.
Baselines separate new changes from existing conditions
Existing findings and new regressions require different treatment.
A repository may already contain boundary violations, cycles, undocumented service connections, or routes without detected clients. Blocking unrelated work until every condition is removed turns an architecture programme into a clean-up project.
A baseline records the structure before a change. enola. compares the branch against that state. Existing findings remain available for investigation; the check reports the findings and relationships introduced or resolved by the change.
This enables a team to enforce a contract from today without claiming that every older exception must be fixed immediately.
Existing conditions should be prioritised against planned work. If a dependency will complicate an upcoming migration, it has a concrete reason to be addressed. If it is stable and isolated, it may remain in the baseline until the cost changes.
The baseline is also a contract with the analysis itself. A baseline created with different extraction rules or a different enola. version may not be comparable. enola. declines to grade an incompatible baseline rather than assigning a change failure to inconsistent measurement. Baseline comparability and gate behaviour.
Policy determines what blocks a merge
A finding and a failure are separate.
enola. runs its explainers and reports what it measured. A policy determines which findings may change the exit code. With no --fail-on policy, findings alone do not fail the check; scope gates, usage errors, and incomparable snapshots are evaluated separately. What fails the build.
This lets teams apply different treatment to different evidence.
A declared layer order can be a hard gate:
enola check --fail-on=layers
A cross-repository connection that has not been declared can be a hard gate:
enola check --fail-on=intent
A dependency-depth outlier may be useful review information without becoming build policy. Its threshold is relative to the repository and may not carry the same meaning across languages or package models.
The policy should contain rules the team is prepared to uphold. A rule that produces unexplained failures, duplicates a compiler check, or has no agreed owner will be bypassed.
Contracts become more important across repositories
A service boundary is often maintained outside the repository that introduces a change.
A mobile client may call a route in a backend repository. A service may publish an event consumed elsewhere. A repository may add an outbound call that is not represented in the loaded service graph.
These relationships create future work because they introduce coordination requirements. The relevant question is not whether cross-repository dependencies are bad. Most distributed systems need them. The question is whether the relationship has an owner, a contract, and a known migration path.
enola's intent checks report a seam that was not declared, as well as a declared seam that the loaded graph did not measure. Route coverage matches outbound calls to loaded route facts, whether extracted from server code or an API description; messaging coverage compares code with loaded AsyncAPI operations. Supported contract and coverage checks.
A missing match is evidence to investigate. It may reveal an undocumented dependency, an incomplete analysis scope, or an external consumer outside the repositories being modelled. The result should not be interpreted without that context.
Coding agents should work against the same contracts
An agent can make a locally correct change that introduces an architectural consequence outside the immediate task.
For example, an agent may import a delivery package from storage to reuse a notification function. The code can compile and tests can pass. The contract failure gives the agent a concrete next action: remove the dependency or choose another implementation.
enola. can expose the architecture through MCP before an edit, run hooks after an agent session, and use the same enola check command in CI. The agent does not receive a vague instruction to "keep the architecture clean." It receives the contracts that apply and a verdict on the actual change. Agent, CLI, and CI workflow.
What enola. establishes
enola. establishes source-derived facts within an explicit analysis scope:
- modules, symbols, routes, storage, and relationships extracted from supported source patterns;
- a before-and-after structural delta;
- contract findings and policy failures;
- extraction exclusions, parse failures, and unresolved relationships.
It does not establish runtime behaviour, future product priorities, ownership quality, or the financial value of a refactoring. It does not decide whether an existing finding is worth paying down.
Those decisions belong to the team.
Technical debt is the additional cost of future work. Architectural contracts prevent teams from adding new cost by accident. enola. supplies the evidence and enforcement loop for those contracts.
FAQ
Is a structural condition the same thing as technical debt?
No. A structural condition, such as a service reading another team's database directly, is just a fact about the codebase. It only becomes technical debt when a reasonable alternative would make expected future work cheaper; without that counterfactual, source analysis alone cannot say whether a dependency is costly or fine.
Can a tool like enola measure how much technical debt a codebase has?
No, and it does not try to. Source analysis can identify structural relationships, such as a boundary crossing or an undeclared cross-repository call, but it cannot see a team's roadmap, ownership boundaries, or incident history, which are what determine whether a relationship is expensive to keep.
Do existing architectural violations block every new pull request?
Not if the team uses a pinned baseline. enola grades a change against the structure recorded before it, so older findings stay visible for prioritization without failing unrelated work. New regressions can affect the finding policy; scope gates and checks that cannot run are evaluated separately.
What decides whether a finding fails the build?
A separate policy, not the finding itself. enola reports what its explainers measured regardless of severity; a --fail-on flag names which categories, such as layer violations or undeclared cross-repository seams, are allowed to turn a finding into a non-zero exit code.