Skip to content
Amendments are welcomed

Chapter 12 Companion: Code, Explanations and Solutions

Code explanations and solutions for Chapter 12 Companion: Code, Explanations and Solutions.

Measuring Progress, Flow and Technical Health
OO Development and Agile Practices: Campus Equipment Lending

This companion supplies a small executable metrics exercise, worked calculations, the required repository-analysis method, a precise change specification and model answers. It follows the Week 11 syllabus. The application remains at Chapter 11; the Java calculator is a separate teaching tool, not a new lending feature or an analytics service.

The issue history is synthetic. The repository-lab template is blank by design: no real student repository has been inspected for this chapter, and no sample numbers are represented as real results. Complete that part with actual team evidence or an instructor-provided real export.

1. Files, toolchain and execution

The complete files appear under File: lab/... headings in the appendix. Create a directory named lab and copy each following fenced block into its named file. The TSV data uses actual tab characters and preserves empty fields, including trailing empty fields. Do not replace tabs with spaces or insert Markdown fences into the files.

Tool or methodPinned choice
CalculatorFlowMetrics v1.0, complete source in this file
Repository methodRM-12 v1.0, defined in Section 4 below
JavaFull JDK 17; locally checked with 17.0.20
Build/testPlain JDK compilation and explicit Java checks; no new dependencies
Optional Git provenance commandGit 2.51.1 syntax and locally inspected version
Running lending projectChapter 11’s Java 17 / Maven 3.9.11 version, unchanged

From lab:

mkdir -p out
javac --release 17 -d out FlowMetrics.java FlowMetricsChecks.java
java -cp out FlowMetricsChecks
java -cp out FlowMetrics issues.tsv 2026-10-01T00:00:00Z 2026-10-11T00:00:00Z --scope

These commands use a POSIX-style terminal, including Git Bash on Windows. In another terminal, create out using its normal directory command; the javac and java arguments remain the same. A JRE alone is insufficient. The assertions throw explicitly, so the -ea option is not required.

Expected checks: FlowMetricsChecks: 17 checks passed.

Expected report:

Completions in [from, until): 5
Completed points: 16
Mean lead days: 4.60
Mean cycle days: 2.80
Median cycle days: 3.00
WIP at until (after events at that instant): 2
Age E: 4.00 days
Age H: 3.00 days
at,scope_points,done_points,remaining_points,wip
2026-10-01T00:00:00Z,20,0,20,1
2026-10-02T00:00:00Z,20,0,20,2
2026-10-03T00:00:00Z,20,3,17,2
2026-10-04T00:00:00Z,23,5,18,1
2026-10-05T00:00:00Z,23,5,18,2
2026-10-06T00:00:00Z,25,10,15,2
2026-10-07T00:00:00Z,25,10,15,3
2026-10-08T00:00:00Z,23,13,10,3
2026-10-09T00:00:00Z,23,13,10,3
2026-10-10T00:00:00Z,23,16,7,2
2026-10-11T00:00:00Z,23,16,7,2

The --scope output samples every 24 hours from from through until when aligned. It is suitable for this midnight-UTC example. It is not a business-calendar or local-midnight plotting engine. Real PR data normally uses the summary without --scope because point estimates are unavailable or inappropriate.

2. Data contract and code explanation

Schema

Each TSV line has exactly six fields. There is no quoting or escaping syntax; use stable IDs without tabs or newlines. The file must have the exact header shown in the appendix.

FieldMeaningMissing value
idUnique work-item ID within this inputInvalid
pointsNonnegative integer estimate held fixed in this exampleBlank means unknown, not zero
acceptedTimestamp admitted to tracked scopeInvalid
startedTimestamp at the chosen start boundaryBlank means not started
doneTimestamp at the chosen finish boundaryBlank means not completed
removedExplicit cancellation/removal timestampBlank means not removed

Use ISO-8601 instants such as 2026-10-06T00:00:00Z, with a timezone offset if converting an original timestamp. Do not submit a bare local time. Preserve original timestamps and their timezone in the evidence record as well as any normalised UTC value.

Item rejects start-before-acceptance, completion without a start, completion before start, and contradictory done/removed states. read rejects duplicate IDs and the wrong number of fields. Unknown points produce NA for completed points if any completed item lacks an estimate; they do not prevent duration calculations. Scope-point output rejects missing estimates rather than inventing them.

The model permits a removed item to have been started; that item leaves WIP at removal but never counts as Done. It does not model reopening, multiple blocked intervals, changing estimates, parent/child aggregation or completed work later removed from the product. Use an event-history model for those cases rather than forcing them into these six columns. Input validity does not prove that the source population is complete or the timestamps reflect the claimed workflow.

Calculation responsibilities

completed selects Done instants in [from, until). summarize uses that cohort for count, points, lead and cycle statistics, and separately inspects all supplied rows for WIP at until. This deliberately permits carried-in work: an item can start before the window and finish within it.

inProgress treats an item as open once its Started event has occurred and until Done or Removed occurs. Events exactly at the snapshot instant are already applied. A completion at until is therefore excluded from that interval’s throughput and also excluded from WIP at that snapshot. There is no contradiction: one is a count over a half-open period, the other a state after boundary events.

days uses elapsed seconds and nanoseconds divided by 86,400 seconds. It does not call Duration.toDays, which would truncate partial days. This matters for real PR timestamps: a one-hour interval is approximately 0.041667 days, not zero. Output is rounded to two decimals for reading; calculations retain unrounded doubles.

An empty completion cohort yields zero completions and NA durations, because an average over no observations is not a measured zero-day average. Completed points for that empty cohort are zero as a sum. The median sorts completed cycle times and uses the middle value, or the average of the two central values for an even-sized cohort.

scopePoints and donePointsAt support this example’s fixed estimates and explicit removals. The first includes accepted items not removed as of the snapshot; the second includes completion events at or before it. They do not infer scope from Git commits.

Meaningful checks

The 17 checks protect independently useful properties: correct example totals and durations; scope addition/removal; completion-window boundaries; WIP at a completion instant; fractional elapsed days; empty-cohort handling; unknown estimates; and invalid event order. They are plain-Java teaching checks, not additions to the running application’s JUnit count.

For an optional fault experiment, use a separate copy. In completed, replace:

i.done().isBefore(until)

with:

!i.done().isAfter(until)

Recompile and run FlowMetricsChecks. The check named Exclusive upper bound must fail: B at 6 October is incorrectly counted in the first window. Restore the expression and rerun. This experiment concerns a reporting interval, not a change to LoanPeriod or the overdue policy.

3. Worked calculations and interpretation

Finished cohort

IDLead calculationLead daysCycle calculationCycle days
A03 − 01203 − 012
B06 − 01506 − 024
C04 − 01304 − 031
D08 − 01708 − 053
G10 − 04610 − 064
Total2314

Mean lead = 23 / 5 = 4.6 elapsed days. Mean cycle = 14 / 5 = 2.8 elapsed days. Sorted cycles are 1, 2, 3, 4, 4, so the median is three. Waiting before start totals nine days and averages 1.8, consistent with 4.6 − 2.8 for this same cohort.

B’s blocked interval [03,05) contributes two elapsed days within its four-day cycle. Subtracting those two days would make the cohort total 12 and mean 2.4, not two. Even 2.4 is not the defined cycle-time statistic and cannot be called coding time without further evidence.

Scope and completion reconciliation

Initial scope = 3 + 5 + 2 + 3 + 5 + 2 = 20 points. Added scope = G(3) + H(2) = 5. Removed scope = F(2). Final scope = 20 + 5 − 2 = 23. Completed points = A(3) + B(5) + C(2) + D(3) + G(3) = 16. Remaining = 23 − 16 = 7, exactly E(5) + H(2).

Throughput is five items in ten elapsed days, or 0.5 item per elapsed day as a rate over this interval. It is not 16 items and not six items. A and C finish in [01,06); B, D and G finish in [06,11). There is no double counting at the shared boundary.

At 11 October, E has age 11 − 07 = 4 days and H has age 11 − 08 = 3 days. Neither has a known completed cycle time. F was removed before starting, so it is neither WIP nor Done. A daily WIP count is a snapshot; it is not an average WIP over all instants.

Cautious interpretation example

“The synthetic iteration finished five items and 16 points. Scope grew by five points and later lost two, leaving seven points across two open items. The remaining-work rise on day 04 reflects a scope addition despite a completion. Completed cycle time averages 2.8 days, while the current open items have ages four and three days. B has a recorded two-day wait for clarification, but this small history cannot establish the causes of all delays. Before forecasting the approval change, inspect E’s unresolved issue and agree the approval data contract.”

This paragraph distinguishes facts, limitations and a next action. It does not claim real team performance or assign responsibility to a person.

4. RM-12 v1.0: required real repository lab

This is the pinned repository-metrics method for this chapter. It uses genuine issue/PR events and the same Java 17 calculator; no paid dashboard, API account or repository-analysis library is necessary. A hosting provider’s UI or an authorised export can supply the data. The source must be real even when an instructor prepares the export.

