Skip to content

feat(contracts): add a gated machine-readable boundary inventory - #3539

Merged
ctwoodwa merged 2 commits into
mainfrom
fix/3524-boundary-inventory
Aug 3, 2026
Merged

feat(contracts): add a gated machine-readable boundary inventory#3539
ctwoodwa merged 2 commits into
mainfrom
fix/3524-boundary-inventory

Conversation

@ctwoodwa

@ctwoodwa ctwoodwa commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

feat(contracts): add a gated machine-readable boundary inventory

The protocol manifest enumerated COVERED operations only, so the uncovered Carrier surface was
invisible rather than documented. That is what made "migrate incrementally" unverifiable — progress
and completeness looked identical, and nobody could answer what was left without re-deriving it.

The inventory is derived from the PRODUCERS — the Tauri command list, the Hull adapter faces and
the production local-node route registrations — not from the schema. An inventory authored from the
schema would restate what is already covered and prove nothing; the point is to name what is not.
232 boundaries: 19 covered, 213 deferred, each deferred entry carrying a reason.

It is gated rather than decorative. A boundary appearing in neither the covered set nor the
inventory fails the build, so the document cannot rot silently the next time somebody adds an
endpoint. A stale inventory would be worse than none, because it reads as authoritative.

Kept as a separate file rather than folded into the manifest: the manifest is the generator's input,
and listing deferred operations there would present them to code generation as implemented
contracts.

No endpoint was migrated. That was explicitly out of scope and remains so.

Proven by adding a fake command to the real handler list — the gate fails naming it.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

Summary by CodeRabbit

  • Documentation

    • Added guidance for tracking runtime boundaries separately from generated protocol manifests.
    • Documented required classifications and explanations for deferred or blocked operations.
  • Validation Improvements

    • Added automated checks to detect missing, outdated, duplicate, or unexplained runtime-boundary entries.
    • Improved consistency checks between documented runtime operations and supported protocol operations.
  • Tests

    • Expanded coverage for supported registration formats and invalid or unrecognized endpoint definitions.
    • Added verification that generated inventories remain synchronized with the project’s declarations.

The protocol manifest enumerated COVERED operations only, so the uncovered Carrier surface was
invisible rather than documented. That is what made "migrate incrementally" unverifiable — progress
and completeness looked identical, and nobody could answer what was left without re-deriving it.

The inventory is derived from the PRODUCERS — the Tauri command list, the Hull adapter faces and
the production local-node route registrations — not from the schema. An inventory authored from the
schema would restate what is already covered and prove nothing; the point is to name what is not.
232 boundaries: 19 covered, 213 deferred, each deferred entry carrying a reason.

It is gated rather than decorative. A boundary appearing in neither the covered set nor the
inventory fails the build, so the document cannot rot silently the next time somebody adds an
endpoint. A stale inventory would be worse than none, because it reads as authoritative.

Kept as a separate file rather than folded into the manifest: the manifest is the generator's input,
and listing deferred operations there would present them to code generation as implemented
contracts.

No endpoint was migrated. That was explicitly out of scope and remains so.

Proven by adding a fake command to the real handler list — the gate fails naming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds runtime-boundary discovery and validation for Tauri, Hull, and local-node operations. It adds a checked-in inventory with covered, deferred, and blocked classifications, plus parser and integration tests.

Changes

Runtime boundary inventory

Layer / File(s) Summary
Boundary discovery and parser coverage
tooling/carrier-contract-codegen/boundary-inventory.mjs, tooling/carrier-contract-codegen/tests/generate.test.mjs
Discovers Tauri commands, Hull faces, and local-node routes. Rejects unknown or malformed registrations. Tests cover supported registration forms and parser failures.
Inventory construction and validation
tooling/carrier-contract-codegen/boundary-inventory.mjs, packages/contracts/protocol/boundary-inventory.json, tooling/carrier-contract-codegen/tests/generate.test.mjs, packages/contracts/protocol/README.md
Builds and validates the runtime inventory against discovered boundaries and manifest.json. The checked-in inventory records covered, deferred, and blocked entries. Documentation describes required classifications, reasons, and normalization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels: needs-deep-review

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning issue [blocking]: The description explains the change well but omits the required ICM Stage, Affected Packages, and Checklist sections. Add the missing template sections and complete the applicable ICM, package, validation, API, documentation, and compatibility checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed praise: The title clearly and concisely describes the gated machine-readable boundary inventory added by this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3524-boundary-inventory

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

thought (non-blocking):

Accessibility audit (advisory)

The sharded axe audit is report-only while the baseline and runtime budget mature.
Unknown moderate-or-higher findings must be resolved or added to the reviewed
accessibility baseline.

Shard Result Findings
1/4 pass 0 axe finding(s), 1 coverage warning(s)
2/4 pass 0 axe finding(s), 1 coverage warning(s)
3/4 pass 0 axe finding(s), 1 coverage warning(s)
4/4 pass No moderate-or-higher findings
Shard 1 report
[shipyard-syncstate-indicator] skipping directionalIcons enforcement: component has not opted in via [data-shipyard-direction="<state>"] sentinel.
Shard 2 report
[shipyard-button] skipping keyboardMap enforcement: component has not opted in via data-shipyard-keyboard-map="enabled".
Shard 3 report
[shipyard-dialog] skipping keyboardMap enforcement: component has not opted in via data-shipyard-keyboard-map="enabled".

