@datakable/core 2.0.1 → 2.0.3

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.
Files changed (125) hide show
  1. package/README.md +3 -2
  2. package/dist/{codes-B9bCK73-.d.cts → codes-4sYQv_7J.d.cts} +42 -12
  3. package/dist/{codes-B9bCK73-.d.ts → codes-4sYQv_7J.d.ts} +42 -12
  4. package/dist/column-filters.d.cts +2 -2
  5. package/dist/column-filters.d.ts +2 -2
  6. package/dist/diagnostics/build.cjs +1 -1
  7. package/dist/diagnostics/build.cjs.map +1 -1
  8. package/dist/diagnostics/build.js +2 -2
  9. package/dist/diagnostics/build.js.map +1 -1
  10. package/dist/diagnostics/console.cjs +15 -4
  11. package/dist/diagnostics/console.cjs.map +1 -1
  12. package/dist/diagnostics/console.js +15 -4
  13. package/dist/diagnostics/console.js.map +1 -1
  14. package/dist/diagnostics/explain.cjs.map +1 -1
  15. package/dist/diagnostics/explain.js.map +1 -1
  16. package/dist/diagnostics/hub.cjs +7 -1
  17. package/dist/diagnostics/hub.cjs.map +1 -1
  18. package/dist/diagnostics/hub.js +7 -1
  19. package/dist/diagnostics/hub.js.map +1 -1
  20. package/dist/diagnostics/prose.cjs +10 -1
  21. package/dist/diagnostics/prose.cjs.map +1 -1
  22. package/dist/diagnostics/prose.js +9 -1
  23. package/dist/diagnostics/prose.js.map +1 -1
  24. package/dist/diagnostics/redact.cjs +1 -1
  25. package/dist/diagnostics/redact.cjs.map +1 -1
  26. package/dist/diagnostics/redact.js +1 -1
  27. package/dist/diagnostics/redact.js.map +1 -1
  28. package/dist/diagnostics/types.cjs.map +1 -1
  29. package/dist/{explain-i0OATi_W.d.ts → explain-DmwN_RG9.d.cts} +13 -6
  30. package/dist/{explain-Q8Cg0cZy.d.cts → explain-Dr3SmMNY.d.ts} +13 -6
  31. package/dist/feature/install.cjs +2 -1
  32. package/dist/feature/install.cjs.map +1 -1
  33. package/dist/feature/install.js +2 -1
  34. package/dist/feature/install.js.map +1 -1
  35. package/dist/feature/slice-guard.cjs +76 -0
  36. package/dist/feature/slice-guard.cjs.map +1 -0
  37. package/dist/feature/slice-guard.js +50 -0
  38. package/dist/feature/slice-guard.js.map +1 -0
  39. package/dist/features/column-filters/codes.cjs +2 -19
  40. package/dist/features/column-filters/codes.cjs.map +1 -1
  41. package/dist/features/column-filters/codes.js +2 -19
  42. package/dist/features/column-filters/codes.js.map +1 -1
  43. package/dist/features/column-filters/column-filters.cjs +37 -22
  44. package/dist/features/column-filters/column-filters.cjs.map +1 -1
  45. package/dist/features/column-filters/column-filters.js +37 -22
  46. package/dist/features/column-filters/column-filters.js.map +1 -1
  47. package/dist/features/column-filters/match.cjs +6 -2
  48. package/dist/features/column-filters/match.cjs.map +1 -1
  49. package/dist/features/column-filters/match.js +6 -2
  50. package/dist/features/column-filters/match.js.map +1 -1
  51. package/dist/features/pagination/pagination.cjs +2 -2
  52. package/dist/features/pagination/pagination.cjs.map +1 -1
  53. package/dist/features/pagination/pagination.js +2 -2
  54. package/dist/features/pagination/pagination.js.map +1 -1
  55. package/dist/features/search/codes.cjs +18 -2
  56. package/dist/features/search/codes.cjs.map +1 -1
  57. package/dist/features/search/codes.js +16 -1
  58. package/dist/features/search/codes.js.map +1 -1
  59. package/dist/features/search/search-engine.cjs +24 -5
  60. package/dist/features/search/search-engine.cjs.map +1 -1
  61. package/dist/features/search/search-engine.js +25 -6
  62. package/dist/features/search/search-engine.js.map +1 -1
  63. package/dist/features/sorting/sorting.cjs +55 -24
  64. package/dist/features/sorting/sorting.cjs.map +1 -1
  65. package/dist/features/sorting/sorting.js +55 -24
  66. package/dist/features/sorting/sorting.js.map +1 -1
  67. package/dist/index.d.cts +26 -17
  68. package/dist/index.d.ts +26 -17
  69. package/dist/intercept/runtime.cjs +3 -2
  70. package/dist/intercept/runtime.cjs.map +1 -1
  71. package/dist/intercept/runtime.js +3 -2
  72. package/dist/intercept/runtime.js.map +1 -1
  73. package/dist/messages/index.cjs +2 -0
  74. package/dist/messages/index.cjs.map +1 -1
  75. package/dist/messages/index.js +2 -0
  76. package/dist/messages/index.js.map +1 -1
  77. package/dist/pagination.d.cts +2 -2
  78. package/dist/pagination.d.ts +2 -2
  79. package/dist/schema.d.cts +2 -2
  80. package/dist/schema.d.ts +2 -2
  81. package/dist/{search-async-entry-CuLYPfY_.d.cts → search-async-entry-BiemO3JT.d.cts} +2 -2
  82. package/dist/{search-async-entry-Cx7xpuVZ.d.ts → search-async-entry-GRj0E06n.d.ts} +2 -2
  83. package/dist/search-async.d.cts +3 -3
  84. package/dist/search-async.d.ts +3 -3
  85. package/dist/search.d.cts +4 -4
  86. package/dist/search.d.ts +4 -4
  87. package/dist/server.d.cts +3 -3
  88. package/dist/server.d.ts +3 -3
  89. package/dist/sorting.d.cts +2 -2
  90. package/dist/sorting.d.ts +2 -2
  91. package/dist/source/async-control.cjs +2 -1
  92. package/dist/source/async-control.cjs.map +1 -1
  93. package/dist/source/async-control.js +2 -1
  94. package/dist/source/async-control.js.map +1 -1
  95. package/dist/source/fetcher.cjs.map +1 -1
  96. package/dist/source/fetcher.js.map +1 -1
  97. package/dist/source/server.cjs +58 -5
  98. package/dist/source/server.cjs.map +1 -1
  99. package/dist/source/server.js +58 -5
  100. package/dist/source/server.js.map +1 -1
  101. package/dist/table/create-table.cjs +30 -20
  102. package/dist/table/create-table.cjs.map +1 -1
  103. package/dist/table/create-table.js +30 -20
  104. package/dist/table/create-table.js.map +1 -1
  105. package/dist/table/index.cjs +3 -0
  106. package/dist/table/index.cjs.map +1 -1
  107. package/dist/table/index.js +5 -1
  108. package/dist/table/index.js.map +1 -1
  109. package/dist/table/state-codes.cjs +72 -0
  110. package/dist/table/state-codes.cjs.map +1 -0
  111. package/dist/table/state-codes.js +45 -0
  112. package/dist/table/state-codes.js.map +1 -0
  113. package/dist/table/types.cjs.map +1 -1
  114. package/dist/table/types.js.map +1 -1
  115. package/dist/validity.d.cts +2 -2
  116. package/dist/validity.d.ts +2 -2
  117. package/dist/version.cjs +1 -1
  118. package/dist/version.cjs.map +1 -1
  119. package/dist/version.js +1 -1
  120. package/dist/version.js.map +1 -1
  121. package/package.json +1 -1
  122. package/dist/ingest/test-schemas.cjs +0 -73
  123. package/dist/ingest/test-schemas.cjs.map +0 -1
  124. package/dist/ingest/test-schemas.js +0 -46
  125. package/dist/ingest/test-schemas.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/diagnostics/explain.ts"],"sourcesContent":["import type { AnyFeature, RowModelContext, Stage } from '../feature/types'\nimport { resolveRowKeys } from '../ingest/row-key'\nimport type { SchemaOutcome } from '../ingest/schema'\nimport type { InvalidRowsMode } from '../ingest/schema-codes'\nimport type { RowKey } from '../pipeline/row'\nimport type { RunRecord } from '../pipeline/runner'\nimport type { RowModel, RowModelStatus } from '../pipeline/types'\nimport './process-guard'\nimport type { explainDetail } from './explain-detail'\nimport { formatRowExplanation, formatTableExplanation } from './explain-text'\nimport { type RedactionOptions, redactDetails, resolveRedaction, scrubSchemaIssue } from './redact'\nimport { readTrace } from './trace-store'\nimport type { Diagnostic, Location, Trigger } from './types'\n\n/** What `createTable` gives `explain()` / `explainRow()`: read-only views of the table. */\nexport interface ExplainHost {\n readonly table: object\n /** Installed features in pipeline (stage) order. */\n readonly features: readonly AnyFeature[]\n /** The current row model (computed if data, state or columns changed, like `getRowModel()`). */\n rowModel(): RowModel<unknown>\n lastRun(): RunRecord | undefined\n /** A step context for replaying a `rowModel` on chosen rows (no diagnostics, no interceptors). */\n replay(name: string): RowModelContext\n data(): readonly unknown[]\n state(): Readonly<Record<string, unknown>>\n rowKey(): unknown\n /** `options.invalidRows` when a schema is set; `undefined` without one. */\n schemaMode(): InvalidRowsMode | undefined\n schema(): SchemaOutcome | undefined\n diagnostics(): readonly Diagnostic[]\n readonly redaction: RedactionOptions\n /** Why a step removed a row (`explainDetail`): it comes with the features (their runtime). */\n readonly detail?: typeof explainDetail\n}\n\n/** One step of {@link TableExplanation}: counts only. */\nexport interface ExplainStep {\n readonly feature: string\n readonly stage: Stage\n /** `'server'`: the server ran the step; its counts are the rows it was given. */\n readonly mode: 'client' | 'server'\n readonly input: number\n readonly output: number\n /** Served from the step memo in this run (nothing changed for it). */\n readonly cached: boolean\n /** The step threw or returned a bad output and passed its input through. */\n readonly failed: boolean\n /**\n * Duration (tracer only, development or `interceptors: [tracer()]`). A step that waited on\n * time-sliced work: the total of its runs and of the slices between them (#61).\n */\n readonly ms?: number\n /** Time-sliced work (#61): the runs {@link ms} spans; absent when the step ran in one. */\n readonly runs?: number\n}\n\n/** What `table.explain()` returns (§8.4); `String(e)` is the console summary. */\nexport interface TableExplanation {\n readonly run: number\n readonly trigger: Trigger\n readonly status: RowModelStatus\n /** Rows after ingest (after the schema dropped invalid rows). */\n readonly rows: number\n /** With a schema: its `invalidRows` mode and how many rows failed it. */\n readonly schema?: { readonly mode: InvalidRowsMode; readonly invalid: number }\n readonly steps: readonly ExplainStep[]\n /** Rows after filter and search (`getRowModel().filteredCount`). */\n readonly filteredCount: number\n /** With a pagination slice: the page shown (0-based), the page count and the page size. */\n readonly page?: { readonly index: number; readonly count: number; readonly size: number }\n /** Rows in `getRowModel().rows`. */\n readonly visible: number\n /** Error and warning diagnostics in `getDiagnostics()`. */\n readonly diagnostics: number\n /** Timings come from the tracer (it saw this run). */\n readonly traced: boolean\n /** Duration of the run (tracer only). */\n readonly ms?: number\n toString(): string\n}\n\n/**\n * `kept` / `removed` by the step, `skipped` (server mode), `failed` (passed its input through),\n * `not-reached` (an earlier step or the schema removed the row).\n */\nexport type RowStepOutcome = 'kept' | 'removed' | 'skipped' | 'failed' | 'not-reached'\n\nexport interface RowStep {\n readonly feature: string\n readonly stage: Stage\n readonly outcome: RowStepOutcome\n /** Kept: the row's position in the step output (0-based) and the output size. */\n readonly position?: number\n readonly of?: number\n /** Why the step removed the row (redacted). */\n readonly detail?: Readonly<Record<string, unknown>>\n}\n\n/** What `table.explainRow(key)` returns (§8.4); `String(e)` is the console report. */\nexport interface RowExplanation {\n /** The key asked for, redacted (a hash under `'safe'`). */\n readonly key: unknown\n readonly found: boolean\n /** Position in `getData()`; absent when not found or dropped by the schema. */\n readonly index?: number\n /** Position in `options.data` of a row the schema dropped. */\n readonly rawIndex?: number\n readonly schema: 'none' | 'valid' | 'flagged' | 'dropped'\n /** Schema issues of a flagged / dropped row (paths + DataKable's own text unless `'full'`). */\n readonly issues?: readonly { path: string; message: string }[]\n readonly steps: readonly RowStep[]\n /** The step that removed the row: a feature name, or `'schema'`. */\n readonly removedBy?: string\n /** The row is in `getRowModel().rows`. */\n readonly visible: boolean\n readonly run: number\n /** Rows after ingest (for \"not found\"). */\n readonly rows: number\n toString(): string\n}\n\nconst NOOP = () => {}\n\n/** Steps that take part in the row model: a stage and a `rowModel` (server mode included). */\nconst pipelineSteps = (features: readonly AnyFeature[]) =>\n features.filter((f) => f.stage !== 'none' && typeof f.rowModel === 'function')\n\nconst isServer = (f: AnyFeature) => f.mode === 'server'\n\n/** `{ pageIndex, pageSize }` of the first paginate-stage slice that has them. */\nexport function pageOf(host: ExplainHost): { index: number; size: number } | undefined {\n for (const f of host.features) {\n if (f.stage !== 'paginate') continue\n const s = host.state()[f.name] as { pageIndex?: unknown; pageSize?: unknown } | undefined\n if (typeof s?.pageIndex === 'number' && typeof s.pageSize === 'number' && s.pageSize > 0) {\n return { index: s.pageIndex, size: s.pageSize }\n }\n }\n return undefined\n}\n\nexport const pageCount = (rows: number, size: number) => Math.ceil(rows / size)\n\n/** Clamped like pagination does (the page actually shown). */\nexport const shownPage = (index: number, count: number) => Math.max(0, Math.min(index, count - 1))\n\n/** `table.explain()` (§8.4): the last run's counts, with tracer timings when it was traced. */\nexport function explainTable(host: ExplainHost): TableExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const trace = readTrace(host.table)\n const traced = trace !== undefined && last !== undefined && trace.run === last.run\n const steps: ExplainStep[] = []\n let flowing = model.totalCount\n for (const f of pipelineSteps(host.features)) {\n if (isServer(f)) {\n steps.push(step(f, 'server', flowing, flowing, false, false))\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) continue\n flowing = rec.output.length\n const t = traced ? trace.steps.get(f.name) : undefined\n steps.push({\n ...step(f, 'client', rec.input.length, rec.output.length, rec.cached, rec.failed),\n ...(t ? { ms: t.ms, ...(t.runs ? { runs: t.runs } : {}) } : {}),\n })\n }\n const mode = host.schemaMode()\n const schema = host.schema()\n const p = pageOf(host)\n const count = p ? pageCount(model.filteredCount, p.size) : 0\n let diagnostics = 0\n for (const d of host.diagnostics()) if (d.level !== 'info') diagnostics++\n const e: TableExplanation = {\n run: last?.run ?? 0,\n trigger: last?.trigger ?? 'init',\n status: model.status,\n rows: model.totalCount,\n ...(mode === undefined ? {} : { schema: { mode, invalid: schema?.invalid.length ?? 0 } }),\n steps,\n filteredCount: model.filteredCount,\n ...(p ? { page: { index: shownPage(p.index, count), count, size: p.size } } : {}),\n visible: model.rows.length,\n diagnostics,\n traced,\n ...(traced && trace.ms !== undefined ? { ms: trace.ms } : {}),\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatTableExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n\nfunction step(\n f: AnyFeature,\n mode: 'client' | 'server',\n input: number,\n output: number,\n cached: boolean,\n failed: boolean,\n): ExplainStep {\n return { feature: f.name, stage: f.stage, mode, input, output, cached, failed }\n}\n\n/** Index of the row whose key equals `key` (`42` matches `'42'`), or -1. */\nfunction findKey(rows: readonly unknown[], rowKey: unknown, key: unknown): number {\n const keyOf = resolveRowKeys(rows, rowKey, NOOP, NOOP)\n const s = String(key)\n for (let i = 0; i < rows.length; i++) {\n const k = keyOf(rows[i], i)\n if (k === key || String(k) === s) return i\n }\n return -1\n}\n\n/**\n * `table.explainRow(key)` (§8.4): replays the row through every stage of the last run — schema,\n * then each step in pipeline order — and names the step that removed it. Survival is read from the\n * recorded step input / output (no per-row bookkeeping during normal runs); a removal by a filter\n * replays its filters on the single row to name the ones that reject it. Read-only: no state\n * change, no notification, no diagnostic. Every value it shows is redacted (§8.2).\n */\nexport function explainRow(host: ExplainHost, key: RowKey): RowExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const data = host.data()\n const r = resolveRedaction(host.redaction, (process.env.NODE_ENV !== \"production\"))\n const where: Location = { kind: 'feature', name: 'explainRow' }\n const shownKey = redactDetails({ key }, { key: 'rowKey' }, where, r).key\n const mode = host.schemaMode()\n const outcome = host.schema()\n const rowKey = host.rowKey()\n\n const index = findKey(data, rowKey, key)\n let schema: RowExplanation['schema'] = mode === undefined ? 'none' : 'valid'\n let rawIndex: number | undefined\n let issues: readonly unknown[] | undefined\n if (index >= 0) {\n issues = outcome?.flagged?.get(index)\n if (issues) schema = 'flagged'\n } else if (outcome && outcome.invalid.length > 0 && rowKey !== undefined && mode !== 'flag') {\n // Dropped by the schema: looked up among the invalid raw rows by the same key.\n const i = findKey(\n outcome.invalid.map((x) => x.raw),\n rowKey,\n key,\n )\n const inv = outcome.invalid[i]\n if (inv) {\n schema = 'dropped'\n rawIndex = inv.index\n issues = inv.issues\n }\n }\n const found = index >= 0 || rawIndex !== undefined\n let removedBy: string | undefined = schema === 'dropped' ? 'schema' : undefined\n const steps: RowStep[] = []\n for (const f of pipelineSteps(host.features)) {\n const base = { feature: f.name, stage: f.stage }\n if (!found || removedBy !== undefined) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n if (isServer(f)) {\n steps.push({ ...base, outcome: 'skipped' })\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n const position = rec.output.indexOf(index)\n if (position >= 0) {\n const of = rec.output.length\n steps.push({ ...base, outcome: rec.failed ? 'failed' : 'kept', position, of })\n continue\n }\n removedBy = f.name\n const detail = host.detail?.(host, f, rec.input, index, r, where)\n steps.push({ ...base, outcome: 'removed', ...(detail ? { detail } : {}) })\n }\n const e: RowExplanation = {\n key: shownKey,\n found,\n ...(index >= 0 ? { index } : {}),\n ...(rawIndex === undefined ? {} : { rawIndex }),\n schema,\n ...(issues ? { issues: issues.map((x, i) => scrubSchemaIssue(x, r, `issues.${i}`)) } : {}),\n steps,\n ...(removedBy === undefined ? {} : { removedBy }),\n visible: index >= 0 && model.rows.some((row) => (row as { index?: unknown }).index === index),\n run: last?.run ?? 0,\n rows: data.length,\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatRowExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/diagnostics/explain.ts"],"sourcesContent":["import type { AnyFeature, RowModelContext, Stage } from '../feature/types'\nimport { resolveRowKeys } from '../ingest/row-key'\nimport type { SchemaOutcome } from '../ingest/schema'\nimport type { InvalidRowsMode } from '../ingest/schema-codes'\nimport type { RowKey } from '../pipeline/row'\nimport type { RunRecord } from '../pipeline/runner'\nimport type { RowModel, RowModelStatus } from '../pipeline/types'\nimport './process-guard'\nimport type { explainDetail } from './explain-detail'\nimport { formatRowExplanation, formatTableExplanation } from './explain-text'\nimport { type RedactionOptions, redactDetails, resolveRedaction, scrubSchemaIssue } from './redact'\nimport { readTrace } from './trace-store'\nimport type { Diagnostic, Location, Trigger } from './types'\n\n/** What `createTable` gives `explain()` / `explainRow()`: read-only views of the table. */\nexport interface ExplainHost {\n readonly table: object\n /** Installed features in pipeline (stage) order. */\n readonly features: readonly AnyFeature[]\n /** The current row model (computed if data, state or columns changed, like `getRowModel()`). */\n rowModel(): RowModel<unknown>\n lastRun(): RunRecord | undefined\n /** A step context for replaying a `rowModel` on chosen rows (no diagnostics, no interceptors). */\n replay(name: string): RowModelContext\n data(): readonly unknown[]\n state(): Readonly<Record<string, unknown>>\n rowKey(): unknown\n /** `options.invalidRows` when a schema is set; `undefined` without one. */\n schemaMode(): InvalidRowsMode | undefined\n schema(): SchemaOutcome | undefined\n diagnostics(): readonly Diagnostic[]\n readonly redaction: RedactionOptions\n /** Why a step removed a row (`explainDetail`): it comes with the features (their runtime). */\n readonly detail?: typeof explainDetail\n}\n\n/** One step of {@link TableExplanation}: counts only. */\nexport interface ExplainStep {\n readonly feature: string\n readonly stage: Stage\n /** `'server'`: the server ran the step; its counts are the rows it was given. */\n readonly mode: 'client' | 'server'\n readonly input: number\n readonly output: number\n /** Served from the step memo in this run (nothing changed for it). */\n readonly cached: boolean\n /** The step threw or returned a bad output and passed its input through. */\n readonly failed: boolean\n /**\n * Duration (tracer only, development or `interceptors: [tracer()]`). A step that waited on\n * time-sliced work: the total of its runs and of the slices between them (#61).\n */\n readonly ms?: number\n /** Time-sliced work (#61): the runs {@link ms} spans; absent when the step ran in one. */\n readonly runs?: number\n}\n\n/** What `table.explain()` returns (§8.4); `String(e)` is the console summary. */\nexport interface TableExplanation {\n readonly run: number\n readonly trigger: Trigger\n readonly status: RowModelStatus\n /** Rows after ingest (after the schema dropped invalid rows). */\n readonly rows: number\n /** With a schema: its `invalidRows` mode and how many rows failed it. */\n readonly schema?: { readonly mode: InvalidRowsMode; readonly invalid: number }\n readonly steps: readonly ExplainStep[]\n /** Rows after filter and search (`getRowModel().filteredCount`). */\n readonly filteredCount: number\n /** With a pagination slice: the page shown (0-based), the page count and the page size. */\n readonly page?: { readonly index: number; readonly count: number; readonly size: number }\n /** Rows in `getRowModel().rows`. */\n readonly visible: number\n /** Error and warning diagnostics in `getDiagnostics()`. */\n readonly diagnostics: number\n /** Timings come from the tracer (it saw this run). */\n readonly traced: boolean\n /** Duration of the run (tracer only). */\n readonly ms?: number\n toString(): string\n}\n\n/**\n * `kept` / `removed` by the step, `skipped` (server mode), `failed` (passed its input through),\n * `not-reached` (an earlier step or the schema removed the row).\n */\nexport type RowStepOutcome = 'kept' | 'removed' | 'skipped' | 'failed' | 'not-reached'\n\nexport interface RowStep {\n readonly feature: string\n readonly stage: Stage\n readonly outcome: RowStepOutcome\n /** Kept: the row's position in the step output (0-based) and the output size. */\n readonly position?: number\n readonly of?: number\n /** Why the step removed the row (redacted). */\n readonly detail?: Readonly<Record<string, unknown>>\n}\n\n/** What `table.explainRow(key)` returns (§8.4); `String(e)` is the console report. */\nexport interface RowExplanation {\n /** The key asked for, redacted (a hash under `'safe'`, `[sensitive]` for a sensitive rowKey). */\n readonly key: unknown\n readonly found: boolean\n /** Position in `getData()`; absent when not found or dropped by the schema. */\n readonly index?: number\n /** Position in `options.data` of a row the schema dropped. */\n readonly rawIndex?: number\n readonly schema: 'none' | 'valid' | 'flagged' | 'dropped'\n /** Schema issues of a flagged / dropped row (paths + DataKable's own text unless `'full'`). */\n readonly issues?: readonly { path: string; message: string }[]\n readonly steps: readonly RowStep[]\n /** The step that removed the row: a feature name, or `'schema'`. */\n readonly removedBy?: string\n /** The row is in `getRowModel().rows`. */\n readonly visible: boolean\n readonly run: number\n /** Rows after ingest (for \"not found\"). */\n readonly rows: number\n toString(): string\n}\n\nconst NOOP = () => {}\n\n/** Steps that take part in the row model: a stage and a `rowModel` (server mode included). */\nconst pipelineSteps = (features: readonly AnyFeature[]) =>\n features.filter((f) => f.stage !== 'none' && typeof f.rowModel === 'function')\n\nconst isServer = (f: AnyFeature) => f.mode === 'server'\n\n/** `{ pageIndex, pageSize }` of the first paginate-stage slice that has them. */\nexport function pageOf(host: ExplainHost): { index: number; size: number } | undefined {\n for (const f of host.features) {\n if (f.stage !== 'paginate') continue\n const s = host.state()[f.name] as { pageIndex?: unknown; pageSize?: unknown } | undefined\n if (typeof s?.pageIndex === 'number' && typeof s.pageSize === 'number' && s.pageSize > 0) {\n return { index: s.pageIndex, size: s.pageSize }\n }\n }\n return undefined\n}\n\nexport const pageCount = (rows: number, size: number) => Math.ceil(rows / size)\n\n/** Clamped like pagination does (the page actually shown). */\nexport const shownPage = (index: number, count: number) => Math.max(0, Math.min(index, count - 1))\n\n/** `table.explain()` (§8.4): the last run's counts, with tracer timings when it was traced. */\nexport function explainTable(host: ExplainHost): TableExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const trace = readTrace(host.table)\n const traced = trace !== undefined && last !== undefined && trace.run === last.run\n const steps: ExplainStep[] = []\n let flowing = model.totalCount\n for (const f of pipelineSteps(host.features)) {\n if (isServer(f)) {\n steps.push(step(f, 'server', flowing, flowing, false, false))\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) continue\n flowing = rec.output.length\n const t = traced ? trace.steps.get(f.name) : undefined\n steps.push({\n ...step(f, 'client', rec.input.length, rec.output.length, rec.cached, rec.failed),\n ...(t ? { ms: t.ms, ...(t.runs ? { runs: t.runs } : {}) } : {}),\n })\n }\n const mode = host.schemaMode()\n const schema = host.schema()\n const p = pageOf(host)\n const count = p ? pageCount(model.filteredCount, p.size) : 0\n let diagnostics = 0\n for (const d of host.diagnostics()) if (d.level !== 'info') diagnostics++\n const e: TableExplanation = {\n run: last?.run ?? 0,\n trigger: last?.trigger ?? 'init',\n status: model.status,\n rows: model.totalCount,\n ...(mode === undefined ? {} : { schema: { mode, invalid: schema?.invalid.length ?? 0 } }),\n steps,\n filteredCount: model.filteredCount,\n ...(p ? { page: { index: shownPage(p.index, count), count, size: p.size } } : {}),\n visible: model.rows.length,\n diagnostics,\n traced,\n ...(traced && trace.ms !== undefined ? { ms: trace.ms } : {}),\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatTableExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n\nfunction step(\n f: AnyFeature,\n mode: 'client' | 'server',\n input: number,\n output: number,\n cached: boolean,\n failed: boolean,\n): ExplainStep {\n return { feature: f.name, stage: f.stage, mode, input, output, cached, failed }\n}\n\n/** Index of the row whose key equals `key` (`42` matches `'42'`), or -1. */\nfunction findKey(rows: readonly unknown[], rowKey: unknown, key: unknown): number {\n const keyOf = resolveRowKeys(rows, rowKey, NOOP, NOOP)\n const s = String(key)\n for (let i = 0; i < rows.length; i++) {\n const k = keyOf(rows[i], i)\n if (k === key || String(k) === s) return i\n }\n return -1\n}\n\n/**\n * `table.explainRow(key)` (§8.4): replays the row through every stage of the last run — schema,\n * then each step in pipeline order — and names the step that removed it. Survival is read from the\n * recorded step input / output (no per-row bookkeeping during normal runs); a removal by a filter\n * replays its filters on the single row to name the ones that reject it. Read-only: no state\n * change, no notification, no diagnostic. Every value it shows is redacted (§8.2).\n */\nexport function explainRow(host: ExplainHost, key: RowKey): RowExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const data = host.data()\n const r = resolveRedaction(host.redaction, (process.env.NODE_ENV !== \"production\"))\n const where: Location = { kind: 'feature', name: 'explainRow' }\n const shownKey = redactDetails({ key }, { key: 'rowKey' }, where, r).key\n const mode = host.schemaMode()\n const outcome = host.schema()\n const rowKey = host.rowKey()\n\n const index = findKey(data, rowKey, key)\n let schema: RowExplanation['schema'] = mode === undefined ? 'none' : 'valid'\n let rawIndex: number | undefined\n let issues: readonly unknown[] | undefined\n if (index >= 0) {\n issues = outcome?.flagged?.get(index)\n if (issues) schema = 'flagged'\n } else if (outcome && outcome.invalid.length > 0 && rowKey !== undefined && mode !== 'flag') {\n // Dropped by the schema: looked up among the invalid raw rows by the same key.\n const i = findKey(\n outcome.invalid.map((x) => x.raw),\n rowKey,\n key,\n )\n const inv = outcome.invalid[i]\n if (inv) {\n schema = 'dropped'\n rawIndex = inv.index\n issues = inv.issues\n }\n }\n const found = index >= 0 || rawIndex !== undefined\n let removedBy: string | undefined = schema === 'dropped' ? 'schema' : undefined\n const steps: RowStep[] = []\n for (const f of pipelineSteps(host.features)) {\n const base = { feature: f.name, stage: f.stage }\n if (!found || removedBy !== undefined) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n if (isServer(f)) {\n steps.push({ ...base, outcome: 'skipped' })\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n const position = rec.output.indexOf(index)\n if (position >= 0) {\n const of = rec.output.length\n steps.push({ ...base, outcome: rec.failed ? 'failed' : 'kept', position, of })\n continue\n }\n removedBy = f.name\n const detail = host.detail?.(host, f, rec.input, index, r, where)\n steps.push({ ...base, outcome: 'removed', ...(detail ? { detail } : {}) })\n }\n const e: RowExplanation = {\n key: shownKey,\n found,\n ...(index >= 0 ? { index } : {}),\n ...(rawIndex === undefined ? {} : { rawIndex }),\n schema,\n ...(issues ? { issues: issues.map((x, i) => scrubSchemaIssue(x, r, `issues.${i}`)) } : {}),\n steps,\n ...(removedBy === undefined ? {} : { removedBy }),\n visible: index >= 0 && model.rows.some((row) => (row as { index?: unknown }).index === index),\n run: last?.run ?? 0,\n rows: data.length,\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatRowExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/diagnostics/explain.ts"],"sourcesContent":["import type { AnyFeature, RowModelContext, Stage } from '../feature/types'\nimport { resolveRowKeys } from '../ingest/row-key'\nimport type { SchemaOutcome } from '../ingest/schema'\nimport type { InvalidRowsMode } from '../ingest/schema-codes'\nimport type { RowKey } from '../pipeline/row'\nimport type { RunRecord } from '../pipeline/runner'\nimport type { RowModel, RowModelStatus } from '../pipeline/types'\nimport './process-guard'\nimport type { explainDetail } from './explain-detail'\nimport { formatRowExplanation, formatTableExplanation } from './explain-text'\nimport { type RedactionOptions, redactDetails, resolveRedaction, scrubSchemaIssue } from './redact'\nimport { readTrace } from './trace-store'\nimport type { Diagnostic, Location, Trigger } from './types'\n\n/** What `createTable` gives `explain()` / `explainRow()`: read-only views of the table. */\nexport interface ExplainHost {\n readonly table: object\n /** Installed features in pipeline (stage) order. */\n readonly features: readonly AnyFeature[]\n /** The current row model (computed if data, state or columns changed, like `getRowModel()`). */\n rowModel(): RowModel<unknown>\n lastRun(): RunRecord | undefined\n /** A step context for replaying a `rowModel` on chosen rows (no diagnostics, no interceptors). */\n replay(name: string): RowModelContext\n data(): readonly unknown[]\n state(): Readonly<Record<string, unknown>>\n rowKey(): unknown\n /** `options.invalidRows` when a schema is set; `undefined` without one. */\n schemaMode(): InvalidRowsMode | undefined\n schema(): SchemaOutcome | undefined\n diagnostics(): readonly Diagnostic[]\n readonly redaction: RedactionOptions\n /** Why a step removed a row (`explainDetail`): it comes with the features (their runtime). */\n readonly detail?: typeof explainDetail\n}\n\n/** One step of {@link TableExplanation}: counts only. */\nexport interface ExplainStep {\n readonly feature: string\n readonly stage: Stage\n /** `'server'`: the server ran the step; its counts are the rows it was given. */\n readonly mode: 'client' | 'server'\n readonly input: number\n readonly output: number\n /** Served from the step memo in this run (nothing changed for it). */\n readonly cached: boolean\n /** The step threw or returned a bad output and passed its input through. */\n readonly failed: boolean\n /**\n * Duration (tracer only, development or `interceptors: [tracer()]`). A step that waited on\n * time-sliced work: the total of its runs and of the slices between them (#61).\n */\n readonly ms?: number\n /** Time-sliced work (#61): the runs {@link ms} spans; absent when the step ran in one. */\n readonly runs?: number\n}\n\n/** What `table.explain()` returns (§8.4); `String(e)` is the console summary. */\nexport interface TableExplanation {\n readonly run: number\n readonly trigger: Trigger\n readonly status: RowModelStatus\n /** Rows after ingest (after the schema dropped invalid rows). */\n readonly rows: number\n /** With a schema: its `invalidRows` mode and how many rows failed it. */\n readonly schema?: { readonly mode: InvalidRowsMode; readonly invalid: number }\n readonly steps: readonly ExplainStep[]\n /** Rows after filter and search (`getRowModel().filteredCount`). */\n readonly filteredCount: number\n /** With a pagination slice: the page shown (0-based), the page count and the page size. */\n readonly page?: { readonly index: number; readonly count: number; readonly size: number }\n /** Rows in `getRowModel().rows`. */\n readonly visible: number\n /** Error and warning diagnostics in `getDiagnostics()`. */\n readonly diagnostics: number\n /** Timings come from the tracer (it saw this run). */\n readonly traced: boolean\n /** Duration of the run (tracer only). */\n readonly ms?: number\n toString(): string\n}\n\n/**\n * `kept` / `removed` by the step, `skipped` (server mode), `failed` (passed its input through),\n * `not-reached` (an earlier step or the schema removed the row).\n */\nexport type RowStepOutcome = 'kept' | 'removed' | 'skipped' | 'failed' | 'not-reached'\n\nexport interface RowStep {\n readonly feature: string\n readonly stage: Stage\n readonly outcome: RowStepOutcome\n /** Kept: the row's position in the step output (0-based) and the output size. */\n readonly position?: number\n readonly of?: number\n /** Why the step removed the row (redacted). */\n readonly detail?: Readonly<Record<string, unknown>>\n}\n\n/** What `table.explainRow(key)` returns (§8.4); `String(e)` is the console report. */\nexport interface RowExplanation {\n /** The key asked for, redacted (a hash under `'safe'`). */\n readonly key: unknown\n readonly found: boolean\n /** Position in `getData()`; absent when not found or dropped by the schema. */\n readonly index?: number\n /** Position in `options.data` of a row the schema dropped. */\n readonly rawIndex?: number\n readonly schema: 'none' | 'valid' | 'flagged' | 'dropped'\n /** Schema issues of a flagged / dropped row (paths + DataKable's own text unless `'full'`). */\n readonly issues?: readonly { path: string; message: string }[]\n readonly steps: readonly RowStep[]\n /** The step that removed the row: a feature name, or `'schema'`. */\n readonly removedBy?: string\n /** The row is in `getRowModel().rows`. */\n readonly visible: boolean\n readonly run: number\n /** Rows after ingest (for \"not found\"). */\n readonly rows: number\n toString(): string\n}\n\nconst NOOP = () => {}\n\n/** Steps that take part in the row model: a stage and a `rowModel` (server mode included). */\nconst pipelineSteps = (features: readonly AnyFeature[]) =>\n features.filter((f) => f.stage !== 'none' && typeof f.rowModel === 'function')\n\nconst isServer = (f: AnyFeature) => f.mode === 'server'\n\n/** `{ pageIndex, pageSize }` of the first paginate-stage slice that has them. */\nexport function pageOf(host: ExplainHost): { index: number; size: number } | undefined {\n for (const f of host.features) {\n if (f.stage !== 'paginate') continue\n const s = host.state()[f.name] as { pageIndex?: unknown; pageSize?: unknown } | undefined\n if (typeof s?.pageIndex === 'number' && typeof s.pageSize === 'number' && s.pageSize > 0) {\n return { index: s.pageIndex, size: s.pageSize }\n }\n }\n return undefined\n}\n\nexport const pageCount = (rows: number, size: number) => Math.ceil(rows / size)\n\n/** Clamped like pagination does (the page actually shown). */\nexport const shownPage = (index: number, count: number) => Math.max(0, Math.min(index, count - 1))\n\n/** `table.explain()` (§8.4): the last run's counts, with tracer timings when it was traced. */\nexport function explainTable(host: ExplainHost): TableExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const trace = readTrace(host.table)\n const traced = trace !== undefined && last !== undefined && trace.run === last.run\n const steps: ExplainStep[] = []\n let flowing = model.totalCount\n for (const f of pipelineSteps(host.features)) {\n if (isServer(f)) {\n steps.push(step(f, 'server', flowing, flowing, false, false))\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) continue\n flowing = rec.output.length\n const t = traced ? trace.steps.get(f.name) : undefined\n steps.push({\n ...step(f, 'client', rec.input.length, rec.output.length, rec.cached, rec.failed),\n ...(t ? { ms: t.ms, ...(t.runs ? { runs: t.runs } : {}) } : {}),\n })\n }\n const mode = host.schemaMode()\n const schema = host.schema()\n const p = pageOf(host)\n const count = p ? pageCount(model.filteredCount, p.size) : 0\n let diagnostics = 0\n for (const d of host.diagnostics()) if (d.level !== 'info') diagnostics++\n const e: TableExplanation = {\n run: last?.run ?? 0,\n trigger: last?.trigger ?? 'init',\n status: model.status,\n rows: model.totalCount,\n ...(mode === undefined ? {} : { schema: { mode, invalid: schema?.invalid.length ?? 0 } }),\n steps,\n filteredCount: model.filteredCount,\n ...(p ? { page: { index: shownPage(p.index, count), count, size: p.size } } : {}),\n visible: model.rows.length,\n diagnostics,\n traced,\n ...(traced && trace.ms !== undefined ? { ms: trace.ms } : {}),\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatTableExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n\nfunction step(\n f: AnyFeature,\n mode: 'client' | 'server',\n input: number,\n output: number,\n cached: boolean,\n failed: boolean,\n): ExplainStep {\n return { feature: f.name, stage: f.stage, mode, input, output, cached, failed }\n}\n\n/** Index of the row whose key equals `key` (`42` matches `'42'`), or -1. */\nfunction findKey(rows: readonly unknown[], rowKey: unknown, key: unknown): number {\n const keyOf = resolveRowKeys(rows, rowKey, NOOP, NOOP)\n const s = String(key)\n for (let i = 0; i < rows.length; i++) {\n const k = keyOf(rows[i], i)\n if (k === key || String(k) === s) return i\n }\n return -1\n}\n\n/**\n * `table.explainRow(key)` (§8.4): replays the row through every stage of the last run — schema,\n * then each step in pipeline order — and names the step that removed it. Survival is read from the\n * recorded step input / output (no per-row bookkeeping during normal runs); a removal by a filter\n * replays its filters on the single row to name the ones that reject it. Read-only: no state\n * change, no notification, no diagnostic. Every value it shows is redacted (§8.2).\n */\nexport function explainRow(host: ExplainHost, key: RowKey): RowExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const data = host.data()\n const r = resolveRedaction(host.redaction, (process.env.NODE_ENV !== \"production\"))\n const where: Location = { kind: 'feature', name: 'explainRow' }\n const shownKey = redactDetails({ key }, { key: 'rowKey' }, where, r).key\n const mode = host.schemaMode()\n const outcome = host.schema()\n const rowKey = host.rowKey()\n\n const index = findKey(data, rowKey, key)\n let schema: RowExplanation['schema'] = mode === undefined ? 'none' : 'valid'\n let rawIndex: number | undefined\n let issues: readonly unknown[] | undefined\n if (index >= 0) {\n issues = outcome?.flagged?.get(index)\n if (issues) schema = 'flagged'\n } else if (outcome && outcome.invalid.length > 0 && rowKey !== undefined && mode !== 'flag') {\n // Dropped by the schema: looked up among the invalid raw rows by the same key.\n const i = findKey(\n outcome.invalid.map((x) => x.raw),\n rowKey,\n key,\n )\n const inv = outcome.invalid[i]\n if (inv) {\n schema = 'dropped'\n rawIndex = inv.index\n issues = inv.issues\n }\n }\n const found = index >= 0 || rawIndex !== undefined\n let removedBy: string | undefined = schema === 'dropped' ? 'schema' : undefined\n const steps: RowStep[] = []\n for (const f of pipelineSteps(host.features)) {\n const base = { feature: f.name, stage: f.stage }\n if (!found || removedBy !== undefined) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n if (isServer(f)) {\n steps.push({ ...base, outcome: 'skipped' })\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n const position = rec.output.indexOf(index)\n if (position >= 0) {\n const of = rec.output.length\n steps.push({ ...base, outcome: rec.failed ? 'failed' : 'kept', position, of })\n continue\n }\n removedBy = f.name\n const detail = host.detail?.(host, f, rec.input, index, r, where)\n steps.push({ ...base, outcome: 'removed', ...(detail ? { detail } : {}) })\n }\n const e: RowExplanation = {\n key: shownKey,\n found,\n ...(index >= 0 ? { index } : {}),\n ...(rawIndex === undefined ? {} : { rawIndex }),\n schema,\n ...(issues ? { issues: issues.map((x, i) => scrubSchemaIssue(x, r, `issues.${i}`)) } : {}),\n steps,\n ...(removedBy === undefined ? {} : { removedBy }),\n visible: index >= 0 && model.rows.some((row) => (row as { index?: unknown }).index === index),\n run: last?.run ?? 0,\n rows: data.length,\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatRowExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/diagnostics/explain.ts"],"sourcesContent":["import type { AnyFeature, RowModelContext, Stage } from '../feature/types'\nimport { resolveRowKeys } from '../ingest/row-key'\nimport type { SchemaOutcome } from '../ingest/schema'\nimport type { InvalidRowsMode } from '../ingest/schema-codes'\nimport type { RowKey } from '../pipeline/row'\nimport type { RunRecord } from '../pipeline/runner'\nimport type { RowModel, RowModelStatus } from '../pipeline/types'\nimport './process-guard'\nimport type { explainDetail } from './explain-detail'\nimport { formatRowExplanation, formatTableExplanation } from './explain-text'\nimport { type RedactionOptions, redactDetails, resolveRedaction, scrubSchemaIssue } from './redact'\nimport { readTrace } from './trace-store'\nimport type { Diagnostic, Location, Trigger } from './types'\n\n/** What `createTable` gives `explain()` / `explainRow()`: read-only views of the table. */\nexport interface ExplainHost {\n readonly table: object\n /** Installed features in pipeline (stage) order. */\n readonly features: readonly AnyFeature[]\n /** The current row model (computed if data, state or columns changed, like `getRowModel()`). */\n rowModel(): RowModel<unknown>\n lastRun(): RunRecord | undefined\n /** A step context for replaying a `rowModel` on chosen rows (no diagnostics, no interceptors). */\n replay(name: string): RowModelContext\n data(): readonly unknown[]\n state(): Readonly<Record<string, unknown>>\n rowKey(): unknown\n /** `options.invalidRows` when a schema is set; `undefined` without one. */\n schemaMode(): InvalidRowsMode | undefined\n schema(): SchemaOutcome | undefined\n diagnostics(): readonly Diagnostic[]\n readonly redaction: RedactionOptions\n /** Why a step removed a row (`explainDetail`): it comes with the features (their runtime). */\n readonly detail?: typeof explainDetail\n}\n\n/** One step of {@link TableExplanation}: counts only. */\nexport interface ExplainStep {\n readonly feature: string\n readonly stage: Stage\n /** `'server'`: the server ran the step; its counts are the rows it was given. */\n readonly mode: 'client' | 'server'\n readonly input: number\n readonly output: number\n /** Served from the step memo in this run (nothing changed for it). */\n readonly cached: boolean\n /** The step threw or returned a bad output and passed its input through. */\n readonly failed: boolean\n /**\n * Duration (tracer only, development or `interceptors: [tracer()]`). A step that waited on\n * time-sliced work: the total of its runs and of the slices between them (#61).\n */\n readonly ms?: number\n /** Time-sliced work (#61): the runs {@link ms} spans; absent when the step ran in one. */\n readonly runs?: number\n}\n\n/** What `table.explain()` returns (§8.4); `String(e)` is the console summary. */\nexport interface TableExplanation {\n readonly run: number\n readonly trigger: Trigger\n readonly status: RowModelStatus\n /** Rows after ingest (after the schema dropped invalid rows). */\n readonly rows: number\n /** With a schema: its `invalidRows` mode and how many rows failed it. */\n readonly schema?: { readonly mode: InvalidRowsMode; readonly invalid: number }\n readonly steps: readonly ExplainStep[]\n /** Rows after filter and search (`getRowModel().filteredCount`). */\n readonly filteredCount: number\n /** With a pagination slice: the page shown (0-based), the page count and the page size. */\n readonly page?: { readonly index: number; readonly count: number; readonly size: number }\n /** Rows in `getRowModel().rows`. */\n readonly visible: number\n /** Error and warning diagnostics in `getDiagnostics()`. */\n readonly diagnostics: number\n /** Timings come from the tracer (it saw this run). */\n readonly traced: boolean\n /** Duration of the run (tracer only). */\n readonly ms?: number\n toString(): string\n}\n\n/**\n * `kept` / `removed` by the step, `skipped` (server mode), `failed` (passed its input through),\n * `not-reached` (an earlier step or the schema removed the row).\n */\nexport type RowStepOutcome = 'kept' | 'removed' | 'skipped' | 'failed' | 'not-reached'\n\nexport interface RowStep {\n readonly feature: string\n readonly stage: Stage\n readonly outcome: RowStepOutcome\n /** Kept: the row's position in the step output (0-based) and the output size. */\n readonly position?: number\n readonly of?: number\n /** Why the step removed the row (redacted). */\n readonly detail?: Readonly<Record<string, unknown>>\n}\n\n/** What `table.explainRow(key)` returns (§8.4); `String(e)` is the console report. */\nexport interface RowExplanation {\n /** The key asked for, redacted (a hash under `'safe'`, `[sensitive]` for a sensitive rowKey). */\n readonly key: unknown\n readonly found: boolean\n /** Position in `getData()`; absent when not found or dropped by the schema. */\n readonly index?: number\n /** Position in `options.data` of a row the schema dropped. */\n readonly rawIndex?: number\n readonly schema: 'none' | 'valid' | 'flagged' | 'dropped'\n /** Schema issues of a flagged / dropped row (paths + DataKable's own text unless `'full'`). */\n readonly issues?: readonly { path: string; message: string }[]\n readonly steps: readonly RowStep[]\n /** The step that removed the row: a feature name, or `'schema'`. */\n readonly removedBy?: string\n /** The row is in `getRowModel().rows`. */\n readonly visible: boolean\n readonly run: number\n /** Rows after ingest (for \"not found\"). */\n readonly rows: number\n toString(): string\n}\n\nconst NOOP = () => {}\n\n/** Steps that take part in the row model: a stage and a `rowModel` (server mode included). */\nconst pipelineSteps = (features: readonly AnyFeature[]) =>\n features.filter((f) => f.stage !== 'none' && typeof f.rowModel === 'function')\n\nconst isServer = (f: AnyFeature) => f.mode === 'server'\n\n/** `{ pageIndex, pageSize }` of the first paginate-stage slice that has them. */\nexport function pageOf(host: ExplainHost): { index: number; size: number } | undefined {\n for (const f of host.features) {\n if (f.stage !== 'paginate') continue\n const s = host.state()[f.name] as { pageIndex?: unknown; pageSize?: unknown } | undefined\n if (typeof s?.pageIndex === 'number' && typeof s.pageSize === 'number' && s.pageSize > 0) {\n return { index: s.pageIndex, size: s.pageSize }\n }\n }\n return undefined\n}\n\nexport const pageCount = (rows: number, size: number) => Math.ceil(rows / size)\n\n/** Clamped like pagination does (the page actually shown). */\nexport const shownPage = (index: number, count: number) => Math.max(0, Math.min(index, count - 1))\n\n/** `table.explain()` (§8.4): the last run's counts, with tracer timings when it was traced. */\nexport function explainTable(host: ExplainHost): TableExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const trace = readTrace(host.table)\n const traced = trace !== undefined && last !== undefined && trace.run === last.run\n const steps: ExplainStep[] = []\n let flowing = model.totalCount\n for (const f of pipelineSteps(host.features)) {\n if (isServer(f)) {\n steps.push(step(f, 'server', flowing, flowing, false, false))\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) continue\n flowing = rec.output.length\n const t = traced ? trace.steps.get(f.name) : undefined\n steps.push({\n ...step(f, 'client', rec.input.length, rec.output.length, rec.cached, rec.failed),\n ...(t ? { ms: t.ms, ...(t.runs ? { runs: t.runs } : {}) } : {}),\n })\n }\n const mode = host.schemaMode()\n const schema = host.schema()\n const p = pageOf(host)\n const count = p ? pageCount(model.filteredCount, p.size) : 0\n let diagnostics = 0\n for (const d of host.diagnostics()) if (d.level !== 'info') diagnostics++\n const e: TableExplanation = {\n run: last?.run ?? 0,\n trigger: last?.trigger ?? 'init',\n status: model.status,\n rows: model.totalCount,\n ...(mode === undefined ? {} : { schema: { mode, invalid: schema?.invalid.length ?? 0 } }),\n steps,\n filteredCount: model.filteredCount,\n ...(p ? { page: { index: shownPage(p.index, count), count, size: p.size } } : {}),\n visible: model.rows.length,\n diagnostics,\n traced,\n ...(traced && trace.ms !== undefined ? { ms: trace.ms } : {}),\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatTableExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n\nfunction step(\n f: AnyFeature,\n mode: 'client' | 'server',\n input: number,\n output: number,\n cached: boolean,\n failed: boolean,\n): ExplainStep {\n return { feature: f.name, stage: f.stage, mode, input, output, cached, failed }\n}\n\n/** Index of the row whose key equals `key` (`42` matches `'42'`), or -1. */\nfunction findKey(rows: readonly unknown[], rowKey: unknown, key: unknown): number {\n const keyOf = resolveRowKeys(rows, rowKey, NOOP, NOOP)\n const s = String(key)\n for (let i = 0; i < rows.length; i++) {\n const k = keyOf(rows[i], i)\n if (k === key || String(k) === s) return i\n }\n return -1\n}\n\n/**\n * `table.explainRow(key)` (§8.4): replays the row through every stage of the last run — schema,\n * then each step in pipeline order — and names the step that removed it. Survival is read from the\n * recorded step input / output (no per-row bookkeeping during normal runs); a removal by a filter\n * replays its filters on the single row to name the ones that reject it. Read-only: no state\n * change, no notification, no diagnostic. Every value it shows is redacted (§8.2).\n */\nexport function explainRow(host: ExplainHost, key: RowKey): RowExplanation {\n const model = host.rowModel()\n const last = host.lastRun()\n const data = host.data()\n const r = resolveRedaction(host.redaction, (process.env.NODE_ENV !== \"production\"))\n const where: Location = { kind: 'feature', name: 'explainRow' }\n const shownKey = redactDetails({ key }, { key: 'rowKey' }, where, r).key\n const mode = host.schemaMode()\n const outcome = host.schema()\n const rowKey = host.rowKey()\n\n const index = findKey(data, rowKey, key)\n let schema: RowExplanation['schema'] = mode === undefined ? 'none' : 'valid'\n let rawIndex: number | undefined\n let issues: readonly unknown[] | undefined\n if (index >= 0) {\n issues = outcome?.flagged?.get(index)\n if (issues) schema = 'flagged'\n } else if (outcome && outcome.invalid.length > 0 && rowKey !== undefined && mode !== 'flag') {\n // Dropped by the schema: looked up among the invalid raw rows by the same key.\n const i = findKey(\n outcome.invalid.map((x) => x.raw),\n rowKey,\n key,\n )\n const inv = outcome.invalid[i]\n if (inv) {\n schema = 'dropped'\n rawIndex = inv.index\n issues = inv.issues\n }\n }\n const found = index >= 0 || rawIndex !== undefined\n let removedBy: string | undefined = schema === 'dropped' ? 'schema' : undefined\n const steps: RowStep[] = []\n for (const f of pipelineSteps(host.features)) {\n const base = { feature: f.name, stage: f.stage }\n if (!found || removedBy !== undefined) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n if (isServer(f)) {\n steps.push({ ...base, outcome: 'skipped' })\n continue\n }\n const rec = last?.steps.find((s) => s.feature === f)\n if (!rec) {\n steps.push({ ...base, outcome: 'not-reached' })\n continue\n }\n const position = rec.output.indexOf(index)\n if (position >= 0) {\n const of = rec.output.length\n steps.push({ ...base, outcome: rec.failed ? 'failed' : 'kept', position, of })\n continue\n }\n removedBy = f.name\n const detail = host.detail?.(host, f, rec.input, index, r, where)\n steps.push({ ...base, outcome: 'removed', ...(detail ? { detail } : {}) })\n }\n const e: RowExplanation = {\n key: shownKey,\n found,\n ...(index >= 0 ? { index } : {}),\n ...(rawIndex === undefined ? {} : { rawIndex }),\n schema,\n ...(issues ? { issues: issues.map((x, i) => scrubSchemaIssue(x, r, `issues.${i}`)) } : {}),\n steps,\n ...(removedBy === undefined ? {} : { removedBy }),\n visible: index >= 0 && model.rows.some((row) => (row as { index?: unknown }).index === index),\n run: last?.run ?? 0,\n rows: data.length,\n toString: () => ((process.env.NODE_ENV !== \"production\") ? formatRowExplanation(e) : JSON.stringify(e)),\n }\n return e\n}\n"],"mappings":"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","names":[]}
@@ -142,7 +142,13 @@ function createDiagnostics(options = {}) {
142
142
  return {
143
143
  table,
144
144
  emit,
145
- getDiagnostics: () => buffer.toArray(),
145
+ // The original error can quote row values (a sensitive column's accessor throwing
146
+ // `bad ssn 123-…`): it reaches onDiagnostic and strict's DatakableError only, so readers of the
147
+ // buffer get a copy without it. `occurrences` stays shared, so counts still aggregate live.
148
+ getDiagnostics: () => buffer.toArray().map((d) => {
149
+ const { cause, ...context } = d.context;
150
+ return cause === void 0 ? d : Object.setPrototypeOf({ ...d, context }, Object.getPrototypeOf(d));
151
+ }),
146
152
  attach(rt) {
147
153
  runtime = rt;
148
154
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/diagnostics/hub.ts"],"sourcesContent":["import { createContext } from '../intercept/context'\nimport type { InterceptorRuntime } from '../intercept/runtime'\nimport { buildDiagnostic, DatakableError, defaultPhase, formatWhere, wallClock } from './build'\nimport type { DiagnosticCode, DiagnosticDetails } from './catalog'\nimport { createConsoleSink } from './console'\nimport './process-guard'\nimport {\n DK_UNSAFE_REDACTION,\n type RedactionOptions,\n type RedactLevel,\n resolveRedaction,\n scrubError,\n} from './redact'\nimport { RingBuffer } from './ring-buffer'\nimport type { Diagnostic, DiagnosticStateSummary, EmitOptions, Phase, Trigger } from './types'\n\n/** Live values the store supplies at emit time (T07+). Missing ones use defaults. */\nexport interface LiveDiagnosticContext {\n trigger?: Trigger\n run?: number\n phase?: Phase\n mode?: Record<string, 'client' | 'server'>\n state?: DiagnosticStateSummary\n}\n\nexport interface DiagnosticsOptions {\n /** `options.name` of the table; default `table#n`. */\n name?: string\n /** The table instance, put on the `diagnostic:emit` ctx. */\n table?: unknown\n /** Dev: the table's creation site. */\n createdAt?: string\n getContext?: () => LiveDiagnosticContext\n /** Dedupe is per data reference: a new reference reports known diagnostics again. */\n getDataRef?: () => unknown\n /** User sink (e.g. Sentry). Throwing is swallowed with one console.error. */\n onDiagnostic?: (d: Diagnostic) => void\n /** Rethrow error-level diagnostics as `DatakableError`. */\n strict?: boolean\n /** Default console sink. Default `true`. */\n console?: boolean\n /** Text builders + dev console format. Default: `process.env.NODE_ENV !== 'production'`. */\n dev?: boolean\n /**\n * Redaction policy (§8.2, `diagnostics.redact`): a level or `{ level, redact(path, value),\n * sensitiveColumns, rowKeySensitive }`. Default `'keys'` in development, `'safe'` in production.\n * Applied while the diagnostic is built, so `diagnostic:emit` interceptors, the `redact` seam and\n * every sink only see redacted data. `'full'` in production warns (`DK_UNSAFE_REDACTION`).\n */\n redaction?: RedactLevel | RedactionOptions\n /**\n * Extra whole-diagnostic rewrite after the `diagnostic:emit` interceptors, before dedupe and\n * sinks. It receives already-redacted data; `fingerprint` is not recomputed.\n */\n redact?: (d: Diagnostic) => Diagnostic\n /** Ring buffer size behind getDiagnostics(). Default 100. */\n bufferSize?: number\n /** Max fingerprints remembered for dedupe. Default 500. */\n dedupeLimit?: number\n /** Console rate limit. Default 20 per 1000 ms. */\n rateLimit?: { max: number; windowMs: number }\n /** Clock (epoch ms) for `occurrences` and the rate limit. Default `wallClock` (never `Date`, #7). */\n now?: () => number\n}\n\nexport interface Diagnostics {\n /** Resolved table name (`context.table`). */\n readonly table: string\n /**\n * Emits a catalog diagnostic through `diagnostic:emit`. Returns the stored diagnostic (the\n * aggregated one on a dedupe hit), or `undefined` if an interceptor dropped it. Never throws,\n * except `DatakableError` under `strict: true`.\n */\n emit<C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n ): Diagnostic | undefined\n /** Oldest first; at most `bufferSize` entries. */\n getDiagnostics(): Diagnostic[]\n /** Runs emits through this runtime's `diagnostic:emit` chain (user interceptors can redact/drop). */\n attach(runtime: InterceptorRuntime): void\n clear(): void\n}\n\nlet tableSeq = 0\n\n/**\n * One per table. Pipeline (§8.2): enrich + redact (build: context, policy redaction, then text,\n * where, fingerprint and docs from the redacted data) → `diagnostic:emit` interceptors → `redact`\n * seam → dedupe (aggregates `occurrences`) → ring buffer → sinks (onDiagnostic, console) → strict.\n */\nexport function createDiagnostics(options: DiagnosticsOptions = {}): Diagnostics {\n const table = options.name ?? `table#${++tableSeq}`\n const dev = options.dev ?? (process.env.NODE_ENV !== \"production\")\n const now = options.now ?? wallClock\n const dedupeLimit = options.dedupeLimit ?? 500\n const buffer = new RingBuffer<Diagnostic>(options.bufferSize ?? 100)\n const seen = new Map<string, Diagnostic>()\n const { onDiagnostic, getContext, getDataRef, redact, strict } = options\n const toConsole =\n options.console === false\n ? undefined\n : createConsoleSink({\n table,\n dev,\n now,\n rateLimit: options.rateLimit ?? { max: 20, windowMs: 1000 },\n })\n\n let runtime: InterceptorRuntime | undefined\n let depth = 0\n let lastRef: unknown\n let failed = false\n\n const fail = (e: unknown) => {\n // The logger never kills the table: one console.error, then silence.\n if (failed) return\n failed = true\n // Production prints the error name only: its message can embed row values (§8.2).\n console.error(\n `[DataKable ${table}] diagnostics pipeline failed; later failures are silent`,\n dev ? e : scrubError(e),\n )\n }\n\n function deliver(input: Diagnostic, quiet?: boolean): Diagnostic {\n const d = redact ? redact(input) : input\n if (getDataRef) {\n const ref = getDataRef()\n if (ref !== lastRef) {\n seen.clear()\n lastRef = ref\n }\n }\n const prev = seen.get(d.fingerprint)\n if (prev) {\n prev.context.occurrences.count++\n prev.context.occurrences.last = d.context.occurrences.last\n return prev\n }\n seen.set(d.fingerprint, d)\n if (seen.size > dedupeLimit) seen.delete(seen.keys().next().value as string)\n buffer.push(d)\n if (onDiagnostic) {\n try {\n onDiagnostic(d)\n } catch (e) {\n fail(e)\n }\n }\n if (!quiet) toConsole?.(d)\n return d\n }\n\n // Resolved per emit: sensitive columns and the rowKey can change with setOptions.\n const redactor = () => {\n const r = resolveRedaction(options.redaction, dev)\n r.onError = fail\n return r\n }\n\n const emit: Diagnostics['emit'] = (code, details, opts = {}) => {\n let out: Diagnostic | undefined\n try {\n const live = getContext?.() ?? {}\n const d = buildDiagnostic(code, details, {\n table,\n ...(options.createdAt === undefined ? {} : { createdAt: options.createdAt }),\n trigger: opts.trigger ?? live.trigger ?? 'init',\n run: opts.run ?? live.run ?? 0,\n phase: opts.phase ?? live.phase ?? defaultPhase(),\n ...(live.mode ? { mode: live.mode } : {}),\n ...(live.state ? { state: live.state } : {}),\n now: now(),\n ...(opts.cause === undefined ? {} : { cause: opts.cause }),\n ...(opts.causedBy === undefined ? {} : { causedBy: opts.causedBy }),\n dev,\n redaction: redactor(),\n })\n // Emits made while the chain runs (e.g. DK_INTERCEPTOR_FAILED for an interceptor on\n // diagnostic:emit) skip the chain, so a broken logger cannot recurse.\n if (runtime && depth === 0) {\n const c = d.context\n // ctx.where reaches slots, fingerprints and issue links unredacted: never the row key.\n const at = d.where.kind === 'row' ? { kind: 'row' as const, index: d.where.index } : d.where\n const ctx = createContext('diagnostic:emit', { diagnostic: d }, formatWhere(at), {\n table: options.table,\n run: c.run,\n trigger: c.trigger,\n phase: c.phase,\n emit,\n })\n depth++\n try {\n runtime.run('diagnostic:emit', ctx, (x) => {\n out = deliver(x.input.diagnostic, opts.quiet)\n })\n } finally {\n depth--\n }\n } else {\n out = deliver(d, opts.quiet)\n }\n } catch (e) {\n // strict: an error-level emit nested in the chain (e.g. DK_INTERCEPTOR_FAILED) still throws.\n if (strict && e instanceof DatakableError) throw e\n fail(e)\n return undefined\n }\n if (strict && out?.level === 'error') throw new DatakableError(out)\n return out\n }\n\n if (!dev && resolveRedaction(options.redaction, dev).level === 'full') {\n emit(DK_UNSAFE_REDACTION.code, { level: 'full' })\n }\n\n return {\n table,\n emit,\n getDiagnostics: () => buffer.toArray(),\n attach(rt) {\n runtime = rt\n },\n clear() {\n buffer.clear()\n seen.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/diagnostics/hub.ts"],"sourcesContent":["import { createContext } from '../intercept/context'\nimport type { InterceptorRuntime } from '../intercept/runtime'\nimport { buildDiagnostic, DatakableError, defaultPhase, formatWhere, wallClock } from './build'\nimport type { DiagnosticCode, DiagnosticDetails } from './catalog'\nimport { createConsoleSink } from './console'\nimport './process-guard'\nimport {\n DK_UNSAFE_REDACTION,\n type RedactionOptions,\n type RedactLevel,\n resolveRedaction,\n scrubError,\n} from './redact'\nimport { RingBuffer } from './ring-buffer'\nimport type { Diagnostic, DiagnosticStateSummary, EmitOptions, Phase, Trigger } from './types'\n\n/** Live values the store supplies at emit time (T07+). Missing ones use defaults. */\nexport interface LiveDiagnosticContext {\n trigger?: Trigger\n run?: number\n phase?: Phase\n mode?: Record<string, 'client' | 'server'>\n state?: DiagnosticStateSummary\n}\n\nexport interface DiagnosticsOptions {\n /** `options.name` of the table; default `table#n`. */\n name?: string\n /** The table instance, put on the `diagnostic:emit` ctx. */\n table?: unknown\n /** Dev: the table's creation site. */\n createdAt?: string\n getContext?: () => LiveDiagnosticContext\n /** Dedupe is per data reference: a new reference reports known diagnostics again. */\n getDataRef?: () => unknown\n /** User sink (e.g. Sentry). Throwing is swallowed with one console.error. */\n onDiagnostic?: (d: Diagnostic) => void\n /** Rethrow error-level diagnostics as `DatakableError`. */\n strict?: boolean\n /** Default console sink. Default `true`. */\n console?: boolean\n /** Text builders + dev console format. Default: `process.env.NODE_ENV !== 'production'`. */\n dev?: boolean\n /**\n * Redaction policy (§8.2, `diagnostics.redact`): a level or `{ level, redact(path, value),\n * sensitiveColumns, rowKeySensitive }`. Default `'keys'` in development, `'safe'` in production.\n * Applied while the diagnostic is built, so `diagnostic:emit` interceptors, the `redact` seam and\n * every sink only see redacted data. `'full'` in production warns (`DK_UNSAFE_REDACTION`).\n */\n redaction?: RedactLevel | RedactionOptions\n /**\n * Extra whole-diagnostic rewrite after the `diagnostic:emit` interceptors, before dedupe and\n * sinks. It receives already-redacted data; `fingerprint` is not recomputed.\n */\n redact?: (d: Diagnostic) => Diagnostic\n /** Ring buffer size behind getDiagnostics(). Default 100. */\n bufferSize?: number\n /** Max fingerprints remembered for dedupe. Default 500. */\n dedupeLimit?: number\n /** Console rate limit. Default 20 per 1000 ms. */\n rateLimit?: { max: number; windowMs: number }\n /** Clock (epoch ms) for `occurrences` and the rate limit. Default `wallClock` (never `Date`, #7). */\n now?: () => number\n}\n\nexport interface Diagnostics {\n /** Resolved table name (`context.table`). */\n readonly table: string\n /**\n * Emits a catalog diagnostic through `diagnostic:emit`. Returns the stored diagnostic (the\n * aggregated one on a dedupe hit), or `undefined` if an interceptor dropped it. Never throws,\n * except `DatakableError` under `strict: true`.\n */\n emit<C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n ): Diagnostic | undefined\n /** Oldest first; at most `bufferSize` entries. */\n getDiagnostics(): Diagnostic[]\n /** Runs emits through this runtime's `diagnostic:emit` chain (user interceptors can redact/drop). */\n attach(runtime: InterceptorRuntime): void\n clear(): void\n}\n\nlet tableSeq = 0\n\n/**\n * One per table. Pipeline (§8.2): enrich + redact (build: context, policy redaction, then text,\n * where, fingerprint and docs from the redacted data) → `diagnostic:emit` interceptors → `redact`\n * seam → dedupe (aggregates `occurrences`) → ring buffer → sinks (onDiagnostic, console) → strict.\n */\nexport function createDiagnostics(options: DiagnosticsOptions = {}): Diagnostics {\n const table = options.name ?? `table#${++tableSeq}`\n const dev = options.dev ?? (process.env.NODE_ENV !== \"production\")\n const now = options.now ?? wallClock\n const dedupeLimit = options.dedupeLimit ?? 500\n const buffer = new RingBuffer<Diagnostic>(options.bufferSize ?? 100)\n const seen = new Map<string, Diagnostic>()\n const { onDiagnostic, getContext, getDataRef, redact, strict } = options\n const toConsole =\n options.console === false\n ? undefined\n : createConsoleSink({\n table,\n dev,\n now,\n rateLimit: options.rateLimit ?? { max: 20, windowMs: 1000 },\n })\n\n let runtime: InterceptorRuntime | undefined\n let depth = 0\n let lastRef: unknown\n let failed = false\n\n const fail = (e: unknown) => {\n // The logger never kills the table: one console.error, then silence.\n if (failed) return\n failed = true\n // Production prints the error name only: its message can embed row values (§8.2).\n console.error(\n `[DataKable ${table}] diagnostics pipeline failed; later failures are silent`,\n dev ? e : scrubError(e),\n )\n }\n\n function deliver(input: Diagnostic, quiet?: boolean): Diagnostic {\n const d = redact ? redact(input) : input\n if (getDataRef) {\n const ref = getDataRef()\n if (ref !== lastRef) {\n seen.clear()\n lastRef = ref\n }\n }\n const prev = seen.get(d.fingerprint)\n if (prev) {\n prev.context.occurrences.count++\n prev.context.occurrences.last = d.context.occurrences.last\n return prev\n }\n seen.set(d.fingerprint, d)\n if (seen.size > dedupeLimit) seen.delete(seen.keys().next().value as string)\n buffer.push(d)\n if (onDiagnostic) {\n try {\n onDiagnostic(d)\n } catch (e) {\n fail(e)\n }\n }\n if (!quiet) toConsole?.(d)\n return d\n }\n\n // Resolved per emit: sensitive columns and the rowKey can change with setOptions.\n const redactor = () => {\n const r = resolveRedaction(options.redaction, dev)\n r.onError = fail\n return r\n }\n\n const emit: Diagnostics['emit'] = (code, details, opts = {}) => {\n let out: Diagnostic | undefined\n try {\n const live = getContext?.() ?? {}\n const d = buildDiagnostic(code, details, {\n table,\n ...(options.createdAt === undefined ? {} : { createdAt: options.createdAt }),\n trigger: opts.trigger ?? live.trigger ?? 'init',\n run: opts.run ?? live.run ?? 0,\n phase: opts.phase ?? live.phase ?? defaultPhase(),\n ...(live.mode ? { mode: live.mode } : {}),\n ...(live.state ? { state: live.state } : {}),\n now: now(),\n ...(opts.cause === undefined ? {} : { cause: opts.cause }),\n ...(opts.causedBy === undefined ? {} : { causedBy: opts.causedBy }),\n dev,\n redaction: redactor(),\n })\n // Emits made while the chain runs (e.g. DK_INTERCEPTOR_FAILED for an interceptor on\n // diagnostic:emit) skip the chain, so a broken logger cannot recurse.\n if (runtime && depth === 0) {\n const c = d.context\n // ctx.where reaches slots, fingerprints and issue links unredacted: never the row key.\n const at = d.where.kind === 'row' ? { kind: 'row' as const, index: d.where.index } : d.where\n const ctx = createContext('diagnostic:emit', { diagnostic: d }, formatWhere(at), {\n table: options.table,\n run: c.run,\n trigger: c.trigger,\n phase: c.phase,\n emit,\n })\n depth++\n try {\n runtime.run('diagnostic:emit', ctx, (x) => {\n out = deliver(x.input.diagnostic, opts.quiet)\n })\n } finally {\n depth--\n }\n } else {\n out = deliver(d, opts.quiet)\n }\n } catch (e) {\n // strict: an error-level emit nested in the chain (e.g. DK_INTERCEPTOR_FAILED) still throws.\n if (strict && e instanceof DatakableError) throw e\n fail(e)\n return undefined\n }\n if (strict && out?.level === 'error') throw new DatakableError(out)\n return out\n }\n\n if (!dev && resolveRedaction(options.redaction, dev).level === 'full') {\n emit(DK_UNSAFE_REDACTION.code, { level: 'full' })\n }\n\n return {\n table,\n emit,\n // The original error can quote row values (a sensitive column's accessor throwing\n // `bad ssn 123-…`): it reaches onDiagnostic and strict's DatakableError only, so readers of the\n // buffer get a copy without it. `occurrences` stays shared, so counts still aggregate live.\n getDiagnostics: () =>\n buffer.toArray().map((d) => {\n const { cause, ...context } = d.context\n return cause === undefined\n ? d\n : Object.setPrototypeOf({ ...d, context }, Object.getPrototypeOf(d))\n }),\n attach(rt) {\n runtime = rt\n },\n clear() {\n buffer.clear()\n seen.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}
@@ -122,7 +122,13 @@ function createDiagnostics(options = {}) {
122
122
  return {
123
123
  table,
124
124
  emit,
125
- getDiagnostics: () => buffer.toArray(),
125
+ // The original error can quote row values (a sensitive column's accessor throwing
126
+ // `bad ssn 123-…`): it reaches onDiagnostic and strict's DatakableError only, so readers of the
127
+ // buffer get a copy without it. `occurrences` stays shared, so counts still aggregate live.
128
+ getDiagnostics: () => buffer.toArray().map((d) => {
129
+ const { cause, ...context } = d.context;
130
+ return cause === void 0 ? d : Object.setPrototypeOf({ ...d, context }, Object.getPrototypeOf(d));
131
+ }),
126
132
  attach(rt) {
127
133
  runtime = rt;
128
134
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/diagnostics/hub.ts"],"sourcesContent":["import { createContext } from '../intercept/context'\nimport type { InterceptorRuntime } from '../intercept/runtime'\nimport { buildDiagnostic, DatakableError, defaultPhase, formatWhere, wallClock } from './build'\nimport type { DiagnosticCode, DiagnosticDetails } from './catalog'\nimport { createConsoleSink } from './console'\nimport './process-guard'\nimport {\n DK_UNSAFE_REDACTION,\n type RedactionOptions,\n type RedactLevel,\n resolveRedaction,\n scrubError,\n} from './redact'\nimport { RingBuffer } from './ring-buffer'\nimport type { Diagnostic, DiagnosticStateSummary, EmitOptions, Phase, Trigger } from './types'\n\n/** Live values the store supplies at emit time (T07+). Missing ones use defaults. */\nexport interface LiveDiagnosticContext {\n trigger?: Trigger\n run?: number\n phase?: Phase\n mode?: Record<string, 'client' | 'server'>\n state?: DiagnosticStateSummary\n}\n\nexport interface DiagnosticsOptions {\n /** `options.name` of the table; default `table#n`. */\n name?: string\n /** The table instance, put on the `diagnostic:emit` ctx. */\n table?: unknown\n /** Dev: the table's creation site. */\n createdAt?: string\n getContext?: () => LiveDiagnosticContext\n /** Dedupe is per data reference: a new reference reports known diagnostics again. */\n getDataRef?: () => unknown\n /** User sink (e.g. Sentry). Throwing is swallowed with one console.error. */\n onDiagnostic?: (d: Diagnostic) => void\n /** Rethrow error-level diagnostics as `DatakableError`. */\n strict?: boolean\n /** Default console sink. Default `true`. */\n console?: boolean\n /** Text builders + dev console format. Default: `process.env.NODE_ENV !== 'production'`. */\n dev?: boolean\n /**\n * Redaction policy (§8.2, `diagnostics.redact`): a level or `{ level, redact(path, value),\n * sensitiveColumns, rowKeySensitive }`. Default `'keys'` in development, `'safe'` in production.\n * Applied while the diagnostic is built, so `diagnostic:emit` interceptors, the `redact` seam and\n * every sink only see redacted data. `'full'` in production warns (`DK_UNSAFE_REDACTION`).\n */\n redaction?: RedactLevel | RedactionOptions\n /**\n * Extra whole-diagnostic rewrite after the `diagnostic:emit` interceptors, before dedupe and\n * sinks. It receives already-redacted data; `fingerprint` is not recomputed.\n */\n redact?: (d: Diagnostic) => Diagnostic\n /** Ring buffer size behind getDiagnostics(). Default 100. */\n bufferSize?: number\n /** Max fingerprints remembered for dedupe. Default 500. */\n dedupeLimit?: number\n /** Console rate limit. Default 20 per 1000 ms. */\n rateLimit?: { max: number; windowMs: number }\n /** Clock (epoch ms) for `occurrences` and the rate limit. Default `wallClock` (never `Date`, #7). */\n now?: () => number\n}\n\nexport interface Diagnostics {\n /** Resolved table name (`context.table`). */\n readonly table: string\n /**\n * Emits a catalog diagnostic through `diagnostic:emit`. Returns the stored diagnostic (the\n * aggregated one on a dedupe hit), or `undefined` if an interceptor dropped it. Never throws,\n * except `DatakableError` under `strict: true`.\n */\n emit<C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n ): Diagnostic | undefined\n /** Oldest first; at most `bufferSize` entries. */\n getDiagnostics(): Diagnostic[]\n /** Runs emits through this runtime's `diagnostic:emit` chain (user interceptors can redact/drop). */\n attach(runtime: InterceptorRuntime): void\n clear(): void\n}\n\nlet tableSeq = 0\n\n/**\n * One per table. Pipeline (§8.2): enrich + redact (build: context, policy redaction, then text,\n * where, fingerprint and docs from the redacted data) → `diagnostic:emit` interceptors → `redact`\n * seam → dedupe (aggregates `occurrences`) → ring buffer → sinks (onDiagnostic, console) → strict.\n */\nexport function createDiagnostics(options: DiagnosticsOptions = {}): Diagnostics {\n const table = options.name ?? `table#${++tableSeq}`\n const dev = options.dev ?? (process.env.NODE_ENV !== \"production\")\n const now = options.now ?? wallClock\n const dedupeLimit = options.dedupeLimit ?? 500\n const buffer = new RingBuffer<Diagnostic>(options.bufferSize ?? 100)\n const seen = new Map<string, Diagnostic>()\n const { onDiagnostic, getContext, getDataRef, redact, strict } = options\n const toConsole =\n options.console === false\n ? undefined\n : createConsoleSink({\n table,\n dev,\n now,\n rateLimit: options.rateLimit ?? { max: 20, windowMs: 1000 },\n })\n\n let runtime: InterceptorRuntime | undefined\n let depth = 0\n let lastRef: unknown\n let failed = false\n\n const fail = (e: unknown) => {\n // The logger never kills the table: one console.error, then silence.\n if (failed) return\n failed = true\n // Production prints the error name only: its message can embed row values (§8.2).\n console.error(\n `[DataKable ${table}] diagnostics pipeline failed; later failures are silent`,\n dev ? e : scrubError(e),\n )\n }\n\n function deliver(input: Diagnostic, quiet?: boolean): Diagnostic {\n const d = redact ? redact(input) : input\n if (getDataRef) {\n const ref = getDataRef()\n if (ref !== lastRef) {\n seen.clear()\n lastRef = ref\n }\n }\n const prev = seen.get(d.fingerprint)\n if (prev) {\n prev.context.occurrences.count++\n prev.context.occurrences.last = d.context.occurrences.last\n return prev\n }\n seen.set(d.fingerprint, d)\n if (seen.size > dedupeLimit) seen.delete(seen.keys().next().value as string)\n buffer.push(d)\n if (onDiagnostic) {\n try {\n onDiagnostic(d)\n } catch (e) {\n fail(e)\n }\n }\n if (!quiet) toConsole?.(d)\n return d\n }\n\n // Resolved per emit: sensitive columns and the rowKey can change with setOptions.\n const redactor = () => {\n const r = resolveRedaction(options.redaction, dev)\n r.onError = fail\n return r\n }\n\n const emit: Diagnostics['emit'] = (code, details, opts = {}) => {\n let out: Diagnostic | undefined\n try {\n const live = getContext?.() ?? {}\n const d = buildDiagnostic(code, details, {\n table,\n ...(options.createdAt === undefined ? {} : { createdAt: options.createdAt }),\n trigger: opts.trigger ?? live.trigger ?? 'init',\n run: opts.run ?? live.run ?? 0,\n phase: opts.phase ?? live.phase ?? defaultPhase(),\n ...(live.mode ? { mode: live.mode } : {}),\n ...(live.state ? { state: live.state } : {}),\n now: now(),\n ...(opts.cause === undefined ? {} : { cause: opts.cause }),\n ...(opts.causedBy === undefined ? {} : { causedBy: opts.causedBy }),\n dev,\n redaction: redactor(),\n })\n // Emits made while the chain runs (e.g. DK_INTERCEPTOR_FAILED for an interceptor on\n // diagnostic:emit) skip the chain, so a broken logger cannot recurse.\n if (runtime && depth === 0) {\n const c = d.context\n // ctx.where reaches slots, fingerprints and issue links unredacted: never the row key.\n const at = d.where.kind === 'row' ? { kind: 'row' as const, index: d.where.index } : d.where\n const ctx = createContext('diagnostic:emit', { diagnostic: d }, formatWhere(at), {\n table: options.table,\n run: c.run,\n trigger: c.trigger,\n phase: c.phase,\n emit,\n })\n depth++\n try {\n runtime.run('diagnostic:emit', ctx, (x) => {\n out = deliver(x.input.diagnostic, opts.quiet)\n })\n } finally {\n depth--\n }\n } else {\n out = deliver(d, opts.quiet)\n }\n } catch (e) {\n // strict: an error-level emit nested in the chain (e.g. DK_INTERCEPTOR_FAILED) still throws.\n if (strict && e instanceof DatakableError) throw e\n fail(e)\n return undefined\n }\n if (strict && out?.level === 'error') throw new DatakableError(out)\n return out\n }\n\n if (!dev && resolveRedaction(options.redaction, dev).level === 'full') {\n emit(DK_UNSAFE_REDACTION.code, { level: 'full' })\n }\n\n return {\n table,\n emit,\n getDiagnostics: () => buffer.toArray(),\n attach(rt) {\n runtime = rt\n },\n clear() {\n buffer.clear()\n seen.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/diagnostics/hub.ts"],"sourcesContent":["import { createContext } from '../intercept/context'\nimport type { InterceptorRuntime } from '../intercept/runtime'\nimport { buildDiagnostic, DatakableError, defaultPhase, formatWhere, wallClock } from './build'\nimport type { DiagnosticCode, DiagnosticDetails } from './catalog'\nimport { createConsoleSink } from './console'\nimport './process-guard'\nimport {\n DK_UNSAFE_REDACTION,\n type RedactionOptions,\n type RedactLevel,\n resolveRedaction,\n scrubError,\n} from './redact'\nimport { RingBuffer } from './ring-buffer'\nimport type { Diagnostic, DiagnosticStateSummary, EmitOptions, Phase, Trigger } from './types'\n\n/** Live values the store supplies at emit time (T07+). Missing ones use defaults. */\nexport interface LiveDiagnosticContext {\n trigger?: Trigger\n run?: number\n phase?: Phase\n mode?: Record<string, 'client' | 'server'>\n state?: DiagnosticStateSummary\n}\n\nexport interface DiagnosticsOptions {\n /** `options.name` of the table; default `table#n`. */\n name?: string\n /** The table instance, put on the `diagnostic:emit` ctx. */\n table?: unknown\n /** Dev: the table's creation site. */\n createdAt?: string\n getContext?: () => LiveDiagnosticContext\n /** Dedupe is per data reference: a new reference reports known diagnostics again. */\n getDataRef?: () => unknown\n /** User sink (e.g. Sentry). Throwing is swallowed with one console.error. */\n onDiagnostic?: (d: Diagnostic) => void\n /** Rethrow error-level diagnostics as `DatakableError`. */\n strict?: boolean\n /** Default console sink. Default `true`. */\n console?: boolean\n /** Text builders + dev console format. Default: `process.env.NODE_ENV !== 'production'`. */\n dev?: boolean\n /**\n * Redaction policy (§8.2, `diagnostics.redact`): a level or `{ level, redact(path, value),\n * sensitiveColumns, rowKeySensitive }`. Default `'keys'` in development, `'safe'` in production.\n * Applied while the diagnostic is built, so `diagnostic:emit` interceptors, the `redact` seam and\n * every sink only see redacted data. `'full'` in production warns (`DK_UNSAFE_REDACTION`).\n */\n redaction?: RedactLevel | RedactionOptions\n /**\n * Extra whole-diagnostic rewrite after the `diagnostic:emit` interceptors, before dedupe and\n * sinks. It receives already-redacted data; `fingerprint` is not recomputed.\n */\n redact?: (d: Diagnostic) => Diagnostic\n /** Ring buffer size behind getDiagnostics(). Default 100. */\n bufferSize?: number\n /** Max fingerprints remembered for dedupe. Default 500. */\n dedupeLimit?: number\n /** Console rate limit. Default 20 per 1000 ms. */\n rateLimit?: { max: number; windowMs: number }\n /** Clock (epoch ms) for `occurrences` and the rate limit. Default `wallClock` (never `Date`, #7). */\n now?: () => number\n}\n\nexport interface Diagnostics {\n /** Resolved table name (`context.table`). */\n readonly table: string\n /**\n * Emits a catalog diagnostic through `diagnostic:emit`. Returns the stored diagnostic (the\n * aggregated one on a dedupe hit), or `undefined` if an interceptor dropped it. Never throws,\n * except `DatakableError` under `strict: true`.\n */\n emit<C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n ): Diagnostic | undefined\n /** Oldest first; at most `bufferSize` entries. */\n getDiagnostics(): Diagnostic[]\n /** Runs emits through this runtime's `diagnostic:emit` chain (user interceptors can redact/drop). */\n attach(runtime: InterceptorRuntime): void\n clear(): void\n}\n\nlet tableSeq = 0\n\n/**\n * One per table. Pipeline (§8.2): enrich + redact (build: context, policy redaction, then text,\n * where, fingerprint and docs from the redacted data) → `diagnostic:emit` interceptors → `redact`\n * seam → dedupe (aggregates `occurrences`) → ring buffer → sinks (onDiagnostic, console) → strict.\n */\nexport function createDiagnostics(options: DiagnosticsOptions = {}): Diagnostics {\n const table = options.name ?? `table#${++tableSeq}`\n const dev = options.dev ?? (process.env.NODE_ENV !== \"production\")\n const now = options.now ?? wallClock\n const dedupeLimit = options.dedupeLimit ?? 500\n const buffer = new RingBuffer<Diagnostic>(options.bufferSize ?? 100)\n const seen = new Map<string, Diagnostic>()\n const { onDiagnostic, getContext, getDataRef, redact, strict } = options\n const toConsole =\n options.console === false\n ? undefined\n : createConsoleSink({\n table,\n dev,\n now,\n rateLimit: options.rateLimit ?? { max: 20, windowMs: 1000 },\n })\n\n let runtime: InterceptorRuntime | undefined\n let depth = 0\n let lastRef: unknown\n let failed = false\n\n const fail = (e: unknown) => {\n // The logger never kills the table: one console.error, then silence.\n if (failed) return\n failed = true\n // Production prints the error name only: its message can embed row values (§8.2).\n console.error(\n `[DataKable ${table}] diagnostics pipeline failed; later failures are silent`,\n dev ? e : scrubError(e),\n )\n }\n\n function deliver(input: Diagnostic, quiet?: boolean): Diagnostic {\n const d = redact ? redact(input) : input\n if (getDataRef) {\n const ref = getDataRef()\n if (ref !== lastRef) {\n seen.clear()\n lastRef = ref\n }\n }\n const prev = seen.get(d.fingerprint)\n if (prev) {\n prev.context.occurrences.count++\n prev.context.occurrences.last = d.context.occurrences.last\n return prev\n }\n seen.set(d.fingerprint, d)\n if (seen.size > dedupeLimit) seen.delete(seen.keys().next().value as string)\n buffer.push(d)\n if (onDiagnostic) {\n try {\n onDiagnostic(d)\n } catch (e) {\n fail(e)\n }\n }\n if (!quiet) toConsole?.(d)\n return d\n }\n\n // Resolved per emit: sensitive columns and the rowKey can change with setOptions.\n const redactor = () => {\n const r = resolveRedaction(options.redaction, dev)\n r.onError = fail\n return r\n }\n\n const emit: Diagnostics['emit'] = (code, details, opts = {}) => {\n let out: Diagnostic | undefined\n try {\n const live = getContext?.() ?? {}\n const d = buildDiagnostic(code, details, {\n table,\n ...(options.createdAt === undefined ? {} : { createdAt: options.createdAt }),\n trigger: opts.trigger ?? live.trigger ?? 'init',\n run: opts.run ?? live.run ?? 0,\n phase: opts.phase ?? live.phase ?? defaultPhase(),\n ...(live.mode ? { mode: live.mode } : {}),\n ...(live.state ? { state: live.state } : {}),\n now: now(),\n ...(opts.cause === undefined ? {} : { cause: opts.cause }),\n ...(opts.causedBy === undefined ? {} : { causedBy: opts.causedBy }),\n dev,\n redaction: redactor(),\n })\n // Emits made while the chain runs (e.g. DK_INTERCEPTOR_FAILED for an interceptor on\n // diagnostic:emit) skip the chain, so a broken logger cannot recurse.\n if (runtime && depth === 0) {\n const c = d.context\n // ctx.where reaches slots, fingerprints and issue links unredacted: never the row key.\n const at = d.where.kind === 'row' ? { kind: 'row' as const, index: d.where.index } : d.where\n const ctx = createContext('diagnostic:emit', { diagnostic: d }, formatWhere(at), {\n table: options.table,\n run: c.run,\n trigger: c.trigger,\n phase: c.phase,\n emit,\n })\n depth++\n try {\n runtime.run('diagnostic:emit', ctx, (x) => {\n out = deliver(x.input.diagnostic, opts.quiet)\n })\n } finally {\n depth--\n }\n } else {\n out = deliver(d, opts.quiet)\n }\n } catch (e) {\n // strict: an error-level emit nested in the chain (e.g. DK_INTERCEPTOR_FAILED) still throws.\n if (strict && e instanceof DatakableError) throw e\n fail(e)\n return undefined\n }\n if (strict && out?.level === 'error') throw new DatakableError(out)\n return out\n }\n\n if (!dev && resolveRedaction(options.redaction, dev).level === 'full') {\n emit(DK_UNSAFE_REDACTION.code, { level: 'full' })\n }\n\n return {\n table,\n emit,\n // The original error can quote row values (a sensitive column's accessor throwing\n // `bad ssn 123-…`): it reaches onDiagnostic and strict's DatakableError only, so readers of the\n // buffer get a copy without it. `occurrences` stays shared, so counts still aggregate live.\n getDiagnostics: () =>\n buffer.toArray().map((d) => {\n const { cause, ...context } = d.context\n return cause === undefined\n ? d\n : Object.setPrototypeOf({ ...d, context }, Object.getPrototypeOf(d))\n }),\n attach(rt) {\n runtime = rt\n },\n clear() {\n buffer.clear()\n seen.clear()\n },\n }\n}\n"],"mappings":"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","names":[]}
@@ -22,6 +22,7 @@ var prose_exports = {};
22
22
  __export(prose_exports, {
23
23
  expandDetails: () => expandDetails,
24
24
  expandProse: () => expandProse,
25
+ proseArg: () => proseArg,
25
26
  proseMessages: () => proseMessages
26
27
  });
27
28
  module.exports = __toCommonJS(prose_exports);
@@ -88,6 +89,7 @@ var proseMessages = {
88
89
  "prims-or-dates": "an array of primitives or Dates",
89
90
  "prim-or-date": "a primitive or a Date",
90
91
  "range-bound": "number | bigint or a valid Date bound",
92
+ "not-nan": "a value other than NaN (NaN equals no cell)",
91
93
  "same-type": "min and max of the same type ({0})",
92
94
  // An option given without its wrapper (`DK_INVALID_OPTION` expected).
93
95
  wrapped: "{0}({1}) from '@datakable/core/{2}'",
@@ -97,10 +99,16 @@ var proseMessages = {
97
99
  "unknown-op": 'Unknown operation "{0}": register it with registerOperation()',
98
100
  "boundary-twice": 'A boundary interceptor is already installed ("{0}"); "{1}" was rejected'
99
101
  };
102
+ function proseArg(v) {
103
+ return v.replace(/[%|]/g, encodeURIComponent);
104
+ }
100
105
  function expandProse(v, dev) {
101
106
  const [key, ...args] = v.split("|");
102
107
  const text = (process.env.NODE_ENV !== "production" && dev ? proseMessages[key] : void 0) ?? (0, import_catalog.installedText)(key);
103
- return typeof text === "string" ? text.replace(/\{(\d)\}/g, (_, n) => expandProse(args[n] ?? "", dev)) : v;
108
+ return typeof text === "string" ? text.replace(/\{(\d)\}/g, (_, n) => {
109
+ const a = args[n] ?? "";
110
+ return a.includes("%") ? decodeURIComponent(a) : expandProse(a, dev);
111
+ }) : v;
104
112
  }
105
113
  var FIELDS = ["fallback", "reason", "problem", "expected"];
106
114
  function expandDetails(details, dev) {
@@ -115,6 +123,7 @@ function expandDetails(details, dev) {
115
123
  0 && (module.exports = {
116
124
  expandDetails,
117
125
  expandProse,
126
+ proseArg,
118
127
  proseMessages
119
128
  });
120
129
  //# sourceMappingURL=prose.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/diagnostics/prose.ts"],"sourcesContent":["import { installedText } from './catalog'\nimport './process-guard'\n\n/**\n * Production detail prose (§8.2, §9.3): the sentences some details carry (`fallback`, `reason`,\n * `problem`) are written as short keys at the emit site, e.g. `fallback: 'cells-undefined'`, or\n * `'page-size|10'` with arguments. A diagnostic built in development, or in production with\n * `@datakable/core/messages` installed, reads the full sentence (the key is expanded when it is\n * built); production without the messages keeps the key, so the bundle carries no prose. A value\n * that is not a known key (a plugin's own `registerFallback` text) is kept as written.\n *\n * `{0}`, `{1}` are the arguments after the key, separated by `|`.\n */\nexport const proseMessages: Readonly<Record<string, string>> = {\n // Cell slots (`DK_CALLBACK_FAILED` / `DK_CALLBACK_BAD_RETURN` fallback).\n 'cells-coerced': 'those cells were coerced with the default formatter',\n 'cells-warn': 'those cells rendered ⚠', // `<DataTable />` (@datakable/react)\n 'cells-undefined': 'the failed cells read as undefined',\n 'cells-empty': \"the failed cells render ''\",\n 'rows-unmatched': 'the failed rows are treated as non-matching',\n 'rows-last': 'the failed rows sort as missing values (last)',\n 'cell-undefined': 'the cell reads as undefined',\n 'row-key-index': \"those rows use the fallback key '#<index>'\",\n 'sorted-native': 'the rows were sorted with the native algorithm',\n 'sorted-default': 'the column was sorted with the default comparator',\n // Operation fallbacks (`intercept/fallbacks.ts`, the boundary's reports).\n 'init-continued': 'table creation continued without the failed step',\n 'options-kept': 'the previous options were kept',\n 'destroy-continued': 'teardown continued; the table is destroyed',\n 'update-dropped': 'the update was dropped (state unchanged)',\n 'data-kept': 'the previous data was kept',\n 'row-model-kept': 'the last good row model was kept',\n 'step-passthrough': 'the step passed its input through',\n 'batch-fallback':\n \"the column's cells use the slot fallback ('' for format, undefined for accessor, \" +\n 'non-matching for filterFn, missing (sorted last) for sortBy)',\n 'results-kept': \"the previous results were kept (status: 'error')\",\n 'diagnostic-dropped': 'the diagnostic was dropped (the logger never kills the table)',\n 'returned-undefined': 'the operation returned undefined',\n // Features.\n 'slice-undefined': 'the slice starts as undefined',\n 'api-not-installed': 'its API was not installed',\n 'call-ignored': 'the call was ignored',\n 'api-missing': 'table.{0}() did nothing (the API of that feature was not installed)',\n 'apis-missing': 'table.{0}() did nothing (the API of those features was not installed)',\n 'page-reset': 'the pagination state was reset to its initial value',\n 'page-index-0': 'pageIndex was replaced by 0',\n 'page-size': 'pageSize was replaced by {0}',\n 'validity-ignored': 'the call was ignored; the validity filter is unchanged',\n 'validity-all': \"the table shows every row, as with 'all'\",\n 'validity-needs-flag':\n 'validity filters rows flagged by schema validation, which needs `schema` and ' +\n \"`invalidRows: 'flag'` (without a schema no row is invalid; with 'drop' invalid rows \" +\n \"are removed, with 'throw' they throw)\",\n 'not-searchable': 'no column is searchable: give the columns to search a `search` weight',\n collation: 'the runtime\\'s collation for \"{0}\"',\n 'not-algorithm':\n '{0} is not a sort algorithm (accepted: auto, tim, radix, counting, native, or a function); ' +\n 'auto was used',\n 'exsorted-failed': 'exsorted {0}Sort could not sort these keys; auto was used',\n 'exsorted-range':\n 'exsorted {0}Sort could not sort these keys (key range above 1,000,000); auto was used',\n 'needs-int-keys':\n '{0} sorts a single column of safe integer keys (numbers or Dates, no sortFn); auto was used',\n // Sorting reasons (DK_SORT_STRATEGY, `debug: true`).\n 'custom-threw': 'the custom algorithm threw',\n 'custom-bad': 'the custom algorithm returned a bad value',\n 'not-safe-int': 'keys are not all safe integers',\n ranks: '{0}, strings as collation ranks',\n 'int-keys': '{0}, safe-integer keys',\n 'auto-nearly': 'auto: safe integers, nearly sorted ({0} descents)',\n 'auto-range': 'auto: safe integers, key range {0} ≤ 4n',\n 'auto-wide': 'auto: safe integers, wide range, n ≥ 10k',\n 'auto-wide-small': 'auto: safe integers, wide range, n < 10k',\n 'auto-ranked-nearly':\n 'auto: collation ranks of a string column, safe integers, nearly sorted ({0} descents)',\n 'auto-ranked-range':\n 'auto: collation ranks of a string column, safe integers, key range {0} ≤ 4n',\n 'auto-ranked-wide':\n 'auto: collation ranks of a string column, safe integers, wide range, n ≥ 10k',\n 'auto-ranked-wide-small':\n 'auto: collation ranks of a string column, safe integers, wide range, n < 10k',\n // Filter values (`DK_FILTER_TYPE_MISMATCH` expected).\n 'prims-or-dates': 'an array of primitives or Dates',\n 'prim-or-date': 'a primitive or a Date',\n 'range-bound': 'number | bigint or a valid Date bound',\n 'same-type': 'min and max of the same type ({0})',\n // An option given without its wrapper (`DK_INVALID_OPTION` expected).\n wrapped: \"{0}({1}) from '@datakable/core/{2}'\",\n // DK_INTERNAL problems.\n escaped: 'an exception escaped DataKable code',\n 'unknown-code': 'unknown diagnostic code \"{0}\": define it with defineDiagnostic() first',\n 'unknown-op': 'Unknown operation \"{0}\": register it with registerOperation()',\n 'boundary-twice': 'A boundary interceptor is already installed (\"{0}\"); \"{1}\" was rejected',\n}\n\n/**\n * The sentence of a prose key (`'page-size|10'` → `'pageSize was replaced by 10'`): from\n * {@link proseMessages} in development (`dev`), else from the installed messages; `v` itself when\n * neither has the key.\n */\nexport function expandProse(v: string, dev: boolean): string {\n const [key, ...args] = v.split('|') as [string, ...string[]]\n const text = ((process.env.NODE_ENV !== \"production\") && dev ? proseMessages[key] : undefined) ?? installedText(key)\n // An argument may be a key itself (`'ranks|custom-threw'`); a missing one reads as ''. (`n` is\n // the digit string: `args['0']` indexes like `args[0]`.)\n return typeof text === 'string'\n ? text.replace(/\\{(\\d)\\}/g, (_, n: number) => expandProse(args[n] ?? '', dev))\n : v\n}\n\n/** The detail fields that may hold a prose key. */\nconst FIELDS = ['fallback', 'reason', 'problem', 'expected'] as const\n\n/** A copy of `details` with its prose keys expanded. */\nexport function expandDetails(\n details: Record<string, unknown>,\n dev: boolean,\n): Record<string, unknown> {\n const out = { ...details }\n for (const f of FIELDS) {\n const v = details[f]\n if (typeof v === 'string') out[f] = expandProse(v, dev)\n }\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA8B;AAC9B,2BAAO;AAYA,IAAM,gBAAkD;AAAA;AAAA,EAE7D,iBAAiB;AAAA,EACjB,cAAc;AAAA;AAAA,EACd,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,kBACE;AAAA,EAEF,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,uBACE;AAAA,EAGF,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,iBACE;AAAA,EAEF,mBAAmB;AAAA,EACnB,kBACE;AAAA,EACF,kBACE;AAAA;AAAA,EAEF,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,cAAc;AAAA,EACd,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,sBACE;AAAA,EACF,qBACE;AAAA,EACF,oBACE;AAAA,EACF,0BACE;AAAA;AAAA,EAEF,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,aAAa;AAAA;AAAA,EAEb,SAAS;AAAA;AAAA,EAET,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,kBAAkB;AACpB;AAOO,SAAS,YAAY,GAAW,KAAsB;AAC3D,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI,EAAE,MAAM,GAAG;AAClC,QAAM,QAAS,QAAQ,IAAI,aAAa,gBAAiB,MAAM,cAAc,GAAG,IAAI,eAAc,8BAAc,GAAG;AAGnH,SAAO,OAAO,SAAS,WACnB,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc,YAAY,KAAK,CAAC,KAAK,IAAI,GAAG,CAAC,IAC3E;AACN;AAGA,IAAM,SAAS,CAAC,YAAY,UAAU,WAAW,UAAU;AAGpD,SAAS,cACd,SACA,KACyB;AACzB,QAAM,MAAM,EAAE,GAAG,QAAQ;AACzB,aAAW,KAAK,QAAQ;AACtB,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,OAAO,MAAM,SAAU,KAAI,CAAC,IAAI,YAAY,GAAG,GAAG;AAAA,EACxD;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/diagnostics/prose.ts"],"sourcesContent":["import { installedText } from './catalog'\nimport './process-guard'\n\n/**\n * Production detail prose (§8.2, §9.3): the sentences some details carry (`fallback`, `reason`,\n * `problem`) are written as short keys at the emit site, e.g. `fallback: 'cells-undefined'`, or\n * `'page-size|10'` with arguments. A diagnostic built in development, or in production with\n * `@datakable/core/messages` installed, reads the full sentence (the key is expanded when it is\n * built); production without the messages keeps the key, so the bundle carries no prose. A value\n * that is not a known key (a plugin's own `registerFallback` text) is kept as written.\n *\n * `{0}`, `{1}` are the arguments after the key, separated by `|`. A value the library does not\n * write itself (a user's option, a column id, a plugin's name) is packed with {@link proseArg}, so\n * a `|` inside it does not split it (#92).\n */\nexport const proseMessages: Readonly<Record<string, string>> = {\n // Cell slots (`DK_CALLBACK_FAILED` / `DK_CALLBACK_BAD_RETURN` fallback).\n 'cells-coerced': 'those cells were coerced with the default formatter',\n 'cells-warn': 'those cells rendered ⚠', // `<DataTable />` (@datakable/react)\n 'cells-undefined': 'the failed cells read as undefined',\n 'cells-empty': \"the failed cells render ''\",\n 'rows-unmatched': 'the failed rows are treated as non-matching',\n 'rows-last': 'the failed rows sort as missing values (last)',\n 'cell-undefined': 'the cell reads as undefined',\n 'row-key-index': \"those rows use the fallback key '#<index>'\",\n 'sorted-native': 'the rows were sorted with the native algorithm',\n 'sorted-default': 'the column was sorted with the default comparator',\n // Operation fallbacks (`intercept/fallbacks.ts`, the boundary's reports).\n 'init-continued': 'table creation continued without the failed step',\n 'options-kept': 'the previous options were kept',\n 'destroy-continued': 'teardown continued; the table is destroyed',\n 'update-dropped': 'the update was dropped (state unchanged)',\n 'data-kept': 'the previous data was kept',\n 'row-model-kept': 'the last good row model was kept',\n 'step-passthrough': 'the step passed its input through',\n 'batch-fallback':\n \"the column's cells use the slot fallback ('' for format, undefined for accessor, \" +\n 'non-matching for filterFn, missing (sorted last) for sortBy)',\n 'results-kept': \"the previous results were kept (status: 'error')\",\n 'diagnostic-dropped': 'the diagnostic was dropped (the logger never kills the table)',\n 'returned-undefined': 'the operation returned undefined',\n // Features.\n 'slice-undefined': 'the slice starts as undefined',\n 'api-not-installed': 'its API was not installed',\n 'call-ignored': 'the call was ignored',\n 'api-missing': 'table.{0}() did nothing (the API of that feature was not installed)',\n 'apis-missing': 'table.{0}() did nothing (the API of those features was not installed)',\n 'page-reset': 'the pagination state was reset to its initial value',\n 'page-index-0': 'pageIndex was replaced by 0',\n 'page-size': 'pageSize was replaced by {0}',\n 'validity-ignored': 'the call was ignored; the validity filter is unchanged',\n 'validity-all': \"the table shows every row, as with 'all'\",\n 'validity-needs-flag':\n 'validity filters rows flagged by schema validation, which needs `schema` and ' +\n \"`invalidRows: 'flag'` (without a schema no row is invalid; with 'drop' invalid rows \" +\n \"are removed, with 'throw' they throw)\",\n 'not-searchable': 'no column is searchable: give the columns to search a `search` weight',\n collation: 'the runtime\\'s collation for \"{0}\"',\n 'not-algorithm':\n '{0} is not a sort algorithm (accepted: auto, tim, radix, counting, native, or a function); ' +\n 'auto was used',\n 'exsorted-failed': 'exsorted {0}Sort could not sort these keys; auto was used',\n 'exsorted-range':\n 'exsorted {0}Sort could not sort these keys (key range above 1,000,000); auto was used',\n 'needs-int-keys':\n '{0} sorts a single column of safe integer keys (numbers or Dates, no sortFn); auto was used',\n // Sorting reasons (DK_SORT_STRATEGY, `debug: true`).\n 'custom-threw': 'the custom algorithm threw',\n 'custom-bad': 'the custom algorithm returned a bad value',\n 'not-safe-int': 'keys are not all safe integers',\n ranks: '{0}, strings as collation ranks',\n 'int-keys': '{0}, safe-integer keys',\n 'auto-nearly': 'auto: safe integers, nearly sorted ({0} descents)',\n 'auto-range': 'auto: safe integers, key range {0} ≤ 4n',\n 'auto-wide': 'auto: safe integers, wide range, n ≥ 10k',\n 'auto-wide-small': 'auto: safe integers, wide range, n < 10k',\n 'auto-ranked-nearly':\n 'auto: collation ranks of a string column, safe integers, nearly sorted ({0} descents)',\n 'auto-ranked-range':\n 'auto: collation ranks of a string column, safe integers, key range {0} ≤ 4n',\n 'auto-ranked-wide':\n 'auto: collation ranks of a string column, safe integers, wide range, n ≥ 10k',\n 'auto-ranked-wide-small':\n 'auto: collation ranks of a string column, safe integers, wide range, n < 10k',\n // Filter values (`DK_FILTER_TYPE_MISMATCH` expected).\n 'prims-or-dates': 'an array of primitives or Dates',\n 'prim-or-date': 'a primitive or a Date',\n 'range-bound': 'number | bigint or a valid Date bound',\n 'not-nan': 'a value other than NaN (NaN equals no cell)',\n 'same-type': 'min and max of the same type ({0})',\n // An option given without its wrapper (`DK_INVALID_OPTION` expected).\n wrapped: \"{0}({1}) from '@datakable/core/{2}'\",\n // DK_INTERNAL problems.\n escaped: 'an exception escaped DataKable code',\n 'unknown-code': 'unknown diagnostic code \"{0}\": define it with defineDiagnostic() first',\n 'unknown-op': 'Unknown operation \"{0}\": register it with registerOperation()',\n 'boundary-twice': 'A boundary interceptor is already installed (\"{0}\"); \"{1}\" was rejected',\n}\n\n/**\n * A user value as one prose argument (`` `unknown-op|${proseArg(op)}` ``): `%` and `|` are\n * escaped (`%25`, `%7C`), so a value holding them is read back whole, not split or expanded.\n */\nexport function proseArg(v: string): string {\n return v.replace(/[%|]/g, encodeURIComponent)\n}\n\n/**\n * The sentence of a prose key (`'page-size|10'` → `'pageSize was replaced by 10'`): from\n * {@link proseMessages} in development (`dev`), else from the installed messages; `v` itself when\n * neither has the key.\n */\nexport function expandProse(v: string, dev: boolean): string {\n const [key, ...args] = v.split('|') as [string, ...string[]]\n const text = ((process.env.NODE_ENV !== \"production\") && dev ? proseMessages[key] : undefined) ?? installedText(key)\n // An argument may be a key itself (`'ranks|custom-threw'`); a missing one reads as ''. An\n // escaped one ({@link proseArg}) is a value. (`n` is the digit string: `args['0']` indexes like\n // `args[0]`.)\n return typeof text === 'string'\n ? text.replace(/\\{(\\d)\\}/g, (_, n: number) => {\n const a = args[n] ?? ''\n return a.includes('%') ? decodeURIComponent(a) : expandProse(a, dev)\n })\n : v\n}\n\n/** The detail fields that may hold a prose key. */\nconst FIELDS = ['fallback', 'reason', 'problem', 'expected'] as const\n\n/** A copy of `details` with its prose keys expanded. */\nexport function expandDetails(\n details: Record<string, unknown>,\n dev: boolean,\n): Record<string, unknown> {\n const out = { ...details }\n for (const f of FIELDS) {\n const v = details[f]\n if (typeof v === 'string') out[f] = expandProse(v, dev)\n }\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA8B;AAC9B,2BAAO;AAcA,IAAM,gBAAkD;AAAA;AAAA,EAE7D,iBAAiB;AAAA,EACjB,cAAc;AAAA;AAAA,EACd,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,kBACE;AAAA,EAEF,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,uBACE;AAAA,EAGF,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,iBACE;AAAA,EAEF,mBAAmB;AAAA,EACnB,kBACE;AAAA,EACF,kBACE;AAAA;AAAA,EAEF,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,cAAc;AAAA,EACd,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,sBACE;AAAA,EACF,qBACE;AAAA,EACF,oBACE;AAAA,EACF,0BACE;AAAA;AAAA,EAEF,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,aAAa;AAAA;AAAA,EAEb,SAAS;AAAA;AAAA,EAET,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,kBAAkB;AACpB;AAMO,SAAS,SAAS,GAAmB;AAC1C,SAAO,EAAE,QAAQ,SAAS,kBAAkB;AAC9C;AAOO,SAAS,YAAY,GAAW,KAAsB;AAC3D,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI,EAAE,MAAM,GAAG;AAClC,QAAM,QAAS,QAAQ,IAAI,aAAa,gBAAiB,MAAM,cAAc,GAAG,IAAI,eAAc,8BAAc,GAAG;AAInH,SAAO,OAAO,SAAS,WACnB,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc;AAC1C,UAAM,IAAI,KAAK,CAAC,KAAK;AACrB,WAAO,EAAE,SAAS,GAAG,IAAI,mBAAmB,CAAC,IAAI,YAAY,GAAG,GAAG;AAAA,EACrE,CAAC,IACD;AACN;AAGA,IAAM,SAAS,CAAC,YAAY,UAAU,WAAW,UAAU;AAGpD,SAAS,cACd,SACA,KACyB;AACzB,QAAM,MAAM,EAAE,GAAG,QAAQ;AACzB,aAAW,KAAK,QAAQ;AACtB,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,OAAO,MAAM,SAAU,KAAI,CAAC,IAAI,YAAY,GAAG,GAAG;AAAA,EACxD;AACA,SAAO;AACT;","names":[]}
@@ -62,6 +62,7 @@ var proseMessages = {
62
62
  "prims-or-dates": "an array of primitives or Dates",
63
63
  "prim-or-date": "a primitive or a Date",
64
64
  "range-bound": "number | bigint or a valid Date bound",
65
+ "not-nan": "a value other than NaN (NaN equals no cell)",
65
66
  "same-type": "min and max of the same type ({0})",
66
67
  // An option given without its wrapper (`DK_INVALID_OPTION` expected).
67
68
  wrapped: "{0}({1}) from '@datakable/core/{2}'",
@@ -71,10 +72,16 @@ var proseMessages = {
71
72
  "unknown-op": 'Unknown operation "{0}": register it with registerOperation()',
72
73
  "boundary-twice": 'A boundary interceptor is already installed ("{0}"); "{1}" was rejected'
73
74
  };
75
+ function proseArg(v) {
76
+ return v.replace(/[%|]/g, encodeURIComponent);
77
+ }
74
78
  function expandProse(v, dev) {
75
79
  const [key, ...args] = v.split("|");
76
80
  const text = (process.env.NODE_ENV !== "production" && dev ? proseMessages[key] : void 0) ?? installedText(key);
77
- return typeof text === "string" ? text.replace(/\{(\d)\}/g, (_, n) => expandProse(args[n] ?? "", dev)) : v;
81
+ return typeof text === "string" ? text.replace(/\{(\d)\}/g, (_, n) => {
82
+ const a = args[n] ?? "";
83
+ return a.includes("%") ? decodeURIComponent(a) : expandProse(a, dev);
84
+ }) : v;
78
85
  }
79
86
  var FIELDS = ["fallback", "reason", "problem", "expected"];
80
87
  function expandDetails(details, dev) {
@@ -88,6 +95,7 @@ function expandDetails(details, dev) {
88
95
  export {
89
96
  expandDetails,
90
97
  expandProse,
98
+ proseArg,
91
99
  proseMessages
92
100
  };
93
101
  //# sourceMappingURL=prose.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/diagnostics/prose.ts"],"sourcesContent":["import { installedText } from './catalog'\nimport './process-guard'\n\n/**\n * Production detail prose (§8.2, §9.3): the sentences some details carry (`fallback`, `reason`,\n * `problem`) are written as short keys at the emit site, e.g. `fallback: 'cells-undefined'`, or\n * `'page-size|10'` with arguments. A diagnostic built in development, or in production with\n * `@datakable/core/messages` installed, reads the full sentence (the key is expanded when it is\n * built); production without the messages keeps the key, so the bundle carries no prose. A value\n * that is not a known key (a plugin's own `registerFallback` text) is kept as written.\n *\n * `{0}`, `{1}` are the arguments after the key, separated by `|`.\n */\nexport const proseMessages: Readonly<Record<string, string>> = {\n // Cell slots (`DK_CALLBACK_FAILED` / `DK_CALLBACK_BAD_RETURN` fallback).\n 'cells-coerced': 'those cells were coerced with the default formatter',\n 'cells-warn': 'those cells rendered ⚠', // `<DataTable />` (@datakable/react)\n 'cells-undefined': 'the failed cells read as undefined',\n 'cells-empty': \"the failed cells render ''\",\n 'rows-unmatched': 'the failed rows are treated as non-matching',\n 'rows-last': 'the failed rows sort as missing values (last)',\n 'cell-undefined': 'the cell reads as undefined',\n 'row-key-index': \"those rows use the fallback key '#<index>'\",\n 'sorted-native': 'the rows were sorted with the native algorithm',\n 'sorted-default': 'the column was sorted with the default comparator',\n // Operation fallbacks (`intercept/fallbacks.ts`, the boundary's reports).\n 'init-continued': 'table creation continued without the failed step',\n 'options-kept': 'the previous options were kept',\n 'destroy-continued': 'teardown continued; the table is destroyed',\n 'update-dropped': 'the update was dropped (state unchanged)',\n 'data-kept': 'the previous data was kept',\n 'row-model-kept': 'the last good row model was kept',\n 'step-passthrough': 'the step passed its input through',\n 'batch-fallback':\n \"the column's cells use the slot fallback ('' for format, undefined for accessor, \" +\n 'non-matching for filterFn, missing (sorted last) for sortBy)',\n 'results-kept': \"the previous results were kept (status: 'error')\",\n 'diagnostic-dropped': 'the diagnostic was dropped (the logger never kills the table)',\n 'returned-undefined': 'the operation returned undefined',\n // Features.\n 'slice-undefined': 'the slice starts as undefined',\n 'api-not-installed': 'its API was not installed',\n 'call-ignored': 'the call was ignored',\n 'api-missing': 'table.{0}() did nothing (the API of that feature was not installed)',\n 'apis-missing': 'table.{0}() did nothing (the API of those features was not installed)',\n 'page-reset': 'the pagination state was reset to its initial value',\n 'page-index-0': 'pageIndex was replaced by 0',\n 'page-size': 'pageSize was replaced by {0}',\n 'validity-ignored': 'the call was ignored; the validity filter is unchanged',\n 'validity-all': \"the table shows every row, as with 'all'\",\n 'validity-needs-flag':\n 'validity filters rows flagged by schema validation, which needs `schema` and ' +\n \"`invalidRows: 'flag'` (without a schema no row is invalid; with 'drop' invalid rows \" +\n \"are removed, with 'throw' they throw)\",\n 'not-searchable': 'no column is searchable: give the columns to search a `search` weight',\n collation: 'the runtime\\'s collation for \"{0}\"',\n 'not-algorithm':\n '{0} is not a sort algorithm (accepted: auto, tim, radix, counting, native, or a function); ' +\n 'auto was used',\n 'exsorted-failed': 'exsorted {0}Sort could not sort these keys; auto was used',\n 'exsorted-range':\n 'exsorted {0}Sort could not sort these keys (key range above 1,000,000); auto was used',\n 'needs-int-keys':\n '{0} sorts a single column of safe integer keys (numbers or Dates, no sortFn); auto was used',\n // Sorting reasons (DK_SORT_STRATEGY, `debug: true`).\n 'custom-threw': 'the custom algorithm threw',\n 'custom-bad': 'the custom algorithm returned a bad value',\n 'not-safe-int': 'keys are not all safe integers',\n ranks: '{0}, strings as collation ranks',\n 'int-keys': '{0}, safe-integer keys',\n 'auto-nearly': 'auto: safe integers, nearly sorted ({0} descents)',\n 'auto-range': 'auto: safe integers, key range {0} ≤ 4n',\n 'auto-wide': 'auto: safe integers, wide range, n ≥ 10k',\n 'auto-wide-small': 'auto: safe integers, wide range, n < 10k',\n 'auto-ranked-nearly':\n 'auto: collation ranks of a string column, safe integers, nearly sorted ({0} descents)',\n 'auto-ranked-range':\n 'auto: collation ranks of a string column, safe integers, key range {0} ≤ 4n',\n 'auto-ranked-wide':\n 'auto: collation ranks of a string column, safe integers, wide range, n ≥ 10k',\n 'auto-ranked-wide-small':\n 'auto: collation ranks of a string column, safe integers, wide range, n < 10k',\n // Filter values (`DK_FILTER_TYPE_MISMATCH` expected).\n 'prims-or-dates': 'an array of primitives or Dates',\n 'prim-or-date': 'a primitive or a Date',\n 'range-bound': 'number | bigint or a valid Date bound',\n 'same-type': 'min and max of the same type ({0})',\n // An option given without its wrapper (`DK_INVALID_OPTION` expected).\n wrapped: \"{0}({1}) from '@datakable/core/{2}'\",\n // DK_INTERNAL problems.\n escaped: 'an exception escaped DataKable code',\n 'unknown-code': 'unknown diagnostic code \"{0}\": define it with defineDiagnostic() first',\n 'unknown-op': 'Unknown operation \"{0}\": register it with registerOperation()',\n 'boundary-twice': 'A boundary interceptor is already installed (\"{0}\"); \"{1}\" was rejected',\n}\n\n/**\n * The sentence of a prose key (`'page-size|10'` → `'pageSize was replaced by 10'`): from\n * {@link proseMessages} in development (`dev`), else from the installed messages; `v` itself when\n * neither has the key.\n */\nexport function expandProse(v: string, dev: boolean): string {\n const [key, ...args] = v.split('|') as [string, ...string[]]\n const text = ((process.env.NODE_ENV !== \"production\") && dev ? proseMessages[key] : undefined) ?? installedText(key)\n // An argument may be a key itself (`'ranks|custom-threw'`); a missing one reads as ''. (`n` is\n // the digit string: `args['0']` indexes like `args[0]`.)\n return typeof text === 'string'\n ? text.replace(/\\{(\\d)\\}/g, (_, n: number) => expandProse(args[n] ?? '', dev))\n : v\n}\n\n/** The detail fields that may hold a prose key. */\nconst FIELDS = ['fallback', 'reason', 'problem', 'expected'] as const\n\n/** A copy of `details` with its prose keys expanded. */\nexport function expandDetails(\n details: Record<string, unknown>,\n dev: boolean,\n): Record<string, unknown> {\n const out = { ...details }\n for (const f of FIELDS) {\n const v = details[f]\n if (typeof v === 'string') out[f] = expandProse(v, dev)\n }\n return out\n}\n"],"mappings":";AAAA,SAAS,qBAAqB;AAC9B,OAAO;AAYA,IAAM,gBAAkD;AAAA;AAAA,EAE7D,iBAAiB;AAAA,EACjB,cAAc;AAAA;AAAA,EACd,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,kBACE;AAAA,EAEF,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,uBACE;AAAA,EAGF,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,iBACE;AAAA,EAEF,mBAAmB;AAAA,EACnB,kBACE;AAAA,EACF,kBACE;AAAA;AAAA,EAEF,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,cAAc;AAAA,EACd,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,sBACE;AAAA,EACF,qBACE;AAAA,EACF,oBACE;AAAA,EACF,0BACE;AAAA;AAAA,EAEF,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,aAAa;AAAA;AAAA,EAEb,SAAS;AAAA;AAAA,EAET,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,kBAAkB;AACpB;AAOO,SAAS,YAAY,GAAW,KAAsB;AAC3D,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI,EAAE,MAAM,GAAG;AAClC,QAAM,QAAS,QAAQ,IAAI,aAAa,gBAAiB,MAAM,cAAc,GAAG,IAAI,WAAc,cAAc,GAAG;AAGnH,SAAO,OAAO,SAAS,WACnB,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc,YAAY,KAAK,CAAC,KAAK,IAAI,GAAG,CAAC,IAC3E;AACN;AAGA,IAAM,SAAS,CAAC,YAAY,UAAU,WAAW,UAAU;AAGpD,SAAS,cACd,SACA,KACyB;AACzB,QAAM,MAAM,EAAE,GAAG,QAAQ;AACzB,aAAW,KAAK,QAAQ;AACtB,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,OAAO,MAAM,SAAU,KAAI,CAAC,IAAI,YAAY,GAAG,GAAG;AAAA,EACxD;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/diagnostics/prose.ts"],"sourcesContent":["import { installedText } from './catalog'\nimport './process-guard'\n\n/**\n * Production detail prose (§8.2, §9.3): the sentences some details carry (`fallback`, `reason`,\n * `problem`) are written as short keys at the emit site, e.g. `fallback: 'cells-undefined'`, or\n * `'page-size|10'` with arguments. A diagnostic built in development, or in production with\n * `@datakable/core/messages` installed, reads the full sentence (the key is expanded when it is\n * built); production without the messages keeps the key, so the bundle carries no prose. A value\n * that is not a known key (a plugin's own `registerFallback` text) is kept as written.\n *\n * `{0}`, `{1}` are the arguments after the key, separated by `|`. A value the library does not\n * write itself (a user's option, a column id, a plugin's name) is packed with {@link proseArg}, so\n * a `|` inside it does not split it (#92).\n */\nexport const proseMessages: Readonly<Record<string, string>> = {\n // Cell slots (`DK_CALLBACK_FAILED` / `DK_CALLBACK_BAD_RETURN` fallback).\n 'cells-coerced': 'those cells were coerced with the default formatter',\n 'cells-warn': 'those cells rendered ⚠', // `<DataTable />` (@datakable/react)\n 'cells-undefined': 'the failed cells read as undefined',\n 'cells-empty': \"the failed cells render ''\",\n 'rows-unmatched': 'the failed rows are treated as non-matching',\n 'rows-last': 'the failed rows sort as missing values (last)',\n 'cell-undefined': 'the cell reads as undefined',\n 'row-key-index': \"those rows use the fallback key '#<index>'\",\n 'sorted-native': 'the rows were sorted with the native algorithm',\n 'sorted-default': 'the column was sorted with the default comparator',\n // Operation fallbacks (`intercept/fallbacks.ts`, the boundary's reports).\n 'init-continued': 'table creation continued without the failed step',\n 'options-kept': 'the previous options were kept',\n 'destroy-continued': 'teardown continued; the table is destroyed',\n 'update-dropped': 'the update was dropped (state unchanged)',\n 'data-kept': 'the previous data was kept',\n 'row-model-kept': 'the last good row model was kept',\n 'step-passthrough': 'the step passed its input through',\n 'batch-fallback':\n \"the column's cells use the slot fallback ('' for format, undefined for accessor, \" +\n 'non-matching for filterFn, missing (sorted last) for sortBy)',\n 'results-kept': \"the previous results were kept (status: 'error')\",\n 'diagnostic-dropped': 'the diagnostic was dropped (the logger never kills the table)',\n 'returned-undefined': 'the operation returned undefined',\n // Features.\n 'slice-undefined': 'the slice starts as undefined',\n 'api-not-installed': 'its API was not installed',\n 'call-ignored': 'the call was ignored',\n 'api-missing': 'table.{0}() did nothing (the API of that feature was not installed)',\n 'apis-missing': 'table.{0}() did nothing (the API of those features was not installed)',\n 'page-reset': 'the pagination state was reset to its initial value',\n 'page-index-0': 'pageIndex was replaced by 0',\n 'page-size': 'pageSize was replaced by {0}',\n 'validity-ignored': 'the call was ignored; the validity filter is unchanged',\n 'validity-all': \"the table shows every row, as with 'all'\",\n 'validity-needs-flag':\n 'validity filters rows flagged by schema validation, which needs `schema` and ' +\n \"`invalidRows: 'flag'` (without a schema no row is invalid; with 'drop' invalid rows \" +\n \"are removed, with 'throw' they throw)\",\n 'not-searchable': 'no column is searchable: give the columns to search a `search` weight',\n collation: 'the runtime\\'s collation for \"{0}\"',\n 'not-algorithm':\n '{0} is not a sort algorithm (accepted: auto, tim, radix, counting, native, or a function); ' +\n 'auto was used',\n 'exsorted-failed': 'exsorted {0}Sort could not sort these keys; auto was used',\n 'exsorted-range':\n 'exsorted {0}Sort could not sort these keys (key range above 1,000,000); auto was used',\n 'needs-int-keys':\n '{0} sorts a single column of safe integer keys (numbers or Dates, no sortFn); auto was used',\n // Sorting reasons (DK_SORT_STRATEGY, `debug: true`).\n 'custom-threw': 'the custom algorithm threw',\n 'custom-bad': 'the custom algorithm returned a bad value',\n 'not-safe-int': 'keys are not all safe integers',\n ranks: '{0}, strings as collation ranks',\n 'int-keys': '{0}, safe-integer keys',\n 'auto-nearly': 'auto: safe integers, nearly sorted ({0} descents)',\n 'auto-range': 'auto: safe integers, key range {0} ≤ 4n',\n 'auto-wide': 'auto: safe integers, wide range, n ≥ 10k',\n 'auto-wide-small': 'auto: safe integers, wide range, n < 10k',\n 'auto-ranked-nearly':\n 'auto: collation ranks of a string column, safe integers, nearly sorted ({0} descents)',\n 'auto-ranked-range':\n 'auto: collation ranks of a string column, safe integers, key range {0} ≤ 4n',\n 'auto-ranked-wide':\n 'auto: collation ranks of a string column, safe integers, wide range, n ≥ 10k',\n 'auto-ranked-wide-small':\n 'auto: collation ranks of a string column, safe integers, wide range, n < 10k',\n // Filter values (`DK_FILTER_TYPE_MISMATCH` expected).\n 'prims-or-dates': 'an array of primitives or Dates',\n 'prim-or-date': 'a primitive or a Date',\n 'range-bound': 'number | bigint or a valid Date bound',\n 'not-nan': 'a value other than NaN (NaN equals no cell)',\n 'same-type': 'min and max of the same type ({0})',\n // An option given without its wrapper (`DK_INVALID_OPTION` expected).\n wrapped: \"{0}({1}) from '@datakable/core/{2}'\",\n // DK_INTERNAL problems.\n escaped: 'an exception escaped DataKable code',\n 'unknown-code': 'unknown diagnostic code \"{0}\": define it with defineDiagnostic() first',\n 'unknown-op': 'Unknown operation \"{0}\": register it with registerOperation()',\n 'boundary-twice': 'A boundary interceptor is already installed (\"{0}\"); \"{1}\" was rejected',\n}\n\n/**\n * A user value as one prose argument (`` `unknown-op|${proseArg(op)}` ``): `%` and `|` are\n * escaped (`%25`, `%7C`), so a value holding them is read back whole, not split or expanded.\n */\nexport function proseArg(v: string): string {\n return v.replace(/[%|]/g, encodeURIComponent)\n}\n\n/**\n * The sentence of a prose key (`'page-size|10'` → `'pageSize was replaced by 10'`): from\n * {@link proseMessages} in development (`dev`), else from the installed messages; `v` itself when\n * neither has the key.\n */\nexport function expandProse(v: string, dev: boolean): string {\n const [key, ...args] = v.split('|') as [string, ...string[]]\n const text = ((process.env.NODE_ENV !== \"production\") && dev ? proseMessages[key] : undefined) ?? installedText(key)\n // An argument may be a key itself (`'ranks|custom-threw'`); a missing one reads as ''. An\n // escaped one ({@link proseArg}) is a value. (`n` is the digit string: `args['0']` indexes like\n // `args[0]`.)\n return typeof text === 'string'\n ? text.replace(/\\{(\\d)\\}/g, (_, n: number) => {\n const a = args[n] ?? ''\n return a.includes('%') ? decodeURIComponent(a) : expandProse(a, dev)\n })\n : v\n}\n\n/** The detail fields that may hold a prose key. */\nconst FIELDS = ['fallback', 'reason', 'problem', 'expected'] as const\n\n/** A copy of `details` with its prose keys expanded. */\nexport function expandDetails(\n details: Record<string, unknown>,\n dev: boolean,\n): Record<string, unknown> {\n const out = { ...details }\n for (const f of FIELDS) {\n const v = details[f]\n if (typeof v === 'string') out[f] = expandProse(v, dev)\n }\n return out\n}\n"],"mappings":";AAAA,SAAS,qBAAqB;AAC9B,OAAO;AAcA,IAAM,gBAAkD;AAAA;AAAA,EAE7D,iBAAiB;AAAA,EACjB,cAAc;AAAA;AAAA,EACd,mBAAmB;AAAA,EACnB,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,kBACE;AAAA,EAEF,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,uBACE;AAAA,EAGF,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,iBACE;AAAA,EAEF,mBAAmB;AAAA,EACnB,kBACE;AAAA,EACF,kBACE;AAAA;AAAA,EAEF,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,cAAc;AAAA,EACd,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,sBACE;AAAA,EACF,qBACE;AAAA,EACF,oBACE;AAAA,EACF,0BACE;AAAA;AAAA,EAEF,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,WAAW;AAAA,EACX,aAAa;AAAA;AAAA,EAEb,SAAS;AAAA;AAAA,EAET,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,kBAAkB;AACpB;AAMO,SAAS,SAAS,GAAmB;AAC1C,SAAO,EAAE,QAAQ,SAAS,kBAAkB;AAC9C;AAOO,SAAS,YAAY,GAAW,KAAsB;AAC3D,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI,EAAE,MAAM,GAAG;AAClC,QAAM,QAAS,QAAQ,IAAI,aAAa,gBAAiB,MAAM,cAAc,GAAG,IAAI,WAAc,cAAc,GAAG;AAInH,SAAO,OAAO,SAAS,WACnB,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc;AAC1C,UAAM,IAAI,KAAK,CAAC,KAAK;AACrB,WAAO,EAAE,SAAS,GAAG,IAAI,mBAAmB,CAAC,IAAI,YAAY,GAAG,GAAG;AAAA,EACrE,CAAC,IACD;AACN;AAGA,IAAM,SAAS,CAAC,YAAY,UAAU,WAAW,UAAU;AAGpD,SAAS,cACd,SACA,KACyB;AACzB,QAAM,MAAM,EAAE,GAAG,QAAQ;AACzB,aAAW,KAAK,QAAQ;AACtB,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,OAAO,MAAM,SAAU,KAAI,CAAC,IAAI,YAAY,GAAG,GAAG;AAAA,EACxD;AACA,SAAO;AACT;","names":[]}
@@ -79,7 +79,7 @@ function custom(r, path, value) {
79
79
  }
80
80
  }
81
81
  function redactKey(r, path, key) {
82
- if (r.rowKeySensitive) return hashRowKey(key);
82
+ if (r.rowKeySensitive) return SENSITIVE;
83
83
  const c = custom(r, path, key);
84
84
  if (c !== void 0) return c;
85
85
  return r.level === "safe" ? hashRowKey(key) : key;