Route A: PR open-to-merge duration: minimum route

  1. Choose a source and rule. Record repository URL, target branch, fixed UTC interval, extraction timestamp and inclusion rule. For a small exercise, select the first five qualifying merged PRs ordered by merge time and then PR number within the interval; if fewer exist, use all and report the actual count. State whether bots, draft PRs and documentation changes are included before selection. This is a bounded duration sample, not repository throughput.
  2. Keep provenance. For each selected PR retain its URL/ID, original opened timestamp, merged timestamp and target branch. Inspect its event history. Exclude or separately document reopened PRs because the supplied one-interval model cannot express multiple open/closed episodes. Use actual precise timestamps; if the UI only gives relative dates, obtain an export with exact timestamps or report that precise calculation is blocked. Do not guess midnight.
  3. Map the data. Set accepted and started both to the PR opened instant; set done to its actual merged instant. Leave points and removed blank. Give it a stable ID such as PR-42. These boundaries intentionally make printed lead and cycle equal; report the result as PR open-to-merge duration, not issue lead time. Do not infer acceptance into a backlog from PR opening.
  4. Calculate. Save the normalised six-column file as repository.tsv. Run the calculator with the chosen interval and without --scope. Manually verify the longest selected duration using its two timestamps. Record count, median and mean for the sample, plus the observed range if helpful.
  5. Interpret and review. State that coding may begin before a PR opens; draft/review waiting remains included; a merge need not be deployed or accepted by a user; the sample may differ from the full population. Ask a peer to trace one row back to the source, or label a solo provenance check accurately.

Example command shape, using your actual interval:

java -cp out FlowMetrics repository.tsv 2026-10-01T00:00:00Z 2026-10-11T00:00:00Z

Those dates are the book example’s window, not a required window for a real repository. Replace them if they do not match your selection. If the input contains only merged sample PRs, do not interpret the displayed WIP as repository WIP: open PRs were not supplied.

Route B: complete-window throughput and open work

To report repository PR throughput, enumerate all qualifying merged PRs in the chosen branch and [from, until) interval. Check pagination and filters; record the number of results and any exclusions. Do not filter by opened date, because that would lose carried-in work. The completion count then measures merges of the stated PR unit, not finished user stories or deployments.

To report WIP at until, also include every qualifying PR opened at or before that instant that had neither merged nor closed at that instant. Historical snapshots require event history: a PR merged after until was still open at until. The calculator can retain the later Done event and correctly treat it as open at the snapshot. A PR closed without merging maps to removed, not done. Capture such rows where needed to reconstruct historical state. Record any reopened cases separately or use a richer event model.

Only a complete population supports full-window throughput/WIP claims. A small verified duration sample is sufficient for Route A and fulfils the required repository-duration exercise; do not mislabel it as Route B. Issue exports are an alternative when they include genuine Started/Done transition history and the same explicit completeness checks. In that route, define what “accepted” actually means before using it.

Optional Git cross-check

For a local repository, Git 2.51.1 can provide a compact provenance inventory without collecting names or emails:

git --version
git rev-parse HEAD
git log --first-parent -20 --format='%H%x09%cI'

%H is the commit hash and %cI the strict ISO committer timestamp. First-parent traversal changes the history being inspected. These records may help match a PR to a revision, but they do not supply PR-open times or prove deployment. The last 20 commits are not a complete interval export, and commit counts are not substituted for the required PR/issue measure.

Evidence template: fill with observed data

FieldYour record
Repository and target branch
Source interface/export and extraction timestamp
Interval and timezone convention
Unit; accepted/start/finish meanings
Inclusion rule; excluded event histories
Route A sample rule or Route B completeness evidence
Raw IDs/URLs and exact timestamps
Normalised file and command
Number analysed; mean; median; longest manual check
Throughput/WIP, only if supported by complete input
One uncertainty; one defensible next question
Reviewer and actual feedback, or solo review

A zero-completion interval is a valid result; report it and retain NA durations. If the team has no usable history, use an instructor-provided real repository export or inspect a suitable public repository. Do not manufacture commits or PRs to claim historical evidence. Synthetic data may be used to practise the commands while arranging the real input, but the real-data lab remains unfinished until that input is examined.

5. CR-12: worked change specification for Chapter 13

Status: specified teaching change; implementation pending. It is not institutional policy and does not substitute for the team’s separately released assessment variant. These choices make the example concrete enough to implement and verify without reopening its basic scope in Chapter 13.

Behaviour and data decisions

IDBook contract
CR12-1Add a recognised VISITOR borrower category. Legacy directory code V maps to it. Existing S/T, inactive and unknown-code behaviour stays as documented; unknown codes never default to STUDENT.
CR12-2Visitors may request only CAM1 or CAM2, for one, two or three calendar days. Use the existing half-open loan period. Zero/negative duration and four or more days are rejected.
CR12-3Reuse the student count limit: fewer than three retained reservations for that borrower before adding one. This is the existing repository-count convention, not an active-loan count.
CR12-4A visitor needs one matching trusted approval record for the exact tuple (borrowerId, itemId, start, endExclusive). The record must have a nonblank sponsor ID and status APPROVED. Approval for another borrower, item or period is insufficient.
CR12-5Capture one request-time instant from an injected Clock. A record is valid when validFrom <= now < validUntil. Equality at expiry is invalid. The loan period need not lie inside the approval-validity window; that window governs when the reservation may be requested.
CR12-6A missing match, DENIED status or an out-of-window record is an expected rejection. It creates no reservation and sends no confirmation.
CR12-7An unreadable register, malformed row, duplicate request key, unknown status or invalid validity interval is an operational failure. It also creates no reservation and sends no confirmation, but is distinguishable from sponsor rejection. No fallback approval or empty-success response.
CR12-8Once approval and existing checks succeed, create the reservation and use existing confirmation behaviour. Retain the current rule that an expected notification failure leaves an already-created reservation in place.
CR12-9The initial release has one visitingResearchersEnabled flag, default false. Disabled visitor requests are explicitly rejected without reading the approval register. Student/staff requests use their existing path under either setting and never require sponsor approval.
CR12-10Approval is checked at reservation time only. Expiry or disabling the flag later does not cancel an existing reservation or block its normal checkout/return. Revocation after reservation is outside this exercise.

The current direct category-taking entry point and the older overload that defaults to STUDENT are trusted teaching APIs, not authentication boundaries. Directory-backed requests obtain their category from the directory; arbitrary caller-supplied categories do not establish real-world identity. Do not claim this exercise prevents hostile impersonation. A production authorisation system would need separate requirements and evidence.

SD-12: count semantics: ReservationCoordinator calls its variable activeCount but counts all stored reservations. Returning a loan does not remove its reservation. Preserve this behaviour for the worked change and record the question: should a future authorised rule count active loans, future reservations, or all retained reservations? Renaming the local variable for clarity is possible as a separate structural edit; changing the count is a functional decision. SD-11’s due-today report behaviour also remains unresolved and unchanged.

Planned approval-register boundary

Choose a local UTF-8 TSV file with exactly eight columns:

borrowerId	itemId	start	endExclusive	sponsorId	status	validFrom	validUntil

Each column is separated by a tab; the example data below contains actual tabs. IDs cannot contain tabs or newlines. Dates are ISO local dates; validity timestamps are ISO instants. Status is exactly APPROVED or DENIED. Require start < endExclusive and validFrom < validUntil. Reject a malformed register as a whole, including an invalid unrelated row; do not silently skip bad records. Require unique request keys within one read. The file is treated as a trusted, sequential example, not an atomic concurrent approval database.

Synthetic valid example for the future implementation:

borrowerId	itemId	start	endExclusive	sponsorId	status	validFrom	validUntil
V1	CAM1	2026-10-20	2026-10-22	S1	APPROVED	2026-10-18T00:00:00Z	2026-10-20T00:00:00Z

At request instant 2026-10-19T12:00:00Z, this approves V1’s two-day CAM1 request starting 20 October. At 2026-10-20T00:00:00Z, the same approval has expired. The row is a specification example, not input to FlowMetrics, and there is no approval parser implemented in Chapter 12.

A candidate application port can obtain the approval decision for a request; its file adapter performs actual I/O and validation. Keep deterministic membership/duration rules apart from source access and time. Do not force identity/approval information into BorrowingPolicy.mayRequest(count, days) through a global flag. Chapter 13 should decide the smallest interface shape that satisfies these examples and fits the existing orchestration.

Acceptance matrix

In visitor success/rejection rows assume a recognised active V1, flag enabled, fewer than three retained reservations, available equipment, valid identifiers and an ordinary working notification sender, except where the row changes one condition. Construct every example independently so one failure does not mask another.

ExampleInput differenceExpected observation
AC1CAM1, two days, matching approved record valid at request timeOne new reservation; existing confirmation format; one notice
AC2CAM2, three days with its own matching recordAllowed; upper duration boundary preserved
AC3Four days, otherwise matching approvalReject; no state or notice change
AC4Zero or negative daysReject; no state or notice change
AC5Item MIC1 with matching approvalReject because item is outside visitor allowlist
AC6Missing approval or DENIED recordExpected rejection; no state or notice change
AC7Approval belongs to V2, CAM2 or a different end dateDoes not authorise the V1/CAM1/requested-period tuple
AC8now == validFromValid if all other conditions hold
AC9now == validUntil, or before validFromRejected as invalid at request time
AC10Unreadable file, malformed row or duplicate matching keyDistinct operational failure; no state or notice change
AC11Three retained reservations already existReject count limit even if loans have been returned
AC12Visitor feature disabledExplicit disabled result; approval source not called
AC13Existing student/staff request, either flag settingExisting result and notification behaviour; approval source not called
AC14Visitor’s approved item overlaps an existing reservationExisting availability rejection; no extra reservation or notice
AC15Valid visitor succeeds but expected notification failure occursReservation retained; existing notification-failure outcome
AC16Visitor reservation created, then approval expires or flag turns offExisting checkout/return flow remains available
AC17Unknown directory code or inactive visitorExisting unknown/inactive semantics; never default to student
AC18Report generated after a visitor loan existsExisting report mapping and SD-11 boundary retained