…ing as coverage

The walker matched only the five .MapGet/Post/Put/Delete/Patch helpers, so three live
production boundaries were absent from an artifact published as complete — the PATCH
maintenance route registered through MapMethods, the peer-to-peer sync WebSocket at /ws,
and /health. It also counted ten routes from the sync and two-user dev harnesses under
tools/, which the host excludes from compilation and which therefore never ship.

Now discovered: MapMethods (one boundary per declared verb), MapWebSocketPath as its own
kind, and MapHealthChecks. Any other .Map<Uppercase>( spelling is a hard error unless it
is listed as a non-boundary with the reason it registers no route — an unknown
registration form has to break the build rather than shrink the denominator in silence.

The Tauri and Hull parsers had the same failure mode against plausible refactors. A
command listed without a module path, a face declared without async, and a face declared
below the private helper each matched nothing and vanished. Both parsers now enumerate
structurally and refuse what they cannot parse.

On the reasons field — 213 deferred entries carried one identical sentence, which read as
213 authored judgements and carried none. The blanket reason moves to a status-wide
default, an entry may still override it, and restating the default is now an error. The
three newly found boundaries carry specific reasons instead.

The summary line reported "covers all 232 discovered operations" while 213 were deferred.
It now prints the covered, deferred and blocked split. Net count 232 to 225.

Proof — the three new forms and the unknown-form refusal are established by mutation. The
gate goes red naming the missing boundary and the unclassified helper. Four new parser
tests feed synthetic producers, and reverting each parser turns exactly its own test red.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
tooling/carrier-contract-codegen/boundary-inventory.mjs (4)

112-114: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

thought: parseTauriCommands accepts the first generate_handler![ in the file, including one inside a comment.

The regex at line 113 is non-greedy and unanchored. A commented-out or #[cfg]-gated generate_handler![...] earlier in lib.rs wins, and the real list is never read. The result is a silently wrong command set, which is the exact fail-open class the header comment at lines 109-111 says these parsers replaced.

Every other branch in this file is fail-closed. Rejecting a file that contains more than one generate_handler![ would keep that property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 112 -
114, Update parseTauriCommands to detect all generate_handler![ occurrences and
reject the source when more than one is present, rather than selecting the first
match. Preserve the existing missing-handler error for zero matches and parse
the command list only when exactly one occurrence is found.

83-88: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

suggestion: the manifest cross-check does not detect two covered boundaries that claim the same operationId.

Lines 86-87 compare the two id lists by membership only. If two covered entries both carry hull.invoke, both checks pass, and a different manifest operation can still look covered because its id happens to appear once. The counts never have to agree.

The covered set is the coverage claim in this artifact, so a duplicated claim is the one error worth catching hardest. Add a uniqueness check.

♻️ Proposed check for duplicate covered operation ids
     const coveredIds = entries.filter((entry) => entry.status === 'covered').map((entry) => entry.operationId).sort()
+    const claimed = new Set()
+    for (const id of coveredIds) {
+      if (claimed.has(id)) errors.push(`manifest operation ${id} is claimed by more than one covered runtime boundary`)
+      claimed.add(id)
+    }
     for (const id of manifestIds) if (!coveredIds.includes(id)) errors.push(`covered manifest operation has no runtime boundary: ${id}`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 83 -
88, Update the manifest cross-check in boundary-inventory.mjs to detect
duplicate operationId values among entries with status === 'covered' before or
alongside the existing membership checks. Use the covered entries’ operation IDs
and report each duplicated claim, while preserving the current manifest/runtime
mismatch validation.

92-102: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

suggestion: coveredOperationId guesses coverage from naming instead of reading the manifest.

For tauri-command and hull-face, this function returns an id for every boundary. It never checks that the id exists in manifest.json. The name transform at line 96 is the sole authority.

Two consequences:

  • A new Tauri command is auto-classified covered at line 49 with an operation id that does not exist. The build fails at line 87 with covered runtime boundary has no manifest operation, which points at the inventory rather than at the missing manifest entry.
  • A manifest rename that does not follow the snake_casecamelCase rule silently breaks the mapping. Lines 94-95 already exist as hand-written exceptions to that rule, which shows the rule is not reliable.

Passing the manifest operation ids into buildInventory and treating a boundary as covered only on a real match would make the classification derived, not guessed. That change also removes the need for the two special cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 92 -
102, Update buildInventory and coveredOperationId to accept the manifest
operation ids and classify tauri-command and hull-face boundaries as covered
only when a manifest id matches, using the boundary’s command/face mapping
rather than guessed naming. Remove the two hard-coded tauri command exceptions
and preserve undefined for unmatched boundaries so missing manifest entries are
reported as uncovered.

216-228: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

thought: braceMatchedBody counts braces inside strings, comments, and template literals.

The scan at lines 220-225 has no lexical state. A } inside a string literal or a template literal in hull-port-adapter.ts closes the class body early. parseHullFaces then enumerates a truncated body, and every face declared below that point disappears.

That is precisely the failure the comment at lines 135-137 says this function fixed, reintroduced through a different mechanism. Line 146 does not catch it, because a truncated body usually still yields at least one face.

Also, line 218 interpolates a RegExp into the message, which renders the source pattern rather than a name. A short label reads better in CI output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 216 -
228, Update braceMatchedBody to scan with lexical awareness, ignoring braces
inside quoted strings, comments, and template literals so only structural braces
affect depth and the complete class body is returned. Also replace the RegExp
interpolation in its missing-opener error with a concise descriptive label,
while preserving the existing source context and unterminated-body handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/contracts/protocol/boundary-inventory.json`:
- Around line 1483-1490: Update the route-inventory generation around
parseLocalNodeRoutes to fail closed when a route expression contains an
unresolved single-brace interpolation such as {action}. Do not emit one
ambiguous inventory row for "{RouteBase}/{{id}}/{action}"; instead reject or
explicitly flag it so the registration is corrected with concrete action values
and each production route can be counted accurately.

In `@packages/contracts/protocol/README.md`:
- Around line 10-14: Update the documented boundary-inventory workflow to
distinguish auto-classified derivable operationIds from entries requiring manual
deferred or blocked classification. For manual entries, instruct maintainers to
run boundary-inventory.mjs --write first, copy the composite id from its error
output, add the classification and reason, then run --write again; remove the
inaccurate “by default” qualifier and avoid implying every producer change needs
manual classification.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs`:
- Around line 134-148: Update parseHullFaces to enumerate every non-blank
class-body member line and fail closed when a line is neither an ignorable line
nor a recognized member, rather than silently skipping unmatched shapes.
Preserve face discovery for methods, accessors, arrow-function properties, and
varying indentation; continue excluding private members and explicitly confirm
the intended treatment of protected members.
- Around line 30-33: Update the sorting in discoverRuntimeBoundaries to use
deterministic JavaScript code-unit ordering for boundary IDs instead of
localeCompare. Preserve ascending order while comparing IDs directly, avoiding
locale or ICU-dependent collation.
- Around line 170-197: Update
tooling/carrier-contract-codegen/boundary-inventory.mjs at lines 170-197,
230-258, and 216-228. Add one shared per-file comment-stripping helper and use
its output for all scanners: ensure the Map registration matchAll ignores
commented code, make argumentAt skip comment spans and report missing arguments
separately from unterminated calls, and update braceMatchedBody to track string
and template-literal state so braces inside literals do not affect depth.
- Around line 45-51: Update the `boundaries` mapping to resolve each entry’s
status first, then emit `operationId` only when that status is `covered` and
emit `reason` only when the status is `deferred` or `blocked`. Preserve prior
status values and covered-operation discovery, while ensuring generated entries
satisfy `validateInventory`.
- Around line 264-273: Update main to distinguish a missing inventory file from
malformed JSON: treat only ENOENT as absent, and report JSON parse/read failures
directly. In the --write branch, validate the freshly built inventory with
validateInventory before writeFile and only print the success message after
validation passes, ensuring written output is accepted by check mode.

In `@tooling/carrier-contract-codegen/tests/generate.test.mjs`:
- Around line 29-34: Extend the tests around validateInventory and
buildInventory with table-driven negative-path cases covering every guarded
validation branch, including duplicate IDs, invalid or missing status/reason
data, operationId and defaultReasons consistency, unlisted or stale boundaries,
and both manifest checks; assert each exact expected error string so deleting
any named check makes the test fail. Add SCHEMA_VERSION to the existing imports,
and invoke buildInventory through its write path with synthetic discovery data
to verify the generated inventory and its compatibility with validateInventory.

---

Nitpick comments:
In `@tooling/carrier-contract-codegen/boundary-inventory.mjs`:
- Around line 112-114: Update parseTauriCommands to detect all
generate_handler![ occurrences and reject the source when more than one is
present, rather than selecting the first match. Preserve the existing
missing-handler error for zero matches and parse the command list only when
exactly one occurrence is found.
- Around line 83-88: Update the manifest cross-check in boundary-inventory.mjs
to detect duplicate operationId values among entries with status === 'covered'
before or alongside the existing membership checks. Use the covered entries’
operation IDs and report each duplicated claim, while preserving the current
manifest/runtime mismatch validation.
- Around line 92-102: Update buildInventory and coveredOperationId to accept the
manifest operation ids and classify tauri-command and hull-face boundaries as
covered only when a manifest id matches, using the boundary’s command/face
mapping rather than guessed naming. Remove the two hard-coded tauri command
exceptions and preserve undefined for unmatched boundaries so missing manifest
entries are reported as uncovered.
- Around line 216-228: Update braceMatchedBody to scan with lexical awareness,
ignoring braces inside quoted strings, comments, and template literals so only
structural braces affect depth and the complete class body is returned. Also
replace the RegExp interpolation in its missing-opener error with a concise
descriptive label, while preserving the existing source context and
unterminated-body handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e28b7b1-425e-466f-9561-2c47b117a77e

📥 Commits

Reviewing files that changed from the base of the PR and between ed9c43b and f9cbcbe.

📒 Files selected for processing (4)
  • packages/contracts/protocol/README.md
  • packages/contracts/protocol/boundary-inventory.json
  • tooling/carrier-contract-codegen/boundary-inventory.mjs
  • tooling/carrier-contract-codegen/tests/generate.test.mjs

Comment on lines +1483 to +1490
{
"id": "local-node-http:POST:apps/local-node-host/Health/RecurringInvoiceRoutes.cs:$\"{RouteBase}/{{id}}/{action}\"",
"kind": "local-node-http",
"method": "POST",
"routeExpression": "$\"{RouteBase}/{{id}}/{action}\"",
"source": "apps/local-node-host/Health/RecurringInvoiceRoutes.cs",
"status": "deferred"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

issue: this entry hides an unknown number of real routes behind one inventory row.

Compare the routeExpression here with every neighbour. Other entries use {{id}}, which is a literal {id} route parameter in a C# interpolated string. This one is $"{RouteBase}/{{id}}/{action}". The final segment uses single braces, so action is a C# variable interpolated at registration time, not a route parameter.

The registration is therefore almost certainly inside a loop or called once per action value. One row in this file represents N production routes, and N is not visible here. The denominator this artifact exists to establish is understated by N-1 for this source.

The walker cannot resolve the value, so it cannot count correctly. Two workable options: give this entry an explicit reason that names the concrete action values it covers, or make parseLocalNodeRoutes reject a route expression that contains a single-brace interpolation it cannot resolve, consistent with the fail-closed rule applied to unknown Map* forms.

🔍 Script to reveal how many routes this registration produces
#!/bin/bash
# Show the RecurringInvoiceRoutes registration and any single-brace interpolated route expressions.
set -euo pipefail

f=apps/local-node-host/Health/RecurringInvoiceRoutes.cs
[ -f "$f" ] && { echo "== $f =="; rg -n -C 12 'MapPost|action' "$f"; } || echo "file not found: $f"

echo
echo "== all route expressions with an unresolved single-brace interpolation =="
jq -r '.boundaries[]
       | select(.routeExpression != null)
       | select(.routeExpression | test("\\$\"") and (test("\\{[a-z][A-Za-z0-9_]*\\}")))
       | "\(.id)"' packages/contracts/protocol/boundary-inventory.json
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/protocol/boundary-inventory.json` around lines 1483 -
1490, Update the route-inventory generation around parseLocalNodeRoutes to fail
closed when a route expression contains an unresolved single-brace interpolation
such as {action}. Do not emit one ambiguous inventory row for
"{RouteBase}/{{id}}/{action}"; instead reject or explicitly flag it so the
registration is corrected with concrete action values and each production route
can be counted accurately.

Comment on lines +10 to +14
operation that has no schema or adapter. After an intentional producer change, add its explicit
classification and reason to the inventory, then normalize it with
`node tooling/carrier-contract-codegen/boundary-inventory.mjs --write`. The command refuses to
classify a new surface by default; the generator suite fails on any unlisted or stale producer
operation and on any deferred/blocked entry without a reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

issue: the documented workflow does not match what the tool does.

Line 10-12 instructs: add the classification and reason to the inventory, then run --write to normalize. A maintainer cannot follow that order for a new boundary. To hand-add an entry they need its exact id, and the id is a composite of kind, method, source, and routeExpression built at boundary-inventory.mjs line 168. They cannot know it before discovery runs.

The real loop is: run --write, read the id out of the thrown error at line 50, hand-add the entry with that id, run --write again. Document that loop.

Two smaller corrections:

  • Line 12-13 says the command "refuses to classify a new surface by default". "by default" tells the reader a flag exists to override it. No such flag exists. Remove the qualifier.
  • A new boundary whose operationId is derivable is auto-classified covered at line 49 with no hand edit. Only boundaries that need deferred or blocked require the manual step. The current text implies every producer change needs one.
📝 Proposed wording
-operation that has no schema or adapter. After an intentional producer change, add its explicit
-classification and reason to the inventory, then normalize it with
-`node tooling/carrier-contract-codegen/boundary-inventory.mjs --write`. The command refuses to
-classify a new surface by default; the generator suite fails on any unlisted or stale producer
-operation and on any deferred/blocked entry without a reason.
+operation that has no schema or adapter. After an intentional producer change, run
+`node tooling/carrier-contract-codegen/boundary-inventory.mjs --write`. A new boundary that maps to
+an existing manifest operation is recorded as `covered` automatically. Any other new boundary makes
+the command fail and print the boundary id it refuses to classify. Add an entry for that id with an
+explicit `status` and `reason`, then run the command again to normalize the file. The generator
+suite fails on any unlisted or stale producer operation and on any deferred/blocked entry without a
+reason.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
operation that has no schema or adapter. After an intentional producer change, add its explicit
classification and reason to the inventory, then normalize it with
`node tooling/carrier-contract-codegen/boundary-inventory.mjs --write`. The command refuses to
classify a new surface by default; the generator suite fails on any unlisted or stale producer
operation and on any deferred/blocked entry without a reason.
operation that has no schema or adapter. After an intentional producer change, run
`node tooling/carrier-contract-codegen/boundary-inventory.mjs --write`. A new boundary that maps to
an existing manifest operation is recorded as `covered` automatically. Any other new boundary makes
the command fail and print the boundary id it refuses to classify. Add an entry for that id with an
explicit `status` and `reason`, then run the command again to normalize the file. The generator
suite fails on any unlisted or stale producer operation and on any deferred/blocked entry without a
reason.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/protocol/README.md` around lines 10 - 14, Update the
documented boundary-inventory workflow to distinguish auto-classified derivable
operationIds from entries requiring manual deferred or blocked classification.
For manual entries, instruct maintainers to run boundary-inventory.mjs --write
first, copy the composite id from its error output, add the classification and
reason, then run --write again; remove the inaccurate “by default” qualifier and
avoid implying every producer change needs manual classification.

Comment on lines +30 to +33
export async function discoverRuntimeBoundaries() {
return [...await discoverTauriCommands(), ...await discoverHullFaces(), ...await discoverLocalNodeRoutes()]
.sort((left, right) => left.id.localeCompare(right.id))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

issue: localeCompare makes the committed inventory order depend on the host locale and ICU build.

Line 32 sorts with left.id.localeCompare(right.id) and passes no locale. That call resolves against the runtime default locale and the ICU collation data in the Node build. Collation also treats punctuation differently from code-unit order: it can weight or ignore characters such as $, ", {, and :, which appear in nearly every id in this artifact.

Two consequences for packages/contracts/protocol/boundary-inventory.json:

  • A maintainer running --write on a machine with a different LANG, or on a small-icu Node build, can reorder the file with no semantic change. The diff is large and the review signal is lost.
  • Order sensitivity undermines clean-checkout reproducibility. A check job and a developer machine should produce byte-identical output.

Two adjacent ids in the committed file already sit where collation rules decide the outcome rather than code points: $"{RouteBase}/{{id}}/{action}" at line 1484 and $"{RouteBase}/{{id}}/generate" at line 1492. Under code-unit order { sorts after g; under many collations it does not.

These ids are machine-generated ASCII identifiers, not human-readable text. Sort them by code unit.

🐛 Proposed fix: deterministic code-unit ordering
 export async function discoverRuntimeBoundaries() {
   return [...await discoverTauriCommands(), ...await discoverHullFaces(), ...await discoverLocalNodeRoutes()]
-    .sort((left, right) => left.id.localeCompare(right.id))
+    .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0))
 }

If you prefer to keep a comparator call, new Intl.Collator('en', { sensitivity: 'variant' }) is still locale-data dependent. Code-unit comparison is the only fully reproducible option here.

#!/bin/bash
# Show whether the committed order matches code-unit order, and where locale collation differs.
set -euo pipefail

f=packages/contracts/protocol/boundary-inventory.json
[ -f "$f" ] || { echo "not found: $f"; exit 0; }

node --input-type=module -e '
import { readFileSync } from "node:fs"
const ids = JSON.parse(readFileSync("packages/contracts/protocol/boundary-inventory.json","utf8")).boundaries.map(b => b.id)
const codeUnit = [...ids].sort((a,b) => (a<b?-1:a>b?1:0))
const locale   = [...ids].sort((a,b) => a.localeCompare(b))
const firstDiff = (x,y) => { for (let i=0;i<x.length;i++) if (x[i]!==y[i]) return i; return -1 }
console.log("entries:", ids.length)
console.log("committed === localeCompare order:", JSON.stringify(ids)===JSON.stringify(locale))
console.log("committed === code-unit order   :", JSON.stringify(ids)===JSON.stringify(codeUnit))
const d = firstDiff(codeUnit, locale)
console.log("first index where the two orders diverge:", d)
if (d >= 0) console.log("  code-unit:", codeUnit[d], "\n  locale   :", locale[d])
console.log("ICU:", typeof Intl.Collator === "function" ? Intl.Collator().resolvedOptions().locale : "none")
'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 30 -
33, Update the sorting in discoverRuntimeBoundaries to use deterministic
JavaScript code-unit ordering for boundary IDs instead of localeCompare.
Preserve ascending order while comparing IDs directly, avoiding locale or
ICU-dependent collation.

Comment on lines +45 to +51
boundaries: discovered.map((boundary) => {
const prior = priorById.get(boundary.id)
const operationId = coveredOperationId(boundary)
if (prior) return { ...boundary, status: prior.status, ...(operationId ? { operationId } : {}), ...(prior.reason ? { reason: prior.reason } : {}) }
if (operationId) return { ...boundary, status: 'covered', operationId }
throw new Error(`new runtime boundary requires an explicit inventory status and reason: ${boundary.id}`)
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

issue (blocking): --write can emit an inventory that validateInventory rejects.

Line 48 attaches operationId whenever coveredOperationId(boundary) returns a value. It does that independently of prior.status. But coveredOperationId is purely name-derived for tauri-command and hull-face: it returns an id for every such boundary, with no manifest lookup.

Two writer/validator conflicts follow:

  1. A tauri-command or hull-face entry that a maintainer classified as deferred or blocked keeps that status, and also gains an operationId. Line 74 then fails it: deferred boundary ... must not claim a manifest operationId.
  2. A covered prior entry that carries a reason re-emits that reason. Line 72 then fails it: covered boundary ... must not carry a deferred/blocked reason.

In both cases --write produces a file that the check run rejects, and the maintainer has no clean path forward. Gate both fields on the resolved status.

🐛 Proposed fix: respect the resolved status when emitting operationId and reason
     boundaries: discovered.map((boundary) => {
       const prior = priorById.get(boundary.id)
       const operationId = coveredOperationId(boundary)
-      if (prior) return { ...boundary, status: prior.status, ...(operationId ? { operationId } : {}), ...(prior.reason ? { reason: prior.reason } : {}) }
+      if (prior) {
+        const covered = prior.status === 'covered'
+        return {
+          ...boundary,
+          status: prior.status,
+          ...(covered && operationId ? { operationId } : {}),
+          ...(!covered && prior.reason ? { reason: prior.reason } : {}),
+        }
+      }
       if (operationId) return { ...boundary, status: 'covered', operationId }
       throw new Error(`new runtime boundary requires an explicit inventory status and reason: ${boundary.id}`)
     }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
boundaries: discovered.map((boundary) => {
const prior = priorById.get(boundary.id)
const operationId = coveredOperationId(boundary)
if (prior) return { ...boundary, status: prior.status, ...(operationId ? { operationId } : {}), ...(prior.reason ? { reason: prior.reason } : {}) }
if (operationId) return { ...boundary, status: 'covered', operationId }
throw new Error(`new runtime boundary requires an explicit inventory status and reason: ${boundary.id}`)
}),
boundaries: discovered.map((boundary) => {
const prior = priorById.get(boundary.id)
const operationId = coveredOperationId(boundary)
if (prior) {
const covered = prior.status === 'covered'
return {
...boundary,
status: prior.status,
...(covered && operationId ? { operationId } : {}),
...(!covered && prior.reason ? { reason: prior.reason } : {}),
}
}
if (operationId) return { ...boundary, status: 'covered', operationId }
throw new Error(`new runtime boundary requires an explicit inventory status and reason: ${boundary.id}`)
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 45 -
51, Update the `boundaries` mapping to resolve each entry’s status first, then
emit `operationId` only when that status is `covered` and emit `reason` only
when the status is `deferred` or `blocked`. Preserve prior status values and
covered-operation discovery, while ensuring generated entries satisfy
`validateInventory`.

Comment on lines +134 to +148
export function parseHullFaces(text, source) {
// Brace-match the whole class rather than stopping at `private requireConnection`: the old bound
// meant any face declared BELOW that helper, or declared without `async`, was silently not a
// boundary. Every method is now enumerated and each one is either a face or explicitly private.
const classBody = braceMatchedBody(text, /export class HullShellPortAdapter implements HullPort \{/, source)
const faces = []
for (const match of classBody.matchAll(/^ {2}(?<modifiers>(?:(?:private|public|protected|static|async|readonly)\s+)*)(?<name>#?[A-Za-z_]\w*)\s*(?:<[^>(]*>)?\s*\(/gm)) {
const { modifiers, name } = match.groups
if (name === 'constructor') continue
if (name.startsWith('#') || /\bprivate\b/.test(modifiers)) continue
faces.push({ id: `hull-face:${name}`, kind: 'hull-face', face: name, source })
}
if (faces.length === 0) throw new Error(`cannot find HullShellPortAdapter faces in ${source}`)
return faces
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

issue: parseHullFaces silently skips member shapes it does not match, which breaks the fail-closed rule the rest of the file follows.

The comment at lines 135-137 states: "Every method is now enumerated and each one is either a face or explicitly private." The regex at line 140 does not enforce that. It requires ( directly after the member name, so these public members match nothing and disappear without a word:

  • an accessor: get status(): HealthReport { ... }get is not in the modifier list, and status is not followed by (;
  • an arrow-function property: announce = async (request: AnnounceRequest) => { ... }= sits between the name and (;
  • a member indented by anything other than exactly two spaces, because line 140 anchors on ^ {2}.

parseLocalNodeRoutes throws on an unclassified Map* form at line 193. parseHullFaces has no equivalent. Line 146 only fires when the count reaches zero, so losing one face of eight is invisible. That is the same "vanished from the denominator" failure the header comment describes.

Enumerating every non-blank line of the class body and rejecting any line that is neither a recognized member nor an ignorable line would restore the invariant.

Note: protected members are also currently counted as faces. Confirm that is intended.

🔍 Script to check which member shapes exist in the real adapter today
#!/bin/bash
# Inspect the real HullShellPortAdapter members and compare against the parser's assumptions.
set -euo pipefail

f=apps/hull/src/protocol/hull-port-adapter.ts
[ -f "$f" ] || { echo "adapter not found at $f"; exit 0; }

echo "== full member outline =="
ast-grep outline "$f" --items all

echo
echo "== accessors, arrow-function properties, and non-2-space members =="
rg -nP '^\s*(get|set)\s+\w+\s*\(|^\s*(public|protected|private|readonly|static|\s)*\w+\s*(<[^>]*>)?\s*=\s*(async\s*)?\(' "$f"

echo
echo "== indentation histogram of member-looking lines =="
rg -nP '^\s+[`#A-Za-z_`]' "$f" | sed -E 's/^[0-9]+:( *).*/\1/' | awk '{print length($0)}' | sort -n | uniq -c
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 134 -
148, Update parseHullFaces to enumerate every non-blank class-body member line
and fail closed when a line is neither an ignorable line nor a recognized
member, rather than silently skipping unmatched shapes. Preserve face discovery
for methods, accessors, arrow-function properties, and varying indentation;
continue excluding private members and explicitly confirm the intended treatment
of protected members.

Comment on lines +170 to +197
for (const match of text.matchAll(/\.(Map[A-Z]\w*)\s*\(/g)) {
const call = match[1]
const argumentStart = match.index + match[0].length
if (NON_BOUNDARY_MAP_CALLS.has(call)) continue
if (HTTP_VERB_MAPS.has(call)) {
push('local-node-http', call.slice(3).toUpperCase(), normalizeExpression(argumentAt(text, argumentStart, 0)))
continue
}
if (call === 'MapMethods') {
const routeExpression = normalizeExpression(argumentAt(text, argumentStart, 0))
const verbs = [...normalizeExpression(argumentAt(text, argumentStart, 1)).matchAll(/"([A-Za-z]+)"/g)].map((verb) => verb[1].toUpperCase())
if (verbs.length === 0) throw new Error(`cannot read the HTTP verbs of a MapMethods registration in ${source}`)
for (const verb of verbs) push('local-node-http', verb, routeExpression)
continue
}
if (call === 'MapWebSocketPath') {
push('local-node-ws', 'WEBSOCKET', normalizeExpression(argumentAt(text, argumentStart, 0)))
continue
}
if (call === 'MapHealthChecks') {
push('local-node-http', 'GET', normalizeExpression(argumentAt(text, argumentStart, 0)))
continue
}
throw new Error(
`unknown endpoint registration form ${call}( in ${source} — classify it: add it to this walker `
+ 'as a boundary, or to NON_BOUNDARY_MAP_CALLS with the reason it registers no route',
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

issue: three hand-rolled scanners parse source text with no lexical awareness of comments or strings.

All three findings share one root cause. Each scanner walks raw file text and assumes every character it sees is live code. Comments, string literals, and template literals are indistinguishable from real syntax. The result is that the artifact can gain phantom boundaries, lose real ones, or fail on well-formed input. That is the exact class of failure the header comments in this file say these parsers were written to end.

  • tooling/carrier-contract-codegen/boundary-inventory.mjs#L170-L197: strip C# comments before the matchAll at line 170, so a commented-out .MapGet( is not counted and a .MapSomething( in prose does not throw at line 193.
  • tooling/carrier-contract-codegen/boundary-inventory.mjs#L230-L258: skip comment spans inside the argumentAt scan loop, so an apostrophe in a // comment cannot open a string that never closes. The same pre-parse comment strip removes the need for this. Also split the line 257 error so a missing argument is not reported as an unterminated call.
  • tooling/carrier-contract-codegen/boundary-inventory.mjs#L216-L228: track string and template-literal state in the braceMatchedBody depth counter, so a } inside a TypeScript string cannot truncate the class body and drop the faces declared below it.

A single shared helper that returns comment-stripped text, applied once per file before parsing, resolves the first two directly and gives the third a consistent pattern to follow.

📍 Affects 1 file
  • tooling/carrier-contract-codegen/boundary-inventory.mjs#L170-L197 (this comment)
  • tooling/carrier-contract-codegen/boundary-inventory.mjs#L230-L258
  • tooling/carrier-contract-codegen/boundary-inventory.mjs#L216-L228
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 170 -
197, Update tooling/carrier-contract-codegen/boundary-inventory.mjs at lines
170-197, 230-258, and 216-228. Add one shared per-file comment-stripping helper
and use its output for all scanners: ensure the Map registration matchAll
ignores commented code, make argumentAt skip comment spans and report missing
arguments separately from unterminated calls, and update braceMatchedBody to
track string and template-literal state so braces inside literals do not affect
depth.

Comment on lines +264 to +273
async function main() {
const discovered = await discoverRuntimeBoundaries()
let current
try { current = JSON.parse(await readFile(inventoryPath, 'utf8')) } catch { current = undefined }
if (process.argv.includes('--write')) {
const inventory = await buildInventory(current)
await writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`)
process.stdout.write(`wrote ${inventory.boundaries.length} runtime boundaries to ${relative(root, inventoryPath)}\n`)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

issue: --write reports success without validating what it wrote, and line 267 hides a malformed inventory.

Two problems in main:

  1. Line 267 catches every failure of JSON.parse and readFile and sets current = undefined. A single trailing comma in boundary-inventory.json is then indistinguishable from a missing file. In check mode the run prints boundary inventory has an unsupported schemaVersion followed by 232 unlisted runtime boundary lines. The real cause, one syntax error, never appears. Distinguish ENOENT from a parse failure and report the parse failure directly.
  2. The --write branch at lines 268-273 writes the file and returns. It never calls validateInventory. Combined with the issue on lines 45-51, --write can print wrote 232 runtime boundaries and still leave the tree in a state that the check run rejects. Validating the freshly built inventory before writing would make --write and the check agree by construction.
🐛 Proposed fix for both points
   let current
-  try { current = JSON.parse(await readFile(inventoryPath, 'utf8')) } catch { current = undefined }
+  try {
+    current = JSON.parse(await readFile(inventoryPath, 'utf8'))
+  } catch (error) {
+    if (error.code !== 'ENOENT') throw new Error(`cannot read ${relative(root, inventoryPath)}: ${error.message}`, { cause: error })
+    current = undefined
+  }
   if (process.argv.includes('--write')) {
     const inventory = await buildInventory(current)
+    const manifest = JSON.parse(await readFile(resolve(root, 'packages/contracts/protocol/manifest.json'), 'utf8'))
+    const errors = validateInventory(inventory, discovered, manifest)
+    if (errors.length) {
+      for (const error of errors) process.stderr.write(`${error}\n`)
+      process.stderr.write('refusing to write an inventory that fails its own validation\n')
+      process.exitCode = 1
+      return
+    }
     await writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function main() {
const discovered = await discoverRuntimeBoundaries()
let current
try { current = JSON.parse(await readFile(inventoryPath, 'utf8')) } catch { current = undefined }
if (process.argv.includes('--write')) {
const inventory = await buildInventory(current)
await writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`)
process.stdout.write(`wrote ${inventory.boundaries.length} runtime boundaries to ${relative(root, inventoryPath)}\n`)
return
}
async function main() {
const discovered = await discoverRuntimeBoundaries()
let current
try {
current = JSON.parse(await readFile(inventoryPath, 'utf8'))
} catch (error) {
if (error.code !== 'ENOENT') throw new Error(`cannot read ${relative(root, inventoryPath)}: ${error.message}`, { cause: error })
current = undefined
}
if (process.argv.includes('--write')) {
const inventory = await buildInventory(current)
const manifest = JSON.parse(await readFile(resolve(root, 'packages/contracts/protocol/manifest.json'), 'utf8'))
const errors = validateInventory(inventory, discovered, manifest)
if (errors.length) {
for (const error of errors) process.stderr.write(`${error}\n`)
process.stderr.write('refusing to write an inventory that fails its own validation\n')
process.exitCode = 1
return
}
await writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`)
process.stdout.write(`wrote ${inventory.boundaries.length} runtime boundaries to ${relative(root, inventoryPath)}\n`)
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/boundary-inventory.mjs` around lines 264 -
273, Update main to distinguish a missing inventory file from malformed JSON:
treat only ENOENT as absent, and report JSON parse/read failures directly. In
the --write branch, validate the freshly built inventory with validateInventory
before writeFile and only print the success message after validation passes,
ensuring written output is accepted by check mode.

Comment on lines +29 to +34
test('boundary inventory classifies every runtime producer operation', async () => {
const inventory = JSON.parse(readFileSync(boundaryInventoryPath, 'utf8'))
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const discovered = await discoverRuntimeBoundaries()
assert.deepEqual(validateInventory(inventory, discovered, manifest), [])
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

issue: validateInventory and buildInventory have no negative-path coverage, so most of the guarded logic fails the falsification test.

This test asserts only that the committed inventory currently validates clean. Every error branch in validateInventory is unexercised: duplicate id (line 62), invalid status (line 64), missing reason (line 70), restated default reason (line 71), covered carrying a reason (line 72), covered without an operationId (line 73), non-covered claiming an operationId (line 74), unused defaultReasons (line 78), unlisted boundary (line 81), stale entry (line 82), and both manifest cross-checks (lines 86-87).

Delete any one of those lines and no check goes RED. buildInventory is not called by any test at all, so the --write path that maintainers are told to run in packages/contracts/protocol/README.md line 12 ships unverified.

The parser tests below do state their falsification correctly. Apply the same standard here. A table-driven test that feeds a synthetic inventory and asserts the exact expected error string per branch would cover this cheaply, and it would have caught the writer/validator conflict flagged on tooling/carrier-contract-codegen/boundary-inventory.mjs lines 45-51.

As per path instructions: "Name the mechanism that executes it and state the falsification: delete the guarded logic, the named check goes RED."

💚 Sketch of the missing negative-path test
+const baseEntry = {
+  id: 'hull-face:announce',
+  kind: 'hull-face',
+  face: 'announce',
+  source: 'apps/hull/src/protocol/hull-port-adapter.ts',
+}
+
+test('validateInventory rejects each malformed entry shape', () => {
+  const cases = [
+    [{ ...baseEntry, status: 'maybe' }, /has invalid status "maybe"/],
+    [{ ...baseEntry, status: 'deferred' }, /has no reason and no default reason/],
+    [{ ...baseEntry, status: 'covered' }, /has no manifest operationId/],
+    [{ ...baseEntry, status: 'covered', operationId: 'hull.announce', reason: 'x' }, /must not carry a deferred\/blocked reason/],
+    [{ ...baseEntry, status: 'blocked', reason: 'x', operationId: 'hull.announce' }, /must not claim a manifest operationId/],
+  ]
+  for (const [entry, expected] of cases) {
+    const errors = validateInventory(
+      { schemaVersion: SCHEMA_VERSION, boundaries: [entry] },
+      [{ id: entry.id }],
+      undefined,
+    )
+    assert.ok(errors.some((error) => expected.test(error)), `expected ${expected} in ${JSON.stringify(errors)}`)
+  }
+})
+
+test('validateInventory reports a boundary that no producer registers', () => {
+  const errors = validateInventory(
+    { schemaVersion: SCHEMA_VERSION, boundaries: [{ ...baseEntry, status: 'covered', operationId: 'hull.announce' }] },
+    [],
+    undefined,
+  )
+  assert.ok(errors.some((error) => /stale boundary inventory entry/.test(error)))
+})

This needs SCHEMA_VERSION added to the import list at lines 7-14.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tooling/carrier-contract-codegen/tests/generate.test.mjs` around lines 29 -
34, Extend the tests around validateInventory and buildInventory with
table-driven negative-path cases covering every guarded validation branch,
including duplicate IDs, invalid or missing status/reason data, operationId and
defaultReasons consistency, unlisted or stale boundaries, and both manifest
checks; assert each exact expected error string so deleting any named check
makes the test fail. Add SCHEMA_VERSION to the existing imports, and invoke
buildInventory through its write path with synthetic discovery data to verify
the generated inventory and its compatibility with validateInventory.

Source: Path instructions

@ctwoodwa
ctwoodwa merged commit 61a1d30 into main Aug 3, 2026
50 checks passed
@ctwoodwa
ctwoodwa deleted the fix/3524-boundary-inventory branch August 3, 2026 00:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant