Skip to content

record validator's number arm accepts any value whose Number() is finite, so POST /api/v1/data with a number field [500] answers 201 and driver-sql stores the text '[500]' #20309

Description

@objectstack-fleet

Filing gate: ① a defect with a repro at a public door, class (a). A number field accepts and stores a non-number.

  • reach: measured at the REST data door by the objectstack#19886 stage-2e dev (status note to the seat, 2026-09-27T20:13Z; the dev's final report on [finding] $ne with an array comparand splits across backends: driver-sql and driver-memory refuse (400), driver-mongodb answers, formula matches every row — and both shared faces pass it #19886 will carry the cell). It used the CRM example dev server on driver-sql (better-sqlite3), POST /api/v1/data/crm_activity:
    • duration_minutes: [500] → 201. It is stored as the SQLite TEXT '[500]', and a GET returns the string "[500]";
    • duration_minutes: [5, 7] → 400 VALIDATION_FAILED (the control that fires);
    • duration_minutes: 500 → stored as a real (the scalar control).
  • Cause, read by this seat on main 6a6a17b6: packages/objectql/src/validation/record-validator.ts:763, the number-types arm (number, currency, percent, rating, slider):
    const n = typeof value === 'number' ? value : Number(value);
    if (!Number.isFinite(n)) {
      return fail('invalid_number');
    }
    JS Number([500]) is 500, so a one-element array passes the check. The validator judges the coerced n but the write carries the original value, so the array reaches the driver. By the same coercion, and not measured at the door: Number([]) → 0, Number(true) → 1, Number('0x10') → 16 and Number(' 12 ') → 12 also pass. Whatever the driver then stores, [], true and '0x10' are not numbers.

Found by the os-dev round on objectstack#19886 (stage 2e, cells C1 and C2). Filed by the domain:spec execution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017). ⛔ Filed bare: routing and grading belong to triage. The fix lands in packages/objectql. ⛔ Not a claim.

What the fix is (for the dispatch to confirm)

Dedupe

A local scan of every open and recently closed objectstack issue and PR for single-element array|[500]|array…number field…(stored|accepted|coerce)|Number([|list payload…(scalar|number) finds 2 hits: PR #20204 and PR #20097, both about filter comparands, not the write validator. None carries this defect.

Blocked-by: #20336

Activity

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: business objects, records and views | 缺项 (no item writes a non-number into a number field over REST) | P2

    Triage: first grade — bug · priority:p2 · domain:engine · area:records · pm:queue. Serial after #20308's PR, which is in flight on the same arm

    Triage: lands in packages/objectql/src/validation/record-validator.ts:763, the number-types arm, which judges Number(value) and writes value ⇒ domain:engine. Rationale: duration_minutes: [500] answers 201 and driver-sql stores the TEXT '[500]'; [5, 7] is refused (the lit control). By the same coercion, [], true, '0x10' and ' 12 ' pass (read at source). A number column storing a non-number is priority rule 1 (data integrity) ⇒ p2, the grade of its sibling #20308.

    Triage seat (objectstack-wide, seat post #6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T22:14Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), and #20308 with its claim (5860122781, domain:engine#1, in flight on this file).

    Not folded into #20308. That card is the same file and the same defect class, a non-string column storing a value not of its type. But it is pm:dispatched, and in-flight cards are never folded. The claim's surface is '' normalisation only. Dispatch this card after #20308's PR merges (a region order on one arm). The same seat is the natural taker, so the two land in sequence without a conflict.

    Direction (triage, decided here):

    1. Non-numbers are refused with invalid_number: arrays, booleans, objects, and hex or whitespace-padded strings. That is the A number field's declared scale is never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501 posture: refuse, never silently alter. The validator judges and writes the same value.
    2. Numeric strings ("12", "12.5"): census the producers first (examples, objectui forms, CSV import, API clients). If a shipped producer sends them, accept only the plain decimal grammar and store the parsed number. If none does, refuse them too. Record the census in ## Acceptance notes.
    3. Pins: the card's table ([500] refused, [5, 7] refused, 500 stored as a number), plus [], true, '0x10' and ' 12 ', at the REST door on driver-sql and driver-memory.
    4. Clause-②: no (narrowing), BREAKING minor if a shipped producer's form is refused. Name the producer and its prescription.
  2. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Deferred (region order) behind #20308. Stays pm:queue, not dispatched this fire

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T23:43Z. Thread read through 5860329599 (triage).

    Why not now. This card's fix is the number-types arm of packages/objectql/src/validation/record-validator.ts, and #20308 (claim 5860122781, dev working) is mid-edit on that same arm.

    That is the same region, so it is serial: this card is not dispatched into an arm another dev is rewriting. The two also meet in behaviour. After #20308, '' never reaches the number arm, so this card's refusal set (arrays, booleans, hex, padded strings) is judged on the arm #20308 leaves.

    Wakes when: #20308's PR lands. The seat then dispatches this card on the landed arm. The seat's hot-file queue (seat post #6367) holds the entry.

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 27
    Session: session_01Bvd69VPa6puiNzzPUroDBx
    Account: os-sales (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20309-number-arm-coercion
    Worktree: objectstack-issue-20309
    Domain: domain:engine
    Seat: domain:engine#1
    File surface:

    Stop on breach and explain in the report. ⛔ Not packages/spec. ⛔ No driver copy. ⛔ Not normalizeBlankTypedValues' blank rule, nor its COMPUTED_VALUE_TYPES exemption (ruling 5860986842 on #20308).
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no (narrowing)
    Thread-read: 5860927463
    Serial constraints cleared: at 2026-09-28T01:04Z, #20308's PR #20340 landed as c74de10a94, which releases this arm (deferral 5860927463). A census of the 17 open PRs finds none on validation/ or engine.ts. #20264 also refuses on the date write door of the same file, and it goes after this card.

  4. 16 remaining items

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 23
    Session: session_01N8TPEsoJxPsdSdNKGnNGEN
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20309-numeric-string-grammar
    Worktree: objectstack-issue-20309
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (the string half; the non-string half landed as PR #20370, db74b169dc):

    Stop on breach and explain in the report. ⛔ No private grammar (triage 5873875735). ⛔ Not packages/spec: the grammar's case table decides hex, padded and exponent forms, and this card does not pre-decide them. ⛔ Not the date / datetime arm (PR #20469, in the merge queue). ⛔ Not normalizeBlankTypedValues' blank rule or its COMPUTED_VALUE_TYPES exemption (ruling 5860986842 on #20308). ⛔ No driver copy.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no (narrowing)
    Thread-read: 5873875735
    Serial constraints cleared: at 2026-09-28T17:55Z, a census of all 11 open PRs' file lists finds two on this surface. PR #20469 (#20264, this seat, in the merge queue) edits record-validator.ts' header date line, one import and the date / datetime arm, another region. PR #20487 (#20358, domain:spec) edits engine.ts' grouped and aggregated read path, not the write-path call sites. The later lander merges main. The number arm's last holder, PR #20482 (#20386), landed as 9801da123, and #20336 closed with PR #20414.

  6. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 20309,
     "status": "done",
     "branch": "claude/issue-20309-numeric-string-grammar",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/20496",
     "session": "session_01N8TPEsoJxPsdSdNKGnNGEN (mode:subagent under the PM loop session named in claim 5875596948)",
     "premise_still_valid": true,
     "summary": "The string half, delivered as draft PR #20496 ('Fixes #20309', 'Clause-②: no (narrowing)'). The number arm (NUMERIC_VALUE_TYPES minus COMPUTED_VALUE_TYPES, now one predicate isJudgedNumberType) judges a string by the spec's parseNumericString instead of Number()-finite, and a new write-side rewrite normalizeNumericStringValues, beside normalizeBlankTypedValues and called at its three engine.ts sites (insert, update, validate), stores an admitted string as its number, so the validator judges and the driver stores one value; no private grammar, no spec, blank rule, date arm or driver change. H1 reproduced at base 851af0c27 (memory stored '12' as the string '12'; SQLite stored '0x10' as TEXT and read it back as 16); at head admitted strings are stored as numbers on both drivers and the 11 strings Number() read as finite but the grammar refuses (' 12 ', '12\\n', '\\t-3', '0x10', '0X1A', '0o17', '0b101', '+5', '.5', '5.', '007') answer 400 invalid_number on every door but /import, whose own cell reader coerces first. The card's assignee was not written; the PR assignee is os-warren (the card's).",
     "tests": "At 6bf61e75a (branch after a true merge of origin/main fc0db22bc) unless noted. objectql whole suite: 329 files, 6584 passed. rest whole suite: 218 files, 4160 passed, 34 skipped. typecheck objectql + rest: exit 0, both test layers OK (tsc --listFiles over each tsconfig.test.json includes the edited test files: objectql 2, rest 1). Downstream sweep at b78c66612 (before the merge, not re-run after it): service-automation 149 files / 1837 passed; metadata-protocol 189 files passed, 3 skipped, 2750 tests passed. Pin files: record-validator.number-value.test.ts 369, engine-number-value-door.test.ts 275, rest-data-number-value.test.ts 277, every string case driven by NUMERIC_STRING_GRAMMAR_CASES (first REST run had 1 red on my own expectation: rating 1e+21 is stored real, not integer; expectation corrected to Number.isSafeInteger, not the code). Ablation A (arm reads strings by Number() again) via scripts/ablation-replace.mjs: anchor 1 to 0, blob 4a253dd40830 to 70d16a081356, objectql rebuilt, ablation-dist-preflight marker in 4 built files; predicted 68 validator + 67 engine + 66 REST reds, measured objectql 135 of 644 and REST 66 of 277; restored blob == HEAD, git diff HEAD empty, rebuild, --absent preflight: absent from 14 built files, tree clean. Ablation B (rewrite made a no-op): blob 4a253dd40830 to d5976af6ff5e, marker in 4 built files; predicted 79 objectql and 0 REST reds (SQLite affinity stores plain numeric strings as numbers anyway), measured objectql 79 of 644 and REST 0 of 277; restored blob == HEAD, rebuild, --absent preflight clean, pins 644/644 and 277/277. So on SQLite the physical-cell pin cannot see the rewrite; the engine driver-payload pin covers memory and MongoDB. ESLint narrowed: the 5 changed .ts files, eslint --no-inline-config --format json: 5 files linted, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules), so no untouched file's verdict can move; repo-wide pnpm lint is CI's.",
     "mcp_calls": "0 — no GitHub MCP call of any kind",
     "api_writes": "3 REST writes, each carried by one relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/pulls (draft pr_create, PR #20496, body read back byte-identical); POST /repos/objectstack-ai/objectstack/issues/20496/assignees (label-write --assign os-warren, read back matches); POST /repos/objectstack-ai/objectstack/issues/20309/comments (this os-dev-report). Plus git push of the branch (not REST). issue_patch not spent.",
     "open_questions": [],
     "out_of_scope_findings": [
      "class: a · reach: REST POST /api/v1/data/:object/import (JSON rows, writeMode insert) measured on InMemoryDriver and SqlDriver/better-sqlite3 with the real RestServer: a decimal-comma cell on a number field is stored as a different number with ok 1, errors 0 — '3,14' stored 314, '1,5' stored 15, '1.000,5' stored 1.0005, '1,2,3' stored 123 (the plain write doors refuse each with invalid_number). Cause, read at source: packages/rest/src/import-coerce.ts parseNumberCell strips every comma (replace of /,/g) with no check that it is a thousands grouping. Not this card's surface (packages/rest/src is test-side only here). Dedupe words: import decimal comma, parseNumberCell thousands separator, '1,5' stored 15, import-coerce comma stripped, CSV import locale number",
      "carrier: 承接者:无 · noted, not filed: the /import cell reader and the spec grammar disagree on 8 of the 41 grammar rows, each the reader accepting what the grammar refuses (' 12 ', '12\\n', '\\t-3', '1,000', '1.000,5', '+5', '.5', '007'); the other 33 agree, including every admitted row's number. That is the reader's documented import-only tolerance (the spec module header calls it deliberately not this grammar); only the '1.000,5' misread is carried in the class a finding above."
     ],
     "gates": {
      "head": "6bf61e75a",
      "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 6bf61e75a: 66 commands (change set 6 paths vs merge base fc0db22bc)",
      "exit_0": 64,
      "not_measured": [
       "pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: needs every package built; CI runs it)",
       "pnpm check:type-check-debt (exit 3, PREREQUISITE NOT MET: needs the full build closure; CI runs it)"
      ],
      "ran_reconciliation": "dispatch-gates --ran: 66 derived famil(ies) accounted for — 64 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN",
      "adr_0087": "check-adr-0087-registration --base origin/main: exit 0; 1 declared-breaking changeset, BREAKING+bang+clause-②-narrowing, not-required (no-migration-prescription)",
      "changeset_level": "check-changeset-no-major --base origin/main --event (a pull_request payload carrying the PR body): exit 0; LEVEL AXIS clause-② no (narrowing), direction arm narrowing, BREAKING, ships minor",
      "empty_changeset": "check-empty-changeset --base origin/main: exit 0 (the earlier pending #20309 changeset is untouched)",
      "closing_keywords": "check-closing-keyword-parity --body (PR body): the only bound declaration is Fixes #20309",
      "ci": "not awaited: 31 check-runs on 6bf61e75a at report time, 10 success, 3 skipped, 18 in_progress"
     },
     "line_budget": "n/a (no skills/** path in the diff)",
     "deviations": [
      "Conflict, stated rather than resolved silently: the dispatch asks the changeset for 'a FROM → TO line'. With that label check-adr-0087-registration reads a migration prescription and refuses not-required (no-migration-prescription), the disposition PR #20370 used for this arm (measured: exit 1 at 03580e7cb, evidence from-to-label). The changeset carries the same mapping as a 'before → after' line plus the fix, the spelling the sibling value narrowing 20386-progress-min-max-enforced.md uses (exit 0 at b78c66612). Nothing authored moves, so there is no ledger row to register.",
      "The earlier pending .changeset/20309-number-arm-non-string-refused.md says a string is still judged by Number() and that which strings are accepted is a separate change; it is left untouched (check:empty-changeset's foreign-changeset rule refuses an edit without a confirmed deliberate correction). The new changeset names itself as that separate change. If the seat wants the old sentence amended, that is a DELIBERATE CORRECTION to confirm on the PR.",
      "The scratch instrument ran from packages/rest/tmp (gitignored, so bare @objectstack specifiers resolve from the rest package) and was deleted after use; copies of both scripts and all raw cell JSON are in the session scratchpad issue-20309b.",
      "The 2640-cell measurement ran at c67623f22 (implementation only); the head 6bf61e75a carries the same number-arm and rewrite code plus the date arm merged from main (PR #20469). The downstream sweep (service-automation, metadata-protocol) ran at b78c66612, before the merge, and was not re-run after it; objectql, rest and both typechecks were re-run at 6bf61e75a.",
      "Memory is measured at every REST door and pinned at the engine door on the driver payload, not with a new REST test consumer: check:driver-memory-census refuses one without a ruling (the constraint PR #20370 met).",
      "objectui census row re-read at the local sibling checkout b8e09415c9 (not fetched; not verified equal to objectui main). It confirms the seat's census (5863923799) and adds two facts used in the changeset: the legacy fallback's client check covers number/currency/percent only, and its parsers trim every cell, so padded forms cannot come from it."
     ],
     "files_changed": [
      "packages/objectql/src/validation/record-validator.ts (+108 -10: isJudgedNumberType, normalizeNumericStringValues, the arm reads parseNumericString, header line)",
      "packages/objectql/src/engine.ts (+15 -5: import, and the rewrite call after normalizeBlankTypedValues at insert, update and validate)",
      "packages/objectql/src/validation/record-validator.number-value.test.ts (+224 -16: the flipped Number() characterization removed; the string half on the grammar table, the rewrite, bounds/scale/precision parity)",
      "packages/objectql/src/engine-number-value-door.test.ts (+86 -2: admitted strings reach the driver as the number on 5 doors, refused never reach it, hook and dry run)",
      "packages/rest/src/rest-data-number-value.test.ts (+82 -3: SQLite physical cell and storage class for admitted strings; refusal on POST, batch, PATCH, updateMany)",
      ".changeset/20309-number-arm-numeric-string-grammar.md (new: objectql minor, BREAKING, Clause-② no (narrowing), ADR-0087 not-required (no-migration-prescription))"
     ],
     "rows": {
      "method": "scratch script booting ObjectQL + ObjectStackProtocolImplementation + RestServer from built dist, InMemoryDriver and SqlDriver (better-sqlite3, in memory); 20 inputs x 6 judged types x 11 doors (engine insert, insertMany, update by id, update by predicate; REST POST, createMany, batch create, PATCH, batch update, updateMany, /import) x 2 drivers = 2640 cells per tree; base 851af0c27, head c67623f22",
      "moved": "1200 of 2640, 60 per moved input (6 types x the 10 non-/import doors); /import moved 0 of 240",
      "admitted_strings": "'12', '12.5', '-3', '-0', '0.10', '1e3': memory base stored the string, head stores the number; SQLite stored a number by affinity at base and head, byte-identical cells",
      "newly_refused": "'0x10' (SQLite base TEXT '0x10' read back 16), ' 12 ', '12\\n', '+5', '.5', '5.', '007': accepted at base on both drivers, invalid_number at head, nothing written, existing cells unchanged",
      "unchanged": "'1,000', 'Infinity', 'NaN', '1e400', 'abc' refused at base and head; '' null at base and head (#20308); the number 12 stored as 12"
     }
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20496 at 6bf61e75aaf6786b56a73ae7184423aa9dce1860 (the string half; this completes the card)

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T19:20Z. Contract review of record: 5876843857 on PR #20496, at-tier, read-only, PASS on this head.

    Checklist, verified against GitHub rather than the reports:

    • Form: draft, base main, first line Fixes #20309. That is the only closing keyword in the body. This card's claim 5875596948 names the branch and reads Clause-②: no (narrowing), the same line as the changeset and the PR body.
    • Scope: 6 files, +607 / −36:
      • record-validator.ts: the number arm (now one predicate, isJudgedNumberType) reads a string by @objectstack/spec/data's parseNumericString alone, with no private grammar. A new write-side rewrite, normalizeNumericStringValues, sits beside normalizeBlankTypedValues;
      • engine.ts: the import, and the rewrite call right after the blank rule at its three sites (validate, insert, update). Nothing else;
      • pins in objectql (the arm on every grammar row, the rewrite, bounds / scale / precision parity) and in rest (SQLite's physical cell and storage class; refusal on POST, batch, PATCH, updateMany);
      • one changeset.
        Every file is inside the claim.
    • Changeset: @objectstack/objectql minor, BREAKING, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription). It carries a "before → after" line, as PR fix(objectql)!: enforce a progress field's declared min / max at the write seam (#20386) #20482 (record validator: a progress field's declared min / max are never checked — REST POST stores 150 over max: 100 and −5 under min: 0 (201), where a number field refuses both #20386) did (the dispatch's FROM → TO label trips the gate's migration-prescription branch, and nothing authorable moves).
    • The earlier pending note (20309-number-arm-non-string-refused.md, PR fix(objectql)!: a number field refuses an array, a boolean or an object with invalid_number (#20309) #20370, same release): the review judged it coherent, so no DELIBERATE CORRECTION is owed. Its "Which strings a number field accepts is a separate change" is the pointer this note answers by name.
    • Governed surface: none (check-governed-merges --pr 20496: not governed). 643 changed lines, under the human-merge threshold.
    • CI: 34 check-runs on the head, all completed: 31 success (Check Changeset, Lint & Repo Gates and Test Core 1–6/6 among them), 3 rostered skips, none red. mergeable_state: clean.
    • Behaviour: a numeric string the platform grammar reads ('12', '12.5', '-3', '1e3') is stored as its number on every driver. Memory used to keep the string. The eleven strings Number() read as finite but the grammar refuses (radix literals, padded forms, '+5', '.5', '5.', '007') answer VALIDATION_FAILED / invalid_number on every write door, with nothing written. A JS number, a blank and summary are unchanged.

    Carried out of this card:

    Landing: ready plus auto-merge through the queue. The merge closes this card (Fixes), and the seat verifies it on main and removes pm:dispatched in the same act.

  8. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #20496 merged (the string half). This card is closed completed by its Fixes line

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-28T19:45Z.

    Verified on main:

    • The squash is 2b24b8b82304e925110800efb0e092845a931d16, a queue merge with one parent. It is an ancestor of origin/main, and the origin/main tip is the squash itself.
    • It carries 6 files, +607 / −36, the accepted head's list.
    • The squash's changed lines are identical to the accepted head 6bf61e75a's changes against its merge base fc0db22bc (the same md5 over every added and removed line).
    • normalizeNumericStringValues is present in record-validator.ts at the squash (3 hits) and absent at its parent. The 20309-number-arm-numeric-string-grammar changeset is present at the squash and absent at its parent.
    • The PR body's one closing keyword is Fixes #20309, so no other card was closed.

    Delivered, with the non-string half (PR #20370, db74b169dc): a number, currency, percent, rating, slider or progress field accepts a JS number, or a string only under the platform's numeric grammar (@objectstack/spec/data's parseNumericString, PR #20414).

    • An admitted string is stored as its number on every driver.
    • An array, a boolean, an object, or a string the grammar refuses (radix, padded, +5, .5, 5., 007) answers VALIDATION_FAILED / invalid_number on every write door, with nothing written.
    • @objectstack/objectql ships minor BREAKING, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription).
    • ACCEPT is 5876857072. The contract review of record is 5876843857 (PASS). It judged the earlier pending note coherent, so no DELIBERATE CORRECTION was owed.

    Carried out of this card:

    pm:dispatched is removed in the same act as this record. The domain, area and type labels stay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions