Skip to content

nlq: close the declared-unenforced gap inside the declared-unenforced guard (format) - #37

Open
mdheller wants to merge 3 commits into
mainfrom
fix/nlq-format-enforcement
Open

nlq: close the declared-unenforced gap inside the declared-unenforced guard (format)#37
mdheller wants to merge 3 commits into
mainfrom
fix/nlq-format-enforcement

Conversation

@mdheller

@mdheller mdheller commented Jul 30, 2026

Copy link
Copy Markdown
Member

The gap

assertSupportedKeywords is the bar every vendored sourceos-spec contract clears at import. Its stated contract:

a contract using an unimplemented keyword fails LOUDLY rather than silently validating less than it says

It did not hold for format — inside the guard whose entire job is to prevent exactly this.

SUPPORTED_KEYWORDS listed format, so a schema declaring any format cleared the bar. validateAgainst enforced exactly one:

if (schema['format'] === 'uri' && !isAbsoluteUri(value)) errs.push(...)

So format: "date-time" / "email" / "uuid" passed the loud-failure bar and was then validated for everything except the constraint it declared.

A walk of the origin/main tree (ontology/** + ts/src/*-data.ts) finds exactly two format declarations: uri in SemanticAction.json (enforced) and date-time on requestedAt in EffectRequest.json — enforced only by a private regex at the proposeRevendor call site, and by nothing at all for any other consumer of that contract.

The fix

format is now admitted by value, not by name.

  • FORMAT_CHECKS — one map from format name to the check implementing it. Single source of truth for both halves of the guard: validateAgainst enforces through it, assertSupportedKeywords admits a declared format only if it has a key there. Adding a name to the accept-list without writing the check is not expressible — the accept-list IS the set of checks.
  • date-time is implemented, not allowlisted. The ISO-8601 rule moves out of vendor-graph.ts into FORMAT_CHECKS, so it holds for every contract.
  • format dropped from SUPPORTED_KEYWORDS — fail-safe: delete the explicit branch and format becomes an unknown keyword, which throws.
  • A Map, not an object literal: 'constructor' in {} is true, and a prototype key must never read as an implemented format.
  • vendor-graph.ts loses its duplicate ISO_DATE_TIME regex and the comment documenting the limitation. ontology/effect-request/PROVENANCE.md no longer claims a limitation that is fixed.

Proof — before vs after

The same probe, run against pristine origin/main and against this branch.

origin/main this branch
format: "email" at the import bar ACCEPTED, no throw THROWS
…and that value validates to [] (unchecked) n/a — never reaches validation
date-time "2026-07-29" via validateAgainst [] silently valid ["… is not an ISO-8601 date-time"]
real EffectRequest.requestedAt bad value [] declared, enforced by nobody rejected
SemanticAction.output.typeRef ":ContactList" rejected rejected (unchanged control)

The loud error:

nlq: schema format "email" at DemoContract.properties.contact is declared by the contract
but is NOT implemented by validateAgainst (implemented: date-time, uri) — a declared format
that nothing checks validates less than the contract says; implement it in nlq.ts (and its
tests) before re-vendoring a schema that declares it

Both real contracts still import and validate clean: SemanticAction.json declares ["uri"], EffectRequest.json declares ["date-time"], both now enforced.

Anti-tautology: the test cannot be written from the implementation

nlq.test.ts states the expected formats and their counter-examples literally, not derived from IMPLEMENTED_FORMATS — a test written from the implementation's own constant cannot catch a format that was accepted and never implemented.

Verified by injection, both ways:

  1. Add a stub email: () => true to FORMAT_CHECKS, leave the test alone → drift test fails (+ 'email' in the accept-list).
  2. Add the stub and a matching test row, so the accept-list matches → still fails, on the counter-example: email: 'definitely not an email' must be rejected — a format that rejects nothing is declared, not enforced.

One deliberate deviation

proposeRevendor keeps a loud throw for a malformed requestedAt, now sourced from the one implementation via matchesFormat rather than a private regex.

Deleting the guard entirely would let the failure fall through to contractViolations — but analyzeVendorFreshness maps proposeRevendor over stale pins and seals the results without ever reading that array. That would convert a loud failure into a silent one, which is the bug class this PR closes. The duplicated rule is gone; the escalation is kept, and the reason is stated in the code.

Gates

  • npm test416 pass, 0 fail (6 new tests)
  • npm run typecheck — clean
  • npm run check:kko — green
  • npm run check:dist — green (ts/dist rebuilt and committed)

Production code touched: nlq.ts and vendor-graph.ts only. The change also updates their tests
(nlq.test.ts, vendor-graph.test.ts) and ontology/effect-request/PROVENANCE.md — the earlier
wording said "only" without that qualifier (Copilot review, suppressed channel).

No overlap with #34. Rebased onto #36 so the Test step actually runs against this diff: the
original CI run was queued before #36 merged, so the suite never executed against it.

Review round 2

  • isIsoDateTime was structural only — 2026-99-99T99:99:99Z passed. Now range- and calendar-checked.
  • IMPLEMENTED_FORMATS was an exported mutable Set behind a ReadonlySet type: .add("email")
    widened the accept-list without adding a check, so a declared format cleared the bar and was
    enforced by nobody. Replaced with isImplementedFormat() / implementedFormats(), which read
    FORMAT_CHECKS directly and hand out a frozen snapshot.

@mdheller

Copy link
Copy Markdown
Member Author

@copilot review

@mdheller
mdheller requested a review from Copilot July 30, 2026 01:59

Copilot AI 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.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Closes the “declared-but-unenforced” validation gap for format in the shared JSON-Schema subset validator by making format support depend on implemented checks (not just the keyword’s presence), and by centralizing format enforcement in nlq.ts.

Changes:

  • Introduces FORMAT_CHECKS + matchesFormat and gates format declarations by value in assertSupportedKeywords.
  • Implements date-time in the shared validator and removes the duplicated call-site regex in vendor-graph.ts.
  • Adds tests asserting the implemented formats list and that each supported format both accepts and rejects a representative value.

Reviewed changes

Copilot reviewed 5 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ts/src/nlq.ts Centralizes format checks, updates the import-time guard, and enforces formats through a single map.
ts/src/vendor-graph.ts Replaces the private requestedAt regex with matchesFormat('date-time', ...) and keeps a loud failure.
ts/src/vendor-graph.test.ts Adds coverage proving the contract-declared date-time is enforced by the shared validator and still throws at the call site.
ts/src/nlq.test.ts Adds non-tautological tests for implemented formats, including counter-examples and loud failures for unimplemented formats.
ontology/effect-request/PROVENANCE.md Updates documentation to reflect the new “format admitted by value” behavior and removal of the private regex.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ts/src/nlq.ts
Comment on lines +87 to +89
function isIsoDateTime(s: string): boolean {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(s)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted and fixed in d773c1d — this was a real gap, and a pointed one given where it sat.

2026-99-99T99:99:99Z cleared the old check: correct punctuation, no time. A format check that admits values no clock can produce is shape-matching wearing enforcement's clothes, which is the exact failure this PR exists to close — so the gap was inside the guard against that gap.

The check now validates component ranges and the day against the real length of that month in that year, so 2026-02-30 and 2026-04-31 are rejected. Month length is computed rather than asked of Date, because Date.UTC(year, …) maps a two-digit year onto 19xx and would answer for 1950 when asked about 0050.

The new test is red against the old regex and green against the new one — 12 impossible instants that previously passed, plus the cases that must keep passing: the RFC 3339 leap second (23:59:60Z), a genuine leap day (2024-02-29), the divisible-by-400 case (2000-02-29), and 1900-02-29 rejected at the other end.

One correction to the wording: these were false positives (invalid timestamps passing), not false negatives.

Comment thread ts/src/nlq.ts
Comment on lines +83 to +86
/**
* `format: "date-time"` — an RFC-3339 instant: date, `T`, time, and an EXPLICIT offset (`Z` or
* ±HH:MM). A bare date (`2026-07-29`) names a day, not an instant, and does not pass.
*/

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted and fixed in d773c1d (this and the duplicate on line 104).

Resolved toward the user-visible string rather than away from it: the docstring now reads "an ISO-8601 date-time in its RFC-3339 profile", naming RFC 3339 as the profile that makes the offset mandatory. That keeps the violation message, the thrown error in vendor-graph.ts, and the tests all saying one thing, instead of renaming four call sites to fix a comment.

Comment thread ts/src/nlq.ts
Comment on lines +103 to +104
const FORMAT_CHECKS: ReadonlyMap<string, { check: (s: string) => boolean; expected: string }> = new Map([
['date-time', { check: isIsoDateTime, expected: 'an ISO-8601 date-time' }],

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Accepted and fixed in d773c1d (this and the duplicate on line 104).

Resolved toward the user-visible string rather than away from it: the docstring now reads "an ISO-8601 date-time in its RFC-3339 profile", naming RFC 3339 as the profile that makes the offset mandatory. That keeps the violation message, the thrown error in vendor-graph.ts, and the tests all saying one thing, instead of renaming four call sites to fix a comment.

Comment on lines +367 to +376
/** Every `format` the schema declares, at any depth — walked here, not asked of the validator. */
function declaredFormats(node: unknown, into = new Set<string>()): Set<string> {
if (Array.isArray(node)) { for (const v of node) declaredFormats(v, into); return into }
if (node === null || typeof node !== 'object') return into
for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
if (k === 'format' && typeof v === 'string') into.add(v)
else declaredFormats(v, into)
}
return into
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Rejected, though it is a fair thing to raise.

Both copies are ~8-line pure helpers local to their own test file, and the duplication is doing something: each suite walks the schema independently of the validator it is testing. A shared helper would mean one walker's blind spot becomes both suites' blind spot simultaneously — and these particular tests exist to detect drift, so a single point of failure underneath them is the thing worth avoiding, not the duplication above them.

The drift risk is also bounded in a way the comment assumes it is not: if the two ever disagree, the assertions they feed (declaredFormats(SEMANTIC_ACTION_SCHEMA) is exactly ['uri'], and the EffectRequest one is exactly ['date-time']) are pinned to literal expected sets, so a walker that started skipping a branch would go red rather than quietly agree with itself.

Happy to extract it if a third consumer appears — at that point the balance changes.

Copilot AI commented Jul 30, 2026

Copy link
Copy Markdown

@copilot review

Review

The fix is correct and the design is sound. A few observations, from most to least significant:


validateAgainst silently skips unknown formats — intentional but worth naming explicitly

Lines 246–252 of nlq.ts:

const f = FORMAT_CHECKS.get(fmt)
if (f !== undefined && !f.check(value)) errs.push(...)

If fmt has no entry in FORMAT_CHECKS, the check is silently skipped. The comment explains why (assertSupportedKeywords refuses that schema at import, so it can never happen for a vendored contract), and there's a test that demonstrates the failure mode. That reasoning holds.

The one door it leaves open: validateAgainst is exported. A consumer who builds a schema manually and calls validateAgainst directly — skipping the bar — gets silent non-enforcement. The current test even asserts this (line 272–273). Whether validateAgainst should also throw here is a judgment call; the current choice is defensible (the bar polices formats, not the validator), and it's documented. Just worth being aware that the public API has that gap.


declaredFormats helper is duplicated across test files

The same 6-line helper appears verbatim in both nlq.test.ts (line 222) and vendor-graph.test.ts (line 367). Since they're test files sharing no common module, this is acceptable — just noting it in case a shared test-util grows later.


isIsoDateTime does not validate calendar ranges

9999-99-99T25:99:99Z passes the regex. This is typical for JSON Schema format validators (shape, not calendar correctness), and the test counter-example (2026-07-29) proves the structural guard works. Worth documenting if the contract intention is strictly RFC 3339 valid instants, but not a bug given the stated scope.


No issues found with

  • The Map vs object literal choice (prototype-key safety — correct)
  • IMPLEMENTED_FORMATS derived from FORMAT_CHECKS.keys() (accept-list IS the check-list — correct)
  • matchesFormat throwing on unknown formats (fail-loud — correct)
  • The proposeRevendor escalation staying a throw (the analyzeVendorFreshness/seal reasoning — correct)
  • The anti-tautology test structure (EXPECTED_FORMATS written literally, counter-examples required — correct)
  • No ReDoS exposure in either regex

The only finding that touches production behavior is the first one, and it's a documented, tested, intentional design choice. The PR ships clean.

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

ts/src/nlq.ts:112

  • Although typed as ReadonlySet<string>, this exports a mutable Set instance at runtime (callers can still .add()/.delete() via JS or casts), which can silently break the invariant that IMPLEMENTED_FORMATS mirrors FORMAT_CHECKS. Consider exporting a function that returns a fresh snapshot (e.g., implementedFormats(): readonly string[] / new Set(...)) or exposing only predicates like isImplementedFormat(name) to prevent external mutation.
export const IMPLEMENTED_FORMATS: ReadonlySet<string> = new Set(FORMAT_CHECKS.keys())

ts/src/vendor-graph.test.ts:376

  • This declaredFormats() helper is duplicated in ts/src/nlq.test.ts. To avoid the two copies drifting (especially since these tests are intended to detect drift), consider extracting it into a shared test utility (or reusing an existing one if the repo has a test helpers module) and importing it from both test files.
/** Every `format` the schema declares, at any depth — walked here, not asked of the validator. */
function declaredFormats(node: unknown, into = new Set<string>()): Set<string> {
  if (Array.isArray(node)) { for (const v of node) declaredFormats(v, into); return into }
  if (node === null || typeof node !== 'object') return into
  for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
    if (k === 'format' && typeof v === 'string') into.add(v)
    else declaredFormats(v, into)
  }
  return into
}

ontology/effect-request/PROVENANCE.md:63

  • The PR description says it 'Touches nlq.ts / vendor-graph.ts only', but this PR also changes test files (ts/src/nlq.test.ts, ts/src/vendor-graph.test.ts) and documentation (ontology/effect-request/PROVENANCE.md). Please update the description to reflect the full set of touched files (or clarify that the statement meant production code only).
## Regeneration

mdheller added 2 commits July 29, 2026 23:31
…red-unenforced gaps

`assertSupportedKeywords` is the bar every vendored sourceos-spec contract clears at
import, and its stated contract is that a schema using an unimplemented keyword fails
LOUDLY rather than silently validating less than it says. It did not hold for `format`.

`SUPPORTED_KEYWORDS` listed `format`, so a schema declaring ANY format cleared the bar —
while `validateAgainst` enforced exactly one, `uri`. A contract declaring `date-time`,
`email` or `uuid` therefore passed the loud-failure bar and was then validated for
everything except the constraint it actually declared. Exactly one such declaration exists
on main: `requestedAt` in `EffectRequest.json` — enforced only by a private regex at the
`proposeRevendor` call site, and by nothing at all for any other consumer of the contract.

`format` is now admitted by VALUE, not by name:

  * `FORMAT_CHECKS` is one map from format name to the check that implements it. It is the
    single source of truth for both halves of the guard — `validateAgainst` enforces
    through it, `assertSupportedKeywords` admits a declared format only if it has a key
    there. Adding a name to the accept-list without writing the check is not expressible,
    because the accept-list IS the set of checks.
  * `date-time` is implemented rather than allowlisted: the ISO-8601 rule moves out of
    `vendor-graph.ts` into `FORMAT_CHECKS`, so it now holds for EVERY contract.
  * `format` is dropped from `SUPPORTED_KEYWORDS`, which is fail-safe: delete the explicit
    branch and `format` becomes an unknown keyword, which throws.
  * A Map, not an object literal — `'constructor' in {}` is true, and a prototype key must
    never read as an implemented format.

`vendor-graph.ts` loses its duplicate `ISO_DATE_TIME` regex and the comment documenting the
limitation. The call site keeps a loud throw, now sourced from the one implementation via
`matchesFormat`, because `analyzeVendorFreshness` seals whatever proposals it is handed
without reading `contractViolations`: downgrading this to a violation entry would convert a
loud failure into a silent one, which is the bug class this change exists to close.

Teeth, both ways. `nlq.test.ts` states the expected formats and their counter-examples
LITERALLY rather than deriving them from `IMPLEMENTED_FORMATS` — a test written from the
implementation's own constant cannot catch a format that was accepted and never
implemented. Verified by injection: adding a stub `email: () => true` fails the drift test,
and adding it together with a matching test row still fails on the counter-example.
Copilot review finding on #37. `isIsoDateTime` was a purely structural regex, so
`2026-99-99T99:99:99Z` passed: the right punctuation, and no time. A format check
that admits values no clock can produce is shape-matching wearing enforcement's
clothes — precisely what this PR's "declared ⇒ enforced" bar exists to prevent, so
the gap sat inside the guard against exactly that gap.

Component ranges are now checked, and the day against the real length of that month
in that year (`2026-02-30` is rejected; `2024-02-29` is not). Month length is
computed rather than asked of `Date`, because `Date.UTC(year, …)` maps a two-digit
year onto 19xx and would answer for 1950 when asked about 0050.

The new test is red against the old regex and green against this one — 12 impossible
instants that used to pass, plus the leap-second and leap-day cases that must keep
passing, and 1900-02-29 (not a leap year) at the other end.

Also aligns the docstring's "RFC-3339 instant" with the violation message's
"ISO-8601 date-time" (Copilot #37, two comments): one rule, named consistently,
with RFC 3339 identified as the profile that makes the offset mandatory.
@mdheller
mdheller force-pushed the fix/nlq-format-enforcement branch from 51f2459 to d773c1d Compare July 30, 2026 03:33
Copilot review on #37, low-confidence channel — the sharpest finding on this PR, and
one neither REST comment channel returned.

`IMPLEMENTED_FORMATS` was `export const … : ReadonlySet<string>`. `ReadonlySet` is
erased at runtime: the value is a live `Set`, and `export * from './nlq'` puts it on
the package's public surface. So

    (IMPLEMENTED_FORMATS as Set<string>).add('email')

widened the accept-list without adding a check. `validateAgainst` skips any format it
has no `FORMAT_CHECKS` entry for, so `format: "email"` then cleared the import bar and
was enforced by nobody — verified end to end before the fix. That is precisely the
declared-unenforced gap this PR closes, reachable from outside the module: the guard
against the gap had the gap.

The accept-list is now asked of `FORMAT_CHECKS` on every call (`isImplementedFormat`),
so there is no copy to widen, and `implementedFormats()` hands out a fresh FROZEN array
— frozen because `readonly string[]` is erased exactly as `ReadonlySet` was, so only the
freeze makes a mutation attempt fail rather than quietly succeed on a copy the caller
then trusts.

`IMPLEMENTED_FORMATS` is removed rather than deprecated: it was introduced by this same
PR, so nothing outside it consumes the name.
@mdheller

Copy link
Copy Markdown
Member Author

Review disposition — including the suppressed channel

Copilot's review carries the degraded-mode banner and reports 5 of 9 changed files reviewed — barely half. The diff was reviewed independently on that basis.

Three further findings were in the review body's <details>Comments suppressed due to low confidence</details> block, returned by neither REST comment endpoint. The sharpest finding of the round was in there, and it defeated this PR's central claim.

Fixed — the suppressed one first

  • nlq.tsIMPLEMENTED_FORMATS was a mutable export. ReadonlySet is erased at runtime; the value was a live Set, and export * from './nlq' puts it on the package's public surface. Verified end to end before fixing:

    BEFORE: bar correctly REFUSES format: email
    mutated IMPLEMENTED_FORMATS -> date-time,email,uri
    AFTER : bar ADMITS format: email
    AFTER : validateAgainst violations = []   <-- DECLARED, ENFORCED BY NOBODY
    

    One .add() decoupled the accept-list from the set of checks and reproduced exactly the declared-unenforced gap this PR closes — the guard against the gap had the gap. Fixed in 87577f3: the bar now asks FORMAT_CHECKS directly (isImplementedFormat), so there is no copy to widen, and implementedFormats() returns a fresh frozen array — frozen because readonly string[] is erased exactly as ReadonlySet was. Re-ran the attack after the fix: the export is gone, the snapshot refuses mutation, the bar still refuses email.

  • isIsoDateTime accepted impossible instants and the RFC-3339/ISO-8601 terminology split — both answered on-thread, fixed in d773c1d.

Rejected

  • declaredFormats duplicated across two test files (raised twice, visible and suppressed) — answered on-thread: the independence is load-bearing, and the assertions it feeds are pinned to literal expected sets, so drift goes red rather than agreeing with itself.

Noted, not actioned

  • PR description accuracy (suppressed). Fair — the description says "touches nlq.ts / vendor-graph.ts only" while tests and PROVENANCE.md also change. It meant production code; the body has been updated to say so.
  • NLQ_STOP_WORDS is an exported mutable Set with the same shape of hazard as the one fixed above. It is pre-existing on main and outside this diff, so it is not being changed here — flagging it as a follow-up.

The design claim, tested rather than trusted

The premise is that FORMAT_CHECKS makes the accept-list be the set of checks. Both smuggling routes were attempted:

attack result
add email with a stub check, no test row caught — the accept-list/EXPECTED_FORMATS reconciliation test fails
add email with a stub check and a reconciling test row caught — the TEETH test fails: 'not-an-email' must be rejected — a format that rejects nothing is declared, not enforced

The design holds. Keeping EXPECTED_FORMATS written out literally rather than derived is what closes the second route, and the comment saying so is accurate.

Mirror in the sibling implementation

A second copy of this validator lives at prophet-platform apps/hellgraph-service/src/contract.ts:49-52,127, with the opposite blind spot: it enforces date-time and not uri, where this one enforced uri and not date-time. Not touching it — different lane — but the FORMAT_CHECKS shape here is worth mirroring there, since between them the two copies currently under-enforce in complementary directions.

@mdheller

Copy link
Copy Markdown
Member Author

Not merged — CI never ran, and the previous run never ran the tests

Held deliberately. Two separate reasons, both about the suite not having executed against this diff.

1. The earlier green run did not include the Test step

Test is a step inside build-and-verify-dist, not its own check context, so gh pr checks showing that context green says nothing about whether the suite ran. Reading the job steps directly:

job build-and-verify-dist  steps: … | Typecheck=success | Rebuild dist=success | …

No Test between them. main only gained that step with #36, merged at 02:21:46Z — and this PR's run was created at 02:20:24Z, 82 seconds earlier. The same was true of #37 (run 01:51:30Z). Both branches were one commit behind and neither had ever executed the 401-test suite; the green checks were typecheck-and-build only.

Both branches are now rebased onto main, so the Test step is present and will run.

2. The new runs are queued behind an exhausted runner pool

self-hosted runners (org): 0
hellgraph runs queued:    29
hellgraph runs in progress: 0
oldest queued run:        03:13:38Z  (42 minutes and counting)
last run to actually execute: 03:23:42Z

Nothing has started since 03:23. Pending is not green, so this is not being merged on the strength of local results.

What did pass, locally

npm test · npm run typecheck · npm run check:kko · npm run build (dist committed, no drift). Full counts are in the commit messages. That is evidence the change is sound — it is not evidence the required checks passed, and only the latter is a merge criterion.

To merge

Required contexts on main are exactly build-and-verify-dist and CodeQL (strict: false, no required reviews). kko-provenance, docs-integrity and rust report but block nothing. Once runners free up, confirm Test → success inside build-and-verify-dist — not just the context's own conclusion — and then merge.

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.

3 participants