These are planned examples, not claims that new tests have run. Existing policy tests, directory tests, command tests and the 159-test Chapter 11 suite remain relevant. Some test setups that enumerate every category will require explicit updates because adding an enum value changes their setup; document that necessary example change without weakening old assertions.

Ordered backlog and impact note

IDOrdered workDependencyExit evidence
C12-1Preserve baseline and confirm CR12/SD12 examplesChapter 11 sourcePassing baseline result; retained revision/copy; reviewed contract
C12-2Add recognised visitor path, policies and explicit wiringC12-1Valid visitor with controlled approval source; old categories still behave correctly
C12-3Implement and test actual approval-file adapterC12-1 data contract; connect after C12-2Temporary-file success, malformed/missing/duplicate failures; no mock replacing the reader under test
C12-4Complete matching/time/failure and no-mutation protectionC12-2, C12-3AC boundary and operational-failure evidence
C12-5Introduce one flag and integrate CLI/demoC12-2–4Both settings; old path; existing reservations usable after disable
C12-6Run full regression, real boundary checks and delivery workflowC12-2–5Observed pipeline/local gate, repeatable demonstration and retrospective for Chapter 13

Status of C12-2–6 in this chapter: planned, not implemented. C12-1’s source preservation is supplied here; the learner records their actual baseline result and review. Do not assign owners, dates or points as if a real team had agreed them. Add those during the team planning activity, using its capacity and dependencies.

Impact rationale: category recognition reaches the directory and wiring; the coordinator enforces every-category policy availability; sponsor approval requires request identity/time that the existing count/duration interface lacks; the file reader needs integration checks; failures must be distinguished before a new reservation is added. The existing repository, loan lifecycle and report do not need a broad redesign for this narrow requirement. Concurrent approval updates and revocation remain outside scope.

Flag plan: default off; enable only for the controlled demonstration after both paths pass. Disabling prevents new visitor reservations, but does not erase existing data or roll back arbitrary side effects. Keep the pre-change revision for code comparison. Record a removal decision during Chapter 14 once the intended release path is accepted; do not let an unnamed temporary flag become permanent by default. No public production deployment is required.

6. Preserve the pre-change version

The supplied Chapter 11 code is the baseline; this chapter makes no edits to its production classes, tests or build file. Its last delivered full run passed 159 JUnit invocations. The metrics tool lives outside that Maven project and has its own checks; do not add its check count to the baseline’s count.

For your working project, first run its documented gate from its project root:

mvn --batch-mode verify

Retain the command, actual result, Java/Maven versions and any failure explanation. In a Git project inspect:

git status --short
git rev-parse HEAD

A commit ID is a sufficient source reference only when the intended baseline files are committed. If the working tree includes changes or untracked files, retain them in a separate complete source copy or make a deliberate scoped commit before recording the baseline. A tag alone does not capture uncommitted work. Avoid resetting the project or overwriting unrelated work to make this record look clean.

For learners assembling source from Markdown without Git, copy the complete Chapter 11 project directory as pre-change-ch12 before making Chapter 13 changes. Keep src, build configuration, resources and scripts. Generated target files are reproducible and need not be preserved as source. Keep a record of the copied file set and the successful command.

Suggested record:

Baseline: Chapter 11 cumulative project
Source revision or complete-copy location:
Uncommitted/untracked source accounted for:
Java and Maven versions:
Command and actual result:
SD-11: due-today inclusion preserved; policy unresolved
SD-12: retained-reservation count preserved; policy unresolved
CR-12: specified; implementation pending
Reviewer or explicitly labelled solo review:

7. Model answers and lab reasoning

Chapter 12.8: AI review

Candidate claimJudgement and correction
Six items / 18 points deliveredContradicted: F is removed, not Done. Five items and 16 points completed.
Mean cycle is two after subtracting blockingContradicted: defined mean is 2.8. Subtracting B’s two days would give 2.4, and would change the measure.
Day 04 rise proves productivity declinedNot established: G adds three points while C completes two. The remaining line rises by one.
Day 08 fall proves five points deliveredContradicted: D completes three; F removes two.
Legacy architecture caused waitingNot established: one mapping clarification is recorded; broad causality needs more evidence.
Remaining work definitely finishes in 0.44 iterationsUnsupported forecast: the division is approximately right, but comparability, capacity and uncertainty are missing.
Rank members by commitsReject the recommendation: commits do not measure individual value or identify the cause of delay.

A corrected response can use the cautious interpretation in Section 3. A useful next step is to inspect E’s unresolved problem and settle the approval data contract. That is actionable without inventing a productivity diagnosis.

Chapter 12.11: concept questions

1. Lead versus cycle. Accepted-to-Done includes the pre-start queue; Started-to-Done does not. B has lead five and cycle four. Another team’s “lead time” might begin at a customer request rather than admission to scope, so the number cannot travel without its event definition and elapsed/business-time convention.

2. Falling burn-down. Removing F reduces remaining scope by two while delivering nothing. Re-estimation could also move a line under another data model. Keep a scope event record and distinguish the completed and scope lines in a burn-up.

3. Equal throughput. Two periods can each finish two items whose earlier start dates differ. A carried-in seven-day item and a newly completed one-day item can contribute equally to the period’s count. Recipients experience different waiting even when completion totals match.

4. Old versus new behaviour. The existing tests describe selected existing contracts. They do not contain sponsor-approval requirements and cannot establish those requirements by passing. Keep them, add new acceptance/unit/integration checks for CR-12, and examine any changes to test setups required by the new category.

Chapter 12.11: applied diagnosis

5. Biased cohort. Filtering on both start and finish being within the week excludes long-running carried-in work that finished this week. Define the completion cohort by the finish event and compute durations from actual start timestamps, even before the window. Report unfinished-item age separately. A start-cohort study is possible, but it answers another question and includes censored unfinished observations.

6. Global approval Boolean. Approval belongs to a specific borrower/item/period at a specific time. A static flag loses that identity and can leak across requests and tests. The current BorrowingPolicy contract deliberately handles count/duration only. Obtain a typed approval decision through an application boundary; validate its match and time using explicit inputs before reservation creation. Retain a pure count/duration policy. A global “approved” state is not an adequate record or security mechanism.

Chapter 12.11: ML serving transfer

Choose a unit such as one requested release of a named model version to a stated serving environment. Define Done as the approved version being demonstrably served at that boundary with required checks, not merely a successful build. Keep build, approval and activation timestamps separate. An unavailable approval registry must not be treated as permission to serve.

Plan a narrow valid activation path, mismatched/expired/denied approval outcomes if those concepts are required, and registry failure evidence. Test the genuine registry boundary separately from orchestration doubles. Preserve the old serving path and record how disabling the new release path affects already active versions. Model accuracy concerns predictive behaviour under an evaluation design; build success concerns construction checks; approval is a governance decision. None substitutes for the others.

Chapter 12.10: lab completion reasoning

the book’s project produces the calculations, corrected interpretation, CR-12 plan and baseline reference. your team’s project contributes real repository evidence and an impact note for its actual released change. Accept different task splits and interfaces when they preserve the observation boundaries and have defensible dependencies. Do not assess by number of commits, charts or AI prompts.

A good impact note identifies the concrete touchpoints, uncertainty and first experiment. A weak note merely lists class names or repeats “add tests.” A good repository result includes traceable timestamps and honest population scope; an attractive chart with guessed timestamps is not an acceptable replacement.

8. Verification record and limits

The supplied calculator was compiled at Java release 17 and its 17 checks passed. The exact synthetic report above was executed. A separate boundary-mutant copy was checked to confirm the exclusive-window assertion detects the deliberately inclusive upper bound; restoration returned the checks to green. The Markdown file listings were reconstructed and run before delivery.

An independent calculation also reconciled the eight source rows, five completions, 16 completed points, 23 final scope points, seven remaining points, the five-item duration means and the daily chart table. Chapter 11’s source was retained without modification. Its prior 159-test result is a baseline reference, not a newly implemented CR-12 result.

No student repository or actual PR history has been analysed in this authoring run. RM-12 supplies the procedure and blank evidence record for that required learner activity. No sponsor approval, stakeholder acceptance, hosted pipeline or peer review is claimed. CR-12’s adapter, tests and feature flag are specifications for Chapter 13. The calculator does not handle reopened histories, revised estimates or business calendars.

Sources and assigned reading are listed in the main chapter. All sample records, contracts, code and answers here are original teaching material.

Appendix: Complete isolated Java exercise

Keep these files outside the running lending application’s source roots. The two Java classes have no package declaration so the plain-JDK commands above work as written.

File: lab/issues.tsv

id	points	accepted	started	done	removed
A	3	2026-10-01T00:00:00Z	2026-10-01T00:00:00Z	2026-10-03T00:00:00Z	
B	5	2026-10-01T00:00:00Z	2026-10-02T00:00:00Z	2026-10-06T00:00:00Z	
C	2	2026-10-01T00:00:00Z	2026-10-03T00:00:00Z	2026-10-04T00:00:00Z	
D	3	2026-10-01T00:00:00Z	2026-10-05T00:00:00Z	2026-10-08T00:00:00Z	
E	5	2026-10-01T00:00:00Z	2026-10-07T00:00:00Z		
F	2	2026-10-01T00:00:00Z			2026-10-08T00:00:00Z
G	3	2026-10-04T00:00:00Z	2026-10-06T00:00:00Z	2026-10-10T00:00:00Z	
H	2	2026-10-06T00:00:00Z	2026-10-08T00:00:00Z		

