Change impact analysis / ENGINEERING NOTE
Your specification changed. What should you regenerate?
A specification changes one number:
Expenses above €500 require manager approval.
The new threshold is €750. Your application has already generated business rules and test cases from the old version. Some of those results have been reviewed by a person.
Selective regeneration starts with deciding which of those outputs need another look.
You could generate everything again. But a test for a missing receipt has nothing to do with the approval threshold. Replacing it creates more work without addressing the actual change. Meanwhile, a €600 expense now has a different expected outcome, and keeping that test unchanged would be wrong.
| Test | Before | After | Initial response |
|---|---|---|---|
| €600 expense | Approval required | Approval not required | Update the expectation |
| €900 expense | Approval required | Approval required | Revalidate the existing test |
| Missing receipt | Request the receipt | Request the receipt | Preserve, if independent |
That middle row is important. A test can depend on a changed requirement without needing a different expected result.
My guiding rule is simple: automate what you can establish, expose what you cannot. Every decision to keep or replace an output needs a reason. When that reason is missing, the system should ask for review.
I worked on document versioning and impact analysis in SESCA, where matching content across versions and preserving useful work were central concerns. Here, I use a fictional expense policy and a small, original reference implementation. It makes no model calls, so we can inspect the planning logic before adding semantic analysis or generation.
Separate text change, meaning change, and impact
I start with three questions:
- Did the source text change? Comparing bytes or a content fingerprint can answer this.
- Did the business meaning change? A moved paragraph, a paraphrase, and a changed threshold need different treatment.
- Which outputs might now be invalid? That depends on the relationships between sources and generated artifacts.
An artifact here is a saved output: a business rule, test case, or other derived record. Marking it invalid means checking whether it still holds. The saved output stays available during that check.
Keeping these questions separate prevents a common shortcut: turning every detected edit into a regeneration request. It also prevents the opposite mistake—assuming a small edit has a small effect.
Changing “require” to “do not require” is a small textual edit with a large consequence. Moving the same sentence from “All expenses” to “International travel” can change its scope without changing the sentence itself.
Here is the path from an edit to a decision:
Source changed
↓
Can I identify the same logical requirement?
↓
Did its meaning or scope change?
↓
Which artifacts depend on it?
↓
Preserve / Review / Regenerate / Retire after review
Uncertainty at any step sends the affected work to review. The sections below make each decision explicit.
Give the source an identity that survives editing
A content hash can tell me that content differs. It cannot, by itself, tell me that two different pieces of content are revisions of the same requirement.
For this example, the approval section keeps the logical ID approval across versions:
{
id: 'approval',
scope: 'all-expenses',
position: 1,
text: 'Expenses above €750 require manager approval.'
}
The policy records its version and the generation configuration used. Each output also records its own revision and what it depends on:
{
id: 'T-600',
revision: 'r1',
dependsOn: ['R-APPROVAL'],
lineageComplete: true,
check: { kind: 'approval', amount: 600 }
}
R-APPROVAL depends on the source section approval. That creates a path from source to rule to test.
The example starts with known identities. Matching sections across arbitrary documents is a separate problem. If a match is ambiguous, I send it to review before carrying over the old relationships.
There is one assumption to watch: lineageComplete. The fixture sets this flag to say that all dependencies are recorded. In a real system, I need a way to check and maintain that claim.
Build systems face a similar problem: missing dependency declarations can produce incorrect builds. Here, a missing relationship could cause me to preserve an outdated test. That is why unknown lineage triggers review. Bazel dependency management
Classify what you can establish, expose what you cannot
For the reference implementation, I deliberately interpret only one exact sentence form:
export function threshold(text) {
const match = /^Expenses above €(\d+) require manager approval\.$/.exec(text);
return match ? Number(match[1]) : null;
}
This is a tiny interpreter for a controlled fixture, not a parser for business specifications. It lets the example demonstrate a known threshold change without claiming to understand arbitrary language.
Everything outside that narrow interpretation follows explicit rules:
| Situation | Classification | Consequence |
|---|---|---|
| Same text, identity, and scope | Unchanged | Consider preservation |
| Same text and scope, different position | Moved | Preserve content; update its source reference |
| Threshold changes in the supported sentence form | Changed | Find dependent artifacts |
| Different or uncertain scope/identity | Review | Resolve before preserving |
| Uninterpreted paraphrase, negation, or exception | Review | Seek additional evidence |
| Source missing from a complete new version | Deleted | Review affected outputs for retirement or replacement |
| Source missing from an incomplete import | Review | Do not infer deletion |
Semantic analysis can help with the unresolved cases. Comparing embeddings, as described in the Sentence Transformers documentation, helps find similar passages. I would use those scores to find candidate matches, then check whether the requirement still holds before preserving an output. Sentence Transformers: semantic textual similarity
The question is more specific than “Are these sentences similar?” It is “Do they impose the same requirement, in the same scope, for the outputs I am about to keep?”
If an LLM helps answer that question, I would store its answer as a proposal with a reason and a review path. The reference code does not implement that classifier. Its tests only establish how unresolved cases are handled.
Find the affected set before deciding the action
The dependency graph for the expense example is small:
Approval source
Threshold: €500 → €750
↓ R-APPROVAL
- T-600 · regenerate
- T-900 · review
- T-COMBINED · regenerate
Receipt source
Requirement unchanged
↓ R-RECEIPT
- T-RECEIPT · preserve
- T-COMBINED · regenerate
Each arrow points from a dependency to an artifact that uses it. The approval change reaches the approval rule, the €600 test, the €900 test, and the combined approval-and-receipt test. It does not reach the receipt-only branch.
This function finds those descendants:
export function descendants(roots, artifacts) {
const found = new Set();
const queue = [...roots];
const visited = new Set();
while (queue.length) {
const id = queue.shift();
if (visited.has(id)) continue;
visited.add(id);
for (const item of artifacts) {
if (item.dependsOn.includes(id)) {
found.add(item.id);
queue.push(item.id);
}
}
}
return [...found].sort();
}
It handles shared descendants without returning duplicates. It uses simple scans because this example has six artifacts; a large graph should index the dependency relationships. The full planner rejects cycles in its input graph.
The traversal answers a reachability question. It does not decide whether a test’s expected result changes. That second decision requires domain knowledge.
For €600, comparing the two thresholds establishes a changed approval outcome. For €900, the outcome is unchanged. I still send the latter to review: its wording or references may need updating, and its old approval does not establish validity against the new source.
For the combined test, the example takes the conservative route and proposes regeneration. A richer interpreter could examine its individual assertions and make a more selective decision.
Produce a plan, not immediate replacements
The planner uses four actions:
- Preserve: keep the existing revision because the conditions it depends on still hold.
- Review: I do not know enough to decide automatically.
- Regenerate: propose a replacement for an artifact affected by a known change.
- Retire after review: a source dependency disappeared; decide whether to retire or replace the output.
Here is an abbreviated plan item:
{
"artifactId": "T-600",
"artifactRevision": "r1",
"action": "regenerate",
"reason": "The expected approval outcome changes for this expense amount.",
"oldSources": [{ "id": "approval", "version": "policy-v1" }],
"newSources": [{ "id": "approval", "version": "policy-v2" }]
}
The plan records which source versions were compared and exactly which output revision needs attention. At this stage, all stored content stays unchanged.
I would keep the prior approved revision available while a replacement is generated and checked, but clearly distinguish it from an output validated against the current source. Approval belongs to the exact revision someone reviewed; it should not move automatically onto generated replacement text.
The planner also broadens review in three situations. If dependency coverage is unknown, it cannot justify preservation. If the generation configuration changes, unchanged source text is insufficient. If a new clause appears, the old graph cannot already contain all of that clause’s potential relationships.
That last case matters: a new exception may affect existing rules even though there are no outgoing edges from its new ID. This example sends all existing artifacts to review rather than claiming that “no recorded dependencies” means “no impact.” It proposes no new artifacts automatically; discovering those belongs to a subsequent requirements step.
Reject a plan that is already out of date
Suppose the planner compares v1 with v2. Before someone applies the result, v3 becomes current. The v2 plan is now stale, even if the comparison was correct when it ran.
The example fingerprints the target policy, generation configuration, and artifact snapshot. Its assertCurrent function rejects a plan if that snapshot has changed. It also catches an artifact being edited or reviewed after planning.
This guard is not a lock. A real application must check the expected state and apply its updates atomically, using an appropriate transaction or conditional-write mechanism. Checking first and writing later leaves a race between those operations. PostgreSQL’s transaction-isolation documentation describes the concurrency behavior applications must account for, including cases where transactions need to be retried. PostgreSQL transaction isolation
The fingerprint itself uses ordered JSON serialization. It is suitable for the controlled example, not a claim to provide a canonical semantic hash of arbitrary records.
Test preservation as carefully as regeneration
The reference example has 16 passing tests. They exercise exact expected actions, not just whether the planner returns an object.
The suite covers threshold changes, unchanged inputs, movement, scope changes, uncertain wording, ambiguous identity, deletions, incomplete imports, unknown and dangling dependencies, new clauses, configuration changes, shared descendants, cycles, duplicate IDs, deterministic output, input immutability, and stale plans.
The default fixture produces this result:
| Artifact | Plan |
|---|---|
| Approval rule | Regenerate |
| €600 approval test | Regenerate |
| €900 approval test | Review |
| Receipt rule | Preserve |
| Receipt test | Preserve |
| Combined test | Regenerate |
Download the reference example ZIP, extract it, and open a terminal in the extracted folder. With Node.js 22.23.3 or later installed, run:
node --test planner.test.mjs
node run.mjs
The download contains the planner, fictional fixtures, tests, and expected output. It needs no additional dependencies.
The tests do not establish how accurately an embedding model or LLM recognises equivalence. If I added one, I would evaluate it separately against labelled pairs, including numbers, exceptions, negation, and scope changes. A deterministic test of “uncertain means review” is not an evaluation of the component producing that uncertainty.
Know when a full rerun is simpler
Selective regeneration requires stable identity, maintained relationships, versioned inputs, and a review process. Those are costs, not incidental implementation details.
For a small specification with cheap generation and little reviewed output, a full rerun followed by comparison may be easier to maintain. If nearly every rule depends on a shared definition, a small edit may legitimately affect most of the graph. If lineage is unreliable, broader revalidation may be the right choice.
I would measure the whole workflow before claiming savings: classification work, generation calls, tokens, retries, elapsed time, review effort, and errors missed by each approach. This example has no performance benchmark. Its four affected artifacts are a fixture result, not a percentage that can be extrapolated to a production workload.
The reusable decision sequence is short:
Identify the source → classify the change → trace dependencies → propose actions → review uncertainty → apply against the intended version.
The value is in being able to explain each action. The €600 test changes for a specific reason. The €900 test is reviewed for a different one. The receipt test stays because its independence is represented, not because a model guessed that it looked unrelated.
Automate what you can establish, expose what you cannot. That rule guides the whole workflow, from matching a requirement to applying a reviewed plan. Selective regeneration earns trust when every preserved or replaced output has a reason—and unresolved questions stay visible.
For the wider project context, see my SESCA case study. If you are working through a similar versioning problem, get in touch.