Commit ee87139
feat(pkg): natsrouter improvements + mongoutil bulk API + new minioutil (#157)
* feat(natsrouter): admission control + panic safety + graceful shutdown + HandlerTimeout
Five improvements to pkg/natsrouter, designed and reviewed via
docs/superpowers/plans/2026-04-30-natsrouter-improvements.md:
- WithMaxConcurrency option + ErrUnavailable error: bounds in-flight
handler goroutines with a configurable cap (default unbounded /
gin-style; set to a positive int to throttle). Excess requests get a
fast ErrUnavailable reply rather than blocking the consumer.
- Spawn-site panic backstop: a recover() wrapper around the spawned
handler goroutine releases the semaphore slot and decrements the
in-flight WaitGroup so a panicking handler can't deadlock the router
or leak permits. The backstop also publishes a "internal error" reply
so the requester sees a response rather than a timeout.
- Shutdown waits for in-flight handlers: r.Shutdown(ctx) drains
the in-flight WaitGroup before returning, with the supplied ctx
bounding the wait. Prevents the long-standing race where
nc.Drain() returns before handler goroutines have finished writing
their replies.
- HandlerTimeout middleware: per-handler deadline that returns
ErrTimeout when the inner handler runs past its budget. Composes
cleanly with the spawn-site backstop.
- Default() constructor + doc.go example + README rewrite: newcomers
can now read pkg/natsrouter/README.md end-to-end and build a
working service without bouncing through the older spec docs.
Reviewed across multiple rounds by bug, architecture, and NATS-expert
agents; consolidated errata applied. Integration tests exercise the
admission cap, the panic backstop (process survives + follow-up
requests succeed), and the shutdown wait.
* fix(search-service): close shutdown gap; rename Header to GetHeader
Two small fixes to search-service/main.go surfaced during the
natsrouter shutdown work:
- Shutdown now waits for the natsrouter's in-flight handlers before
draining nc, closing the previous race where requests arriving
during shutdown could be dropped after the consumer drained but
before handler goroutines finished.
- Rename Header() -> GetHeader() to match the *otelnats.Conn idiom
the rest of the codebase uses.
* feat(mongoutil): promote generic helpers + add three-layer bulk-write API
Two changes that share the same package boundary:
1. PROMOTION (refactor). Move the generic Mongo helpers out of
history-service/internal/mongorepo/ into the shared pkg/mongoutil:
- Collection[T] generic wrapper (FindOne / FindByID / FindMany
with (nil, nil) not-found semantics; Aggregate; AggregatePaged
with $facet pagination + 16 MB caveat documented; Raw escape
hatch).
- OffsetPageRequest / OffsetPage[T] / EmptyPage[T] /
NewOffsetPageRequest pagination types.
- QueryOption + WithProjection / WithSort / WithLimit / WithSkip
functional options.
history-service/internal/mongorepo retains domain-specific repos
(subscription, threadroom, pipelines) and flips imports to
mongoutil. The TestCollection_* integration tests extract from
subscription_test.go into pkg/mongoutil/collection_integration_test.go;
subscription_test.go keeps only TestSubscriptionRepo_*. New
mongorepo/setup_test.go is a thin testutil.MongoDB wrapper used by
the remaining domain tests. Mock file regenerated to use the new
imports. No logic changes to the domain code -- pure relocation
plus reference flips.
2. BULK-WRITE API (new). Three layers mirroring FindOne/FindByID:
- BulkWrite (foundation): wraps mongo-driver's BulkWrite with
explicit SetOrdered(false), short-circuits on empty input to
(nil, nil), preserves the partial-success *BulkResult on
partial failure so callers can inspect WriteErrors via
errors.As(err, &mongo.BulkWriteException{}).
- BulkUpsert (typed convenience): builds N UpsertModels from
items + filter mapper. \$set MERGE semantics (preserves stored
fields not in T). bsonSetWithoutID strips _id from the marshal
payload because MongoDB rejects updates to the immutable _id;
_id is set on insert from the upsert filter.
- BulkUpsertByID (ergonomic): pure pass-through with built-in
bson.M{"_id": idFn(item)} filter. Cheapest possible bulk-upsert
pattern (always-indexed _id, single B-tree lookup).
- InsertMany (sibling to bulk-upsert): write-only batch with
SetOrdered(false), returns (int64, error) so partial-failure
callers see how many items got through.
BulkResult mirrors mongo.BulkWriteResult fields the wrapper
exposes; UpsertModel + DeleteModel are stateless write-model
constructors; fromDriverResult is the driver-to-wrapper mapper.
Empty-input contract is (nil, nil) for the bulk methods and
(0, nil) for InsertMany; both are documented at the type level.
Designed via docs/superpowers/specs/2026-05-06-mongoutil-extension-
and-miniout-design.md (spec) and executed via the matching plan.
Spec was reviewed across 2 rounds by bug, spec-consistency, senior
architecture, and Mongo-expert agents before any code landed; each
plan task was implemented via TDD and reviewed by spec-compliance
plus code-quality agents (with Mongo expert on tasks involving
substantive driver interaction). The "Post-merge amendments"
section in the spec captures changes that landed after the
resolution log was finalized (BulkUpsert _id strip, bsonSetWithoutID
location + error wrapping, BulkUpsertByID always-indexed note).
* feat(minioutil): typed JSON-blob wrapper around minio-go/v7
New pkg/minioutil follows the pkg/<provider>util convention used by
cassutil/mongoutil/natsutil/valkeyutil. Surface:
- Connect(ctx, endpoint, useSSL, accessKey, secretKey)(*minio.Client, error)
Construct-only. Does NOT issue a connectivity probe -- a probe via
ListBuckets requires s3:ListAllMyBuckets (account-wide IAM); real
production deployments scope credentials to one bucket via
s3:ListBucket on that bucket's ARN, so probing here would force
broader IAM than the package needs. NewBucket carries the actual
fail-fast probe (bucket-scoped BucketExists). The ctx parameter
is retained for signature symmetry with valkeyutil.Connect.
- Bucket[T any] + NewBucket[T](ctx, client, bucketName) (*Bucket[T], error)
Typed wrapper binding a *minio.Client to a single bucket and a
JSON-marshalable payload type T. NewBucket calls
client.BucketExists at construction so a misconfigured
MINIO_BUCKET env var fails the service at startup rather than
failing every Get/Put silently. Does NOT create the bucket --
provisioning is owned by ops/IaC.
- Bucket[T].Put(ctx, key, v)
Marshals v as JSON, uploads with explicit Content-Type:
application/json; charset=utf-8 so downstream tools (S3 CLI,
browsers, other languages) can identify the payload format
without out-of-band knowledge.
- Bucket[T].Get(ctx, key) (*T, error)
Returns (nil, nil) on missing key, matching Collection.FindOne's
not-found semantics. The NoSuchKey response surfaces from
obj.Stat() (minio-go's GetObject is lazy); errors.As against
minio.ErrorResponse with Code=="NoSuchKey" is the discriminator.
Two HTTP round trips per Get (HEAD via Stat, then GET via
Decode); acceptable for the small-JSON-blob workload.
- Bucket[T].List(ctx, prefix, maxKeys) ([]string, error)
Lists object keys within the bucket (NOT bucket names). maxKeys=0
falls back to defaultListCap (1000 -- matches S3's per-page cap)
to prevent accidental unbounded scans on misuse. context.WithCancel
+ defer cancel() is load-bearing: minio-go's ListObjects spawns a
goroutine that fills the returned channel, and breaking out of
the range loop without cancelling leaks that goroutine. MaxKeys
is plumbed through to ListObjectsOptions so the server returns
only min(maxKeys, 1000) per page rather than the full default.
- Bucket[T].Delete(ctx, key) error
Idempotent on non-versioned buckets -- relies on S3 native
semantics (DELETE returns 204 regardless of prior existence).
- Bucket[T].Raw() *minio.Client and Bucket[T].Name() string
Escape hatches mirroring Collection[T].Raw() so callers needing
features the wrapper does not surface (presigned URLs, multipart,
conditional Put, tagging, versioning) can reach the underlying
client without giving up the bucket binding.
Test infrastructure:
- pkg/testutil.MinIO(t, prefix) mirrors testutil.MongoDB: sync.Once-
shared MinIO container across the test process plus a per-test
bucket name derived from a stable fnv hash of t.Name(). Bucket
names are S3-valid (lowercase letters, digits, hyphens; capped at
63 chars). Best-effort cleanup with a 30s timeout.
- pkg/testutil/testimages adds the MinIO image pin so all
minio-touching integration tests track the same version.
- pkg/minioutil/minio_test.go has only the trivial Bucket[T].Raw /
Name accessor unit test. Hand-rolled S3 stub-server unit tests
were considered and rejected during round-2 review -- emulating
S3's wire format is fragile against minio-go upgrades and
provides false confidence; testcontainers MinIO is fast enough
(~1m for the full suite) to be the sole behavioral backstop.
- The List integration test uses goleak.IgnoreCurrent() snapshotted
before the List call to verify the channel-cleanup pattern
without false-positives from minio-go's HTTP keepalive
goroutines (IdleConnTimeout=60s, longer than goleak's drain
budget; goleak's defaults do not cover net/http.persistConn).
New deps (all Apache-2.0):
- github.com/minio/minio-go/v7 -- MinIO Go SDK
- github.com/testcontainers/testcontainers-go/modules/minio -- test only
- go.uber.org/goleak -- test only, for the List leak guard
Designed via docs/superpowers/specs/2026-05-06-mongoutil-extension-
and-miniout-design.md (the same design doc that covers the
mongoutil extension; see prior commit). 7 implementation tasks
(Tasks 9-15), each TDD + reviewed by spec-compliance, code-quality,
and S3-expert agents on the load-bearing tasks (Connect, NewBucket,
Get NoSuchKey, List goroutine cleanup).
* fix(natsrouter,testutil): close Add+Wait race; tighten panic-backstop test fidelity; document best-effort cleanup
CodeRabbit review on PR #157 surfaced three items on the squashed
branch:
(MAJOR) pkg/natsrouter/router.go — Shutdown's wg.Wait() phase could
race with Add(1) calls from still-draining subscriptions when the
closeLoop broke early on ctx.Done(). Per sync.WaitGroup docs, Add
concurrent with Wait is undefined when the counter is zero (panic
risk). Fix: track allClosed; only enter the wg.Wait block when every
subscription confirmed close. If ctx expired before all subs closed,
surface the error and let the caller's deadline take precedence --
remaining handler goroutines continue in the background until process
exit. Comment block rewritten to explain the invariant.
(NIT) pkg/natsrouter/integration_test.go — TestIntegration_SpawnSite-
PanicBackstop used WithMaxConcurrency(2). With cap=2, a leaked
semaphore slot would still leave capacity for the follow-up "ok"
request, masking a cleanup regression. Switched to cap=1 so a leaked
slot blocks the second request and the test actually observes slot
release. Added a comment explaining why cap=1 is load-bearing.
(NIT) pkg/testutil/minio.go — replaced two `_ = container.Terminate(ctx)`
calls with explicit "best-effort cleanup" comments per CLAUDE.md
"never ignore errors silently — comment if intentionally discarded".
The discarded errors are intentional (init already failed; Docker
reaps the container on test-process exit either way), now documented.
* feat(model): expand Employee struct with full org hierarchy + Org/OrgType
Replaces the 3-field Employee placeholder (AccountName, Name, EngName)
with the full schema looked up from the employee MongoDB collection.
New fields cover:
- Identity: ID (_id), EmployeeID, EmployeeCategory, EmployeeRoom,
Status, RosterCode, SiteID, Company.
- Organisational hierarchy: 8 levels x 4 attrs each (description / id /
name / tcName) for Department, Department1, Division, Division1,
Section, Section1, Function, Function1; plus DivTCName, ManagedOrgIDs,
Orgs []Org, and OrgCode (computed; bson:"-").
- Contact: Mail, Phone, Phones, JOSLocationURL, Location, LocationURL.
- Shift: ShiftCode, ShiftCodeDescEn/Zh, ShiftStartTime, ShiftEndTime.
- Supervisor: SupervisorAccountName, SupervisorID, SupervisorName,
SupervisorEngName, SupervisorPhone, SupervisorPhones.
Renames the Go field EngName -> EnglishName (bson tag stays "engName"
so wire format is unchanged). Zero callers of model.Employee in the
codebase today, so the rename has no breakage.
Adds two supporting types:
- OrgType (string enum: section1, section, department1, department,
division1, division, function1, function).
- Org (id / description / name / tcName / type).
Every field has both bson and json tags per CLAUDE.md "All model
structs get both json and bson tags". Empty/optional fields use
omitempty consistently.
* refactor(mongoutil,minioutil): trim verbose godocs to essentials
Per project rule: max ~2 lines of comment, only when WHY is non-obvious.
Cuts roughly 250 lines of doc that restated MinIO/Mongo SDK behavior,
behavior already implied by function names + signatures, or
multi-paragraph rationales better captured in commit history.
What's kept:
- One-line WHY notes for non-obvious behavior (e.g. (nil, nil)
not-found contract, FindMany returns []T{} not nil for JSON, $set
is MERGE not REPLACE, _id stripping is required because Mongo
rejects updates to immutable _id, defer cancel() is load-bearing
to avoid a goroutine leak).
- One-line caveats with real footgun risk (omitempty caveat on
BulkUpsert, 16 MB BSON limit on AggregatePaged $facet).
- A brief explanation of why minioutil.Connect doesn't probe at
startup (least-privilege IAM rationale).
What's gone:
- Multi-paragraph "Implementation note" sections that restated
what the code clearly shows.
- Field-by-field BulkResult godoc; replaced with a one-line summary
+ the empty-input contract on the type itself.
- Long pitfall lists where 1 keyword would do.
- "Standard wiring" code-example blocks in godoc.
- Restatements of S3 / Mongo native behavior callers can find in
upstream docs.
Net: ~250 fewer comment lines across 5 files, no logic change.
* fix: address CodeRabbit + apply max-2-line comment rule
CodeRabbit (3 findings):
- pkg/mongoutil/bulk.go: UpsertModel/DeleteModel return concrete
*mongo.UpdateOneModel/*mongo.DeleteOneModel instead of the
interface (Go idiom + CLAUDE.md "accept interfaces, return
structs"). bulk_test.go drops the now-redundant type assertions.
- pkg/natsrouter/integration_test.go panic-backstop test: typed
payload struct instead of map[string]any (CLAUDE.md "never
map[string]interface{} for NATS payloads").
- pkg/natsrouter/integration_test.go shutdown-wait test: client
goroutines now collected via sync.WaitGroup; nc.Request errors
drain through an error channel and are asserted, no more silent
drops or tail goroutines past the assertion boundary.
Comment trim (every modified file outside pkg/natsrouter):
Max 2 lines per comment, mostly 1-liners. Cuts the BulkUpsert
godoc from 5 lines to 2; InsertMany 3->1; minioutil.Connect 3->1;
testutil.MinIO 11->2; search-service shutdown 5->1; plus several
2->1 collapses. Zero logic change.
* feat(restyutil): typed Resty client wrapper for outbound HTTP
New pkg/restyutil produces a *resty.Client with codebase defaults
wired in: OTel transport (so trace context propagates on every
outbound call), slog response logging at debug level, 30s timeout.
Options compose on top: WithTimeout / WithHeader / WithBearerToken /
WithRetries.
Replaces the raw net/http boilerplate seen in pkg/oidc and
pkg/searchengine — services adopting this can write
client.R().SetContext(ctx).SetBody(req).SetResult(&resp).Post("/path")
instead of building requests, checking statuses, and hand-rolling
JSON decode at every call site.
Adds github.com/go-resty/resty/v2 (Apache-2.0) as the canonical
HTTP client per CLAUDE.md (the rule was previously aspirational --
nothing in the repo used Resty before this).
Tests cover defaults, every Option, end-to-end round-trip via
httptest, and retry-on-5xx behaviour.
* fix(restyutil): apply 3-reviewer findings (drop WithRetries, add OnError + WithTransport, log hygiene)
Three reviewers (bug / code-quality / senior-engineer) consolidated
the same actionable items:
- Drop WithRetries entirely. Resty's default retry condition only
fires on transport errors, not 5xx, so the option was misleading
without an accompanying retry-condition helper. YAGNI-applied:
no current call site needs retries; bring it back when a real
caller has a concrete retry policy in mind.
- Rename WithRetries' max parameter -> moot (option dropped). The
shadow of the Go 1.21 builtin `max` is no longer present.
- Add OnError hook so transport errors (connect refused, DNS, ctx
deadline, TLS) log too. Without it the helper REGRESSED
observability vs. the raw net/http boilerplate it replaces.
- Add WithTransport(http.RoundTripper) Option. Wraps the supplied
transport with otelhttp so OTel propagation is preserved by
construction, even with custom TLS / proxy / dialer. Required
before pkg/oidc (which needs InsecureSkipVerify in dev) can
migrate.
- Strip query string from URL in slog log fields. URLs can carry
?token=... or ?api_key=... in the wild; CLAUDE.md "Never log
tokens, passwords, or full message bodies".
- Propagate request_id from ctx into the slog line via
natsutil.RequestIDFromContext. CLAUDE.md "include in all log
lines".
Tests cover OnError firing on transport error, WithTransport
preserving the custom round-tripper, request_id propagation, and
query-string stripping.
* fix(natsrouter,docs): close post-Shutdown dispatch race; sync stale plan docs
CodeRabbit pass on the rebased branch surfaced 1 real bug + 5 cleanups.
(MAJOR) router.go: Shutdown previously had a window where late NATS
callbacks (subscription mid-drain or fired after ctx expiry) could
still call admit() + wg.Add(1) + spawn a handler goroutine AFTER
Shutdown returned. That goroutine could then race teardown of caller-
owned dependencies (DB closed, NATS drained). Add a stopping atomic.Bool
gate that Shutdown sets BEFORE any drain step; natsHandler checks it
first and replies busy instead of admitting. Existing in-flight
handlers still drain through the wg.Wait path; only NEW dispatches
post-Shutdown-start are rejected.
(NIT) router.go: removed the exported Registrar interface — used only
by Register/RegisterNoBody/RegisterVoid in this package, no external
mocks, no alternative implementation. Their first parameter is now
*Router directly. Pure simplification: existing callers (which pass
*Router today) keep working. CLAUDE.md "interfaces in the consumer".
(NIT) restyutil_test.go: switched the two log-capture tests from
slog.NewTextHandler to NewJSONHandler; CLAUDE.md "always JSON, never
text-format". Updated the matched assertions.
(DOC) 2026-04-30-natsrouter-improvements.md: marked the original
"default 100" + per-route WithConcurrency wording as superseded with
a post-implementation note. Shipped behavior: unbounded default,
single router-level WithMaxConcurrency.
(DOC) 2026-05-06-mongoutil-extension-and-minioutil.md Task 9: dropped
the ListBuckets startup probe from the Connect code block; added a
post-implementation note pointing at the spec's amendments section.
Future agents executing this plan get the IAM-compliant code.
(DOC) Same plan, Task 14: replaced the stale defer goleak.VerifyNone(t)
pattern with goleak.IgnoreCurrent() baseline; updated the comment to
explain why minio-go's HTTP keepalive goroutines (IdleConnTimeout=60s)
require it.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 61f128a commit ee87139
46 files changed
Lines changed: 7260 additions & 321 deletions
File tree
- docs/superpowers
- plans
- specs
- history-service/internal
- mongorepo
- service
- mocks
- pkg
- minioutil
- model
- mongoutil
- natsrouter
- restyutil
- testutil
- testimages
- search-service
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1001 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 2822 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 607 additions & 0 deletions
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
| 15 | + | |
14 | 16 | | |
15 | 17 | | |
16 | 18 | | |
| |||
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
| 25 | + | |
23 | 26 | | |
24 | 27 | | |
25 | 28 | | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
30 | 34 | | |
31 | 35 | | |
| 36 | + | |
32 | 37 | | |
33 | 38 | | |
34 | 39 | | |
| |||
57 | 62 | | |
58 | 63 | | |
59 | 64 | | |
| 65 | + | |
60 | 66 | | |
61 | 67 | | |
62 | 68 | | |
63 | 69 | | |
64 | 70 | | |
| 71 | + | |
65 | 72 | | |
66 | 73 | | |
67 | 74 | | |
| |||
78 | 85 | | |
79 | 86 | | |
80 | 87 | | |
| 88 | + | |
81 | 89 | | |
82 | 90 | | |
83 | 91 | | |
84 | 92 | | |
| 93 | + | |
85 | 94 | | |
| 95 | + | |
86 | 96 | | |
87 | 97 | | |
88 | 98 | | |
| |||
99 | 109 | | |
100 | 110 | | |
101 | 111 | | |
| 112 | + | |
102 | 113 | | |
103 | 114 | | |
104 | 115 | | |
105 | 116 | | |
106 | 117 | | |
107 | 118 | | |
108 | 119 | | |
| 120 | + | |
109 | 121 | | |
110 | 122 | | |
| 123 | + | |
111 | 124 | | |
112 | 125 | | |
113 | 126 | | |
| |||
117 | 130 | | |
118 | 131 | | |
119 | 132 | | |
| 133 | + | |
120 | 134 | | |
121 | | - | |
122 | 135 | | |
123 | 136 | | |
124 | 137 | | |
125 | 138 | | |
126 | 139 | | |
127 | 140 | | |
| 141 | + | |
128 | 142 | | |
129 | 143 | | |
130 | 144 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
| 67 | + | |
66 | 68 | | |
67 | 69 | | |
68 | 70 | | |
| |||
77 | 79 | | |
78 | 80 | | |
79 | 81 | | |
| 82 | + | |
| 83 | + | |
80 | 84 | | |
81 | 85 | | |
82 | 86 | | |
| |||
94 | 98 | | |
95 | 99 | | |
96 | 100 | | |
| 101 | + | |
| 102 | + | |
97 | 103 | | |
98 | 104 | | |
99 | 105 | | |
| |||
121 | 127 | | |
122 | 128 | | |
123 | 129 | | |
| 130 | + | |
124 | 131 | | |
125 | 132 | | |
| 133 | + | |
| 134 | + | |
126 | 135 | | |
127 | 136 | | |
128 | 137 | | |
| |||
140 | 149 | | |
141 | 150 | | |
142 | 151 | | |
| 152 | + | |
| 153 | + | |
143 | 154 | | |
144 | 155 | | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
145 | 160 | | |
146 | 161 | | |
147 | 162 | | |
| |||
183 | 198 | | |
184 | 199 | | |
185 | 200 | | |
| 201 | + | |
| 202 | + | |
186 | 203 | | |
187 | 204 | | |
188 | 205 | | |
| |||
205 | 222 | | |
206 | 223 | | |
207 | 224 | | |
| 225 | + | |
| 226 | + | |
208 | 227 | | |
209 | 228 | | |
210 | 229 | | |
| |||
224 | 243 | | |
225 | 244 | | |
226 | 245 | | |
| 246 | + | |
| 247 | + | |
227 | 248 | | |
228 | 249 | | |
229 | 250 | | |
230 | 251 | | |
| 252 | + | |
| 253 | + | |
231 | 254 | | |
232 | 255 | | |
233 | 256 | | |
| |||
247 | 270 | | |
248 | 271 | | |
249 | 272 | | |
250 | | - | |
251 | | - | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
252 | 277 | | |
253 | 278 | | |
254 | 279 | | |
| |||
283 | 308 | | |
284 | 309 | | |
285 | 310 | | |
| 311 | + | |
| 312 | + | |
286 | 313 | | |
287 | 314 | | |
288 | 315 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
16 | | - | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | | - | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
34 | | - | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
0 commit comments