File: lab/FlowMetrics.java

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

/** Chapter 12 teaching tool v1.0. TSV input, UTC instants, elapsed 24-hour days. */
public final class FlowMetrics {
    public record Item(String id, Integer points, Instant accepted, Instant started,
                       Instant done, Instant removed) {
        public Item {
            if (id == null || id.isBlank()) throw new IllegalArgumentException("Blank id");
            if (points != null && points < 0) throw new IllegalArgumentException("Negative points");
            Objects.requireNonNull(accepted, "accepted");
            if (started != null && started.isBefore(accepted))
                throw new IllegalArgumentException("Start before acceptance: " + id);
            if (done != null && (started == null || done.isBefore(started)))
                throw new IllegalArgumentException("Invalid completion: " + id);
            if (removed != null && (done != null || removed.isBefore(accepted)
                    || (started != null && removed.isBefore(started))))
                throw new IllegalArgumentException("Invalid removal: " + id);
        }
    }

    public record Summary(int completed, String completedPoints, double meanLeadDays,
                          double meanCycleDays, double medianCycleDays, int wip) { }

    private FlowMetrics() { }

    public static List<Item> read(Path path) throws Exception {
        var lines = Files.readAllLines(path);
        String header = "id\tpoints\taccepted\tstarted\tdone\tremoved";
        if (lines.isEmpty() || !lines.get(0).equals(header))
            throw new IllegalArgumentException("Expected exact TSV header");
        List<Item> result = new ArrayList<>();
        var ids = new HashSet<String>();
        for (int line = 1; line < lines.size(); line++) {
            if (lines.get(line).isBlank()) continue;
            String[] f = lines.get(line).split("\t", -1);
            if (f.length != 6) throw new IllegalArgumentException("Six fields required at line " + (line + 1));
            if (!ids.add(f[0])) throw new IllegalArgumentException("Duplicate id: " + f[0]);
            result.add(new Item(f[0], f[1].isEmpty() ? null : Integer.valueOf(f[1]),
                    parse(f[2]), parse(f[3]), parse(f[4]), parse(f[5])));
        }
        return List.copyOf(result);
    }

    private static Instant parse(String text) {
        return text.isEmpty() ? null : Instant.parse(text);
    }

    public static double days(Instant start, Instant end) {
        Duration d = Duration.between(start, end);
        return d.getSeconds() / 86400.0 + d.getNano() / 86400_000_000_000.0;
    }

    public static List<Item> completed(List<Item> items, Instant from, Instant until) {
        if (!from.isBefore(until)) throw new IllegalArgumentException("Empty or reversed window");
        return items.stream().filter(i -> i.done() != null && !i.done().isBefore(from)
                && i.done().isBefore(until)).toList();
    }

    public static boolean inProgress(Item i, Instant at) {
        return i.started() != null && !i.started().isAfter(at)
                && (i.done() == null || i.done().isAfter(at))
                && (i.removed() == null || i.removed().isAfter(at));
    }

    public static Summary summarize(List<Item> items, Instant from, Instant until) {
        List<Item> cohort = completed(items, from, until);
        double[] cycles = cohort.stream().mapToDouble(i -> days(i.started(), i.done())).sorted().toArray();
        double lead = cohort.stream().mapToDouble(i -> days(i.accepted(), i.done())).average().orElse(Double.NaN);
        double cycle = java.util.Arrays.stream(cycles).average().orElse(Double.NaN);
        double median = cycles.length == 0 ? Double.NaN : cycles.length % 2 == 1
                ? cycles[cycles.length / 2] : (cycles[cycles.length / 2 - 1] + cycles[cycles.length / 2]) / 2;
        String points = cohort.stream().anyMatch(i -> i.points() == null) ? "NA"
                : Long.toString(cohort.stream().mapToLong(i -> i.points()).sum());
        return new Summary(cohort.size(), points, lead, cycle, median,
                (int) items.stream().filter(i -> inProgress(i, until)).count());
    }

    public static long scopePoints(List<Item> items, Instant at) {
        return items.stream().filter(i -> !i.accepted().isAfter(at)
                && (i.removed() == null || i.removed().isAfter(at))).mapToLong(i -> {
                    if (i.points() == null) throw new IllegalArgumentException("Scope point missing: " + i.id());
                    return i.points();
                }).sum();
    }

    public static long donePointsAt(List<Item> items, Instant at) {
        return items.stream().filter(i -> i.done() != null && !i.done().isAfter(at)).mapToLong(i -> {
            if (i.points() == null) throw new IllegalArgumentException("Done point missing: " + i.id());
            return i.points();
        }).sum();
    }

    private static String number(double n) {
        return Double.isNaN(n) ? "NA" : String.format(Locale.ROOT, "%.2f", n);
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 3 && args.length != 4)
            throw new IllegalArgumentException("Usage: FlowMetrics file.tsv fromInstant untilInstant [--scope]");
        if (args.length == 4 && !args[3].equals("--scope")) throw new IllegalArgumentException("Unknown option");
        List<Item> items = read(Path.of(args[0]));
        Instant from = Instant.parse(args[1]);
        Instant until = Instant.parse(args[2]);
        Summary s = summarize(items, from, until);
        System.out.println("Completions in [from, until): " + s.completed());
        System.out.println("Completed points: " + s.completedPoints());
        System.out.println("Mean lead days: " + number(s.meanLeadDays()));
        System.out.println("Mean cycle days: " + number(s.meanCycleDays()));
        System.out.println("Median cycle days: " + number(s.medianCycleDays()));
        System.out.println("WIP at until (after events at that instant): " + s.wip());
        for (Item i : items) if (inProgress(i, until))
            System.out.println("Age " + i.id() + ": " + number(days(i.started(), until)) + " days");
        if (args.length == 4) {
            System.out.println("at,scope_points,done_points,remaining_points,wip");
            for (Instant at = from; !at.isAfter(until); at = at.plus(Duration.ofDays(1))) {
                long scope = scopePoints(items, at);
                long done = donePointsAt(items, at);
                Instant sample = at;
                long wip = items.stream().filter(i -> inProgress(i, sample)).count();
                System.out.println(at + "," + scope + "," + done + "," + (scope - done) + "," + wip);
            }
        }
    }
}

File: lab/FlowMetricsChecks.java

import java.nio.file.Path;
import java.time.Instant;
import java.util.List;

/** Plain Java checks throw explicitly; -ea is not required. */
public final class FlowMetricsChecks {
    private static int checks;
    private static Instant day(int n) { return Instant.parse("2026-10-%02dT00:00:00Z".formatted(n)); }
    private static void check(boolean value, String description) {
        checks++;
        if (!value) throw new AssertionError(description);
    }
    private static void invalid(Runnable operation, String description) {
        checks++;
        try { operation.run(); } catch (IllegalArgumentException expected) { return; }
        throw new AssertionError(description);
    }
    public static void main(String[] args) throws Exception {
        var data = FlowMetrics.read(Path.of("issues.tsv"));
        var s = FlowMetrics.summarize(data, day(1), day(11));
        check(s.completed() == 5 && s.completedPoints().equals("16"), "Count and points differ");
        check(Math.abs(s.meanCycleDays() - 2.8) < 1e-9, "Mean cycle");
        check(Math.abs(s.meanLeadDays() - 4.6) < 1e-9, "Mean lead");
        check(s.medianCycleDays() == 3, "Median cycle");
        check(s.wip() == 2, "Open items remain WIP");
        check(FlowMetrics.scopePoints(data, day(4)) == 23, "Added scope counted");
        check(FlowMetrics.scopePoints(data, day(8)) == 23, "Removed scope excluded");
        check(FlowMetrics.donePointsAt(data, day(8)) == 13, "Removal is not completion");
        check(FlowMetrics.completed(data, day(1), day(6)).size() == 2, "Exclusive upper bound");
        check(FlowMetrics.completed(data, day(6), day(11)).size() == 3, "Inclusive lower bound");
        check(!FlowMetrics.inProgress(data.get(1), day(6)), "Done at snapshot is not WIP");
        check(Math.abs(FlowMetrics.days(day(1), day(1).plusSeconds(3600)) - 1.0 / 24) < 1e-9,
                "Do not truncate fractional days");
        var empty = FlowMetrics.summarize(List.of(), day(1), day(2));
        check(empty.completed() == 0 && Double.isNaN(empty.meanCycleDays()), "No completions is not zero duration");
        var unknown = new FlowMetrics.Item("PR1", null, day(1), day(1), day(2), null);
        check(FlowMetrics.summarize(List.of(unknown), day(1), day(3)).completedPoints().equals("NA"), "Unknown points");
        invalid(() -> new FlowMetrics.Item("X", 1, day(2), day(1), null, null), "Invalid ordering rejected");
        invalid(() -> new FlowMetrics.Item("X", 1, day(1), day(1), day(2), day(3)), "Done plus removed rejected");
        invalid(() -> FlowMetrics.summarize(data, day(3), day(3)), "Invalid window rejected");
        System.out.println("FlowMetricsChecks: " + checks + " checks passed");
    }
}