A Survival Strategy for Software Engineers in the AI Era: Estimating the Ceiling of Automation
0. Key points
Section titled “0. Key points”- The only stage of development that LLM code generation makes faster is implementation. By Amdahl’s law, if implementation accounts for a fraction of the total, then even an infinitely fast implementation stage caps the overall speedup at (Corollary 3.2).
- As generation becomes cheap, what becomes relatively expensive is verification. Even with the cost of generation driven to , the expected total cost cannot fall below the verification cost divided by the acceptance probability (Corollary 4.3). This is the source of the value of people who can review.
- Verification cannot be handed over to a machine wholesale. Every non-trivial property of the meaning of a program is undecidable (Theorem 5.1).
- Nor can one leave “what to build” vague and let the model generate it. Selecting one behaviour out of candidates requires at least bits of instruction (Proposition 6.2). This encoding is what prompt engineering really is.
- The value of architecture can be quantified too. Splitting elements suitably into modules cuts the number of interactions to be inspected from to (Proposition 7.1).
- The conclusion is that what one should learn is problem framing, architecture and the ability to verify, together with the low-level knowledge and the mathematics that support them.
1. Motivation: what did code generation change?
Section titled “1. Motivation: what did code generation change?”When OpenAI published its evaluation of Codex in 2021, the shock felt by many engineers came down to a single point: working code emerges from a description of a function written in natural language [5]. Completion tools such as GitHub Copilot then entered day-to-day practice, and by 2023 there were measurements of productivity from a randomised controlled trial (RCT) [6].
Reactions to this change split into two camps. One says that engineering jobs will disappear; the other says the tools are toys and nothing essential has changed. I regard both as poorly grounded. The former extrapolates measured numbers far beyond what was measured; the latter ignores the measured numbers altogether.
This article takes a different position: estimate the size of the change stage by stage. Fortunately, software engineering already has a classical instrument for answering “if only part of the work gets faster, how much faster does the whole get?” — Amdahl’s 1967 argument [1], together with Brooks’s 1987 distinction between essential and accidental complexity [2]. In Brooks’s vocabulary, code generation attacks mainly the accidental side: not the problem itself, but the drudgery of expressing it to a machine. Essential complexity, that is, deciding what ought to be built, survives a change of tools.
Yet merely chanting that the essence remains does not tell anyone what to learn. So this article answers four questions with equations.
- If implementation gets faster, how much faster does the whole get?
- If generation gets cheaper, what becomes relatively expensive?
- Can the thing that becomes expensive be automated?
- What should be designed so that it does not become expensive?
2. Preliminaries: decomposing development into stages
Section titled “2. Preliminaries: decomposing development into stages”Let us fix the object of discussion. We split the work of building software into the following six stages. Names differ between organisations, but the ordering and the roles are broadly common.
Definition 2.1(Development stages and the automation ratio)
Suppose the total working time of software development decomposes as a sum of the times of six stages:
Here framing is the stage of deciding what problem is to be solved at all; specification is the stage of describing what must be solved without ambiguity; design is the stage of fixing the structure of the solution (module decomposition, interfaces, data representation); implementation is the stage of turning the design into code; verification is the stage of confirming that the code meets the specification; and operation is the stage of monitoring and repair after deployment.
Let be the set of stages a given tool makes faster, and call the fraction of the total occupied by their combined time,
the automation ratio of that tool.
flowchart LR A["Framing<br/>what to solve"] --> B["Specification<br/>removing ambiguity"] B --> C["Design<br/>decomposition and interfaces"] C --> D["Implementation<br/>writing code"] D --> E["Verification<br/>does it meet the spec"] E --> F["Operation<br/>monitoring and repair"] E -.->|if rejected, go back| D D:::auto classDef auto stroke-width:3px
The boxed stage in the figure, implementation, is the one code generation speeds up directly. The dashed arrow represents the loop back to implementation when verification fails. That loop will matter later (Proposition 4.2).
3. The ceiling of automation (Amdahl’s law)
Section titled “3. The ceiling of automation (Amdahl’s law)”We begin with the most naive question. If implementation becomes times faster, how much faster does the whole become?
Proposition 3.1(Generalised Amdahl's law)
Let the total working time be , and suppose a fraction of it is accelerated by a speed factor while the remaining fraction is unchanged. Then the total time after acceleration and the overall speedup are given by
Proof(Proposition 3.1)
The accelerated part takes time . To become times faster means doing the same work in of the time, so that part now takes . The unaccelerated part still takes . The two run in sequence, so the times add:
Since and we have (excluding ), so , and
follows.
From this comes the most important consequence in this article.
Corollary 3.2(The ceiling of automation)
In the setting of Proposition 3.1 with , however large the speed factor is made,
and moreover for every finite .
Proof(Corollary 3.2)
As we have , so the denominator in Proposition 3.1 converges to . Since gives , the limit of the quotient is .
If is finite, then gives , so the denominator is strictly greater than , whence . (For both sides equal and the inequality becomes an equality; that case is trivial, which is why we assumed .)
Put differently: the fraction of work that does not get faster determines, by itself, the reciprocal of the headroom for the whole. If implementation is 30% of the total, then even reducing implementation time to zero makes the whole only times faster. Without touching the other 70%, no more than that is available even in principle.
Example 3.3(Extrapolating the Copilot RCT numbers to the whole)
In the RCT of Peng et al. [6], the group using GitHub Copilot completed the task of implementing an HTTP server in JavaScript in 55.8% less time than the group that did not. Converted to a speed factor,
The point to notice is that this task isolates the implementation stage. Framing and specification were supplied in advance by the experimenters.
So let us provisionally set the share of implementation in real work at and apply Proposition 3.1 (this is an assumption, not a measurement):
Overall this is about a factor of 1.20, that is, roughly 17% less time. Even with ,
only a factor of 1.39. That is the size of the gap between the fact that implementation became 2.26 times faster and the claim that development became 2.26 times faster.
The next figure plots the overall speedup against the automation ratio , for the speed factor and for .
In the left half of the graph, the region where is at most , even the unrealistic ideal does not push the speedup past a factor of . The payoff from investing in automation is governed by the automation ratio itself.
4. Generation and verification: what gets cheap and what gets expensive
Section titled “4. Generation and verification: what gets cheap and what gets expensive”Corollary 3.2 says that the other stages determine the headroom. So which stage becomes relatively heavy once generation is cheap? The answer is verification. To see it, we model the generate-verify loop. Counting the cost of generation and the cost of verification separately is the same framework as the delegation cost model(Definition 2.1)[LLMs and Programming], restricted here to a single unit of work.
Definition 4.1(The generate-verify cycle)
For a single unit of work (one function, one change) we assume the following.
- Each act of generation costs .
- Deciding whether the generated artefact meets the specification costs .
- Each generation meets the specification with probability , independently across attempts.
- Verification decides correctly (no false negatives and no false positives).
- If the artefact is judged not to meet the specification, generation is retried.
We write for the expected total cost until the first acceptance.
Proposition 4.2(Expected cost of the generate-verify cycle)
Under the assumptions of Definition 4.1, the number of attempts until acceptance follows a geometric distribution with success probability , so and therefore
Proof(Proposition 4.2)
Each attempt succeeds independently with probability , so occurs when the first attempts fail and the -th succeeds:
Setting , the expectation is
For the geometric series may be differentiated termwise in , giving
(this step is legitimate because a power series may be differentiated termwise inside its radius of convergence). Substituting ,
Each attempt costs the constant amount , generation plus verification, so the total cost is . By linearity of expectation,
Corollary 4.3(The verification floor)
In the setting of Proposition 4.2, however small the generation cost is made,
and as . That is, the lower bound on the expected total cost is determined not by generation but by verification and the acceptance probability alone.
Proof(Corollary 4.3)
From and we get . From we get , hence . The limit as is , which follows from continuity in .
This corollary is the core claim of the article. In a world where generation is effectively free, the cost is determined solely by , the weight of verification, and , the probability of passing on the first try. And besides making the model smarter, there are two ways to raise : making the specification precise (Section 6) and designing a structure that is easy to verify (Section 7). Both are human work. Note also that when is large enough, the floor itself exceeds the cost of doing the work oneself, and delegation stops paying off in principle (Corollary 3.2[LLMs and Programming]).
Definition 4.4(pass@k)
Draw solutions independently from the model for a single task; the probability that at least one of them passes verification is called pass@k [5]. Writing for the probability that one sample passes, and assuming the samples are independent,
(so ).
Example 4.5(pass@k rises, but so does the number of verifications)
In the evaluation of Chen et al., Codex-12B solved HumanEval tasks 28.8% of the time with one sample and 70.2% of the time with 100 samples [5]. Feeding into the independence model of Definition 4.4 gives
and since , we get , so pass@100 ought to be essentially . The measured 70.2% is far below that. This is a symptom of the independence assumption failing: the model repeats the same misunderstanding over and over.
More importantly, this 70.2% is only the probability that a correct solution is somewhere among 100 candidates. To learn which one is correct, all 100 must be verified. In the language of Proposition 4.2, the strategy of lowering and increasing the number of attempts raises the total spent on by the same amount. That the economics do not change however many regenerations are stacked up is shown in another form in regeneration does not move the break-even point(Proposition 3.3)[LLMs and Programming]. In domains where verification is not automated, this strategy does not work.
5. Verification cannot be fully automated (Rice’s theorem)
Section titled “5. Verification cannot be fully automated (Rice’s theorem)”The obvious objection is: let the AI do the verification too. In part that is right — test generation and static analysis keep improving. But a machine that decides in general whether a program meets a specification does not exist, whether or not it is an AI. This is what Rice’s theorem of 1953 [4] says.
Theorem 5.1(Rice's theorem)
Let be an (acceptable) numbering of the partial computable functions. Let be a set of partial computable functions that is non-trivial, that is, and is not the set of all partial computable functions. Then the index set
is undecidable.
Proof(Theorem 5.1)
Write for the partial function that is undefined everywhere.
First suppose . Since we may pick a partial computable function . Fix an index with .
For an arbitrary instance of the halting problem, consider the following two-argument partial computable function:
The function is computable. Indeed, on input the procedure “first run until it halts, and once it halts run and output the result” computes directly.
By the s-m-n theorem there is a total computable function such that for all . Then:
- If halts, then for every , so , that is, .
- If does not halt, then is undefined at every , so , that is, .
Hence if were decidable, then together with the total computability of it would be decidable whether halts, contradicting the undecidability of the halting problem. Therefore is undecidable.
Now suppose . Consider the complement , the set of all partial computable functions not in . Since is non-trivial, so is , and . By the previous paragraph is undecidable. But , and the complement of a decidable set is decidable, so assuming decidable yields a contradiction. Hence is undecidable in this case as well.
What Rice’s theorem concerns are extensional properties, those determined solely by the function a program computes. A syntactic property such as “does the source code have fewer than 100 lines?” is of course decidable, and this is no contradiction. On the other hand “does this function halt on every input?”, “does this function return a sorted list?” and “does this function satisfy specification ?” are all non-trivial extensional properties, hence undecidable.
The theorem also does not say that nothing can be learned about individual programs. Type systems, contracts, tests and model checking all obtain practical precision either by making the verdict conservative (answering “unknown” or “reject” when unsure) or by restricting the class of programs considered. What the theorem forbids is only the existence of a complete, sound, always-halting universal decision procedure.
The practical implication is this. Verification will forever be an assemblage of approximations designed by humans. Which tests to write, which invariants to express in types, which properties to assert: deciding these cannot in principle be moved to the generation side. The more code AI writes, the higher the price of an engineer as a designer of verification.
6. The information content of framing: specifications cannot be compressed
Section titled “6. The information content of framing: specifications cannot be compressed”We have seen that verification is what bites. Next comes raising , the probability of passing on the first try, which is a matter of the precision of the specification. Everyone knows intuitively that a vague instruction will not produce what one wants; this can be stated exactly by counting.
Definition 6.1(Specification code)
Let be a finite set of realisable behaviours with . Regard an instruction (a prompt) as a finite bit string and fix a deterministic map from instructions to behaviours ( being the model together with the decoding procedure). When is surjective, that is, when every behaviour in is obtained by some instruction, we call a specification code for . We write for the length of an instruction .
Proposition 6.2(Lower bound on the description length of a specification)
Let be a specification code for with in the sense of Definition 6.1. For each put , the length of a shortest instruction realising . Then
In particular, if only instructions of length at most are used, at most behaviours are reachable.
Proof(Proposition 6.2)
Count the bit strings of length at most . There are exactly strings of length , so the total over lengths through is
(a geometric sum). Since is a map, the behaviours obtained from instructions of length at most number at most this many.
Now put . Every element of is then obtained by an instruction of length at most . Surjectivity of gives , that is, . Taking of both sides, , and since is an integer, . Furthermore , so , whence .
The proposition looks like it is stating the obvious, but its implication is sharp. The length of the instruction must be at least the logarithm of the number of behaviours one wishes to distinguish. However smart the model becomes, this lower bound does not move. What intelligence can change is only which behaviours get the short codewords, that is, the quality of the defaults. A good model is one that assigns short codes to the behaviours most people want.
Example 6.3(Counting the design decisions of a single API in bits)
Consider building one paginated list endpoint. Here are the binary decisions that must be settled before implementation.
- Offset-based or cursor-based pagination?
- Is a request exceeding the maximum page size an error, or is it clamped to the maximum?
- Are deleted records included or excluded?
- When the sort key ties, is a stable secondary key added or not?
- Is a total count returned or not?
- Is authorisation per record or per collection?
- Is an empty result a 200 with an empty array, or a 404?
- Are unknown query parameters ignored, or a 400?
- Are timestamps fixed to UTC, or in the requested time zone?
- Is the rate limit per user or per API key?
- Is the response cacheable or not?
- On partial failure, is what was retrieved returned, or does the whole request fail?
That is possibilities. By Proposition 6.2, distinguishing them requires at least 12 bits of instruction. The English sentence “build me an API that returns a list of users” certainly carries far more than 12 bits of information, but almost none of it concerns these 12 axes. The model therefore has no option but to fill them with defaults, and some of those will conflict with the intended specification. This is exactly how in Proposition 4.2 gets driven down.
Conversely, the work of enumerating these 12 items and settling them is precisely where the value lies. Outwardly it looks the same as “writing a prompt”, but in substance it is framing and specification themselves.
If prompt engineering is understood as a bag of tricks for fooling the model, it becomes worthless with the next model generation. From the viewpoint of Proposition 6.2, its real content is the encoding of the behaviour space: enumerating which axes split the space into how many cases, and conveying that in the shortest description. This skill does not decay when the model changes. What decays is only the part that depended on the quirks of a particular model.
For a fuller discussion see LLMs and programming. In particular, the conditions under which delegation pays off once the labour of making instructions precise is included are collected in Proposition 3.1[LLMs and Programming].
7. Architecture: what does decomposition save?
Section titled “7. Architecture: what does decomposition save?”Finally we turn to lowering , the cost of verification. The largest factor making verification heavy is the interaction between parts. The criteria for module decomposition stated by Parnas in 1972 [3] were aimed at exactly this. Here too, counting quantifies the matter.
Proposition 7.1(Reduction of the objects of inspection by module decomposition)
Suppose a system consists of elements. Without decomposition, if any two elements may interact directly, the number of pairs to inspect is .
Now let divide , split the elements into modules of size , and assume (i) within a module any two elements may interact, and (ii) interaction between distinct modules is concentrated into a single interface per pair of modules. Then the number of objects to inspect is
Regarding as a function of a real variable , it is strictly convex, its minimiser is the unique positive solution of , and for large . At that point
which is strictly smaller than the of the undecomposed case.
Proof(Proposition 7.1)
First we check the formula for . Each module has elements, so it contains pairs, and there are modules, giving
The pairs of modules number , and the sum is the claimed formula.
Next, convexity. For ,
so is strictly convex. Hence has at most one solution, and that solution is the minimiser. Rearranging ,
The map is continuous and strictly increasing for and diverges to as , so there is exactly one positive solution . For large the value is also large, so the term is negligible against , giving .
Writing with , we get
while the remaining is , hence . Therefore . Since , this is strictly smaller than the undecomposed case.
Example 7.2(The concrete saving at n = 100)
Take . Without decomposition one inspects
pairs.
With (ten elements per module),
exactly one tenth.
The optimum solves ; at we get and at we get , so lies between 17 and 18. Evaluating at the integer,
(an approximation, since does not divide ). Roughly 380 pairs, one thirteenth of the undecomposed case.
What this calculation shows is that decomposition is not a case of “the more the better”. Making too large lets the number of inter-module interfaces take over. At we get , worse than . This is the simplest model of the pain experienced by organisations that have carved their microservices too finely.
Now the connection to LLMs. The length of context a model can see at once is finite. A design with small — one in which understanding a module requires reading only that module and a few interfaces — is at the same time a design in which the model is likely to emit correct code. In the language of Proposition 4.2, a good decomposition lowers and raises simultaneously. Far from losing value in the AI era, architectural design has become a parameter that determines the AI’s performance.
8. What to learn
Section titled “8. What to learn”We now translate the four results above into a priority order for learning.
| Skill | Grounds | Degree of replacement by generation |
|---|---|---|
| Framing (what to solve) | Corollary 3.2: the unautomated stages set the ceiling | Low. Purposes are not supplied from outside |
| Specification (removing ambiguity) | Proposition 6.2: the lower bound on description length is model-independent | Low. Only the quality of defaults improves |
| Architectural design | Proposition 7.1: lowers and raises | Low. Requires grasping the constraints |
| Designing verification (tests, types, invariants) | Corollary 4.3, Theorem 5.1 | Low. Full automation is impossible in principle |
| Routine implementation (CRUD, transformations, boilerplate APIs) | Example 3.3 | High. This is what got faster |
| Memorising syntax and library APIs | — | Very high |
| Low-level understanding (memory, concurrency, performance) | A prerequisite for verification; needed to judge the soundness of generated code | Low |
| Mathematical foundations (discrete mathematics, probability, complexity, linear algebra) | The tools for articulating invariants and lower bounds | Low |
A word about the last two rows. Low-level knowledge and mathematics matter not because AI cannot do them, but because they are the language of verification.
Judging whether generated code is correct requires a vocabulary for stating correctness. “This loop is , so it will not run at .” “This shared counter has a data race.” “This floating-point summation loses precision through cancellation.” “The error probability of this randomised algorithm is bounded by .” None of these judgements emerges from reading the code alone. Only someone holding a model — of complexity, of the memory model, of numerical analysis, of probability — can make them. As Corollary 4.3 says, what sets the floor on cost is verification. Someone without the vocabulary of verification cannot lower that floor.
The role of mathematics is treated in more detail in Why relearn mathematics. For instance, how to read a warning with 99% detection rate and 1% false-positive rate is handled in Example 5.4[Relearning Mathematics], and the condition on the learning rate under which gradient descent does not diverge in Theorem 4.3[Relearning Mathematics].
9. Exercises
Section titled “9. Exercises”Exercise 9.1Easy
In a certain development team, implementation accounts for 25% of the total working time.
(1) If introducing a code generation tool makes the implementation stage 5 times faster, find the overall speedup.
(2) Find the value that the overall speedup cannot exceed even if the implementation time is reduced to zero.
(3) Discuss how much the non-implementation stages must be reduced in order to make the whole twice as fast.
Solution
(1) Substitute and into Proposition 3.1:
The speedup is a factor of 1.25 (a 20% reduction in time).
(2) By Corollary 3.2,
so the ceiling is about a factor of 1.33. The 1.25 of part (1) already achieves 94% of this ceiling; making code generation faster still leaves only about 7% of headroom.
(3) Doubling the whole requires . Even with implementation reduced to zero, the remaining time is , which is greater than . Hence doubling is impossible in principle by speeding up implementation alone. On top of zeroing implementation, the remaining must be cut further to , that is, by a third. Unless the other stages (framing, specification, design, verification, operation) are touched, a target of twice as fast cannot even be set.
Exercise 9.2Standard
In the model of Definition 4.1, take generation cost , verification cost and success probability per attempt .
(1) Find the expected total cost until acceptance.
(2) By what percentage does the expected total cost fall when the generation cost becomes ?
(3) By what percentage does it fall if instead the specification is made precise, raising from to (with unchanged)? Compare with (2).
(4) If one adopts the strategy of generating candidates independently and verifying them all, find the probability that at least one passes and the total verification cost.
Solution
(1) By Proposition 4.2,
(2) With we get . The reduction is
that is 20%. As Corollary 4.3 says, is the floor, and no amount of cheapening generation gets below it.
(3) . The reduction is
that is 50%. Doubling the success rate (50% off) beats driving the generation cost to zero (20% off). This is a consequence of sitting in the denominator of Proposition 4.2 and in the numerator. Since the success rate is a quantity raised by the precision of the specification and the quality of the design (Example 6.3, Proposition 7.1), the human-side stages are the better place to invest.
(4) By Definition 4.4,
about 92.2%. However, learning which one passes requires verifying all five, so the verification cost is . That exceeds the expected total cost 12.5 of part (1). The strategy of “generate many and pick” is advantageous only where verification is cheap, that is, where automated tests are complete.
Exercise 9.3Standard
Suppose the behaviour of a system is determined by 20 independent binary design decisions, so that .
(1) Using Proposition 6.2, find the lower bound on the maximum shortest-instruction length needed to express every behaviour.
(2) If one decides to use only instructions of length at most 15 bits, find how many behaviours are at least unreachable.
(3) Translate the result of (2) into what “getting by with a short prompt” means in practice.
Solution
(1) Here . By Proposition 6.2,
(the ceiling is 21 because ). So some behaviour requires an instruction of at least 20 bits.
(2) There are bit strings of length at most 15 (the geometric sum in the proof of Proposition 6.2 with ). The image of the map is no larger than this, so at most 65535 behaviours are reachable. Hence the number of unreachable behaviours is at least
about 93.8% of the whole.
(3) A short prompt yields only the behaviour obtained once the model’s defaults are filled in. Every one of the 20 decisions not specified explicitly is left to the model’s prior. If the defaults happen to match one’s intent, fine; but a single mismatched decision makes the artefact fail, which lowers in Proposition 4.2.
So the strategy of “instruct briefly and iterate quickly” works only (i) in routine domains where the defaults align well with the intent, or (ii) in domains where verification is cheap and mismatches are detected at once. Elsewhere, enumerating the axes of decision and stating them explicitly turns out to be the fastest route after all.
References
Section titled “References”- G. M. Amdahl, “Validity of the single processor approach to achieving large scale computing capabilities”, AFIPS Spring Joint Computer Conference (1967), 483–485.
- F. P. Brooks, Jr., “No Silver Bullet: Essence and Accidents of Software Engineering”, IEEE Computer 20(4) (1987), 10–19.
- D. L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”, Communications of the ACM 15(12) (1972), 1053–1058.
- H. G. Rice, “Classes of recursively enumerable sets and their decision problems”, Transactions of the American Mathematical Society 74 (1953), 358–366.
- M. Chen et al., “Evaluating Large Language Models Trained on Code”, arXiv:2107.03374 (2021). arXiv
- S. Peng, E. Kalliamvakou, P. Cihon, M. Demirer, “The Impact of AI on Developer Productivity: Evidence from GitHub Copilot”, arXiv:2302.06590 (2023). arXiv
Appendix: when verification is imperfect
Section titled “Appendix: when verification is imperfect”In the body we assumed that verification never errs, but real verification — tests, reviews — does miss things. Here we estimate the effect.
Drop the assumption “verification decides correctly” from Definition 4.1 and replace it as follows. Let be the probability that a faulty artefact is wrongly judged acceptable (the miss rate), and let the probability that a correct artefact is wrongly rejected be . This is the same quantity as the false positive rate of the verifier(Definition 2.2)[LLMs and Programming]. The probability that one generation genuinely meets the specification remains .
The probability that one attempt is judged acceptable is then the sum of the probability that it is genuinely correct and passes, and the probability that it is faulty and slips through, namely . By the same computation as Proposition 4.2, the expected cost until a verdict is reached is
so the apparent cost falls as grows. But the conditional probability that an artefact is actually correct given that a verdict of acceptance has been reached is, by Bayes’ theorem,
This is nothing other than Theorem 7.2[LLMs and Programming] rewritten in the notation of this article. For instance with and we get , so about 23% of what was accepted is in fact wrong.
These errors do not vanish; they move to the operation stage. That is, in Definition 2.1 grows. And the cost of responding to an incident is generally far higher than the cost of a fix during development. Lowering — through test coverage, review quality, static guarantees from types — is therefore worth doing even at the price of apparent speed. The “floor” of Corollary 4.3 looks lower than it really is when measured while ignoring . This is why quality metrics deserve to be re-measured in an era of fast generation.
Report an error in this article ・Operated by: Mugen Giken LLC ・Pricing ・Terms ・Legal notice
© 2026 夢現技研合同会社 ・Feeding the text to an LLM is welcome. Code samples are MIT licensed.