@datakable/core 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/{codes-B9bCK73-.d.cts → codes-AX8XkMNY.d.cts} +6 -2
- package/dist/{codes-B9bCK73-.d.ts → codes-AX8XkMNY.d.ts} +6 -2
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/explain.cjs.map +1 -1
- package/dist/diagnostics/explain.js.map +1 -1
- package/dist/diagnostics/hub.cjs +7 -1
- package/dist/diagnostics/hub.cjs.map +1 -1
- package/dist/diagnostics/hub.js +7 -1
- package/dist/diagnostics/hub.js.map +1 -1
- package/dist/diagnostics/redact.cjs +1 -1
- package/dist/diagnostics/redact.cjs.map +1 -1
- package/dist/diagnostics/redact.js +1 -1
- package/dist/diagnostics/redact.js.map +1 -1
- package/dist/diagnostics/types.cjs.map +1 -1
- package/dist/{explain-Q8Cg0cZy.d.cts → explain-BVE3ln3G.d.ts} +3 -3
- package/dist/{explain-i0OATi_W.d.ts → explain-pKAnnxEW.d.cts} +3 -3
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/pagination.d.cts +2 -2
- package/dist/pagination.d.ts +2 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-CuLYPfY_.d.cts → search-async-entry-ByMJbHjc.d.cts} +2 -2
- package/dist/{search-async-entry-Cx7xpuVZ.d.ts → search-async-entry-CFQODE08.d.ts} +2 -2
- package/dist/search-async.d.cts +3 -3
- package/dist/search-async.d.ts +3 -3
- package/dist/search.d.cts +4 -4
- package/dist/search.d.ts +4 -4
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +2 -2
- package/dist/sorting.d.ts +2 -2
- package/dist/table/types.cjs.map +1 -1
- package/dist/table/types.js.map +1 -1
- package/dist/validity.d.cts +2 -2
- package/dist/validity.d.ts +2 -2
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @datakable/core
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@datakable/core)
|
|
4
|
+
[](https://www.npmjs.com/package/@datakable/core)
|
|
5
|
+
[](https://www.npmjs.com/package/@datakable/core)
|
|
6
|
+
[](https://github.com/Ink01101011/datakable/blob/main/LICENSE)
|
|
7
|
+
|
|
3
8
|
The headless, framework-agnostic core of DataKable: a table store, a row pipeline built from
|
|
4
9
|
features (column filters, validity, search, sorting, pagination), data modes, schema validation,
|
|
5
10
|
diagnostics and `explain()`. No React. For React, use [`datakable/react`](https://github.com/Ink01101011/datakable/tree/main/packages/datakable) or
|
|
@@ -399,8 +404,9 @@ for (const d of table.getDiagnostics()) console.log(d.code, d.where, d.fix)
|
|
|
399
404
|
- **Redaction** (`diagnostics.redact`): `'safe'` logs paths, indices, counts, value types and
|
|
400
405
|
hashed row keys (production default); `'keys'` adds raw row keys (development default); `'full'`
|
|
401
406
|
adds row values. A custom `redact(path, value)` can rewrite any field. Columns with
|
|
402
|
-
`sensitive: true` are never logged at any level
|
|
403
|
-
original goes to
|
|
407
|
+
`sensitive: true` are never logged at any level (a sensitive `rowKey` is logged as
|
|
408
|
+
`[sensitive]`, not hashed), and fetcher errors are logged by name only (the original goes to
|
|
409
|
+
`onDiagnostic` as `context.cause`; `getDiagnostics()` leaves it out).
|
|
404
410
|
- **Production text:** production builds keep `code`, `where`, `context` and a docs URL and drop
|
|
405
411
|
the `problem` / `impact` / `fix` text. To keep it:
|
|
406
412
|
|
|
@@ -55,7 +55,11 @@ interface DiagnosticContext {
|
|
|
55
55
|
};
|
|
56
56
|
/** e.g. `DK_SCHEMA_INVALID_ROW#run57` */
|
|
57
57
|
causedBy?: string;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Original error (ES2022 cause). Reaches code you install (`onDiagnostic`, `diagnostic:emit`
|
|
60
|
+
* interceptors, the `redact` seam) and, under `strict`, the thrown `DatakableError`'s `cause`;
|
|
61
|
+
* `getDiagnostics()`, `toJSON` and the console leave it out.
|
|
62
|
+
*/
|
|
59
63
|
cause?: unknown;
|
|
60
64
|
}
|
|
61
65
|
interface Diagnostic {
|
|
@@ -119,7 +123,7 @@ interface RedactionOptions {
|
|
|
119
123
|
redact?: RedactFn;
|
|
120
124
|
/** Ids / keys of `sensitive: true` columns: never logged at any level. */
|
|
121
125
|
sensitiveColumns?: () => Iterable<string>;
|
|
122
|
-
/** The `rowKey` path is, is inside or contains a sensitive column: keys
|
|
126
|
+
/** The `rowKey` path is, is inside or contains a sensitive column: keys logged as `[sensitive]` at every level. */
|
|
123
127
|
rowKeySensitive?: () => boolean;
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
@@ -55,7 +55,11 @@ interface DiagnosticContext {
|
|
|
55
55
|
};
|
|
56
56
|
/** e.g. `DK_SCHEMA_INVALID_ROW#run57` */
|
|
57
57
|
causedBy?: string;
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Original error (ES2022 cause). Reaches code you install (`onDiagnostic`, `diagnostic:emit`
|
|
60
|
+
* interceptors, the `redact` seam) and, under `strict`, the thrown `DatakableError`'s `cause`;
|
|
61
|
+
* `getDiagnostics()`, `toJSON` and the console leave it out.
|
|
62
|
+
*/
|
|
59
63
|
cause?: unknown;
|
|
60
64
|
}
|
|
61
65
|
interface Diagnostic {
|
|
@@ -119,7 +123,7 @@ interface RedactionOptions {
|
|
|
119
123
|
redact?: RedactFn;
|
|
120
124
|
/** Ids / keys of `sensitive: true` columns: never logged at any level. */
|
|
121
125
|
sensitiveColumns?: () => Iterable<string>;
|
|
122
|
-
/** The `rowKey` path is, is inside or contains a sensitive column: keys
|
|
126
|
+
/** The `rowKey` path is, is inside or contains a sensitive column: keys logged as `[sensitive]` at every level. */
|
|
123
127
|
rowKeySensitive?: () => boolean;
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { U as columnFiltersDiagnostics } from './codes-
|
|
2
|
-
import { G as FilterFor, a1 as Path, P as LooseRow, a2 as PathValue, e as FeatureTypes, F as Feature } from './explain-
|
|
1
|
+
export { U as columnFiltersDiagnostics } from './codes-AX8XkMNY.cjs';
|
|
2
|
+
import { G as FilterFor, a1 as Path, P as LooseRow, a2 as PathValue, e as FeatureTypes, F as Feature } from './explain-pKAnnxEW.cjs';
|
|
3
3
|
import { a as DataMode } from './mode-DFV8ryXo.cjs';
|
|
4
4
|
|
|
5
5
|
/** The value a filter compares against: the element type for array-valued cells (§6.1). */
|
package/dist/column-filters.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { U as columnFiltersDiagnostics } from './codes-
|
|
2
|
-
import { G as FilterFor, a1 as Path, P as LooseRow, a2 as PathValue, e as FeatureTypes, F as Feature } from './explain-
|
|
1
|
+
export { U as columnFiltersDiagnostics } from './codes-AX8XkMNY.js';
|
|
2
|
+
import { G as FilterFor, a1 as Path, P as LooseRow, a2 as PathValue, e as FeatureTypes, F as Feature } from './explain-BVE3ln3G.js';
|
|
3
3
|
import { a as DataMode } from './mode-DFV8ryXo.js';
|
|
4
4
|
|
|
5
5
|
/** The value a filter compares against: the element type for array-valued cells (§6.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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,qBAA+B;AAM/B,2BAAO;AAEP,0BAA6D;AAC7D,oBAAyF;AACzF,yBAA0B;AA+G1B,IAAM,OAAO,MAAM;AAAC;AAGpB,IAAM,gBAAgB,CAAC,aACrB,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,UAAU,OAAO,EAAE,aAAa,UAAU;AAE/E,IAAM,WAAW,CAAC,MAAkB,EAAE,SAAS;AAGxC,SAAS,OAAO,MAAgE;AACrF,aAAW,KAAK,KAAK,UAAU;AAC7B,QAAI,EAAE,UAAU,WAAY;AAC5B,UAAM,IAAI,KAAK,MAAM,EAAE,EAAE,IAAI;AAC7B,QAAI,OAAO,GAAG,cAAc,YAAY,OAAO,EAAE,aAAa,YAAY,EAAE,WAAW,GAAG;AACxF,aAAO,EAAE,OAAO,EAAE,WAAW,MAAM,EAAE,SAAS;AAAA,IAChD;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,YAAY,CAAC,MAAc,SAAiB,KAAK,KAAK,OAAO,IAAI;AAGvE,IAAM,YAAY,CAAC,OAAe,UAAkB,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,QAAQ,CAAC,CAAC;AAG1F,SAAS,aAAa,MAAqC;AAChE,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,YAAQ,8BAAU,KAAK,KAAK;AAClC,QAAM,SAAS,UAAU,UAAa,SAAS,UAAa,MAAM,QAAQ,KAAK;AAC/E,QAAM,QAAuB,CAAC;AAC9B,MAAI,UAAU,MAAM;AACpB,aAAW,KAAK,cAAc,KAAK,QAAQ,GAAG;AAC5C,QAAI,SAAS,CAAC,GAAG;AACf,YAAM,KAAK,KAAK,GAAG,UAAU,SAAS,SAAS,OAAO,KAAK,CAAC;AAC5D;AAAA,IACF;AACA,UAAM,MAAM,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC;AACnD,QAAI,CAAC,IAAK;AACV,cAAU,IAAI,OAAO;AACrB,UAAM,IAAI,SAAS,MAAM,MAAM,IAAI,EAAE,IAAI,IAAI;AAC7C,UAAM,KAAK;AAAA,MACT,GAAG,KAAK,GAAG,UAAU,IAAI,MAAM,QAAQ,IAAI,OAAO,QAAQ,IAAI,QAAQ,IAAI,MAAM;AAAA,MAChF,GAAI,IAAI,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC,EAAG,IAAI,CAAC;AAAA,IAC/D,CAAC;AAAA,EACH;AACA,QAAM,OAAO,KAAK,WAAW;AAC7B,QAAM,SAAS,KAAK,OAAO;AAC3B,QAAM,IAAI,OAAO,IAAI;AACrB,QAAM,QAAQ,IAAI,UAAU,MAAM,eAAe,EAAE,IAAI,IAAI;AAC3D,MAAI,cAAc;AAClB,aAAW,KAAK,KAAK,YAAY,EAAG,KAAI,EAAE,UAAU,OAAQ;AAC5D,QAAM,IAAsB;AAAA,IAC1B,KAAK,MAAM,OAAO;AAAA,IAClB,SAAS,MAAM,WAAW;AAAA,IAC1B,QAAQ,MAAM;AAAA,IACd,MAAM,MAAM;AAAA,IACZ,GAAI,SAAS,SAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,SAAS,QAAQ,QAAQ,UAAU,EAAE,EAAE;AAAA,IACvF;AAAA,IACA,eAAe,MAAM;AAAA,IACrB,GAAI,IAAI,EAAE,MAAM,EAAE,OAAO,UAAU,EAAE,OAAO,KAAK,GAAG,OAAO,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC;AAAA,IAC/E,SAAS,MAAM,KAAK;AAAA,IACpB;AAAA,IACA;AAAA,IACA,GAAI,UAAU,MAAM,OAAO,SAAY,EAAE,IAAI,MAAM,GAAG,IAAI,CAAC;AAAA,IAC3D,UAAU,MAAQ,QAAQ,IAAI,aAAa,mBAAgB,4CAAuB,CAAC,IAAI,KAAK,UAAU,CAAC;AAAA,EACzG;AACA,SAAO;AACT;AAEA,SAAS,KACP,GACA,MACA,OACA,QACA,QACA,QACa;AACb,SAAO,EAAE,SAAS,EAAE,MAAM,OAAO,EAAE,OAAO,MAAM,OAAO,QAAQ,QAAQ,OAAO;AAChF;AAGA,SAAS,QAAQ,MAA0B,QAAiB,KAAsB;AAChF,QAAM,YAAQ,+BAAe,MAAM,QAAQ,MAAM,IAAI;AACrD,QAAM,IAAI,OAAO,GAAG;AACpB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,MAAM,KAAK,CAAC,GAAG,CAAC;AAC1B,QAAI,MAAM,OAAO,OAAO,CAAC,MAAM,EAAG,QAAO;AAAA,EAC3C;AACA,SAAO;AACT;AASO,SAAS,WAAW,MAAmB,KAA6B;AACzE,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,OAAO,KAAK,KAAK;AACvB,QAAM,QAAI,gCAAiB,KAAK,WAAY,QAAQ,IAAI,aAAa,YAAa;AAClF,QAAM,QAAkB,EAAE,MAAM,WAAW,MAAM,aAAa;AAC9D,QAAM,eAAW,6BAAc,EAAE,IAAI,GAAG,EAAE,KAAK,SAAS,GAAG,OAAO,CAAC,EAAE;AACrE,QAAM,OAAO,KAAK,WAAW;AAC7B,QAAM,UAAU,KAAK,OAAO;AAC5B,QAAM,SAAS,KAAK,OAAO;AAE3B,QAAM,QAAQ,QAAQ,MAAM,QAAQ,GAAG;AACvC,MAAI,SAAmC,SAAS,SAAY,SAAS;AACrE,MAAI;AACJ,MAAI;AACJ,MAAI,SAAS,GAAG;AACd,aAAS,SAAS,SAAS,IAAI,KAAK;AACpC,QAAI,OAAQ,UAAS;AAAA,EACvB,WAAW,WAAW,QAAQ,QAAQ,SAAS,KAAK,WAAW,UAAa,SAAS,QAAQ;AAE3F,UAAM,IAAI;AAAA,MACR,QAAQ,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG;AAAA,MAChC;AAAA,MACA;AAAA,IACF;AACA,UAAM,MAAM,QAAQ,QAAQ,CAAC;AAC7B,QAAI,KAAK;AACP,eAAS;AACT,iBAAW,IAAI;AACf,eAAS,IAAI;AAAA,IACf;AAAA,EACF;AACA,QAAM,QAAQ,SAAS,KAAK,aAAa;AACzC,MAAI,YAAgC,WAAW,YAAY,WAAW;AACtE,QAAM,QAAmB,CAAC;AAC1B,aAAW,KAAK,cAAc,KAAK,QAAQ,GAAG;AAC5C,UAAM,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,EAAE,MAAM;AAC/C,QAAI,CAAC,SAAS,cAAc,QAAW;AACrC,YAAM,KAAK,EAAE,GAAG,MAAM,SAAS,cAAc,CAAC;AAC9C;AAAA,IACF;AACA,QAAI,SAAS,CAAC,GAAG;AACf,YAAM,KAAK,EAAE,GAAG,MAAM,SAAS,UAAU,CAAC;AAC1C;AAAA,IACF;AACA,UAAM,MAAM,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC;AACnD,QAAI,CAAC,KAAK;AACR,YAAM,KAAK,EAAE,GAAG,MAAM,SAAS,cAAc,CAAC;AAC9C;AAAA,IACF;AACA,UAAM,WAAW,IAAI,OAAO,QAAQ,KAAK;AACzC,QAAI,YAAY,GAAG;AACjB,YAAM,KAAK,IAAI,OAAO;AACtB,YAAM,KAAK,EAAE,GAAG,MAAM,SAAS,IAAI,SAAS,WAAW,QAAQ,UAAU,GAAG,CAAC;AAC7E;AAAA,IACF;AACA,gBAAY,EAAE;AACd,UAAM,SAAS,KAAK,SAAS,MAAM,GAAG,IAAI,OAAO,OAAO,GAAG,KAAK;AAChE,UAAM,KAAK,EAAE,GAAG,MAAM,SAAS,WAAW,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC,EAAG,CAAC;AAAA,EAC3E;AACA,QAAM,IAAoB;AAAA,IACxB,KAAK;AAAA,IACL;AAAA,IACA,GAAI,SAAS,IAAI,EAAE,MAAM,IAAI,CAAC;AAAA,IAC9B,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;AAAA,IAC7C;AAAA,IACA,GAAI,SAAS,EAAE,QAAQ,OAAO,IAAI,CAAC,GAAG,UAAM,gCAAiB,GAAG,GAAG,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC;AAAA,IACxF;AAAA,IACA,GAAI,cAAc,SAAY,CAAC,IAAI,EAAE,UAAU;AAAA,IAC/C,SAAS,SAAS,KAAK,MAAM,KAAK,KAAK,CAAC,QAAS,IAA4B,UAAU,KAAK;AAAA,IAC5F,KAAK,MAAM,OAAO;AAAA,IAClB,MAAM,KAAK;AAAA,IACX,UAAU,MAAQ,QAAQ,IAAI,aAAa,mBAAgB,0CAAqB,CAAC,IAAI,KAAK,UAAU,CAAC;AAAA,EACvG;AACA,SAAO;AACT;","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":[]}
|
package/dist/diagnostics/hub.cjs
CHANGED
|
@@ -142,7 +142,13 @@ function createDiagnostics(options = {}) {
|
|
|
142
142
|
return {
|
|
143
143
|
table,
|
|
144
144
|
emit,
|
|
145
|
-
|
|
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":[]}
|
package/dist/diagnostics/hub.js
CHANGED
|
@@ -122,7 +122,13 @@ function createDiagnostics(options = {}) {
|
|
|
122
122
|
return {
|
|
123
123
|
table,
|
|
124
124
|
emit,
|
|
125
|
-
|
|
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":[]}
|
|
@@ -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
|
|
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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys hashed at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level.\n if (r.rowKeySensitive) return hashRowKey(key)\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys logged as `[sensitive]` at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level. No key-derived value at all:\n // an unsalted 32-bit hash of an SSN or email is reversible by brute force.\n if (r.rowKeySensitive) return SENSITIVE\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
@@ -44,7 +44,7 @@ function custom(r, path, value) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function redactKey(r, path, key) {
|
|
47
|
-
if (r.rowKeySensitive) return
|
|
47
|
+
if (r.rowKeySensitive) return SENSITIVE;
|
|
48
48
|
const c = custom(r, path, key);
|
|
49
49
|
if (c !== void 0) return c;
|
|
50
50
|
return r.level === "safe" ? hashRowKey(key) : key;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys hashed at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level.\n if (r.rowKeySensitive) return hashRowKey(key)\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\nimport './process-guard'\nimport type { Location } from './types'\n\ndeclare module './catalog' {\n interface DiagnosticCatalog {\n DK_UNSAFE_REDACTION: { level: 'full' }\n }\n}\n\n/**\n * Redaction levels (§8.2). `'safe'`: paths, indices, counts, value types, hashed row keys\n * (production default). `'keys'`: + raw row keys (development default). `'full'`: + row values.\n */\nexport type RedactLevel = 'safe' | 'keys' | 'full'\n\n/**\n * Custom rule: return a string to log instead of `value`, or `undefined` for the built-in policy.\n * `path` names the field, e.g. `where.key`, `details.value`, `details.keys.2`, `details.issues.0`.\n * Sensitive columns never reach it.\n */\nexport type RedactFn = (path: string, value: unknown) => string | undefined\n\nexport interface RedactionOptions {\n /** Default `'keys'` in development, `'safe'` in production. */\n level?: RedactLevel\n redact?: RedactFn\n /** Ids / keys of `sensitive: true` columns: never logged at any level. */\n sensitiveColumns?: () => Iterable<string>\n /** The `rowKey` path is, is inside or contains a sensitive column: keys logged as `[sensitive]` at every level. */\n rowKeySensitive?: () => boolean\n}\n\n/**\n * Which detail fields of a diagnostic carry user data, declared on its `defineDiagnostic` entry.\n * Undeclared fields are logged as-is, so they must hold only DataKable-safe data (paths, ids,\n * indices, counts). Text builders receive the redacted values: treat these fields as opaque and\n * only interpolate them.\n *\n * - `rowKey` / `rowKeys`: a row key / an array of them (hashed under `'safe'`).\n * - `value` / `values`: a cell value / an array of them (type summary unless `'full'`). The column\n * is `details.column` when it is a string, else the `where` column id.\n * - `schemaIssues`: one Standard Schema issue or an array of them (`scrubSchemaIssue`).\n * - `error`: a thrown value, e.g. a fetcher error (`scrubError`).\n */\nexport type RedactField = 'rowKey' | 'rowKeys' | 'value' | 'values' | 'schemaIssues' | 'error'\n\n/** A resolved policy, built once per emit. */\nexport interface Redactor {\n level: RedactLevel\n custom: RedactFn | undefined\n sensitive: ReadonlySet<string>\n rowKeySensitive: boolean\n /** Called when the custom rule throws; the value is logged as `[redacted]`. */\n onError?: (error: unknown) => void\n}\n\nexport const SENSITIVE = '[sensitive]'\nexport const REDACTED = '[redacted]'\n\nconst LEVELS: readonly string[] = ['safe', 'keys', 'full']\nconst EMPTY: ReadonlySet<string> = new Set()\n\nexport function resolveRedaction(\n option: RedactLevel | RedactionOptions | undefined,\n dev: boolean,\n): Redactor {\n const o: RedactionOptions = typeof option === 'string' ? { level: option } : (option ?? {})\n const level = o.level ?? (dev ? 'keys' : 'safe')\n const cols = o.sensitiveColumns?.()\n return {\n // An unknown level gets the strictest policy; validating the option is optionValidator's job.\n level: LEVELS.includes(level) ? level : 'safe',\n custom: o.redact,\n sensitive: cols ? new Set(cols) : EMPTY,\n rowKeySensitive: o.rowKeySensitive?.() ?? false,\n }\n}\n\n/** Short 32-bit FNV-1a hash of `String(key)` (UTF-16 code units), as `h:xxxxxxxx`. */\nexport function hashRowKey(key: unknown): string {\n const s = String(key)\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n return `h:${(h >>> 0).toString(16).padStart(8, '0')}`\n}\n\n/** The value's type, never its contents: `string(12)`, `number`, `array(3)`, `date`, … */\nexport function describeValue(v: unknown): string {\n if (v === null) return 'null'\n if (typeof v === 'string') return `string(${v.length})`\n if (typeof v === 'number') return Number.isNaN(v) ? 'number(NaN)' : 'number'\n if (Array.isArray(v)) return `array(${v.length})`\n if (v instanceof Date) return 'date'\n return typeof v\n}\n\nfunction custom(r: Redactor, path: string, value: unknown): string | undefined {\n if (!r.custom) return undefined\n try {\n return r.custom(path, value)\n } catch (e) {\n r.onError?.(e)\n return REDACTED\n }\n}\n\nfunction redactKey(r: Redactor, path: string, key: unknown): unknown {\n // A sensitive rowKey column wins over the custom rule and the level. No key-derived value at all:\n // an unsalted 32-bit hash of an SSN or email is reversible by brute force.\n if (r.rowKeySensitive) return SENSITIVE\n const c = custom(r, path, key)\n if (c !== undefined) return c\n return r.level === 'safe' ? hashRowKey(key) : key\n}\n\nfunction redactValue(r: Redactor, path: string, value: unknown, column?: string): unknown {\n if (column !== undefined && underSensitive(r, column)) return SENSITIVE\n const c = custom(r, path, value)\n if (c !== undefined) return c\n return r.level === 'full' ? value : describeValue(value)\n}\n\ntype IssuePath = ReadonlyArray<PropertyKey | { key: PropertyKey }>\n\n/** Joins a Standard Schema issue path: `['user', 0, { key: 'email' }]` → `user.0.email`. */\nfunction issuePath(path: IssuePath | undefined): string {\n if (!path) return ''\n return path.map((s) => String(typeof s === 'object' && s !== null ? s.key : s)).join('.')\n}\n\n/** `a` and `b` are the same path or one is inside the other (`contact` ↔ `contact.email`). */\nexport const pathsOverlap = (a: string, b: string): boolean =>\n a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)\n\n/** `path` is, is inside or contains a sensitive column (#45). */\nfunction underSensitive(r: Redactor, path: string): boolean {\n for (const col of r.sensitive) if (pathsOverlap(path, col)) return true\n return false\n}\n\n/**\n * A schema issue as `{ path, message }`. Validators embed received values in their messages, so\n * under `'safe'` / `'keys'` the message is DataKable's own text. So it is at every level when the\n * table has a sensitive column: any message (a root-level refinement, another field's) may embed\n * its value (#46).\n */\nexport function scrubSchemaIssue(\n issue: unknown,\n r: Redactor,\n fieldPath = 'issue',\n): { path: string; message: string } {\n const i = (typeof issue === 'object' && issue !== null ? issue : {}) as {\n message?: unknown\n path?: IssuePath\n }\n let path: string\n try {\n path = issuePath(i.path)\n } catch {\n path = ''\n }\n const own = `invalid value at ${path || '(root)'}`\n if (r.sensitive.size > 0) return { path, message: own }\n const c = custom(r, fieldPath, i.message)\n if (c !== undefined) return { path, message: c }\n return { path, message: r.level === 'full' && typeof i.message === 'string' ? i.message : own }\n}\n\n/**\n * A thrown value as `error.name` + `code` only (fetcher errors embed URLs, tokens, payloads). The\n * full error belongs in `context.cause`, which reaches `onDiagnostic` only.\n */\nexport function scrubError(e: unknown): string {\n if (typeof e !== 'object' || e === null) return typeof e\n try {\n const { name, code } = e as { name?: unknown; code?: unknown }\n const n = typeof name === 'string' && name ? name : 'object'\n return typeof code === 'string' || typeof code === 'number' ? `${n} (${code})` : n\n } catch {\n return 'object'\n }\n}\n\n/** Hashes / passes through `where.key` per the policy; other locations hold no user data. */\nexport function redactLocation(where: Location, r: Redactor): Location {\n if (where.kind !== 'row' || where.key === undefined) return where\n const key = redactKey(r, 'where.key', where.key)\n return { kind: 'row', index: where.index, key: typeof key === 'string' ? key : String(key) }\n}\n\n/** A shallow copy of `details` with the declared fields redacted; the input is never mutated. */\nexport function redactDetails(\n details: Record<string, unknown>,\n fields: Readonly<Record<string, RedactField | undefined>> | undefined,\n where: Location,\n r: Redactor,\n): Record<string, unknown> {\n if (!fields) return details\n const out: Record<string, unknown> = { ...details }\n const column =\n typeof details.column === 'string'\n ? details.column\n : where.kind === 'column'\n ? where.id\n : undefined\n for (const name of Object.keys(fields)) {\n const kind = fields[name]\n if (kind === undefined || !(name in details)) continue\n const v = details[name]\n const p = `details.${name}`\n const each = (f: (x: unknown, path: string) => unknown) =>\n Array.isArray(v) ? v.map((x, i) => f(x, `${p}.${i}`)) : f(v, p)\n switch (kind) {\n case 'rowKey':\n out[name] = redactKey(r, p, v)\n break\n case 'rowKeys':\n out[name] = each((x, path) => redactKey(r, path, x))\n break\n case 'value':\n out[name] = redactValue(r, p, v, column)\n break\n case 'values':\n out[name] = each((x, path) => redactValue(r, path, x, column))\n break\n case 'schemaIssues':\n out[name] = each((x, path) => scrubSchemaIssue(x, r, path))\n break\n case 'error':\n out[name] = custom(r, p, v) ?? scrubError(v)\n break\n }\n }\n return out\n}\n\n/** Message text (§8.2): development builds, or production via `@datakable/core/messages`. */\nexport const redactionMessages: DiagnosticMessages<'DK_UNSAFE_REDACTION'> = {\n DK_UNSAFE_REDACTION: {\n problem: (d) => `diagnostics.redact is '${d.level}' in a production build`,\n impact: () =>\n 'raw row keys and cell values (except sensitive columns) reach onDiagnostic, ' +\n 'getDiagnostics() and the console',\n fix: () => [\n \"remove diagnostics.redact in production (the default is 'safe')\",\n \"or use 'keys' to log raw row keys without cell values\",\n 'mark personal-data columns with sensitive: true so they are never logged',\n ],\n },\n}\n\n/** `'full'` in a production build (§8.2: \"warns if enabled in production\"). Emitted by the hub. */\nexport const DK_UNSAFE_REDACTION = defineDiagnostic(\n {\n code: 'DK_UNSAFE_REDACTION',\n level: 'warn',\n where: () => ({ kind: 'option', path: 'diagnostics.redact' }),\n },\n (process.env.NODE_ENV !== \"production\") && redactionMessages.DK_UNSAFE_REDACTION,\n)\n"],"mappings":"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","names":[]}
|