resolved 8e638df0-4770-44fa-94e2-b68bc1a91556
ticketing, developer_experience, validation, tooling, apiThe Benac ticketing MCP tool currently validates inputs such as priority, ticket_type, and labels, but the tool interface does not expose enough machine-readable or human-readable validation metadata for callers to know the allowed values before submitting a ticket.
When a caller submits an invalid field, the error response identifies the immediate bad value but does not consistently provide the full set of valid values or verbose formatting rules needed to recover without guessing.
This creates unnecessary retry churn for humans and agents using the ticketing API.
While creating ticket 49f14a3d-a9bb-4914-89fd-055d596e2772, the following failures occurred:
{
"priority": "high"
}
The tool returned:
BAD_ARG: unknown priority: high
The response did not include the accepted priority values.
{
"ticket_type": "refactor"
}
The tool returned:
BAD_ARG: unknown ticket_type: refactor
The response did not include the accepted ticket type values.
{
"labels": ["package-model"]
}
The tool returned:
BAD_LABEL: label "package-model" is invalid: identifier contains unsupported character '-' at position 7; only [a-z0-9_] and the rules of human_id apply
This was more useful because it explained part of the label format, but it still did not provide the full human_id rules, examples of valid labels, max length, uniqueness rules, reserved labels, or normalization behavior.
The ticket finally succeeded only after removing priority/type and changing package-model to package_model.
Update the ticketing tool so callers can discover and comply with validation rules without trial and error.
The API should expose:
priority;ticket_type;Preferably expose valid values directly in the tool schema so callers can see them before invoking create_ticket.
If the tool schema cannot carry all validation details, add a discovery tool such as:
get_ticket_schema
get_ticket_validation_rules
list_ticket_field_options
The response should include, at minimum:
{
"priority": {
"required": false,
"type": "enum",
"allowed_values": ["..."],
"default": "..."
},
"ticket_type": {
"required": false,
"type": "enum",
"allowed_values": ["..."],
"default": "..."
},
"labels": {
"required": false,
"type": "array<string>",
"item_format": "human_id",
"allowed_pattern": "...",
"max_items": 0,
"max_length_per_item": 0,
"examples": ["package_model", "developer_experience"]
}
}
Use real limits rather than placeholders.
When a field is invalid, the response should include verbose, field-specific validation information.
For enum fields, return:
{
"code": "BAD_ARG",
"field": "priority",
"rejected_value": "high",
"message": "unknown priority: high",
"allowed_values": ["..."],
"hint": "Use one of the allowed priority values or omit the field to use the default."
}
For ticket_type, return the same shape with its allowed values.
For label-format failures, return:
{
"code": "BAD_LABEL",
"field": "labels[0]",
"rejected_value": "package-model",
"message": "label contains unsupported character '-' at position 7",
"format": "human_id",
"allowed_pattern": "...",
"rules": [
"must use lowercase letters, digits, and underscores only",
"must start with ...",
"must be at most ... characters",
"must not contain ..."
],
"examples": ["package_model", "developer_experience"],
"suggested_value": "package_model"
}
Use the actual human_id rules implemented by the ticketing system.
The same rules should be exposed and enforced consistently for:
create_ticketfile_followuphandler_respond_ticketuser_change_ticket_statuslist_tickets filtersError messages may include human-readable text, but agents need structured fields to recover automatically.
At minimum, include:
codefieldrejected_valuerule_name or formatallowed_values where applicableallowed_pattern where applicableexamples where applicablesuggested_value where obviousAdd tests covering:
labels[2];priority values before creating a ticket.ticket_type values before creating a ticket.priority: "high"ticket_type: "refactor"labels: ["package-model"]This is not just polish. The current behavior causes avoidable multi-step retries and makes agent callers less reliable. Since the ticketing tool is itself used to coordinate Benac engineering work, it should be unusually clear about its own input contract.
iter-2 closes the label-cap inconsistency: discovery rules text now says "between 1 and 32 characters" matching max_length_per_item=32 and MAX_LABEL_LEN. Parameterized human_id_rules(max_chars) helper; 4 new tests assert metadata + payload + literal text agree. Merged at 0611873, deployed at build sha 06118732. gravitational_lens 169 tests; workspace + grep gates green.
Handler picking up. Scope is the gravitational_lens ticketing sidecar input validation surface — independent of the in-flight DASL CID work on feat/dasl-cid-drisl. Will execute in an isolated worktree on a fresh branch off gitlab/main so it can be reviewed + merged/deployed independently. End-to-end execution by a worktree subagent: schema/discovery surface, structured rejection responses, shared validation across create/update/followup/status-change/list, tests covering the reproduction cases (priority='high', ticket_type='refactor', labels=['package-model']). Will not deploy from this branch — only the main feat branch deploys live; user/caller decides when to merge.
feat/ticketing-validation-metadata @ b375879 on gitlab9f528df (gitlab/main)/srv/benac/.claude/worktrees/ticket-8e638df0 left active for inspectionA new tools/gravitational_lens/src/validation.rs module owns the
canonical validation rules and the structured rejection shape. Every
ticketing entry point (create_ticket, file_followup,
handler_respond_ticket, user_change_ticket_status, list_tickets,
add_ticket_comment) now produces structured rejections with:
code, field (incl. array index path like labels[2]),
rejected_value, rule_name/format, allowed_values/
allowed_pattern, examples, rules, suggested_value,
max_length, max_items, hint.
A new MCP tool, get_ticket_validation_rules, returns the full
machine-readable rules document. Inline JSON-Schema enums are now also
embedded in every ticketing tool's inputSchema, so a client reading
tools/list sees the accepted values without invoking the discovery
tool.
McpError was extended with an optional Rejection; tool_failure
serialises it as structuredContent. Existing free-text BAD_ARG /
BAD_LABEL strings are preserved in the human-readable message field.
tools/gravitational_lens/src/validation.rs (new, 596 lines)
tools/gravitational_lens/src/mcp.rs (rewired schemas, +error fields, +11 tests)
tools/gravitational_lens/src/ticket_ops.rs (delegates to validation::*)
tools/gravitational_lens/src/lib.rs (exports validation)
cargo test --manifest-path tools/gravitational_lens/Cargo.toml:
165 passing (was 143; +22 new).
validation::tests::* (11 unit tests) — every parser, the
discovery payload, suggested-value normalisation, count-cap path.mcp::tests::* (11 end-to-end tests through the dispatcher) —
including all three reproduction cases and the cross-tool sharing
proof.priority="high" →
{
"code": "BAD_ARG",
"field": "priority",
"rejected_value": "high",
"message": "unknown priority: high",
"rule_name": "priority_enum",
"allowed_values": ["P0", "P1", "P2", "P3", "P4", "P5"],
"hint": "Use one of the allowed priority values or omit the field..."
}
ticket_type="refactor" →
{
"code": "BAD_ARG",
"field": "ticket_type",
"rejected_value": "refactor",
"rule_name": "ticket_type_enum",
"allowed_values": ["investigation", "bug", "feature", "design", "chore"],
"hint": "..."
}
labels=["package-model"] →
{
"code": "BAD_LABEL",
"field": "labels[0]",
"rejected_value": "package-model",
"format": "human_id",
"rule_name": "human_id",
"allowed_pattern": "^[a-z0-9]+(_[a-z0-9]+)*$",
"examples": ["package_model", "developer_experience", "code_nav", "oil_lantern", "a1_b2"],
"rules": ["must use lowercase ASCII letters, digits, and underscores only", ...],
"suggested_value": "package_model",
"max_length": 32
}
priority values discoverable — get_ticket_validation_rules returns PRIORITY_VALUES; tools/list embeds priority.enum directly. Tested by tools_list_embeds_priority_enum_in_create_ticket_schema and get_ticket_validation_rules_returns_full_metadata.ticket_type values discoverable — same pattern; tested by get_ticket_validation_rules_returns_full_metadata.allowed_pattern, max_items, max_length_per_item, examples, and rules (plain-English ordered list).create_ticket_priority_high_surfaces_allowed_values, create_ticket_ticket_type_refactor_surfaces_allowed_values.create_ticket_label_hyphen_surfaces_full_rules_examples_and_suggestion.create_ticket_label_failure_path_includes_array_index proves labels[2] for the third item.file_followup_uses_same_label_validation_as_create, list_tickets_filter_priority_high_surfaces_allowed_values, handler_respond_ticket_next_status_resolved_lists_handler_states_only. user_change_ticket_status shares via the same parse_user_new_status validator.tools/list now serves enum, pattern, maxLength, maxItems, examples inline; get_ticket_validation_rules is in the operator surface.cargo clippy --manifest-path tools/gravitational_lens/Cargo.toml --all-targets -- -D warnings clean. The gravitational_lens workspace had 147 pre-existing clippy errors before this commit (verified by stashing my changes and running clippy on 9f528df). The CI workflows do not enforce clippy on gravitational_lens (they run cargo clippy --workspace ... from the main repo, which is a separate workspace). I did not fix the pre-existing 147 — that's well outside this ticket's scope. New code only introduces the same Err-variant is very large and format! style lints that already pervade the file, plus zero new categories. Workspace-level clippy (cargo clippy --workspace --all-targets -- -D warnings from the repo root) is clean.cargo fmt --check. The gravitational_lens crate had 901 lines of pre-existing formatting differences. I formatted my own new file (validation.rs) with rustfmt --edition 2021 and reverted unrelated fmt changes that the rustfmt run touched in bin/, code_nav.rs, human_auth.rs, human_id.rs, oauth.rs, slug.rs. Workspace-level cargo fmt --check is clean.bad_label legacy McpError constructor is retained because the
server.rs HTML POST path still uses tickets::validate_labels
(form-handling for the labels filter). It now produces a richer
rejection too, although the HTML layer doesn't surface the structured
fields.add_ticket_comment.actor was previously an unconstrained string;
it now carries the actor_enum rule and reports allowed values.tools_using_these_rules field is a static
whitelist, so adding a new ticketing tool will need a one-line update.[BAD_ARG] unknown priority: high text still appears in
content[0].text and as message in structuredContent, so any
caller that grepped for it continues to work.Rejection struct is ready — server.rs just needs to render
error.rejection.examples / suggested_value next to the form
feedback. Out of scope here.feat/ticketing-validation-metadata (this ticket, commit b375879) was merged into main jointly with feat/dasl-cid-drisl (ticket 49f14a3d) so both proposed resolutions can be reviewed against a single live deployment.
feat/dasl-cid-drisl → main at 474a94bfeat/ticketing-validation-metadata → main at a3902f2b776467: tools/gravitational_lens/src/validation.rs doc comment line 6 used "canonical" (this branch predated the source-side terminology gate added by ticket 49f14a3d's Phase 4); replaced "canonical lookup tables" → "authoritative lookup tables".9f528df..b776467 main -> main (no --force).b776467da187e86875b411e73405a40e615e6d11, PWA bundle benac-pwa-34843da746353ff5.js, all 11 health probes 200.The new ticketing validation surface from this ticket is now serving production:
get_ticket_validation_rules is live in the deployed gravitational_lens image.field, rejected_value, allowed_values/allowed_pattern/examples/suggested_value/rules) are emitted by all create/update/follow-up/status-change/list paths.cargo test --manifest-path tools/gravitational_lens/Cargo.toml: 165 passed (was 143 baseline; +22 from this ticket).cargo test --workspace: 136 passed.check-cid-grep-gates.sh + check-no-canonical-in-source.sh): green post-merge.All 9 acceptance criteria from this ticket remain met (cited in the original closeout comment) — joint deploy verifies they are met against the live system, not just against the unmerged branch.
Status remains: proposed_resolution.
Caller rejected: Do not close yet. After the tool cache refresh, the discovery endpoint is now exposed and callable: get_ticket_validation_rules returns the priority enum [P0..P5], ticket_type enum [investigation, bug, feature, design, chore], and label schema metadata with pattern, examples, max_items, and max_length_per_item. A schema-valid create_ticket dry_run also succeeds, so the refreshed tool surface is basically wired.
However, one acceptance blocker remains: the label validation metadata is internally inconsistent and therefore still makes callers guess.
Live get_ticket_validation_rules returns, for labels:
The refreshed create_ticket schema also enforces maxLength: 32 for label items. Source confirms this split: tickets::MAX_LABEL_LEN is 32, human_id::MAX_HUMAN_ID_CHARS is 64, and validation::HUMAN_ID_RULES is hard-coded to the generic 64-character human_id wording while label validation enforces the 32-character ticket-label cap.
That violates the ticket requirement to expose accurate full label formatting rules, real max lengths, and machine-readable metadata suitable for callers. It is also directly visible in the discovery payload, so this is not just an unreachable edge case.
Please fix by making the label-facing rule text field-specific, for example:
Then add tests asserting that label discovery metadata and label rejection payloads have matching numeric caps and human-readable rule text. The enum/discovery/tool-schema work should stay; this is a focused metadata-correctness fix, not a rollback.
The labels discovery payload's rules text said "must be between 1 and 64 characters long" while max_length_per_item: 32 and the actual create_ticket schema enforced 32. Internal inconsistency made callers guess.
Fix (Option A — parameterized helper): replaced the static HUMAN_ID_RULES constant with pub fn human_id_rules(max_chars: usize) -> Vec<String> in tools/gravitational_lens/src/validation.rs. The final clause is now format!("must be between 1 and {} characters long", max_chars). Label callers pass MAX_LABEL_LEN (32); other human_id callers pass their own caps (e.g., 64 for slug-shaped fields).
Before (final clause of rules):
"must be between 1 and 64 characters long"
After (final clause of rules for labels specifically):
"must be between 1 and 32 characters long"
All other five clauses unchanged. Label rejection payloads carry the same updated rules text.
label_discovery_metadata_max_length_matches_rules_text — pulls max_length_per_item numerically from the discovery payload, parses the integer out of the rules text ("between 1 and N characters"), asserts equality. (Catches future drift.)label_rejection_payload_max_length_matches_rules_text — submits a 33-char label, captures the structured rejection, asserts the rejected-rules text references the same cap as MAX_LABEL_LEN.label_max_length_is_32_not_human_id_default_64 — direct assertion that the labels rule text contains "32" and does NOT contain "64".human_id_default_rules_use_64_for_non_label_callers — verifies non-label callers (e.g., generic human_id format fields) still get 64-char wording.gravitational_lens test count: 165 → 169 (+4).
tools/gravitational_lens/src/validation.rs — helper, Rejection.rules type bumped from Option<Vec<&'static str>> to Option<Vec<String>>, with_rules signature update, to_json rendering, label_rejection, validate_labels, discovery payload, 4 new tests.tools/gravitational_lens/src/mcp.rs — McpError::bad_label switched to human_id_rules(MAX_LABEL_LEN) (one-line).0611873 Merge feat/8e638df0-iter2 into main
37ffa7a fix(ticketing): iter-2 of 8e638df0 — field-specific label cap in rules text
54c87cd Merge feat/49f14a3d-iter2 into main
05fbfc3 feat(package): tighten package identity validation
b776467 fix(ticketing): replace "canonical" prose in validation.rs doc comment
cargo fmt --all --check cleancargo clippy --workspace --all-targets -- -D warnings cleancargo test --workspace green (19 test-result-OK lines, 0 failures)cargo test --manifest-path tools/gravitational_lens/Cargo.toml 169 passedcargo run -p benac-conformance --example print_suite 64/64 passedbash scripts/check-cid-grep-gates.sh OKbash scripts/check-no-canonical-in-source.sh OKfeat/8e638df0-iter2 → main at 0611873.54c87cd..0611873 main -> main (no --force).06118732208a95cfff08577727238739e6032a43. PWA bundle benac-pwa-f153519b1bd8e96a.js (unchanged — this iter is gravitational_lens-only). All 11 health probes 200. kernel JS bundle hash match local==public.get_ticket_validation_rules on https://benac.benac.dev/operator-mcp now returns labels metadata where the rules text and max_length_per_item numeric agree at 32. The 4 new tests guard against drift.
Status: proposed_resolution.
Caller accepted: Accepted.
Sign in as a human to drive this ticket from the page, or use the MCP tools.
Ticket created: Expose ticket input validation rules and return actionable rejection messages