@golemui/gui-mcp 1.2.0 → 1.2.1-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/json.js CHANGED
@@ -1,4 +1,4 @@
1
- import { G, J, a, b, c, g, d, e, f, v } from "./get-concept-DRFSt-XX.js";
1
+ import { G, J, a, b, c, g, d, e, f, v } from "./get-concept-CnyfqgLF.js";
2
2
  export {
3
3
  G as GET_CONCEPT_TOOL,
4
4
  J as JSON_GENERATE_FROM_OPENAPI_TOOL,
package/lib.js CHANGED
@@ -1,5 +1,5 @@
1
- import { G, J, a, b, c, g, d, e, f, v } from "./get-concept-DRFSt-XX.js";
2
- import { D, a as a2, b as b2, c as c2, d as d2, e as e2, g as g2, l, f as f2, t } from "./list-dx-factories-BLhvXI07.js";
1
+ import { G, J, a, b, c, g, d, e, f, v } from "./get-concept-CnyfqgLF.js";
2
+ import { D, a as a2, b as b2, c as c2, d as d2, e as e2, g as g2, l, f as f2, t } from "./list-dx-factories-Td5MhXfg.js";
3
3
  export {
4
4
  D as DX_CHECK_CODE_TOOL,
5
5
  a2 as DX_GET_SPEC_TOOL,
@@ -1,4 +1,4 @@
1
- import { h as checkReactiveExpression } from "./get-concept-DRFSt-XX.js";
1
+ import { h as checkReactiveExpression, i as checkBooleanValidatorRules } from "./get-concept-CnyfqgLF.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { resolve, join, dirname } from "node:path";
@@ -6,11 +6,13 @@ import { fileURLToPath } from "node:url";
6
6
  import { Decoder, err, ok, object, array, string, oneOf, boolean, optional, succeed, number, literal, lazy, discriminatedUnion, record } from "ts.data.json";
7
7
  import "subscript/justin";
8
8
  import { pipe, map, distinctUntilChanged, filter } from "rxjs";
9
+ const BOOLEAN_FACTORIES = ["checkbox", "booleanInput"];
9
10
  const CONFIG_FIELDS = ["include", "exclude", "disabled", "readonly"];
10
11
  function lintDxSnippet(ts, sourceText, lineOffset) {
11
12
  const sf = ts.createSourceFile("__dx_lint__.ts", sourceText, ts.ScriptTarget.ES2020, true);
12
13
  const diagnostics = [];
13
14
  const expressionWarnings = [];
15
+ const validatorWarnings = [];
14
16
  const posOf = (node) => {
15
17
  const p = sf.getLineAndCharacterOfPosition(node.getStart(sf));
16
18
  return { line: p.line + 1 - lineOffset, column: p.character + 1 };
@@ -26,7 +28,29 @@ function lintDxSnippet(ts, sourceText, lineOffset) {
26
28
  while (ts.isPropertyAccessExpression(e)) e = e.expression;
27
29
  return ts.isIdentifier(e) && e.text === "gui";
28
30
  };
29
- const visit = (node, inTemplate) => {
31
+ const guiFactoryName = (node) => {
32
+ if (!ts.isCallExpression(node) || !isGuiCall(node)) return void 0;
33
+ return ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : void 0;
34
+ };
35
+ const checkBooleanValidator = (validatorObj) => {
36
+ const shape = {};
37
+ for (const p of validatorObj.properties) {
38
+ if (!ts.isPropertyAssignment(p)) return;
39
+ const name = nameOf(p.name);
40
+ if (name === "required") {
41
+ if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) shape.required = true;
42
+ else if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) shape.required = false;
43
+ else return;
44
+ }
45
+ if (name === "const") {
46
+ if (ts.isIdentifier(p.initializer) && p.initializer.text === "undefined") continue;
47
+ shape.const = true;
48
+ }
49
+ }
50
+ const { line, column } = posOf(validatorObj);
51
+ validatorWarnings.push(...checkBooleanValidatorRules(shape, `validator@${line}:${column}`));
52
+ };
53
+ const visit = (node, inTemplate, inBooleanFactory) => {
30
54
  if (ts.isObjectLiteralExpression(node)) {
31
55
  const spreadsGui = node.properties.some(
32
56
  (p) => ts.isSpreadAssignment(p) && isGuiCall(p.expression)
@@ -56,11 +80,16 @@ function lintDxSnippet(ts, sourceText, lineOffset) {
56
80
  expressionWarnings.push(f);
57
81
  }
58
82
  }
83
+ if (inBooleanFactory && ts.isPropertyAssignment(node) && (nameOf(node.name) === "validator" || nameOf(node.name)?.startsWith("validator.") === true) && ts.isObjectLiteralExpression(node.initializer)) {
84
+ checkBooleanValidator(node.initializer);
85
+ }
59
86
  const childInTemplate = inTemplate || ts.isPropertyAssignment(node) && nameOf(node.name) === "template";
60
- ts.forEachChild(node, (child) => visit(child, childInTemplate));
87
+ const factory = guiFactoryName(node);
88
+ const childInBooleanFactory = factory !== void 0 ? BOOLEAN_FACTORIES.includes(factory) : inBooleanFactory;
89
+ ts.forEachChild(node, (child) => visit(child, childInTemplate, childInBooleanFactory));
61
90
  };
62
- visit(sf, false);
63
- return { diagnostics, expressionWarnings };
91
+ visit(sf, false, false);
92
+ return { diagnostics, expressionWarnings, validatorWarnings };
64
93
  }
65
94
  let cachedRequire;
66
95
  function getRequire() {
@@ -200,9 +229,13 @@ ${body}`;
200
229
  hint: hintFor(d, flat)
201
230
  };
202
231
  });
203
- const { diagnostics: lintDiagnostics, expressionWarnings } = lintDxSnippet(ts, full, lineOffset);
232
+ const {
233
+ diagnostics: lintDiagnostics,
234
+ expressionWarnings,
235
+ validatorWarnings
236
+ } = lintDxSnippet(ts, full, lineOffset);
204
237
  const diagnostics = [...tscDiagnostics, ...lintDiagnostics];
205
- return { ok: diagnostics.length === 0, diagnostics, expressionWarnings };
238
+ return { ok: diagnostics.length === 0, diagnostics, expressionWarnings, validatorWarnings };
206
239
  }
207
240
  async function checkDxCode(input) {
208
241
  if (typeof input?.code !== "string" || input.code.trim() === "") {
@@ -212,7 +245,7 @@ async function checkDxCode(input) {
212
245
  }
213
246
  const DX_CHECK_CODE_TOOL = {
214
247
  name: "dx_check_code",
215
- description: "Type-check GolemUI **DX code** (the `gui.*` fluent builder, written in TypeScript) against the real `@golemui` type declarations, and return compiler diagnostics. This is for `gui.*` *code* — distinct from `json_validate_form_definition`, which validates a JSON form-definition *object*. GolemUI is not in any model's training data, so generated `gui.*` code is frequently a confident fabrication that does not compile; this is the only trustworthy check (inspection misses it). Beyond type errors it also catches two defects the compiler cannot see: a misplaced `include`/`exclude` attached as a sibling of a `gui.*` spread (`{ ...gui.inputs.x(...), include }` compiles but silently never hides the field — put `include`/`exclude` INSIDE the config argument), and reactive-expression mistakes in `when` strings (linted by the same engine as `json_validate_form_definition`). Pass the `gui.*` snippet as `code` (a bare array of `gui.inputs.*` items is fine — a `@golemui/gui-shared` import is added if missing). Returns `{ ok, diagnostics, expressionWarnings }`; each diagnostic has a TypeScript `code` (0 for the static lints), `message`, `line`/`column`, and — for recognized GolemUI mistakes — a `hint` with the fix. `expressionWarnings` are advisory and do not flip `ok`. Treat `ok: false` as blocking: apply the fixes and re-check until `ok` is true.",
248
+ description: "Type-check GolemUI **DX code** (the `gui.*` fluent builder, written in TypeScript) against the real `@golemui` type declarations, and return compiler diagnostics. This is for `gui.*` *code* — distinct from `json_validate_form_definition`, which validates a JSON form-definition *object*. GolemUI is not in any model's training data, so generated `gui.*` code is frequently a confident fabrication that does not compile; this is the only trustworthy check (inspection misses it). Beyond type errors it also catches defects the compiler cannot see: a misplaced `include`/`exclude` attached as a sibling of a `gui.*` spread (`{ ...gui.inputs.x(...), include }` compiles but silently never hides the field — put `include`/`exclude` INSIDE the config argument), reactive-expression mistakes in `when` strings, and the mandatory-checkbox trap (a `checkbox`/`booleanInput` validator with only half of the `required: true` + `const: true` pair — it will not force the box to be checked). Both lints share their rule engines with `json_validate_form_definition`. Pass the `gui.*` snippet as `code` (a bare array of `gui.inputs.*` items is fine — a `@golemui/gui-shared` import is added if missing). Returns `{ ok, diagnostics, expressionWarnings, validatorWarnings }`; each diagnostic has a TypeScript `code` (0 for the static lints), `message`, `line`/`column`, and — for recognized GolemUI mistakes — a `hint` with the fix. `expressionWarnings` and `validatorWarnings` are advisory and do not flip `ok` — surface them and apply their `suggestion`. Treat `ok: false` as blocking: apply the fixes and re-check until `ok` is true.",
216
249
  inputSchema: {
217
250
  type: "object",
218
251
  properties: {
@@ -480,7 +513,7 @@ const FRAMEWORK_SETUP = {
480
513
  vanilla: "RENDER (vanilla JS) — `import { gui } from '@golemui/gui-shared'; import '@golemui/gui-lit';` (registers `<gui-form>`), then `const el = document.querySelector('gui-form'); el.config = { formDef: form }; el.addEventListener('" + formEventNames.submit + "', (e) => { /* e.detail.data */ });`. In TypeScript, type the element — `import type { FormElement } from '@golemui/gui-lit'; const el = document.querySelector<FormElement>('gui-form');` — the published types do not register `gui-form` in `HTMLElementTagNameMap`, so an untyped `querySelector` yields `Element` and `el.config` fails to compile."
481
514
  };
482
515
  function commonNote(fw = "react") {
483
- return "GolemUI builds FORMS — data collection and validation. It is NOT a general-purpose UI toolkit: it never renders documents, page content, or markdown for display. A form is just an array of these items: `export const form = [ /* items */ ];`. " + FRAMEWORK_SETUP[fw] + " Import the component stylesheet ONCE — `@golemui/gui-components/index.css` — or the form renders unstyled. To RECEIVE A SUBMIT: add a `gui.actions.button({ label, actionType: 'submit' })` to the form and listen for the submit on the host component (the RENDER line above shows how for your framework) — the handler gets a `FormSubmitEvent` whose `.data` is the collected form data. To DISABLE submit until the form is valid, add `disabled: { when: '$formIsInvalid' }` to that button (`$formIsInvalid` is a built-in validity flag) — see the conditional-and-state-props pattern. The SAME `formDef` renders in every framework (React/Angular/Vue/Lit/vanilla) — only the host wrapper changes. FORM-LEVEL CONFIG — `formDef` is ALWAYS the bare array. Anything form-wide (named `states`, `validateOn`) goes in a sibling `formConfig` on the config (`config={{ formDef: form, formConfig: { states, validateOn } }}`), NEVER inside `formDef`. Do NOT wrap the array as `{ states, form: [...] }` and pass THAT as `formDef` — `formDef` is typed `Record<string, any>` so it COMPILES, but the `gui.*` items are never resolved and the form renders BLANK with no error. See the form-level-states pattern. Common fields like `include`/`exclude` (conditional visibility) go INSIDE a factory’s config argument — never spread them onto the result (`{ ...gui.inputs.x(...), include }` compiles but silently does nothing). See the conditional-visibility pattern. STATIC CONTENT — a section heading or any non-input text/block is the HOST’s job, not GolemUI’s: use `gui.displays.display(() => <h2>…</h2>)` returning your framework’s own node (React JSX, Vue/Angular/Lit node) — it needs no dependency and always renders. MARKDOWN has exactly ONE use: `gui.inputs.markdown`, an INPUT where the user EDITS markdown (its value is their markdown string). There is NO markdown-for-display widget — never use markdown to render a heading or content; use `display` for that. VALIDATOR `type` — one rule, three cases (so you never have to guess): (1) choice widgets (`dropdown`, `radiogroup`, `select`) REQUIRE an explicit `type`: `validator: { type: 'string', required: true }`. (2) `repeater` (array) validators auto-supply `type: 'array'` — supply only the rules, e.g. `validator: { required: true, minItems: 1 }`, never `type`. (3) everything else (text, number, date) takes the loose validator with NO `type`: `validator: { required: true }`. EVENT HANDLERS — `onChange`/`onLoad`/`onFilter`/`onBlur` (inputs/layouts) and `onClick` (actions) are FUNCTIONS, never bare strings: return a string to dispatch a host event by that name (`onChange: () => 'languageChanged'`), or take the event to push live changes (`onChange: (event) => event.update({ path: 'city', options: [...] })`).";
516
+ return "GolemUI builds FORMS — data collection and validation. It is NOT a general-purpose UI toolkit: it never renders documents, page content, or markdown for display. A form is just an array of these items: `export const form = [ /* items */ ];`. " + FRAMEWORK_SETUP[fw] + " Import the component stylesheet ONCE — `@golemui/gui-components/index.css` — or the form renders unstyled. To RECEIVE A SUBMIT: add a `gui.actions.button({ label, actionType: 'submit' })` to the form and listen for the submit on the host component (the RENDER line above shows how for your framework) — the handler gets a `FormSubmitEvent` whose `.data` is the collected form data. To DISABLE submit until the form is valid, add `disabled: { when: '$formIsInvalid || $form.<requiredField> === undefined' }` to that button. `$formIsInvalid` is a built-in validity flag, but validation NEVER runs at mount, so on the pristine form it is `false` and `$formIsInvalid` ALONE leaves the button ENABLED while required fields are still empty — the extra data check covers that gap. See the conditional-and-state-props pattern. The SAME `formDef` renders in every framework (React/Angular/Vue/Lit/vanilla) — only the host wrapper changes. FORM-LEVEL CONFIG — `formDef` is ALWAYS the bare array. Anything form-wide (named `states`, `validateOn`) goes in a sibling `formConfig` on the config (`config={{ formDef: form, formConfig: { states, validateOn } }}`), NEVER inside `formDef`. Do NOT wrap the array as `{ states, form: [...] }` and pass THAT as `formDef` — `formDef` is typed `Record<string, any>` so it COMPILES, but the `gui.*` items are never resolved and the form renders BLANK with no error. See the form-level-states pattern. Common fields like `include`/`exclude` (conditional visibility) go INSIDE a factory’s config argument — never spread them onto the result (`{ ...gui.inputs.x(...), include }` compiles but silently does nothing). See the conditional-visibility pattern. STATIC CONTENT — a section heading or any non-input text/block is the HOST’s job, not GolemUI’s: use `gui.displays.display(() => <h2>…</h2>)` returning your framework’s own node (React JSX, Vue/Angular/Lit node) — it needs no dependency and always renders. MARKDOWN has exactly ONE use: `gui.inputs.markdown`, an INPUT where the user EDITS markdown (its value is their markdown string). There is NO markdown-for-display widget — never use markdown to render a heading or content; use `display` for that. VALIDATOR `type` — one rule, three cases (so you never have to guess): (1) choice widgets (`dropdown`, `radiogroup`, `select`) REQUIRE an explicit `type`: `validator: { type: 'string', required: true }`. (2) `repeater` (array) validators auto-supply `type: 'array'` — supply only the rules, e.g. `validator: { required: true, minItems: 1 }`, never `type`. (3) everything else (text, number, date) takes the loose validator with NO `type`: `validator: { required: true }`. EVENT HANDLERS — `onChange`/`onLoad`/`onFilter`/`onBlur` (inputs/layouts) and `onClick` (actions) are FUNCTIONS, never bare strings: return a string to dispatch a host event by that name (`onChange: () => 'languageChanged'`), or take the event to push live changes (`onChange: (event) => event.update({ path: 'city', options: [...] })`).";
484
517
  }
485
518
  const PATTERNS = [
486
519
  {
@@ -506,12 +539,24 @@ const PATTERNS = [
506
539
  {
507
540
  name: "conditionalAndStateProps",
508
541
  title: "Conditional & state-driven props (enable/disable, show/hide, readonly)",
509
- example: "gui.actions.button({ label: 'Submit', actionType: 'submit', disabled: { when: '$formIsInvalid' } })",
542
+ example: "gui.actions.button({ label: 'Submit', actionType: 'submit', disabled: { when: '$formIsInvalid || $form.email === undefined' } })",
510
543
  notes: [
511
- "ENABLE/DISABLE & READONLY: `disabled` and `readonly` are typed `boolean | { when: <expr> }`. To gate the submit button on validity: `gui.actions.button({ label, actionType: 'submit', disabled: { when: '$formIsInvalid' } })`. `$formIsInvalid` is a built-in validity flag — do not declare it as a state.",
544
+ "ENABLE/DISABLE & READONLY: `disabled` and `readonly` are typed `boolean | { when: <expr> }`. `$formIsInvalid` is a built-in validity flag — do not declare it as a state.",
545
+ "PRISTINE-FORM TRAP: validation never runs at mount, so `$formIsInvalid` starts `false` and `disabled: { when: '$formIsInvalid' }` leaves the submit button ENABLED while required fields are still empty (it disables only after the first validated interaction). Gate the button with a compound expression that also checks the data — the example above adds `|| $form.email === undefined` for a form whose required field is `email`. The runtime still blocks the actual submit while invalid, so this is a UX concern, not a data-integrity one.",
512
546
  "SHOW/HIDE: `include` / `exclude` are typed `{ in: ['stateName'] }` / `{ from: ['stateName'] }` (state lists) or `{ when: <expr> }`. Every state name in `in`/`from` MUST be declared in `formConfig.states`; an undeclared name leaves the widget hidden forever (the engine logs an error to the console).",
513
547
  "These are ALL typed config keys — pass them inside the factory’s config argument. NEVER reach a prop by casting the factory result and assigning a key (`(btn as any)['disabled.formValid'] = false`) or by spreading (`{ ...gui.actions.button(...), disabled }`): keys added that way are SILENTLY ignored and the behavior never fires. If a prop is not on the typed config, you are guessing — it is not a real field."
514
548
  ]
549
+ },
550
+ {
551
+ name: "validationMessages",
552
+ title: "Validation rules & custom error messages (required, const, messages)",
553
+ example: "gui.inputs.textInput('email', { label: 'Email', validator: { required: true, format: 'email', messages: { invalid: 'Email is required', required: 'Email is required', format: 'Enter a valid email address' } } })",
554
+ notes: [
555
+ 'Every validator accepts a `messages` map from rule name to custom error text (string or i18n `{ key, params?, default? }`). Keys per type — string: `invalid`, `required`, `minLength`, `maxLength`, `pattern`, `format`, `enum`, `const`; number: `invalid`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `enum`, `const`; boolean: `invalid`, `const`; array: `invalid`, `required`, `minItems`, `maxItems`. The special key `invalid` customizes the base type check. Set custom messages for every rule you use — the library defaults (e.g. "Invalid input: expected string, received undefined") are developer-facing, not user-facing.',
556
+ 'ALWAYS pair `messages.required` with `messages.invalid` (same text): an `undefined`/`null` value — the pristine or cleared field, the most common empty state — fails the base TYPE check and shows the `invalid` message; the `required` rule only fires on present-but-empty values (`""`, `[]`). That is also why number/boolean validators have NO `required` message key — for them `messages.invalid` is the only way to word the missing-value error. `null` is never valid, even on non-required fields.',
557
+ "MANDATORY CHECKBOX: boolean `required: true` does NOT force it checked (`false` is a valid boolean) and `const: true` alone lets the pristine `undefined` pass. Use both plus paired messages — see the `gui.inputs.checkbox` entry for the full recipe.",
558
+ 'WHEN VALIDATION RUNS: only on user interaction, per `formConfig.validateOn` — `"eager"` (default: first change or blur anywhere validates the whole form), `"change"`, `"blur"`, `"submit"`, or an array. NOTHING validates at mount, which is why `$formIsInvalid` starts `false` — see the conditional-and-state-props pattern for gating the submit button correctly. Submit clicks always re-validate everything and the runtime blocks the submit event while invalid.'
559
+ ]
515
560
  }
516
561
  ];
517
562
  const INPUTS = [
@@ -548,9 +593,13 @@ const INPUTS = [
548
593
  factory: "checkbox",
549
594
  namespace: "inputs",
550
595
  docSlug: "checkbox",
551
- call: "gui.inputs.checkbox(path, { label, defaultValue? })",
552
- example: "gui.inputs.checkbox('terms', { label: 'I accept the terms', defaultValue: false })",
553
- notes: ["A single boolean rendered as a checkbox."]
596
+ call: "gui.inputs.checkbox(path, { label, defaultValue?, validator? })",
597
+ example: "gui.inputs.checkbox('terms', { label: 'I accept the terms', validator: { required: true, const: true, messages: { invalid: 'You must accept the terms', const: 'You must accept the terms' } } })",
598
+ notes: [
599
+ "A single boolean rendered as a checkbox.",
600
+ "MANDATORY CHECKBOX (terms acceptance): `required: true` alone is a silent trap — an unchecked box holding `false` is a valid boolean and PASSES; only `const: true` rejects `false`. And `const: true` alone lets the pristine `undefined` pass (non-required validators are optional). Use BOTH, as in the example.",
601
+ "Set BOTH `messages.invalid` and `messages.const` to the same text: a never-touched box fails the type check (`invalid` message), a checked-then-unchecked box fails the `const` rule (`const` message). See the validation-messages pattern."
602
+ ]
554
603
  },
555
604
  {
556
605
  factory: "textarea",
@@ -598,7 +647,8 @@ const INPUTS = [
598
647
  example: "gui.inputs.datePicker('startDate', { label: 'Coverage start', minDate: '2025-01-01', validator: { required: true } })",
599
648
  notes: [
600
649
  "THE DEFAULT single-date field: a text field with a popover calendar (click to open). Prefer this for most dates. (`calendar` = always-visible inline calendar; `dateInput` = typed entry, no calendar UI.) Accepts the loose `{ required: true }` validator.",
601
- 'Bound the selectable range with **`minDate`** / **`maxDate`** — ISO `YYYY-MM-DD` strings. For "today or later" set `minDate` to today’s date; for "not in the future" set `maxDate` to today. Same `minDate`/`maxDate` on `calendar`, `dateInput`, and the range date widgets.'
650
+ 'Bound the selectable range with **`minDate`** / **`maxDate`** — ISO `YYYY-MM-DD` strings. For "today or later" set `minDate` to today’s date; for "not in the future" set `maxDate` to today. Same `minDate`/`maxDate` on `calendar`, `dateInput`, and the range date widgets.',
651
+ 'A partially typed date abandoned on focus leave flips the value to null — flagging the field even when optional — and surfaces an "incomplete" error; **`incompleteMessage`** overrides its wording. The same prop exists on every typed date/time widget (the inputs, the pickers, the range inputs/pickers, and the inline `dateTimeCalendar`/`rangeDateTimeCalendar`), and an emptied widget clears the error on the next focus leave.'
602
652
  ]
603
653
  },
604
654
  {
@@ -625,10 +675,11 @@ const INPUTS = [
625
675
  namespace: "inputs",
626
676
  docSlug: "dateinput",
627
677
  call: "gui.inputs.dateInput(path, { label, minDate?, maxDate?, validator? })",
628
- example: "gui.inputs.dateInput('startDate', { label: 'Start date', validator: { required: true } })",
678
+ example: "gui.inputs.dateInput('startDate', { label: 'Start date', incompleteMessage: 'Incomplete date!', validator: { required: true } })",
629
679
  notes: [
630
680
  "Typed date entry, NO calendar UI — use only when keyboard-first entry is wanted. For most dates use `datePicker` (popover calendar) instead. Accepts the loose `{ required: true }`.",
631
- "`minDate` / `maxDate` (ISO `YYYY-MM-DD` strings) constrain the accepted range — see `datePicker`."
681
+ "`minDate` / `maxDate` (ISO `YYYY-MM-DD` strings) constrain the accepted range — see `datePicker`.",
682
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves a partial entry — see `datePicker`.'
632
683
  ]
633
684
  },
634
685
  {
@@ -638,7 +689,8 @@ const INPUTS = [
638
689
  call: "gui.inputs.timeInput(path, { label, hourFormat?, minuteStep?, validator? })",
639
690
  example: "gui.inputs.timeInput('meetingTime', { label: 'Meeting time', minuteStep: 15 })",
640
691
  notes: [
641
- "Typed time entry (hh:mm segments). Emits an ISO time string (`HH:mm:ss`) — pair with the `{ format: 'time' }` validator. `hourFormat` forces '12'/'24' (default: locale); `minuteStep` sets the arrow-key minute increment."
692
+ "Typed time entry (hh:mm segments). Emits an ISO time string (`HH:mm:ss`) — pair with the `{ format: 'time' }` validator. `hourFormat` forces '12'/'24' (default: locale); `minuteStep` sets the arrow-key minute increment.",
693
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves a partial entry — see `datePicker`.'
642
694
  ]
643
695
  },
644
696
  {
@@ -648,7 +700,8 @@ const INPUTS = [
648
700
  call: "gui.inputs.timePicker(path, { label, minTime?, maxTime?, minuteStep?, disabledRanges?, allowCustomTime?, validator? })",
649
701
  example: "gui.inputs.timePicker('meetingTime', { label: 'Meeting time', minTime: '09:00', maxTime: '18:00', minuteStep: 30 })",
650
702
  notes: [
651
- "Time field with a popover list of slots built from `minTime`..`maxTime` stepping `minuteStep` (default 30). `disabledRanges` (`{ start, end }[]`, inclusive) greys slots out. Typing is off unless `allowCustomTime: true`. Emits `HH:mm:ss` — pair with the `{ format: 'time' }` validator."
703
+ "Time field with a popover list of slots built from `minTime`..`maxTime` stepping `minuteStep` (default 30). `disabledRanges` (`{ start, end }[]`, inclusive) greys slots out. Typing is off unless `allowCustomTime: true`. Emits `HH:mm:ss` — pair with the `{ format: 'time' }` validator.",
704
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves a partial typed entry — see `datePicker`.'
652
705
  ]
653
706
  },
654
707
  {
@@ -658,7 +711,8 @@ const INPUTS = [
658
711
  call: "gui.inputs.dateTimeInput(path, { label, hourFormat?, minuteStep?, validator? })",
659
712
  example: "gui.inputs.dateTimeInput('meetingAt', { label: 'Meeting at' })",
660
713
  notes: [
661
- "Typed date+time entry in one locale-ordered row. Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`) — pair with the `{ format: 'date-time' }` validator. `hourFormat`/`minuteStep` as in `timeInput`."
714
+ "Typed date+time entry in one locale-ordered row. Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`) — pair with the `{ format: 'date-time' }` validator. `hourFormat`/`minuteStep` as in `timeInput`.",
715
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves a partial entry — see `datePicker`.'
662
716
  ]
663
717
  },
664
718
  {
@@ -680,7 +734,8 @@ const INPUTS = [
680
734
  example: "gui.inputs.dateTimeCalendar('appointmentAt', { label: 'Appointment', minTime: '09:00', maxTime: '18:00' })",
681
735
  notes: [
682
736
  "An INLINE calendar with an embedded time picker: a segmented time input between the header and the days grid opens a time grid in place of the days (like the year selector).",
683
- "Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`) only when BOTH day and time are selected — pair with a `{ type: 'string', format: 'date-time' }` validator. Picking a different day clears the time and resets the value to null.",
737
+ "Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`) only when BOTH day and time are selected — pair with a `{ type: 'string', format: 'date-time' }` validator. The time picker is usable before a day is picked, and picking a different day KEEPS the chosen time (re-emitting the full value), so the two halves can be chosen in either order.",
738
+ 'Half-finished entries never emit: the value goes to null — flagging the field even when it is optional — only once focus leaves the widget with one half missing, which surfaces an "incomplete" message (`incompleteMessage` overrides its wording).',
684
739
  "`disabledTimeRanges` entries take `start`/`end` ISO times plus optional `date` (ISO date) and/or `weekdays` (getDay() numbering: 0=Sunday … 6=Saturday) to scope the range to specific days."
685
740
  ]
686
741
  },
@@ -693,7 +748,7 @@ const INPUTS = [
693
748
  notes: [
694
749
  "A compact date-time FIELD that opens a `dateTimeCalendar` POPOVER on focus — the space-saving counterpart to the inline `dateTimeCalendar`, like `datePicker` is to `calendar`.",
695
750
  "Emits a local ISO date-time (`YYYY-MM-DDTHH:mm:ss`); the popover closes only when BOTH day and time are selected. Pair with a `{ type: 'string', format: 'date-time' }` validator.",
696
- "Takes the same time props as `dateTimeCalendar` (`minTime`/`maxTime`/`minuteStep`/`disabledTimeRanges` with per-date/weekday scoping/`allowCustomTime`) plus `icon` and `invalidDateMessage` for the typed input."
751
+ "Takes the same time props as `dateTimeCalendar` (`minTime`/`maxTime`/`minuteStep`/`disabledTimeRanges` with per-date/weekday scoping/`allowCustomTime`) plus `icon`, `invalidDateMessage` and `incompleteMessage` for the typed input."
697
752
  ]
698
753
  },
699
754
  {
@@ -753,7 +808,10 @@ const INPUTS = [
753
808
  docSlug: "range-date-input",
754
809
  call: "gui.inputs.rangeDateInput(path, { label? })",
755
810
  example: "gui.inputs.rangeDateInput('stayDates', { label: 'Stay dates' })",
756
- notes: ["Typed start–end date **range** entry (the range sibling of `dateInput`)."]
811
+ notes: [
812
+ "Typed start–end date **range** entry (the range sibling of `dateInput`).",
813
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves with only one endpoint filled — see `datePicker`.'
814
+ ]
757
815
  },
758
816
  {
759
817
  factory: "rangeTimeInput",
@@ -762,7 +820,8 @@ const INPUTS = [
762
820
  call: "gui.inputs.rangeTimeInput(path, { label?, minTime?, maxTime? })",
763
821
  example: "gui.inputs.rangeTimeInput('shift', { label: 'Shift', minTime: '06:00:00', maxTime: '22:00:00' })",
764
822
  notes: [
765
- "Typed start–end time **range** entry (the range sibling of `timeInput`); value is `TimeRange[]`. End time must be after start time."
823
+ "Typed start–end time **range** entry (the range sibling of `timeInput`); value is `TimeRange[]`. End time must be after start time.",
824
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves with only one endpoint filled — see `datePicker`.'
766
825
  ]
767
826
  },
768
827
  {
@@ -773,7 +832,8 @@ const INPUTS = [
773
832
  example: "gui.inputs.rangeDateTimeInput('window', { label: 'Window', minDateTime: '2026-03-01T06:00:00', maxDateTime: '2026-03-31T22:00:00' })",
774
833
  notes: [
775
834
  "Typed start–end date-time **range** entry (the range sibling of `dateTimeInput`); value is `DateTimeRange[]`. A backward selection reorders (swaps) instead of erroring.",
776
- "Each endpoint is an instant, so it is bounded by instants: use **`minDateTime`** / **`maxDateTime`** (ISO `YYYY-MM-DDTHH:mm:ss`), not `minDate`/`maxDate`. There is no `minTime`/`maxTime` here — a per-day time window is a different constraint from an instant bound."
835
+ "Each endpoint is an instant, so it is bounded by instants: use **`minDateTime`** / **`maxDateTime`** (ISO `YYYY-MM-DDTHH:mm:ss`), not `minDate`/`maxDate`. There is no `minTime`/`maxTime` here — a per-day time window is a different constraint from an instant bound.",
836
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves with only one endpoint filled — see `datePicker`.'
777
837
  ]
778
838
  },
779
839
  {
@@ -783,7 +843,7 @@ const INPUTS = [
783
843
  call: "gui.inputs.rangeDateTimeCalendar(path, { label?, minDateTime?, maxDateTime?, disabledRanges?, startTimeLabel?, endTimeLabel? })",
784
844
  example: "gui.inputs.rangeDateTimeCalendar('stay', { label: 'Stay', startTimeLabel: 'Check-in', endTimeLabel: 'Check-out' })",
785
845
  notes: [
786
- "INLINE range calendar with TWO embedded time pickers (start/end); value is `DateTimeRange[]`, rendered as pills. Pick a date range, then a start time (enables the end time), then an end time to commit a pill. A day holding more than one range shows a count badge.",
846
+ 'INLINE range calendar with TWO embedded time pickers (start/end); value is `DateTimeRange[]`, rendered as pills. A day span and the two times can be chosen in ANY order — both time pickers are usable from the start — and the pill is committed once all four pieces are present. Starting a new day span keeps the times already chosen. A day holding more than one range shows a count badge. Leaving the widget with a half-finished selection surfaces an "incomplete" error over the untouched pills (`incompleteMessage` overrides its wording); leaving it emptied clears the error.',
787
847
  "Everything is in instant-space: bounds are **`minDateTime`** / **`maxDateTime`** and **`disabledRanges`** are `DateTimeRange[]` instant spans (block a whole day with `00:00:00`–`23:59:59`). There is no `minDate`/`maxDate`/`minTime`/`maxTime`/`disabledTimeRanges` — a time-of-day constraint cannot bound a multi-day span."
788
848
  ]
789
849
  },
@@ -794,7 +854,7 @@ const INPUTS = [
794
854
  call: "gui.inputs.rangeDateTimePicker(path, { label?, minDateTime?, maxDateTime?, disabledRanges?, startTimeLabel?, endTimeLabel? })",
795
855
  example: "gui.inputs.rangeDateTimePicker('stay', { label: 'Stay', startTimeLabel: 'Check-in', endTimeLabel: 'Check-out' })",
796
856
  notes: [
797
- "POPOVER date-time range picker: the typed `rangeDateTimeInput` as the trigger (pills live there) with the `rangeDateTimeCalendar` in a dropdown. Value is `DateTimeRange[]`. Committing a pill keeps the popover open so several ranges can be added; it closes on outside-click, blur or Escape.",
857
+ 'POPOVER date-time range picker: the typed `rangeDateTimeInput` as the trigger (pills live there) with the `rangeDateTimeCalendar` in a dropdown. Value is `DateTimeRange[]`. Committing a pill keeps the popover open so several ranges can be added; it closes on outside-click, blur or Escape. A half-finished selection is held by the picker, so it survives closing and reopening the popover. Leaving it that way surfaces an "incomplete" error (`incompleteMessage` overrides its wording).',
798
858
  "Everything is in instant-space: bounds are **`minDateTime`** / **`maxDateTime`** and **`disabledRanges`** are `DateTimeRange[]` instant spans (block a whole day with `00:00:00`–`23:59:59`). There is no `minDate`/`maxDate`/`minTime`/`maxTime`/`disabledTimeRanges`."
799
859
  ]
800
860
  },
@@ -804,7 +864,9 @@ const INPUTS = [
804
864
  docSlug: "range-date-picker",
805
865
  call: "gui.inputs.rangeDatePicker(path, { label? })",
806
866
  example: "gui.inputs.rangeDatePicker('stayDates', { label: 'Stay dates' })",
807
- notes: ["Popover calendar for a start–end date **range** (the range sibling of `datePicker`)."]
867
+ notes: [
868
+ 'Popover calendar for a start–end date **range** (the range sibling of `datePicker`). A span with only its first day picked is held by the picker, so it survives closing and reopening the popover. Leaving it that way surfaces an "incomplete" error (`incompleteMessage` overrides its wording).'
869
+ ]
808
870
  },
809
871
  {
810
872
  factory: "rangeTimePicker",
@@ -813,7 +875,8 @@ const INPUTS = [
813
875
  call: "gui.inputs.rangeTimePicker(path, { label?, minTime?, maxTime? })",
814
876
  example: "gui.inputs.rangeTimePicker('shift', { label: 'Shift', minTime: '06:00:00', maxTime: '22:00:00' })",
815
877
  notes: [
816
- "Two-list popover for a start–end time **range** (the range sibling of `timePicker`); value is `TimeRange[]`. The out list floors one slot after the chosen in so end is strictly after start."
878
+ "Two-list popover for a start–end time **range** (the range sibling of `timePicker`); value is `TimeRange[]`. Either list can be used first — an end picked before a start simply waits — and the range commits once both are set. Once an in is chosen the out list floors one slot after it so end is strictly after start.",
879
+ '`incompleteMessage` overrides the "incomplete" error surfaced when focus leaves with only one endpoint set — see `datePicker`.'
817
880
  ]
818
881
  }
819
882
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golemui/gui-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.1-rc.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -43,10 +43,10 @@
43
43
  "ajv": "^8.17.1",
44
44
  "ajv-formats": "^3.0.1",
45
45
  "typescript": "^5.9.2",
46
- "@golemui/gui-schemas": "1.2.0",
47
- "@golemui/gui-shared": "1.2.0",
48
- "@golemui/gui-validators": "1.2.0",
49
- "@golemui/core": "1.2.0",
46
+ "@golemui/gui-schemas": "1.2.1-rc.0",
47
+ "@golemui/gui-shared": "1.2.1-rc.0",
48
+ "@golemui/gui-validators": "1.2.1-rc.0",
49
+ "@golemui/core": "1.2.1-rc.0",
50
50
  "@modelcontextprotocol/sdk": "^1.0.0",
51
51
  "zod": "^4.0.0",
52
52
  "@standard-schema/spec": "^1.0.0"
@@ -21,7 +21,7 @@ export declare const GET_CONCEPT_TOOL: {
21
21
  readonly properties: {
22
22
  readonly concept: {
23
23
  readonly type: "string";
24
- readonly description: "The concept to explain. Currently supported: `\"states\"`, `\"string-interpolation\"`.";
24
+ readonly description: string;
25
25
  readonly enum: string[];
26
26
  };
27
27
  };
@@ -0,0 +1,32 @@
1
+ export type BooleanValidatorFinding = {
2
+ path: string;
3
+ message: string;
4
+ suggestion: string;
5
+ };
6
+ /**
7
+ * Semantic lint for boolean validators — the mandatory-checkbox trap.
8
+ *
9
+ * The runtime facts (see `@golemui/gui-validators`): a boolean validator has no
10
+ * `required` refine, so `required: true` only rejects a MISSING value
11
+ * (`undefined`/`null`) — an unchecked box holding `false` passes. And a validator
12
+ * without `required: true` is wrapped in `optional()`, so `const: true` alone lets
13
+ * the pristine `undefined` pass. Forcing a checkbox to be checked therefore needs
14
+ * BOTH rules — either half alone validates something the author almost never means.
15
+ *
16
+ * Like `reactive-expressions.ts`, this is the shared engine for both surfaces:
17
+ * {@link lintBooleanValidators} walks a JSON form definition, while the DX path
18
+ * (`dx-lint.ts`) extracts each boolean-widget validator from the AST and funnels it
19
+ * through {@link checkBooleanValidatorRules} — one rule set, no drift.
20
+ */
21
+ export declare function checkBooleanValidatorRules(validator: {
22
+ required?: unknown;
23
+ const?: unknown;
24
+ }, path: string): BooleanValidatorFinding[];
25
+ /**
26
+ * Walks a JSON form definition and applies {@link checkBooleanValidatorRules} to every
27
+ * boolean validator — the `validator` key and its state-suffixed variants
28
+ * (`"validator.<stateName>"`) on any widget, at any nesting depth (layout `children`,
29
+ * repeater `props.template`). Only validators declaring `"type": "boolean"` are
30
+ * checked; shape errors on other types are the JSON Schemas' job.
31
+ */
32
+ export declare function lintBooleanValidators(formDefinition: unknown): BooleanValidatorFinding[];