@burdenoff/microfe-bigconsole 2026.1005.4 → 2026.1007.10
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/dist/bigconsole/assistant/assistantProduct.d.ts.map +1 -1
- package/dist/bigconsole/assistant/assistantProduct.js +2 -1
- package/dist/bigconsole/assistant/assistantProduct.js.map +1 -1
- package/dist/bigconsole/assistant/recordAdapters.d.ts.map +1 -1
- package/dist/bigconsole/assistant/recordAdapters.js +27 -26
- package/dist/bigconsole/assistant/recordAdapters.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistantProduct.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/assistant/assistantProduct.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qDAAqD,CAAC;AACjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4DAA4D,CAAC;AACvG,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,6CAA6C,CAAC;AAQjG;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,oBAAoB,EAoCxD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"assistantProduct.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/assistant/assistantProduct.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qDAAqD,CAAC;AACjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4DAA4D,CAAC;AACvG,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,6CAA6C,CAAC;AAQjG;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,oBAAoB,EAoCxD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,qBAmB/B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAC/C,IAAI,CAAC,6BAA6B,EAAE,UAAU,GAAG,kBAAkB,GAAG,cAAc,CAAC,CACtF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,EAAE,0BA+B1C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistantProduct.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantProduct.ts"],"sourcesContent":["/**\n * BigConsole's answers to the shared assistant's product seam, as one value.\n *\n * ★★ Every capability field on fe-libs' `AssistantProductProvider` is OPTIONAL,\n * and a missing one does not throw — it degrades SILENTLY. No `gatherPageContext`\n * sends every turn with no page context; no `buildPrompt` drops both the\n * confirm-before-mutate guardrail and the language directive; no\n * `storageFolders` starts a second folder beside the one holding every saved\n * attachment.\n *\n * So the value is typed `Required<…>` over the provider's own props: forgetting\n * a field, or fe-libs ADDING one, is a type error in this file rather than a\n * quiet regression in production.\n */\nimport type { LocalUiCommandOptions } from '@burdenoff/fe-libs/shared/assistant/localUiCommands';\nimport type { AssistantEntityRoute } from '@burdenoff/fe-libs/shared/assistant/uiActions/entityRoutes';\nimport type { AssistantProductProviderProps } from '@burdenoff/fe-libs/shared/assistant/product';\n\nimport { buildPromptForMode } from './assistantPrompt';\nimport { BIGCONSOLE_RECORD_ADAPTERS } from './recordAdapters';\nimport { BIGCONSOLE_ASSISTANT_MODES } from './modes';\nimport { describeCurrentPage, gatherPageContext } from './pageContext';\nimport { BIGCONSOLE_PRODUCT } from './product';\n\n/**\n * Where this product's assistant files land in the workspace file library.\n *\n * ★★ Capitalised as a DISPLAY name while the product id is `bigconsole`, which\n * is exactly why fe-libs cannot derive it: its fallback is\n * `/${product}/AI Assistant/…` → `/bigconsole/AI Assistant/…`.\n *\n * These strings become load-bearing the moment anything is saved. A wrong value\n * does not fail loudly — it quietly starts a SECOND folder beside the first and\n * orphans everything in the original. Today nothing can be orphaned: BigConsole's\n * legacy transport extracts uploaded files into the prompt and saves nothing to\n * the library, so no `/BigConsole/AI Assistant/*` path exists yet. That makes\n * this a free choice now and an expensive one the moment BC4 ships attachments.\n */\nexport const STORAGE_FOLDERS = {\n attachments: '/BigConsole/AI Assistant/Attachments',\n artifacts: '/BigConsole/AI Assistant/Artifacts',\n} as const;\n\n/**\n * The pages of ONE record that no sidebar entry names (BOFF-8060 N1b), so \"take me\n * there\" can open them from a link in the assistant's own reply.\n *\n * ★★ `exclude` lists the router's STATIC siblings of each `:id` segment\n * (`BigConsoleRoutes.tsx`): `datasinks/new` is the builder, not a data source.\n * `entityRoutes.test.ts` resolves every template through the real route table and\n * checks every static sibling against them, so the two lists cannot drift from the\n * router unnoticed.\n *\n * ★ A workflow's address is `workflows/workflow/<key>/<version>` (a slug key such\n * as `github_user_profile` or a UUID, plus `v1`); the version-less\n * `workflows/:workflowId` is a different page and is deliberately not declared.\n *\n * ★ `/integrations/connections` is the app shell's Connected Systems page, shown\n * to whoever can see the Integrations entry.\n */\nexport const ENTITY_ROUTES: readonly AssistantEntityRoute[] = [\n {\n id: 'bigconsole-datasink',\n pattern: '/bigconsole/datasinks/:id',\n params: { id: { kind: 'id', exclude: ['new'] } },\n visibleWith: 'bigconsole-datasinks',\n noun: { key: 'entity.datasink', fallback: 'data source' },\n },\n {\n id: 'bigconsole-dashboard',\n pattern: '/bigconsole/dashboards/:id',\n params: { id: { kind: 'id' } },\n visibleWith: 'bigconsole-dashboards',\n noun: { key: 'entity.dashboard', fallback: 'dashboard' },\n },\n {\n id: 'bigconsole-parser',\n pattern: '/bigconsole/parsers/:id',\n params: { id: { kind: 'id', exclude: ['new'] } },\n visibleWith: 'bigconsole-parsers',\n noun: { key: 'entity.parser', fallback: 'parser' },\n },\n {\n id: 'bigconsole-workflow',\n pattern: '/bigconsole/workflows/workflow/:key/:version',\n params: { key: { kind: 'slug' }, version: { kind: 'slug' } },\n visibleWith: 'bigconsole-workflows-list',\n noun: { key: 'entity.workflow', fallback: 'workflow' },\n },\n {\n id: 'bigconsole-connections',\n pattern: '/integrations/connections',\n params: {},\n visibleWith: 'integrations',\n noun: { key: 'entity.connections', fallback: 'Connected Systems' },\n },\n];\n\n/**\n * How the panel reads a UI command it carries out itself (BOFF-7802), for\n * BigConsole.\n *\n * ★★ `mode-only` because BigConsole's CONTENT has themes of its own: a\n * dashboard has a theme, so \"use the dark theme\" in a dashboard builder is a\n * request about the dashboard, and must reach the model rather than flip the\n * app. fe-libs names this exact case (`LocalUiCommandOptions.themeWords`). Only\n * \"<name> mode\" (\"dark mode\") or a theme qualified as the app's (\"change the\n * app theme to dark\") still changes the app locally, with no turn.\n *\n * ★ Its own exported constant so the seam test can pin the field by identity,\n * like every other field on the seam value.\n */\nexport const LOCAL_UI_COMMANDS: LocalUiCommandOptions = {\n themeWords: 'mode-only',\n entityRoutes: ENTITY_ROUTES,\n // BOFF-8060 N1b: \"open the Coffee Shop Sales dashboard\" / data source, by name.\n records: BIGCONSOLE_RECORD_ADAPTERS,\n};\n\n/**\n * Every seam field, required — the provider's props minus its children, and\n * minus the two answers BigConsole deliberately does not give.\n *\n * ★★ `reportTokenUsage` is omitted ON EVIDENCE, not by oversight, and this\n * `Omit` member is the only place that decision is expressed in the type system.\n * `wspace-bigconsole-svc` declares NO token-usage mutation at all, and nothing in\n * this MFE reports one today. The obvious \"fix\" — wiring fe-libs' shared\n * reporter — would bill VibeControls for BigConsole turns: `reportAiTokenUsage`\n * is declared only in `wspace-vibecontrols-svc`, against the\n * `vibecontrols.ai.credits` quota. Metering BigConsole is a backend change.\n *\n * Note that BigConsole's `bigconsole.*` quotas are a different mechanism —\n * they cap entity counts, not AI spend — so their presence is not evidence that\n * turns are metered.\n *\n * ★★ `confirmWrite` is omitted so a HIGH-risk write proposal is confirmed in\n * fe-libs' shared `AssistantWriteConfirmDialog` (BOFF-7803 5a). That is the\n * documented default when a product has no dialog of its own, and BigConsole\n * has none. A product dialog must meet the whole contract fe-libs sets for it\n * (readback, target count, consequences, the exact confirm label, sensitive\n * values masked, never `window.confirm`), and the shared one already does, so a\n * BigConsole copy would only be a second place for that to go wrong. The\n * bigconsole-app ambient declaration of this value mirrors this `Omit`.\n */\nexport type BigConsoleAssistantProduct = Required<\n Omit<AssistantProductProviderProps, 'children' | 'reportTokenUsage' | 'confirmWrite'>\n>;\n\n/**\n * ★ Each field points at a product-owned implementation BY IDENTITY — no inline\n * arrow wrappers. The seam guard asserts `toBe`, because a wrapper is how a\n * function silently stops being the one the tests cover.\n *\n * ★ `writeAccess` is deliberately absent: it is a PROP on the shared panel, not\n * a seam field, so it belongs to BC4's panel mount rather than here. Same for\n * the preview controller, which the panel takes as `panelProps.preview`.\n */\nexport const BIGCONSOLE_ASSISTANT_PRODUCT: BigConsoleAssistantProduct = {\n product: BIGCONSOLE_PRODUCT,\n modes: BIGCONSOLE_ASSISTANT_MODES,\n describeCurrentPage,\n gatherPageContext,\n buildPrompt: buildPromptForMode,\n storageFolders: STORAGE_FOLDERS,\n localUiCommands: LOCAL_UI_COMMANDS,\n /**\n * ★ On. That is fe-libs' own default (on unless `false`, BOFF-7801) and\n * what BigConsole does today: this seam never set the field, so its turns\n * have been reported by default. The reports carry timings and coarse\n * dimensions only, and BigConsole is the first proposal-v1 product, so its\n * latency is the measurement the rollout needs.\n */\n reportTurnTimings: true,\n /**\n * ★★★ BigConsole is the FIRST proposal-v1 product (BOFF-7803 5a). At\n * `'proposal-v1'` fe-libs provisions Ask and Build (`api-calls`) through the\n * `assistant:use`-gated `provisionAssistantSandbox`, requests the proposal\n * flow for Build, and lets a spoken Build turn through when the sandbox's\n * own `/health` also says v1. Every change then waits for the button on its\n * card; the sandbox and wspace-sandbox-svc decide whether it runs.\n *\n * ★★ That provisioning path has NO `createSandbox` fallback: while\n * wspace-sandbox-svc answers it with 503 (its session relay switched off),\n * BigConsole's assistant cannot start at all. This flag reaches users only\n * when bigconsole-app pins the microfe version that carries it, so the app\n * bump is the release switch, and it waits for the relay.\n */\n writeApproval: 'proposal-v1',\n};\n"],"mappings":";;;;;;AAsCA,IAAa,IAAkB;CAC7B,aAAa;CACb,WAAW;CACZ,EAoHY,IAA2D;CACtE,SAAS;CACT,OAAO;CACP;CACA;CACA,aAAa;CACb,gBAAgB;CAChB,iBApDsD;EACtD,YAAY;EACZ,cAtD4D;GAC5D;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI;KAAE,MAAM;KAAM,SAAS,CAAC,MAAM;KAAE,EAAE;IAChD,aAAa;IACb,MAAM;KAAE,KAAK;KAAmB,UAAU;KAAe;IAC1D;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI,EAAE,MAAM,MAAM,EAAE;IAC9B,aAAa;IACb,MAAM;KAAE,KAAK;KAAoB,UAAU;KAAa;IACzD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI;KAAE,MAAM;KAAM,SAAS,CAAC,MAAM;KAAE,EAAE;IAChD,aAAa;IACb,MAAM;KAAE,KAAK;KAAiB,UAAU;KAAU;IACnD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ;KAAE,KAAK,EAAE,MAAM,QAAQ;KAAE,SAAS,EAAE,MAAM,QAAQ;KAAE;IAC5D,aAAa;IACb,MAAM;KAAE,KAAK;KAAmB,UAAU;KAAY;IACvD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE;IACV,aAAa;IACb,MAAM;KAAE,KAAK;KAAsB,UAAU;KAAqB;IACnE;GACF;EAoBC,SAAS;EACV;CAuDC,mBAAmB;CAenB,eAAe;CAChB"}
|
|
1
|
+
{"version":3,"file":"assistantProduct.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantProduct.ts"],"sourcesContent":["/**\n * BigConsole's answers to the shared assistant's product seam, as one value.\n *\n * ★★ Every capability field on fe-libs' `AssistantProductProvider` is OPTIONAL,\n * and a missing one does not throw — it degrades SILENTLY. No `gatherPageContext`\n * sends every turn with no page context; no `buildPrompt` drops both the\n * confirm-before-mutate guardrail and the language directive; no\n * `storageFolders` starts a second folder beside the one holding every saved\n * attachment.\n *\n * So the value is typed `Required<…>` over the provider's own props: forgetting\n * a field, or fe-libs ADDING one, is a type error in this file rather than a\n * quiet regression in production.\n */\nimport type { LocalUiCommandOptions } from '@burdenoff/fe-libs/shared/assistant/localUiCommands';\nimport type { AssistantEntityRoute } from '@burdenoff/fe-libs/shared/assistant/uiActions/entityRoutes';\nimport type { AssistantProductProviderProps } from '@burdenoff/fe-libs/shared/assistant/product';\n\nimport { buildPromptForMode } from './assistantPrompt';\nimport { BIGCONSOLE_RECORD_ADAPTERS } from './recordAdapters';\nimport { BIGCONSOLE_ASSISTANT_MODES } from './modes';\nimport { describeCurrentPage, gatherPageContext } from './pageContext';\nimport { BIGCONSOLE_PRODUCT } from './product';\n\n/**\n * Where this product's assistant files land in the workspace file library.\n *\n * ★★ Capitalised as a DISPLAY name while the product id is `bigconsole`, which\n * is exactly why fe-libs cannot derive it: its fallback is\n * `/${product}/AI Assistant/…` → `/bigconsole/AI Assistant/…`.\n *\n * These strings become load-bearing the moment anything is saved. A wrong value\n * does not fail loudly — it quietly starts a SECOND folder beside the first and\n * orphans everything in the original. Today nothing can be orphaned: BigConsole's\n * legacy transport extracts uploaded files into the prompt and saves nothing to\n * the library, so no `/BigConsole/AI Assistant/*` path exists yet. That makes\n * this a free choice now and an expensive one the moment BC4 ships attachments.\n */\nexport const STORAGE_FOLDERS = {\n attachments: '/BigConsole/AI Assistant/Attachments',\n artifacts: '/BigConsole/AI Assistant/Artifacts',\n} as const;\n\n/**\n * The pages of ONE record that no sidebar entry names (BOFF-8060 N1b), so \"take me\n * there\" can open them from a link in the assistant's own reply.\n *\n * ★★ `exclude` lists the router's STATIC siblings of each `:id` segment\n * (`BigConsoleRoutes.tsx`): `datasinks/new` is the builder, not a data source.\n * `entityRoutes.test.ts` resolves every template through the real route table and\n * checks every static sibling against them, so the two lists cannot drift from the\n * router unnoticed.\n *\n * ★ A workflow's address is `workflows/workflow/<key>/<version>` (a slug key such\n * as `github_user_profile` or a UUID, plus `v1`); the version-less\n * `workflows/:workflowId` is a different page and is deliberately not declared.\n *\n * ★ `/integrations/connections` is the app shell's Connected Systems page, shown\n * to whoever can see the Integrations entry.\n */\nexport const ENTITY_ROUTES: readonly AssistantEntityRoute[] = [\n {\n id: 'bigconsole-datasink',\n pattern: '/bigconsole/datasinks/:id',\n params: { id: { kind: 'id', exclude: ['new'] } },\n visibleWith: 'bigconsole-datasinks',\n noun: { key: 'entity.datasink', fallback: 'data source' },\n },\n {\n id: 'bigconsole-dashboard',\n pattern: '/bigconsole/dashboards/:id',\n params: { id: { kind: 'id' } },\n visibleWith: 'bigconsole-dashboards',\n noun: { key: 'entity.dashboard', fallback: 'dashboard' },\n },\n {\n id: 'bigconsole-parser',\n pattern: '/bigconsole/parsers/:id',\n params: { id: { kind: 'id', exclude: ['new'] } },\n visibleWith: 'bigconsole-parsers',\n noun: { key: 'entity.parser', fallback: 'parser' },\n },\n {\n id: 'bigconsole-workflow',\n pattern: '/bigconsole/workflows/workflow/:key/:version',\n params: { key: { kind: 'slug' }, version: { kind: 'slug' } },\n visibleWith: 'bigconsole-workflows-list',\n noun: { key: 'entity.workflow', fallback: 'workflow' },\n },\n {\n id: 'bigconsole-connections',\n pattern: '/integrations/connections',\n params: {},\n visibleWith: 'integrations',\n noun: { key: 'entity.connections', fallback: 'Connected Systems' },\n },\n];\n\n/**\n * How the panel reads a UI command it carries out itself (BOFF-7802), for\n * BigConsole.\n *\n * ★★ `mode-only` because BigConsole's CONTENT has themes of its own: a\n * dashboard has a theme, so \"use the dark theme\" in a dashboard builder is a\n * request about the dashboard, and must reach the model rather than flip the\n * app. fe-libs names this exact case (`LocalUiCommandOptions.themeWords`). Only\n * \"<name> mode\" (\"dark mode\") or a theme qualified as the app's (\"change the\n * app theme to dark\") still changes the app locally, with no turn.\n *\n * ★ Its own exported constant so the seam test can pin the field by identity,\n * like every other field on the seam value.\n */\nexport const LOCAL_UI_COMMANDS: LocalUiCommandOptions = {\n themeWords: 'mode-only',\n entityRoutes: ENTITY_ROUTES,\n // BOFF-8060 N1b: \"open the Coffee Shop Sales dashboard\" / data source, by name.\n records: BIGCONSOLE_RECORD_ADAPTERS,\n /**\n * ★ BOFF-8060 N2 (opt-in): each turn also offers the model the pages this\n * person may be taken to (fe-libs `buildUiCatalogue`): built ONLY from the\n * RBAC-filtered sidebar once its permissions have settled, never while they\n * load, after a failed permission fetch, or for a simulated role; capped at\n * 120 entries and 8 KB; labels and paths only, no record data.\n *\n * It is a body key on the sandbox prompt request. An AI runtime without N2\n * ignores an unknown key (the `conversationId` / `writeReply` precedent), so\n * this is inert on the prod runtime pinned today (2026.928.98) and active\n * where the N2 runtime runs (alpha, 2026.1007.103). Turning it off is this\n * one line.\n */\n uiCatalogue: true,\n};\n\n/**\n * Every seam field, required — the provider's props minus its children, and\n * minus the two answers BigConsole deliberately does not give.\n *\n * ★★ `reportTokenUsage` is omitted ON EVIDENCE, not by oversight, and this\n * `Omit` member is the only place that decision is expressed in the type system.\n * `wspace-bigconsole-svc` declares NO token-usage mutation at all, and nothing in\n * this MFE reports one today. The obvious \"fix\" — wiring fe-libs' shared\n * reporter — would bill VibeControls for BigConsole turns: `reportAiTokenUsage`\n * is declared only in `wspace-vibecontrols-svc`, against the\n * `vibecontrols.ai.credits` quota. Metering BigConsole is a backend change.\n *\n * Note that BigConsole's `bigconsole.*` quotas are a different mechanism —\n * they cap entity counts, not AI spend — so their presence is not evidence that\n * turns are metered.\n *\n * ★★ `confirmWrite` is omitted so a HIGH-risk write proposal is confirmed in\n * fe-libs' shared `AssistantWriteConfirmDialog` (BOFF-7803 5a). That is the\n * documented default when a product has no dialog of its own, and BigConsole\n * has none. A product dialog must meet the whole contract fe-libs sets for it\n * (readback, target count, consequences, the exact confirm label, sensitive\n * values masked, never `window.confirm`), and the shared one already does, so a\n * BigConsole copy would only be a second place for that to go wrong. The\n * bigconsole-app ambient declaration of this value mirrors this `Omit`.\n */\nexport type BigConsoleAssistantProduct = Required<\n Omit<AssistantProductProviderProps, 'children' | 'reportTokenUsage' | 'confirmWrite'>\n>;\n\n/**\n * ★ Each field points at a product-owned implementation BY IDENTITY — no inline\n * arrow wrappers. The seam guard asserts `toBe`, because a wrapper is how a\n * function silently stops being the one the tests cover.\n *\n * ★ `writeAccess` is deliberately absent: it is a PROP on the shared panel, not\n * a seam field, so it belongs to BC4's panel mount rather than here. Same for\n * the preview controller, which the panel takes as `panelProps.preview`.\n */\nexport const BIGCONSOLE_ASSISTANT_PRODUCT: BigConsoleAssistantProduct = {\n product: BIGCONSOLE_PRODUCT,\n modes: BIGCONSOLE_ASSISTANT_MODES,\n describeCurrentPage,\n gatherPageContext,\n buildPrompt: buildPromptForMode,\n storageFolders: STORAGE_FOLDERS,\n localUiCommands: LOCAL_UI_COMMANDS,\n /**\n * ★ On. That is fe-libs' own default (on unless `false`, BOFF-7801) and\n * what BigConsole does today: this seam never set the field, so its turns\n * have been reported by default. The reports carry timings and coarse\n * dimensions only, and BigConsole is the first proposal-v1 product, so its\n * latency is the measurement the rollout needs.\n */\n reportTurnTimings: true,\n /**\n * ★★★ BigConsole is the FIRST proposal-v1 product (BOFF-7803 5a). At\n * `'proposal-v1'` fe-libs provisions Ask and Build (`api-calls`) through the\n * `assistant:use`-gated `provisionAssistantSandbox`, requests the proposal\n * flow for Build, and lets a spoken Build turn through when the sandbox's\n * own `/health` also says v1. Every change then waits for the button on its\n * card; the sandbox and wspace-sandbox-svc decide whether it runs.\n *\n * ★★ That provisioning path has NO `createSandbox` fallback: while\n * wspace-sandbox-svc answers it with 503 (its session relay switched off),\n * BigConsole's assistant cannot start at all. This flag reaches users only\n * when bigconsole-app pins the microfe version that carries it, so the app\n * bump is the release switch, and it waits for the relay.\n */\n writeApproval: 'proposal-v1',\n};\n"],"mappings":";;;;;;AAsCA,IAAa,IAAkB;CAC7B,aAAa;CACb,WAAW;CACZ,EAkIY,IAA2D;CACtE,SAAS;CACT,OAAO;CACP;CACA;CACA,aAAa;CACb,gBAAgB;CAChB,iBAlEsD;EACtD,YAAY;EACZ,cAtD4D;GAC5D;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI;KAAE,MAAM;KAAM,SAAS,CAAC,MAAM;KAAE,EAAE;IAChD,aAAa;IACb,MAAM;KAAE,KAAK;KAAmB,UAAU;KAAe;IAC1D;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI,EAAE,MAAM,MAAM,EAAE;IAC9B,aAAa;IACb,MAAM;KAAE,KAAK;KAAoB,UAAU;KAAa;IACzD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE,IAAI;KAAE,MAAM;KAAM,SAAS,CAAC,MAAM;KAAE,EAAE;IAChD,aAAa;IACb,MAAM;KAAE,KAAK;KAAiB,UAAU;KAAU;IACnD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ;KAAE,KAAK,EAAE,MAAM,QAAQ;KAAE,SAAS,EAAE,MAAM,QAAQ;KAAE;IAC5D,aAAa;IACb,MAAM;KAAE,KAAK;KAAmB,UAAU;KAAY;IACvD;GACD;IACE,IAAI;IACJ,SAAS;IACT,QAAQ,EAAE;IACV,aAAa;IACb,MAAM;KAAE,KAAK;KAAsB,UAAU;KAAqB;IACnE;GACF;EAoBC,SAAS;EAcT,aAAa;EACd;CAuDC,mBAAmB;CAenB,eAAe;CAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordAdapters.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/assistant/recordAdapters.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recordAdapters.d.ts","sourceRoot":"","sources":["../../../src/bigconsole/assistant/recordAdapters.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,2DAA2D,CAAC;AACnE,OAAO,EAAwB,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEvF,0FAA0F;AAC1F,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,eAAO,MAAM,uBAAuB,gNAMnC,CAAC;AAEF,eAAO,MAAM,uBAAuB,oMAMnC,CAAC;AAEF,gGAAgG;AAChG,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACrC;AAuKD,wBAAgB,8BAA8B,CAAC,IAAI,GAAE,iBAA6B,GAAG,SAAS,sBAAsB,EAAE,CAuBrH;AAED,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAAqC,CAAC"}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { getStoredWorkspaceId as e, graphqlFetch as t } from "@burdenoff/fe-libs/shared/graphql";
|
|
2
|
-
|
|
2
|
+
import { normaliseRecordName as n } from "@burdenoff/fe-libs/shared/assistant/uiActions/recordNames";
|
|
3
|
+
var r = "\n query AssistantFindDashboards($search: String, $first: Int) {\n listDashboards(search: $search, first: $first, includeArchived: false) {\n edges { node { id name workspaceId } }\n }\n }\n", i = "\n query AssistantFindDataSinks($search: String, $first: Int) {\n listDataSinks(search: $search, first: $first) {\n edges { node { id name key workspaceId deletedAt } }\n }\n }\n", a = {
|
|
3
4
|
graphqlFetch: t,
|
|
4
5
|
getWorkspaceId: e
|
|
5
|
-
},
|
|
6
|
-
let t = e.filter((e) => [...e].length >= 3);
|
|
7
|
-
return
|
|
6
|
+
}, o = n, s = (e) => [...new Set(e.split(" ").filter((e) => e.length > 0))].sort((e, t) => [...t].length - [...e].length), c = "and", l = (e) => {
|
|
7
|
+
let t = e.filter((e) => [...e].length >= 3), n = [...t.filter((e) => e !== c), ...t.filter((e) => e === c)];
|
|
8
|
+
return n.length > 0 ? n : e.filter((e) => [...e].length === 2);
|
|
8
9
|
};
|
|
9
|
-
async function
|
|
10
|
+
async function u(e, t, n, r, i, a, c) {
|
|
10
11
|
let u = e.getWorkspaceId();
|
|
11
12
|
if (!u) return [];
|
|
12
|
-
let d =
|
|
13
|
+
let d = o(i), f = s(d);
|
|
13
14
|
if (f.length === 0) return [];
|
|
14
|
-
let [p, m] =
|
|
15
|
-
if (
|
|
16
|
-
let
|
|
15
|
+
let [p, m] = l(f), h = async (i) => {
|
|
16
|
+
if (a.aborted) return null;
|
|
17
|
+
let o = await e.graphqlFetch({
|
|
17
18
|
gateway: "workspace",
|
|
18
19
|
query: t,
|
|
19
20
|
operationName: n,
|
|
@@ -24,35 +25,35 @@ async function c(e, t, n, r, i, c, l) {
|
|
|
24
25
|
workspaceToken: !0,
|
|
25
26
|
workspaceId: u,
|
|
26
27
|
organizationId: !0,
|
|
27
|
-
signal:
|
|
28
|
+
signal: a,
|
|
28
29
|
suppressGlobalErrorEvent: !0
|
|
29
30
|
});
|
|
30
|
-
if (
|
|
31
|
-
let
|
|
32
|
-
if (!Array.isArray(
|
|
33
|
-
let
|
|
34
|
-
for (let e of
|
|
31
|
+
if (o.errors?.length) return null;
|
|
32
|
+
let s = o.data?.[r]?.edges;
|
|
33
|
+
if (!Array.isArray(s)) return null;
|
|
34
|
+
let l = [];
|
|
35
|
+
for (let e of s) {
|
|
35
36
|
let t = e?.node;
|
|
36
37
|
if (!t || typeof t.id != "string" || t.id.length === 0 || typeof t.workspaceId == "string" && t.workspaceId !== u) continue;
|
|
37
|
-
let n =
|
|
38
|
-
n !== null &&
|
|
38
|
+
let n = c(t);
|
|
39
|
+
n !== null && l.push({
|
|
39
40
|
params: { id: t.id },
|
|
40
41
|
name: n
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
return {
|
|
44
|
-
rows:
|
|
45
|
-
matches:
|
|
45
|
+
rows: s.length,
|
|
46
|
+
matches: l
|
|
46
47
|
};
|
|
47
|
-
}, g = (e) => h(e).catch(() => null), _ = (e) =>
|
|
48
|
+
}, g = (e) => h(e).catch(() => null), _ = (e) => o(e.name) === d, [v, y] = await Promise.all([h(d), p !== void 0 && p !== d ? g(p) : null]);
|
|
48
49
|
if (v === null) return [];
|
|
49
50
|
let b = [...v.matches], x = (e) => {
|
|
50
51
|
for (let t of e?.matches ?? []) _(t) && !b.some((e) => e.params.id === t.params.id) && b.push(t);
|
|
51
52
|
};
|
|
52
53
|
return x(y), y !== null && y.rows >= 10 && !b.some(_) && m !== void 0 && e.getWorkspaceId() === u && x(await g(m)), b;
|
|
53
54
|
}
|
|
54
|
-
var
|
|
55
|
-
function
|
|
55
|
+
var d = (e) => typeof e == "string" && e.trim().length > 0 ? e.trim() : null;
|
|
56
|
+
function f(e = a) {
|
|
56
57
|
return [{
|
|
57
58
|
id: "bigconsole-dashboard",
|
|
58
59
|
nouns: ["dashboard", "dashboards"],
|
|
@@ -61,7 +62,7 @@ function u(e = i) {
|
|
|
61
62
|
fallback: "dashboard"
|
|
62
63
|
},
|
|
63
64
|
entityRouteId: "bigconsole-dashboard",
|
|
64
|
-
find: (t,
|
|
65
|
+
find: (t, n) => u(e, r, "AssistantFindDashboards", "listDashboards", t, n, (e) => d(e.name))
|
|
65
66
|
}, {
|
|
66
67
|
id: "bigconsole-datasink",
|
|
67
68
|
nouns: [
|
|
@@ -77,11 +78,11 @@ function u(e = i) {
|
|
|
77
78
|
fallback: "data source"
|
|
78
79
|
},
|
|
79
80
|
entityRouteId: "bigconsole-datasink",
|
|
80
|
-
find: (t, n) =>
|
|
81
|
+
find: (t, n) => u(e, i, "AssistantFindDataSinks", "listDataSinks", t, n, (e) => e.deletedAt ? null : d(e.name) ?? d(e.key))
|
|
81
82
|
}];
|
|
82
83
|
}
|
|
83
|
-
var
|
|
84
|
+
var p = f();
|
|
84
85
|
//#endregion
|
|
85
|
-
export {
|
|
86
|
+
export { p as BIGCONSOLE_RECORD_ADAPTERS };
|
|
86
87
|
|
|
87
88
|
//# sourceMappingURL=recordAdapters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordAdapters.js","names":[],"sources":["../../../src/bigconsole/assistant/recordAdapters.ts"],"sourcesContent":["/**\n * BigConsole's named-record adapters for the shared assistant (BOFF-8060 N1b):\n * \"open the Coffee Shop Sales dashboard\", \"open the Orders data source\".\n *\n * An adapter only LOOKS A RECORD UP BY NAME through the READ API the product's\n * own list pages already use (`listDashboards`, `listDataSinks`, with their\n * `search` argument), as the signed-in person: the workspace token is theirs and\n * the gateway's `@rbac` filters the list, so a record they may not read never\n * comes back. The adapter produces route params, never a path: fe-libs fills the\n * entity route declared in `assistantProduct.ts` (`ENTITY_ROUTES`) and re-validates\n * it, and only for a person who can see the route's sidebar entry.\n *\n * ★ Raw operation strings, as in `conversationHistoryApi.ts`: two tiny read-only\n * selections used only here. `tests/recordAdapters.test.ts` validates both against\n * the committed workspace schema, so a renamed field fails there, not in prod.\n *\n * ★ What the server is asked (BOFF-8060 F8). The shared planner hands over a\n * NORMALISED name (lower case, every punctuation mark a space) while the server's\n * `search` is a plain substring match on the RAW name. The WHOLE SENTENCE is what this\n * lookup has always asked, and what it answered stays the answer; but \"boff 7926 embed\n * all widgets prod check\" is not a substring of \"BOFF-7926 embed all widgets (prod\n * check)\", so a name with punctuation is never found by it. ONE WORD has no punctuation\n * or space in it, so it is a substring of the raw name: it is asked in parallel (and at\n * most once more), and ONLY a row whose normalised name equals the sentence exactly is\n * taken from it. The answer is therefore exactly what the old lookup answered, plus the\n * exact names it could not see: a partial row from a one-word page would give the\n * planner candidates the whole sentence never offered (\"an ambiguous name asks, never\n * guesses\"). The matching itself stays with the shared planner.\n *\n * ★ Nothing here writes, opens a form, or touches the DOM.\n */\nimport type {\n AssistantRecordAdapter,\n AssistantRecordMatch,\n} from '@burdenoff/fe-libs/shared/assistant/uiActions/recordNames';\nimport { getStoredWorkspaceId, graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\n/** More than a handful of candidates is never offered (the panel offers at most four). */\nexport const RECORD_LOOKUP_PAGE_SIZE = 10;\n\nexport const FIND_DASHBOARDS_BY_NAME = `\n query AssistantFindDashboards($search: String, $first: Int) {\n listDashboards(search: $search, first: $first, includeArchived: false) {\n edges { node { id name workspaceId } }\n }\n }\n`;\n\nexport const FIND_DATA_SINKS_BY_NAME = `\n query AssistantFindDataSinks($search: String, $first: Int) {\n listDataSinks(search: $search, first: $first) {\n edges { node { id name key workspaceId deletedAt } }\n }\n }\n`;\n\n/** The two things the adapters need from the app; injectable so tests run without a network. */\nexport interface RecordAdapterDeps {\n graphqlFetch: typeof graphqlFetch;\n getWorkspaceId: () => string | null;\n}\nconst LIVE_DEPS: RecordAdapterDeps = { graphqlFetch, getWorkspaceId: getStoredWorkspaceId };\n\ninterface RawNode {\n id?: unknown;\n name?: unknown;\n key?: unknown;\n workspaceId?: unknown;\n deletedAt?: unknown;\n}\ntype ListData = Record<string, { edges?: Array<{ node?: RawNode | null } | null> | null } | null | undefined>;\n\n/**\n * The shared planner's own normalisation (fe-libs `recordNames.ts`, not exported):\n * it is applied to the request and to every record name before they are compared,\n * so the adapter reasons about names exactly as the planner will. Idempotent, so a\n * name that arrives already normalised is unchanged.\n */\nconst normalise = (value: string): string =>\n value\n .normalize('NFKC')\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\s]/gu, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n\n/**\n * The distinct words of a normalised request, best search term first: the longest\n * (it narrows the server's substring match the most), the earlier word on a tie.\n * `Array.prototype.sort` is stable, so the original order is the tie-break.\n */\nconst searchTerms = (wanted: string): string[] =>\n [...new Set(wanted.split(' ').filter((word) => word.length > 0))].sort((a, b) => [...b].length - [...a].length);\n\n/**\n * The words worth asking the server with on their own, best first (`terms` is longest first, earlier on a tie):\n * those of at least three characters; only if there are none, those of exactly two (\"HR-IT\" is typed \"hr it\").\n * A word of one character is in too many names, so it is never asked alone. A one-word page can only ADD exact\n * names, so a short word costs the planner nothing.\n */\nconst searchWords = (terms: readonly string[]): string[] => {\n const long = terms.filter((word) => [...word].length >= 3);\n return long.length > 0 ? long : terms.filter((word) => [...word].length === 2);\n};\n\n/** One answer of the server: how many rows it sent, and its usable rows as matches, in the server's order. */\ninterface Page {\n rows: number;\n matches: AssistantRecordMatch[];\n}\n\n/**\n * Look a record up by name as the signed-in person and map its nodes to matches.\n *\n * ★ The answer is what the whole-sentence request alone has always answered, plus the\n * exact names it cannot see. The rows of the whole-sentence request are returned\n * unchanged and in order: ONE page of RECORD_LOOKUP_PAGE_SIZE rows, whose partial\n * matches the planner weighs as it always has. A one-word request can only ADD a row\n * whose normalised name equals the sentence exactly (a record that is not deleted and\n * is in this workspace, like any other row); it never adds a partial one.\n *\n * Failure (no workspace, a GraphQL error, a refusal, an abort) of the whole-sentence\n * request is \"no match\": the sentence then goes to the model exactly as it would have.\n * A one-word request that fails or is refused is ignored: it could only have added\n * exact names, and the answer the old lookup gave stands.\n *\n * At most THREE list requests in TWO round trips, usually ONE:\n * Round 1, in parallel:\n * (a) `search` = the whole normalised sentence, as ever;\n * (b) `search` = the longest word, which has no punctuation or space in it, so it is\n * a substring of the raw name whatever punctuation the name has. Only for a\n * sentence of more than one word: its longest word of at least three characters or,\n * if none is that long, its longest word of two (never a word of one character).\n * Round 2, ONE request, only if no exact name has been found, (b) came back full (the\n * exact record may be past it) and the workspace is still the one round 1 asked\n * about: the next distinct word by the same rule.\n * No request is made without `search`, and none once the panel has stopped waiting.\n */\nasync function findByName(\n deps: RecordAdapterDeps,\n query: string,\n operationName: string,\n field: string,\n name: string,\n signal: AbortSignal,\n pick: (node: RawNode) => string | null\n): Promise<readonly AssistantRecordMatch[]> {\n const workspaceId = deps.getWorkspaceId();\n if (!workspaceId) return [];\n const wanted = normalise(name);\n const terms = searchTerms(wanted);\n // Nothing to match: the planner plans a request with no words as \"none\".\n if (terms.length === 0) return [];\n const [best, next] = searchWords(terms);\n\n /** One list request for `search`; `null` is \"no usable answer\". */\n const ask = async (search: string): Promise<Page | null> => {\n // Once the panel has stopped waiting nothing is worth asking.\n if (signal.aborted) return null;\n const result = await deps.graphqlFetch<ListData>({\n gateway: 'workspace',\n query,\n operationName,\n variables: { search, first: RECORD_LOOKUP_PAGE_SIZE },\n // The person's own workspace token, minted for THIS workspace.\n workspaceToken: true,\n workspaceId,\n organizationId: true,\n signal,\n suppressGlobalErrorEvent: true,\n });\n if (result.errors?.length) return null;\n const edges = result.data?.[field]?.edges;\n if (!Array.isArray(edges)) return null;\n const matches: AssistantRecordMatch[] = [];\n for (const edge of edges) {\n const node = edge?.node;\n if (!node || typeof node.id !== 'string' || node.id.length === 0) continue;\n // A list answered for another workspace (a switch mid-request) is not used.\n if (typeof node.workspaceId === 'string' && node.workspaceId !== workspaceId) continue;\n const label = pick(node);\n if (label === null) continue;\n matches.push({ params: { id: node.id }, name: label });\n }\n return { rows: edges.length, matches };\n };\n /** A one-word request never rejects: when it fails there is simply nothing to add. */\n const askWord = (word: string): Promise<Page | null> => ask(word).catch(() => null);\n const isExact = (match: AssistantRecordMatch): boolean => normalise(match.name) === wanted;\n\n // Round 1, in parallel.\n const [whole, word] = await Promise.all([ask(wanted), best !== undefined && best !== wanted ? askWord(best) : null]);\n // As it always was: the whole-sentence request refused or failed is \"no match\".\n if (whole === null) return [];\n\n const answer = [...whole.matches];\n /** Adds the exact names of a one-word page that the answer does not hold yet, and nothing else. */\n const addExact = (page: Page | null): void => {\n for (const match of page?.matches ?? []) {\n if (isExact(match) && !answer.some((known) => known.params.id === match.params.id)) answer.push(match);\n }\n };\n addExact(word);\n\n // Round 2: ONE request, only while the exact record may still be past the one-word page.\n if (\n word !== null &&\n word.rows >= RECORD_LOOKUP_PAGE_SIZE &&\n !answer.some(isExact) &&\n next !== undefined &&\n // a workspace switch since round 1 would make this request (and its answer) about the wrong workspace\n deps.getWorkspaceId() === workspaceId\n ) {\n addExact(await askWord(next));\n }\n return answer;\n}\n\nconst text = (value: unknown): string | null =>\n typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;\n\nexport function createBigConsoleRecordAdapters(deps: RecordAdapterDeps = LIVE_DEPS): readonly AssistantRecordAdapter[] {\n const dashboards: AssistantRecordAdapter = {\n id: 'bigconsole-dashboard',\n nouns: ['dashboard', 'dashboards'],\n noun: { key: 'entity.dashboard', fallback: 'dashboard' },\n entityRouteId: 'bigconsole-dashboard',\n find: (name, signal) =>\n findByName(deps, FIND_DASHBOARDS_BY_NAME, 'AssistantFindDashboards', 'listDashboards', name, signal, (node) =>\n text(node.name)\n ),\n };\n const dataSinks: AssistantRecordAdapter = {\n id: 'bigconsole-datasink',\n nouns: ['data source', 'data sources', 'data sink', 'data sinks', 'datasink', 'datasinks'],\n noun: { key: 'entity.datasink', fallback: 'data source' },\n entityRouteId: 'bigconsole-datasink',\n find: (name, signal) =>\n findByName(deps, FIND_DATA_SINKS_BY_NAME, 'AssistantFindDataSinks', 'listDataSinks', name, signal, (node) =>\n // A deleted data source is not a page to open; an unnamed one is called by its key.\n node.deletedAt ? null : (text(node.name) ?? text(node.key))\n ),\n };\n return [dashboards, dataSinks];\n}\n\nexport const BIGCONSOLE_RECORD_ADAPTERS: readonly AssistantRecordAdapter[] = createBigConsoleRecordAdapters();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"recordAdapters.js","names":[],"sources":["../../../src/bigconsole/assistant/recordAdapters.ts"],"sourcesContent":["/**\n * BigConsole's named-record adapters for the shared assistant (BOFF-8060 N1b):\n * \"open the Coffee Shop Sales dashboard\", \"open the Orders data source\".\n *\n * An adapter only LOOKS A RECORD UP BY NAME through the READ API the product's\n * own list pages already use (`listDashboards`, `listDataSinks`, with their\n * `search` argument), as the signed-in person: the workspace token is theirs and\n * the gateway's `@rbac` filters the list, so a record they may not read never\n * comes back. The adapter produces route params, never a path: fe-libs fills the\n * entity route declared in `assistantProduct.ts` (`ENTITY_ROUTES`) and re-validates\n * it, and only for a person who can see the route's sidebar entry.\n *\n * ★ Raw operation strings, as in `conversationHistoryApi.ts`: two tiny read-only\n * selections used only here. `tests/recordAdapters.test.ts` validates both against\n * the committed workspace schema, so a renamed field fails there, not in prod.\n *\n * ★ What the server is asked (BOFF-8060 F8). The shared planner hands over a\n * NORMALISED name (lower case, \"&\" as \"and\", every other punctuation mark a space) while the server's\n * `search` is a plain substring match on the RAW name. The WHOLE SENTENCE is what this\n * lookup has always asked, and what it answered stays the answer; but \"boff 7926 embed\n * all widgets prod check\" is not a substring of \"BOFF-7926 embed all widgets (prod\n * check)\", so a name with punctuation is never found by it. ONE WORD has no punctuation\n * or space in it, so it is a substring of the raw name: it is asked in parallel (and at\n * most once more), and ONLY a row whose normalised name equals the sentence exactly is\n * taken from it. The answer is therefore exactly what the old lookup answered, plus the\n * exact names it could not see: a partial row from a one-word page would give the\n * planner candidates the whole sentence never offered (\"an ambiguous name asks, never\n * guesses\"). The matching itself stays with the shared planner.\n *\n * ★ Nothing here writes, opens a form, or touches the DOM.\n */\nimport { normaliseRecordName } from '@burdenoff/fe-libs/shared/assistant/uiActions/recordNames';\nimport type {\n AssistantRecordAdapter,\n AssistantRecordMatch,\n} from '@burdenoff/fe-libs/shared/assistant/uiActions/recordNames';\nimport { getStoredWorkspaceId, graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\n/** More than a handful of candidates is never offered (the panel offers at most four). */\nexport const RECORD_LOOKUP_PAGE_SIZE = 10;\n\nexport const FIND_DASHBOARDS_BY_NAME = `\n query AssistantFindDashboards($search: String, $first: Int) {\n listDashboards(search: $search, first: $first, includeArchived: false) {\n edges { node { id name workspaceId } }\n }\n }\n`;\n\nexport const FIND_DATA_SINKS_BY_NAME = `\n query AssistantFindDataSinks($search: String, $first: Int) {\n listDataSinks(search: $search, first: $first) {\n edges { node { id name key workspaceId deletedAt } }\n }\n }\n`;\n\n/** The two things the adapters need from the app; injectable so tests run without a network. */\nexport interface RecordAdapterDeps {\n graphqlFetch: typeof graphqlFetch;\n getWorkspaceId: () => string | null;\n}\nconst LIVE_DEPS: RecordAdapterDeps = { graphqlFetch, getWorkspaceId: getStoredWorkspaceId };\n\ninterface RawNode {\n id?: unknown;\n name?: unknown;\n key?: unknown;\n workspaceId?: unknown;\n deletedAt?: unknown;\n}\ntype ListData = Record<string, { edges?: Array<{ node?: RawNode | null } | null> | null } | null | undefined>;\n\n/**\n * ★ BOFF-8680: the shared planner's OWN normalisation, imported, never copied.\n * fe-libs `normaliseRecordName` (`canonical`) is applied to the request and to\n * every record name before they are compared, so the adapter reasons about names\n * exactly as the planner will: \"&\" reads as \"and\" (\"Sales & Marketing\" is \"sales\n * and marketing\"), a Hindi or Tamil word keeps its vowel signs (combining marks),\n * and a zero-width joiner or non-joiner inside an Indic word is dropped. The local\n * copy this replaces stripped combining marks and kept \"&\" a separator, so an\n * exact name with either was never recognised as exact. Idempotent, so a name\n * that arrives already normalised is unchanged.\n */\nconst normalise = normaliseRecordName;\n\n/**\n * The distinct words of a normalised request, best search term first: the longest\n * (it narrows the server's substring match the most), the earlier word on a tie.\n * `Array.prototype.sort` is stable, so the original order is the tie-break.\n */\nconst searchTerms = (wanted: string): string[] =>\n [...new Set(wanted.split(' ').filter((word) => word.length > 0))].sort((a, b) => [...b].length - [...a].length);\n\n/** What the planner makes of \"&\" (`normaliseRecordName`): the word \"and\". */\nconst AND = 'and';\n\n/**\n * The words worth asking the server with on their own, best first (`terms` is longest first, earlier on a tie):\n * those of at least three characters; only if there are none, those of exactly two (\"HR-IT\" is typed \"hr it\").\n * A word of one character is in too many names, so it is never asked alone. A one-word page can only ADD exact\n * names, so a short word costs the planner nothing.\n */\nconst searchWords = (terms: readonly string[]): string[] => {\n const long = terms.filter((word) => [...word].length >= 3);\n // ★ BOFF-8680: \"and\" is asked LAST among them. The planner writes \"&\" as \"and\" (\"Q&A Hub\" arrives as\n // \"q and a hub\"), and \"and\" is not a substring of a raw name spelt with \"&\", so asked first it would spend the\n // one-word request on a page that cannot hold the record. Every other word keeps its place.\n const ranked = [...long.filter((word) => word !== AND), ...long.filter((word) => word === AND)];\n return ranked.length > 0 ? ranked : terms.filter((word) => [...word].length === 2);\n};\n\n/** One answer of the server: how many rows it sent, and its usable rows as matches, in the server's order. */\ninterface Page {\n rows: number;\n matches: AssistantRecordMatch[];\n}\n\n/**\n * Look a record up by name as the signed-in person and map its nodes to matches.\n *\n * ★ The answer is what the whole-sentence request alone has always answered, plus the\n * exact names it cannot see. The rows of the whole-sentence request are returned\n * unchanged and in order: ONE page of RECORD_LOOKUP_PAGE_SIZE rows, whose partial\n * matches the planner weighs as it always has. A one-word request can only ADD a row\n * whose normalised name equals the sentence exactly (a record that is not deleted and\n * is in this workspace, like any other row); it never adds a partial one.\n *\n * Failure (no workspace, a GraphQL error, a refusal, an abort) of the whole-sentence\n * request is \"no match\": the sentence then goes to the model exactly as it would have.\n * A one-word request that fails or is refused is ignored: it could only have added\n * exact names, and the answer the old lookup gave stands.\n *\n * At most THREE list requests in TWO round trips, usually ONE:\n * Round 1, in parallel:\n * (a) `search` = the whole normalised sentence, as ever;\n * (b) `search` = the longest word, which has no punctuation or space in it, so it is\n * a substring of the raw name whatever punctuation the name has. Only for a\n * sentence of more than one word: its longest word of at least three characters or,\n * if none is that long, its longest word of two (never a word of one character).\n * Round 2, ONE request, only if no exact name has been found, (b) came back full (the\n * exact record may be past it) and the workspace is still the one round 1 asked\n * about: the next distinct word by the same rule.\n * No request is made without `search`, and none once the panel has stopped waiting.\n */\nasync function findByName(\n deps: RecordAdapterDeps,\n query: string,\n operationName: string,\n field: string,\n name: string,\n signal: AbortSignal,\n pick: (node: RawNode) => string | null\n): Promise<readonly AssistantRecordMatch[]> {\n const workspaceId = deps.getWorkspaceId();\n if (!workspaceId) return [];\n const wanted = normalise(name);\n const terms = searchTerms(wanted);\n // Nothing to match: the planner plans a request with no words as \"none\".\n if (terms.length === 0) return [];\n const [best, next] = searchWords(terms);\n\n /** One list request for `search`; `null` is \"no usable answer\". */\n const ask = async (search: string): Promise<Page | null> => {\n // Once the panel has stopped waiting nothing is worth asking.\n if (signal.aborted) return null;\n const result = await deps.graphqlFetch<ListData>({\n gateway: 'workspace',\n query,\n operationName,\n variables: { search, first: RECORD_LOOKUP_PAGE_SIZE },\n // The person's own workspace token, minted for THIS workspace.\n workspaceToken: true,\n workspaceId,\n organizationId: true,\n signal,\n suppressGlobalErrorEvent: true,\n });\n if (result.errors?.length) return null;\n const edges = result.data?.[field]?.edges;\n if (!Array.isArray(edges)) return null;\n const matches: AssistantRecordMatch[] = [];\n for (const edge of edges) {\n const node = edge?.node;\n if (!node || typeof node.id !== 'string' || node.id.length === 0) continue;\n // A list answered for another workspace (a switch mid-request) is not used.\n if (typeof node.workspaceId === 'string' && node.workspaceId !== workspaceId) continue;\n const label = pick(node);\n if (label === null) continue;\n matches.push({ params: { id: node.id }, name: label });\n }\n return { rows: edges.length, matches };\n };\n /** A one-word request never rejects: when it fails there is simply nothing to add. */\n const askWord = (word: string): Promise<Page | null> => ask(word).catch(() => null);\n const isExact = (match: AssistantRecordMatch): boolean => normalise(match.name) === wanted;\n\n // Round 1, in parallel.\n const [whole, word] = await Promise.all([ask(wanted), best !== undefined && best !== wanted ? askWord(best) : null]);\n // As it always was: the whole-sentence request refused or failed is \"no match\".\n if (whole === null) return [];\n\n const answer = [...whole.matches];\n /** Adds the exact names of a one-word page that the answer does not hold yet, and nothing else. */\n const addExact = (page: Page | null): void => {\n for (const match of page?.matches ?? []) {\n if (isExact(match) && !answer.some((known) => known.params.id === match.params.id)) answer.push(match);\n }\n };\n addExact(word);\n\n // Round 2: ONE request, only while the exact record may still be past the one-word page.\n if (\n word !== null &&\n word.rows >= RECORD_LOOKUP_PAGE_SIZE &&\n !answer.some(isExact) &&\n next !== undefined &&\n // a workspace switch since round 1 would make this request (and its answer) about the wrong workspace\n deps.getWorkspaceId() === workspaceId\n ) {\n addExact(await askWord(next));\n }\n return answer;\n}\n\nconst text = (value: unknown): string | null =>\n typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;\n\nexport function createBigConsoleRecordAdapters(deps: RecordAdapterDeps = LIVE_DEPS): readonly AssistantRecordAdapter[] {\n const dashboards: AssistantRecordAdapter = {\n id: 'bigconsole-dashboard',\n nouns: ['dashboard', 'dashboards'],\n noun: { key: 'entity.dashboard', fallback: 'dashboard' },\n entityRouteId: 'bigconsole-dashboard',\n find: (name, signal) =>\n findByName(deps, FIND_DASHBOARDS_BY_NAME, 'AssistantFindDashboards', 'listDashboards', name, signal, (node) =>\n text(node.name)\n ),\n };\n const dataSinks: AssistantRecordAdapter = {\n id: 'bigconsole-datasink',\n nouns: ['data source', 'data sources', 'data sink', 'data sinks', 'datasink', 'datasinks'],\n noun: { key: 'entity.datasink', fallback: 'data source' },\n entityRouteId: 'bigconsole-datasink',\n find: (name, signal) =>\n findByName(deps, FIND_DATA_SINKS_BY_NAME, 'AssistantFindDataSinks', 'listDataSinks', name, signal, (node) =>\n // A deleted data source is not a page to open; an unnamed one is called by its key.\n node.deletedAt ? null : (text(node.name) ?? text(node.key))\n ),\n };\n return [dashboards, dataSinks];\n}\n\nexport const BIGCONSOLE_RECORD_ADAPTERS: readonly AssistantRecordAdapter[] = createBigConsoleRecordAdapters();\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-bigconsole",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1007.10",
|
|
4
4
|
"description": "BigConsole - AI-powered analytics and dashboard platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@apollo/client": "^4.0.11",
|
|
53
|
-
"@burdenoff/fe-libs": "2026.
|
|
53
|
+
"@burdenoff/fe-libs": "2026.1007.3",
|
|
54
54
|
"@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
|
|
55
55
|
"@iconify/react": "^6.0.2",
|
|
56
56
|
"@monaco-editor/react": "^4.7.0",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"zustand": "^5.0.3"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
-
"@burdenoff/fe-libs": "2026.
|
|
88
|
+
"@burdenoff/fe-libs": "2026.1007.3",
|
|
89
89
|
"@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
|
|
90
90
|
"@eslint/js": "^10.0.1",
|
|
91
91
|
"@graphql-codegen/add": "^5.0.0",
|