@datakable/core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/catalog/codes.cjs +12 -8
- package/dist/catalog/codes.cjs.map +1 -1
- package/dist/catalog/codes.js +12 -8
- package/dist/catalog/codes.js.map +1 -1
- package/dist/catalog/slots.cjs +10 -6
- package/dist/catalog/slots.cjs.map +1 -1
- package/dist/catalog/slots.js +10 -6
- package/dist/catalog/slots.js.map +1 -1
- package/dist/{codes-BKdS9EiO.d.cts → codes-B9bCK73-.d.cts} +46 -8
- package/dist/{codes-BKdS9EiO.d.ts → codes-B9bCK73-.d.ts} +46 -8
- package/dist/column/column-def.cjs.map +1 -1
- package/dist/column/get-value.cjs +6 -1
- package/dist/column/get-value.cjs.map +1 -1
- package/dist/column/get-value.js +6 -1
- package/dist/column/get-value.js.map +1 -1
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/build.cjs +10 -4
- package/dist/diagnostics/build.cjs.map +1 -1
- package/dist/diagnostics/build.js +8 -3
- package/dist/diagnostics/build.js.map +1 -1
- package/dist/diagnostics/catalog.cjs +11 -5
- package/dist/diagnostics/catalog.cjs.map +1 -1
- package/dist/diagnostics/catalog.js +10 -5
- package/dist/diagnostics/catalog.js.map +1 -1
- package/dist/diagnostics/codes.cjs +12 -8
- package/dist/diagnostics/codes.cjs.map +1 -1
- package/dist/diagnostics/codes.js +12 -8
- package/dist/diagnostics/codes.js.map +1 -1
- package/dist/diagnostics/console.cjs +13 -6
- package/dist/diagnostics/console.cjs.map +1 -1
- package/dist/diagnostics/console.js +13 -6
- package/dist/diagnostics/console.js.map +1 -1
- package/dist/diagnostics/env.cjs +1 -0
- package/dist/diagnostics/env.cjs.map +1 -1
- package/dist/diagnostics/env.js +1 -0
- package/dist/diagnostics/env.js.map +1 -1
- package/dist/diagnostics/explain-text.cjs +2 -1
- package/dist/diagnostics/explain-text.cjs.map +1 -1
- package/dist/diagnostics/explain-text.js +2 -1
- package/dist/diagnostics/explain-text.js.map +1 -1
- package/dist/diagnostics/explain.cjs +3 -2
- package/dist/diagnostics/explain.cjs.map +1 -1
- package/dist/diagnostics/explain.js +3 -2
- package/dist/diagnostics/explain.js.map +1 -1
- package/dist/diagnostics/hub.cjs +6 -5
- package/dist/diagnostics/hub.cjs.map +1 -1
- package/dist/diagnostics/hub.js +7 -6
- package/dist/diagnostics/hub.js.map +1 -1
- package/dist/diagnostics/process-guard.cjs +26 -0
- package/dist/diagnostics/process-guard.cjs.map +1 -0
- package/dist/diagnostics/process-guard.js +9 -0
- package/dist/diagnostics/process-guard.js.map +1 -0
- package/dist/diagnostics/prose.cjs +120 -0
- package/dist/diagnostics/prose.cjs.map +1 -0
- package/dist/diagnostics/prose.js +93 -0
- package/dist/diagnostics/prose.js.map +1 -0
- package/dist/diagnostics/redact.cjs +7 -4
- package/dist/diagnostics/redact.cjs.map +1 -1
- package/dist/diagnostics/redact.js +6 -4
- package/dist/diagnostics/redact.js.map +1 -1
- package/dist/diagnostics/trace-store.cjs +6 -0
- package/dist/diagnostics/trace-store.cjs.map +1 -1
- package/dist/diagnostics/trace-store.js +4 -0
- package/dist/diagnostics/trace-store.js.map +1 -1
- package/dist/diagnostics/tracer.cjs +74 -13
- package/dist/diagnostics/tracer.cjs.map +1 -1
- package/dist/diagnostics/tracer.js +75 -14
- package/dist/diagnostics/tracer.js.map +1 -1
- package/dist/diagnostics/types.cjs.map +1 -1
- package/dist/{explain-aagaYHcy.d.cts → explain-Q8Cg0cZy.d.cts} +13 -4
- package/dist/{explain-DcUZfFh5.d.ts → explain-i0OATi_W.d.ts} +13 -4
- package/dist/feature/codes.cjs +1 -0
- package/dist/feature/codes.cjs.map +1 -1
- package/dist/feature/codes.js +1 -0
- package/dist/feature/codes.js.map +1 -1
- package/dist/feature/define.cjs +3 -1
- package/dist/feature/define.cjs.map +1 -1
- package/dist/feature/define.js +3 -1
- package/dist/feature/define.js.map +1 -1
- package/dist/feature/install.cjs +4 -3
- package/dist/feature/install.cjs.map +1 -1
- package/dist/feature/install.js +4 -3
- package/dist/feature/install.js.map +1 -1
- package/dist/feature/runtime.cjs.map +1 -1
- package/dist/feature/runtime.js.map +1 -1
- package/dist/features/column-filters/codes.cjs +1 -0
- package/dist/features/column-filters/codes.cjs.map +1 -1
- package/dist/features/column-filters/codes.js +1 -0
- package/dist/features/column-filters/codes.js.map +1 -1
- package/dist/features/column-filters/column-filters.cjs +4 -2
- package/dist/features/column-filters/column-filters.cjs.map +1 -1
- package/dist/features/column-filters/column-filters.js +4 -2
- package/dist/features/column-filters/column-filters.js.map +1 -1
- package/dist/features/column-filters/match.cjs +5 -5
- package/dist/features/column-filters/match.cjs.map +1 -1
- package/dist/features/column-filters/match.js +5 -5
- package/dist/features/column-filters/match.js.map +1 -1
- package/dist/features/pagination/codes.cjs +6 -3
- package/dist/features/pagination/codes.cjs.map +1 -1
- package/dist/features/pagination/codes.js +6 -3
- package/dist/features/pagination/codes.js.map +1 -1
- package/dist/features/pagination/pagination.cjs +19 -14
- package/dist/features/pagination/pagination.cjs.map +1 -1
- package/dist/features/pagination/pagination.js +19 -14
- package/dist/features/pagination/pagination.js.map +1 -1
- package/dist/features/search/codes.cjs +1 -0
- package/dist/features/search/codes.cjs.map +1 -1
- package/dist/features/search/codes.js +1 -0
- package/dist/features/search/codes.js.map +1 -1
- package/dist/features/search/search-index.cjs +105 -48
- package/dist/features/search/search-index.cjs.map +1 -1
- package/dist/features/search/search-index.js +106 -49
- package/dist/features/search/search-index.js.map +1 -1
- package/dist/features/search/search.cjs +82 -24
- package/dist/features/search/search.cjs.map +1 -1
- package/dist/features/search/search.js +82 -24
- package/dist/features/search/search.js.map +1 -1
- package/dist/features/search/skipped-codes.cjs +45 -0
- package/dist/features/search/skipped-codes.cjs.map +1 -0
- package/dist/features/search/skipped-codes.js +20 -0
- package/dist/features/search/skipped-codes.js.map +1 -0
- package/dist/features/sorting/codes.cjs +1 -0
- package/dist/features/sorting/codes.cjs.map +1 -1
- package/dist/features/sorting/codes.js +1 -0
- package/dist/features/sorting/codes.js.map +1 -1
- package/dist/features/sorting/sorting.cjs +19 -23
- package/dist/features/sorting/sorting.cjs.map +1 -1
- package/dist/features/sorting/sorting.js +19 -23
- package/dist/features/sorting/sorting.js.map +1 -1
- package/dist/features/sorting/strategy.cjs +4 -4
- package/dist/features/sorting/strategy.cjs.map +1 -1
- package/dist/features/sorting/strategy.js +4 -4
- package/dist/features/sorting/strategy.js.map +1 -1
- package/dist/features/validity/codes.cjs +1 -0
- package/dist/features/validity/codes.cjs.map +1 -1
- package/dist/features/validity/codes.js +1 -0
- package/dist/features/validity/codes.js.map +1 -1
- package/dist/features/validity/validity.cjs +3 -3
- package/dist/features/validity/validity.cjs.map +1 -1
- package/dist/features/validity/validity.js +3 -3
- package/dist/features/validity/validity.js.map +1 -1
- package/dist/index.d.cts +16 -11
- package/dist/index.d.ts +16 -11
- package/dist/ingest/data-check-codes.cjs +4 -2
- package/dist/ingest/data-check-codes.cjs.map +1 -1
- package/dist/ingest/data-check-codes.js +4 -2
- package/dist/ingest/data-check-codes.js.map +1 -1
- package/dist/ingest/data-checks.cjs +1 -1
- package/dist/ingest/data-checks.cjs.map +1 -1
- package/dist/ingest/data-checks.js +1 -1
- package/dist/ingest/data-checks.js.map +1 -1
- package/dist/ingest/row-key-codes.cjs +1 -0
- package/dist/ingest/row-key-codes.cjs.map +1 -1
- package/dist/ingest/row-key-codes.js +1 -0
- package/dist/ingest/row-key-codes.js.map +1 -1
- package/dist/ingest/row-key.cjs +23 -20
- package/dist/ingest/row-key.cjs.map +1 -1
- package/dist/ingest/row-key.js +23 -20
- package/dist/ingest/row-key.js.map +1 -1
- package/dist/ingest/schema-codes.cjs +1 -0
- package/dist/ingest/schema-codes.cjs.map +1 -1
- package/dist/ingest/schema-codes.js +1 -0
- package/dist/ingest/schema-codes.js.map +1 -1
- package/dist/ingest/unique-fields.cjs +2 -2
- package/dist/ingest/unique-fields.cjs.map +1 -1
- package/dist/ingest/unique-fields.js +2 -2
- package/dist/ingest/unique-fields.js.map +1 -1
- package/dist/intercept/boundary.cjs +4 -2
- package/dist/intercept/boundary.cjs.map +1 -1
- package/dist/intercept/boundary.js +4 -2
- package/dist/intercept/boundary.js.map +1 -1
- package/dist/intercept/cell-batch.cjs +7 -4
- package/dist/intercept/cell-batch.cjs.map +1 -1
- package/dist/intercept/cell-batch.js +7 -4
- package/dist/intercept/cell-batch.js.map +1 -1
- package/dist/intercept/codes.cjs +1 -0
- package/dist/intercept/codes.cjs.map +1 -1
- package/dist/intercept/codes.js +1 -0
- package/dist/intercept/codes.js.map +1 -1
- package/dist/intercept/fallbacks.cjs +12 -20
- package/dist/intercept/fallbacks.cjs.map +1 -1
- package/dist/intercept/fallbacks.js +12 -20
- package/dist/intercept/fallbacks.js.map +1 -1
- package/dist/intercept/runtime.cjs +7 -9
- package/dist/intercept/runtime.cjs.map +1 -1
- package/dist/intercept/runtime.js +7 -9
- package/dist/intercept/runtime.js.map +1 -1
- package/dist/messages/index.cjs +4 -1
- package/dist/messages/index.cjs.map +1 -1
- package/dist/messages/index.js +4 -1
- package/dist/messages/index.js.map +1 -1
- package/dist/pagination.d.cts +2 -2
- package/dist/pagination.d.ts +2 -2
- package/dist/pipeline/codes.cjs +1 -0
- package/dist/pipeline/codes.cjs.map +1 -1
- package/dist/pipeline/codes.js +1 -0
- package/dist/pipeline/codes.js.map +1 -1
- package/dist/pipeline/column-cache.cjs +94 -76
- package/dist/pipeline/column-cache.cjs.map +1 -1
- package/dist/pipeline/column-cache.js +92 -75
- package/dist/pipeline/column-cache.js.map +1 -1
- package/dist/pipeline/dev-checks.cjs +1 -0
- package/dist/pipeline/dev-checks.cjs.map +1 -1
- package/dist/pipeline/dev-checks.js +1 -0
- package/dist/pipeline/dev-checks.js.map +1 -1
- package/dist/pipeline/env-codes.cjs +3 -2
- package/dist/pipeline/env-codes.cjs.map +1 -1
- package/dist/pipeline/env-codes.js +3 -2
- package/dist/pipeline/env-codes.js.map +1 -1
- package/dist/pipeline/perf-codes.cjs +1 -0
- package/dist/pipeline/perf-codes.cjs.map +1 -1
- package/dist/pipeline/perf-codes.js +1 -0
- package/dist/pipeline/perf-codes.js.map +1 -1
- package/dist/pipeline/row.cjs +8 -7
- package/dist/pipeline/row.cjs.map +1 -1
- package/dist/pipeline/row.js +8 -7
- package/dist/pipeline/row.js.map +1 -1
- package/dist/pipeline/runner.cjs +17 -9
- package/dist/pipeline/runner.cjs.map +1 -1
- package/dist/pipeline/runner.js +17 -9
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pipeline/slice.cjs +3 -1
- package/dist/pipeline/slice.cjs.map +1 -1
- package/dist/pipeline/slice.js +3 -1
- package/dist/pipeline/slice.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-C3mXxLP8.d.cts → search-async-entry-CuLYPfY_.d.cts} +2 -2
- package/dist/{search-async-entry-BmQYT120.d.ts → search-async-entry-Cx7xpuVZ.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 +8 -6
- package/dist/search.d.ts +8 -6
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +6 -3
- package/dist/sorting.d.ts +6 -3
- package/dist/source/codes.cjs +1 -0
- package/dist/source/codes.cjs.map +1 -1
- package/dist/source/codes.js +1 -0
- package/dist/source/codes.js.map +1 -1
- package/dist/source/fetch-codes.cjs +1 -0
- package/dist/source/fetch-codes.cjs.map +1 -1
- package/dist/source/fetch-codes.js +1 -0
- package/dist/source/fetch-codes.js.map +1 -1
- package/dist/table/codes.cjs +1 -0
- package/dist/table/codes.cjs.map +1 -1
- package/dist/table/codes.js +1 -0
- package/dist/table/codes.js.map +1 -1
- package/dist/table/create-table.cjs +39 -9
- package/dist/table/create-table.cjs.map +1 -1
- package/dist/table/create-table.js +39 -9
- package/dist/table/create-table.js.map +1 -1
- package/dist/table/data-mutation.cjs +1 -0
- package/dist/table/data-mutation.cjs.map +1 -1
- package/dist/table/data-mutation.js +1 -0
- package/dist/table/data-mutation.js.map +1 -1
- package/dist/table/internals.cjs.map +1 -1
- package/dist/table/internals.js.map +1 -1
- package/dist/table/ssr-codes.cjs +1 -0
- package/dist/table/ssr-codes.cjs.map +1 -1
- package/dist/table/ssr-codes.js +1 -0
- package/dist/table/ssr-codes.js.map +1 -1
- package/dist/table/ssr.cjs +4 -1
- package/dist/table/ssr.cjs.map +1 -1
- package/dist/table/ssr.js +4 -1
- package/dist/table/ssr.js.map +1 -1
- package/dist/table/state-feedback.cjs +19 -0
- package/dist/table/state-feedback.cjs.map +1 -1
- package/dist/table/state-feedback.js +19 -0
- package/dist/table/state-feedback.js.map +1 -1
- 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/dist/versioning/codes.cjs +18 -0
- package/dist/versioning/codes.cjs.map +1 -1
- package/dist/versioning/codes.js +18 -0
- package/dist/versioning/codes.js.map +1 -1
- package/dist/versioning/registry.cjs +4 -2
- package/dist/versioning/registry.cjs.map +1 -1
- package/dist/versioning/registry.js +4 -2
- package/dist/versioning/registry.js.map +1 -1
- package/package.json +39 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\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` column is sensitive: row keys are 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 && r.sensitive.has(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\nfunction underSensitive(r: Redactor, path: string): boolean {\n if (r.sensitive.size === 0) return false\n for (const col of r.sensitive) if (path === col || path.startsWith(`${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'` (and for sensitive columns) the message is DataKable's own text.\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 (underSensitive(r, path)) 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 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,5 +1,6 @@
|
|
|
1
1
|
// src/diagnostics/redact.ts
|
|
2
2
|
import { defineDiagnostic } from "./catalog.js";
|
|
3
|
+
import "./process-guard.js";
|
|
3
4
|
var SENSITIVE = "[sensitive]";
|
|
4
5
|
var REDACTED = "[redacted]";
|
|
5
6
|
var LEVELS = ["safe", "keys", "full"];
|
|
@@ -49,7 +50,7 @@ function redactKey(r, path, key) {
|
|
|
49
50
|
return r.level === "safe" ? hashRowKey(key) : key;
|
|
50
51
|
}
|
|
51
52
|
function redactValue(r, path, value, column) {
|
|
52
|
-
if (column !== void 0 && r
|
|
53
|
+
if (column !== void 0 && underSensitive(r, column)) return SENSITIVE;
|
|
53
54
|
const c = custom(r, path, value);
|
|
54
55
|
if (c !== void 0) return c;
|
|
55
56
|
return r.level === "full" ? value : describeValue(value);
|
|
@@ -58,9 +59,9 @@ function issuePath(path) {
|
|
|
58
59
|
if (!path) return "";
|
|
59
60
|
return path.map((s) => String(typeof s === "object" && s !== null ? s.key : s)).join(".");
|
|
60
61
|
}
|
|
62
|
+
var pathsOverlap = (a, b) => a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`);
|
|
61
63
|
function underSensitive(r, path) {
|
|
62
|
-
|
|
63
|
-
for (const col of r.sensitive) if (path === col || path.startsWith(`${col}.`)) return true;
|
|
64
|
+
for (const col of r.sensitive) if (pathsOverlap(path, col)) return true;
|
|
64
65
|
return false;
|
|
65
66
|
}
|
|
66
67
|
function scrubSchemaIssue(issue, r, fieldPath = "issue") {
|
|
@@ -72,7 +73,7 @@ function scrubSchemaIssue(issue, r, fieldPath = "issue") {
|
|
|
72
73
|
path = "";
|
|
73
74
|
}
|
|
74
75
|
const own = `invalid value at ${path || "(root)"}`;
|
|
75
|
-
if (
|
|
76
|
+
if (r.sensitive.size > 0) return { path, message: own };
|
|
76
77
|
const c = custom(r, fieldPath, i.message);
|
|
77
78
|
if (c !== void 0) return { path, message: c };
|
|
78
79
|
return { path, message: r.level === "full" && typeof i.message === "string" ? i.message : own };
|
|
@@ -150,6 +151,7 @@ export {
|
|
|
150
151
|
SENSITIVE,
|
|
151
152
|
describeValue,
|
|
152
153
|
hashRowKey,
|
|
154
|
+
pathsOverlap,
|
|
153
155
|
redactDetails,
|
|
154
156
|
redactLocation,
|
|
155
157
|
redactionMessages,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/redact.ts"],"sourcesContent":["import { type DiagnosticMessages, defineDiagnostic } from './catalog'\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` column is sensitive: row keys are 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 && r.sensitive.has(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\nfunction underSensitive(r: Redactor, path: string): boolean {\n if (r.sensitive.size === 0) return false\n for (const col of r.sensitive) if (path === col || path.startsWith(`${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'` (and for sensitive columns) the message is DataKable's own text.\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 (underSensitive(r, path)) 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 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":[]}
|
|
@@ -20,10 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/diagnostics/trace-store.ts
|
|
21
21
|
var trace_store_exports = {};
|
|
22
22
|
__export(trace_store_exports, {
|
|
23
|
+
SLICED: () => SLICED,
|
|
23
24
|
readTrace: () => readTrace,
|
|
25
|
+
sliceTimer: () => sliceTimer,
|
|
24
26
|
writeTrace: () => writeTrace
|
|
25
27
|
});
|
|
26
28
|
module.exports = __toCommonJS(trace_store_exports);
|
|
29
|
+
var SLICED = "sliced";
|
|
30
|
+
var sliceTimer = {};
|
|
27
31
|
var traces = /* @__PURE__ */ new WeakMap();
|
|
28
32
|
var readTrace = (table) => traces.get(table);
|
|
29
33
|
var writeTrace = (table, trace) => {
|
|
@@ -31,7 +35,9 @@ var writeTrace = (table, trace) => {
|
|
|
31
35
|
};
|
|
32
36
|
// Annotate the CommonJS export names for ESM import in node:
|
|
33
37
|
0 && (module.exports = {
|
|
38
|
+
SLICED,
|
|
34
39
|
readTrace,
|
|
40
|
+
sliceTimer,
|
|
35
41
|
writeTrace
|
|
36
42
|
});
|
|
37
43
|
//# sourceMappingURL=trace-store.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/trace-store.ts"],"sourcesContent":["import type { Trigger } from './types'\n\n/** One step of the traced run: counts (never rows) and its duration (§9.1: bounded). */\nexport interface TraceStep {\n readonly input: number\n readonly output: number\n readonly ms: number\n}\n\n/** What the tracer kept of the last `pipeline:run` of a table (the last run only). */\nexport interface Trace {\n readonly run: number\n readonly trigger: Trigger\n /** Duration of the whole run; `undefined` while it is in progress. */\n ms: number | undefined\n /** Steps that ran (a step served from its memo is absent), by feature name. */\n readonly steps: Map<string, TraceStep>\n /** The last `state:dispatch` (its `where`: a method / feature name) and its duration. */\n dispatch: { where: string; ms: number } | undefined\n}\n\n/** Kept apart from the tracer so `explain()` reads traces without bundling the tracer itself. */\nconst traces = new WeakMap<object, Trace>()\n\n/** The last traced run of `table`, if a tracer is installed. */\nexport const readTrace = (table: object): Trace | undefined => traces.get(table)\n\nexport const writeTrace = (table: object, trace: Trace): void => {\n traces.set(table, trace)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/trace-store.ts"],"sourcesContent":["import type { ContextBase } from '../intercept/context'\nimport type { Trigger } from './types'\n\n/** One step of the traced run: counts (never rows) and its duration (§9.1: bounded). */\nexport interface TraceStep {\n readonly input: number\n readonly output: number\n /** With time-sliced work (#61): the runs that waited on it, this one, and the slices between. */\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 the tracer kept of the last `pipeline:run` of a table (the last run only). */\nexport interface Trace {\n readonly run: number\n readonly trigger: Trigger\n /** Duration of the whole run; `undefined` while it is in progress. */\n ms: number | undefined\n /** Steps that ran (a step served from its memo is absent), by feature name. */\n readonly steps: Map<string, TraceStep>\n /** The last `state:dispatch` (its `where`: a method / feature name) and its duration. */\n dispatch: { where: string; ms: number } | undefined\n}\n\n/**\n * Set on the `pipeline:step` ctx's `meta` by the runner when the step waits on time-sliced work\n * (its result comes in a later run).\n */\nexport const SLICED = 'sliced'\n\n/**\n * The tracer's (#61): the runner reports each slice of time-sliced work run between pipeline runs\n * (no step is timing it). Unset until a tracer is created.\n */\nexport const sliceTimer: {\n current?: ((base: ContextBase, feature: string, ms: number) => void) | undefined\n} = {}\n\n/** Kept apart from the tracer so `explain()` reads traces without bundling the tracer itself. */\nconst traces = new WeakMap<object, Trace>()\n\n/** The last traced run of `table`, if a tracer is installed. */\nexport const readTrace = (table: object): Trace | undefined => traces.get(table)\n\nexport const writeTrace = (table: object, trace: Trace): void => {\n traces.set(table, trace)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BO,IAAM,SAAS;AAMf,IAAM,aAET,CAAC;AAGL,IAAM,SAAS,oBAAI,QAAuB;AAGnC,IAAM,YAAY,CAAC,UAAqC,OAAO,IAAI,KAAK;AAExE,IAAM,aAAa,CAAC,OAAe,UAAuB;AAC/D,SAAO,IAAI,OAAO,KAAK;AACzB;","names":[]}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// src/diagnostics/trace-store.ts
|
|
2
|
+
var SLICED = "sliced";
|
|
3
|
+
var sliceTimer = {};
|
|
2
4
|
var traces = /* @__PURE__ */ new WeakMap();
|
|
3
5
|
var readTrace = (table) => traces.get(table);
|
|
4
6
|
var writeTrace = (table, trace) => {
|
|
5
7
|
traces.set(table, trace);
|
|
6
8
|
};
|
|
7
9
|
export {
|
|
10
|
+
SLICED,
|
|
8
11
|
readTrace,
|
|
12
|
+
sliceTimer,
|
|
9
13
|
writeTrace
|
|
10
14
|
};
|
|
11
15
|
//# sourceMappingURL=trace-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/trace-store.ts"],"sourcesContent":["import type { Trigger } from './types'\n\n/** One step of the traced run: counts (never rows) and its duration (§9.1: bounded). */\nexport interface TraceStep {\n readonly input: number\n readonly output: number\n readonly ms: number\n}\n\n/** What the tracer kept of the last `pipeline:run` of a table (the last run only). */\nexport interface Trace {\n readonly run: number\n readonly trigger: Trigger\n /** Duration of the whole run; `undefined` while it is in progress. */\n ms: number | undefined\n /** Steps that ran (a step served from its memo is absent), by feature name. */\n readonly steps: Map<string, TraceStep>\n /** The last `state:dispatch` (its `where`: a method / feature name) and its duration. */\n dispatch: { where: string; ms: number } | undefined\n}\n\n/** Kept apart from the tracer so `explain()` reads traces without bundling the tracer itself. */\nconst traces = new WeakMap<object, Trace>()\n\n/** The last traced run of `table`, if a tracer is installed. */\nexport const readTrace = (table: object): Trace | undefined => traces.get(table)\n\nexport const writeTrace = (table: object, trace: Trace): void => {\n traces.set(table, trace)\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/trace-store.ts"],"sourcesContent":["import type { ContextBase } from '../intercept/context'\nimport type { Trigger } from './types'\n\n/** One step of the traced run: counts (never rows) and its duration (§9.1: bounded). */\nexport interface TraceStep {\n readonly input: number\n readonly output: number\n /** With time-sliced work (#61): the runs that waited on it, this one, and the slices between. */\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 the tracer kept of the last `pipeline:run` of a table (the last run only). */\nexport interface Trace {\n readonly run: number\n readonly trigger: Trigger\n /** Duration of the whole run; `undefined` while it is in progress. */\n ms: number | undefined\n /** Steps that ran (a step served from its memo is absent), by feature name. */\n readonly steps: Map<string, TraceStep>\n /** The last `state:dispatch` (its `where`: a method / feature name) and its duration. */\n dispatch: { where: string; ms: number } | undefined\n}\n\n/**\n * Set on the `pipeline:step` ctx's `meta` by the runner when the step waits on time-sliced work\n * (its result comes in a later run).\n */\nexport const SLICED = 'sliced'\n\n/**\n * The tracer's (#61): the runner reports each slice of time-sliced work run between pipeline runs\n * (no step is timing it). Unset until a tracer is created.\n */\nexport const sliceTimer: {\n current?: ((base: ContextBase, feature: string, ms: number) => void) | undefined\n} = {}\n\n/** Kept apart from the tracer so `explain()` reads traces without bundling the tracer itself. */\nconst traces = new WeakMap<object, Trace>()\n\n/** The last traced run of `table`, if a tracer is installed. */\nexport const readTrace = (table: object): Trace | undefined => traces.get(table)\n\nexport const writeTrace = (table: object, trace: Trace): void => {\n traces.set(table, trace)\n}\n"],"mappings":";AA6BO,IAAM,SAAS;AAMf,IAAM,aAET,CAAC;AAGL,IAAM,SAAS,oBAAI,QAAuB;AAGnC,IAAM,YAAY,CAAC,UAAqC,OAAO,IAAI,KAAK;AAExE,IAAM,aAAa,CAAC,OAAe,UAAuB;AAC/D,SAAO,IAAI,OAAO,KAAK;AACzB;","names":[]}
|
|
@@ -30,16 +30,70 @@ var import_trace_store = require("./trace-store.cjs");
|
|
|
30
30
|
var STEP_BUDGET_MS = 50;
|
|
31
31
|
var now = () => typeof performance === "object" && typeof performance?.now === "function" ? performance.now() : Date.now();
|
|
32
32
|
var SEEN = "tracer";
|
|
33
|
+
var work = /* @__PURE__ */ new WeakMap();
|
|
34
|
+
function workOf(table) {
|
|
35
|
+
let w = work.get(table);
|
|
36
|
+
if (!w) {
|
|
37
|
+
w = { jobs: /* @__PURE__ */ new Map(), slots: /* @__PURE__ */ new Map() };
|
|
38
|
+
work.set(table, w);
|
|
39
|
+
}
|
|
40
|
+
return w;
|
|
41
|
+
}
|
|
42
|
+
function hint(emit, w, feature, stage, ms, rows, sliced) {
|
|
43
|
+
let slot;
|
|
44
|
+
let top = ["", 0];
|
|
45
|
+
for (const [where, t] of w.slots) {
|
|
46
|
+
if (t[1] > top[1]) {
|
|
47
|
+
slot = where;
|
|
48
|
+
top = t;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
w.slots.clear();
|
|
52
|
+
if (ms <= STEP_BUDGET_MS) return;
|
|
53
|
+
emit("DK_PERF_HINT", {
|
|
54
|
+
feature,
|
|
55
|
+
stage,
|
|
56
|
+
ms,
|
|
57
|
+
rows,
|
|
58
|
+
budget: STEP_BUDGET_MS,
|
|
59
|
+
...sliced ? { sliced } : {},
|
|
60
|
+
// Named when it took at least half of the time.
|
|
61
|
+
...slot && top[1] * 2 >= ms ? { slot, column: top[0], share: Math.min(1, top[1] / ms) } : {}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function onSlice(base, feature, ms) {
|
|
65
|
+
const table = base.table;
|
|
66
|
+
const trace = (0, import_trace_store.readTrace)(table);
|
|
67
|
+
if (!trace) return;
|
|
68
|
+
const w = workOf(table);
|
|
69
|
+
const job = w.jobs.get(feature);
|
|
70
|
+
if (job) job.ms += ms;
|
|
71
|
+
hint(base.emit, w, feature, job?.stage ?? "", ms, trace.steps.get(feature)?.input ?? 0, true);
|
|
72
|
+
}
|
|
33
73
|
function tracer() {
|
|
34
74
|
(0, import_catalog.keepDiagnostics)(import_perf_codes.perfDiagnostics);
|
|
75
|
+
import_trace_store.sliceTimer.current = onSlice;
|
|
35
76
|
return {
|
|
36
77
|
name: "tracer",
|
|
37
|
-
on: ["pipeline:run", "pipeline:step", "state:dispatch"],
|
|
78
|
+
on: ["pipeline:run", "pipeline:step", "state:dispatch", "column:batch"],
|
|
79
|
+
// column:batch: one timing per batch of a column, never per cell (§3.4).
|
|
80
|
+
hot: true,
|
|
38
81
|
intercept(ctx, next) {
|
|
39
82
|
const table = ctx.table;
|
|
40
83
|
if (ctx.meta.has(SEEN) || typeof table !== "object" || table === null) return next();
|
|
41
84
|
ctx.meta.set(SEEN, true);
|
|
42
85
|
const start = now();
|
|
86
|
+
if (ctx.op === "column:batch") {
|
|
87
|
+
try {
|
|
88
|
+
return next();
|
|
89
|
+
} finally {
|
|
90
|
+
const slots = workOf(table).slots;
|
|
91
|
+
const t = slots.get(ctx.where);
|
|
92
|
+
const ms = now() - start;
|
|
93
|
+
if (t) t[1] += ms;
|
|
94
|
+
else slots.set(ctx.where, [ctx.input.column, ms]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
43
97
|
if (ctx.op === "pipeline:run") {
|
|
44
98
|
const trace = {
|
|
45
99
|
run: ctx.run,
|
|
@@ -56,25 +110,32 @@ function tracer() {
|
|
|
56
110
|
}
|
|
57
111
|
}
|
|
58
112
|
if (ctx.op === "pipeline:step") {
|
|
113
|
+
const w = workOf(table);
|
|
114
|
+
w.slots.clear();
|
|
59
115
|
const out = next();
|
|
60
|
-
const
|
|
116
|
+
const own = now() - start;
|
|
117
|
+
const { feature, stage, indices } = ctx.input;
|
|
118
|
+
const pending = w.jobs;
|
|
119
|
+
const job = pending.get(feature);
|
|
120
|
+
let ms = own;
|
|
121
|
+
let runs;
|
|
122
|
+
if (ctx.meta.has(import_trace_store.SLICED)) {
|
|
123
|
+
pending.set(feature, { stage, ms: (job?.ms ?? 0) + own, runs: (job?.runs ?? 0) + 1 });
|
|
124
|
+
} else if (job) {
|
|
125
|
+
pending.delete(feature);
|
|
126
|
+
ms += job.ms;
|
|
127
|
+
runs = job.runs + 1;
|
|
128
|
+
}
|
|
61
129
|
const trace = (0, import_trace_store.readTrace)(table);
|
|
62
130
|
if (trace?.run === ctx.run) {
|
|
63
|
-
trace.steps.set(
|
|
64
|
-
input:
|
|
131
|
+
trace.steps.set(feature, {
|
|
132
|
+
input: indices.length,
|
|
65
133
|
output: out.length,
|
|
66
|
-
ms
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
if (ms > STEP_BUDGET_MS) {
|
|
70
|
-
ctx.emit("DK_PERF_HINT", {
|
|
71
|
-
feature: ctx.input.feature,
|
|
72
|
-
stage: ctx.input.stage,
|
|
73
134
|
ms,
|
|
74
|
-
|
|
75
|
-
budget: STEP_BUDGET_MS
|
|
135
|
+
...runs ? { runs } : {}
|
|
76
136
|
});
|
|
77
137
|
}
|
|
138
|
+
hint(ctx.emit, w, feature, stage, own, indices.length, false);
|
|
78
139
|
return out;
|
|
79
140
|
}
|
|
80
141
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/tracer.ts"],"sourcesContent":["import type { Interceptor } from '../intercept/define'\nimport { perfDiagnostics } from '../pipeline/perf-codes'\nimport { keepDiagnostics } from './catalog'\nimport { readTrace, type Trace, writeTrace } from './trace-store'\n\n/**\n * The longest a single step may take (ms): §9.3's longest-task budget. A traced step over it\n * reports `DK_PERF_HINT` (§8.3) with its numbers.\n */\nexport const STEP_BUDGET_MS = 50\n\ntype Traced = 'pipeline:run' | 'pipeline:step' | 'state:dispatch'\n\nconst now = (): number =>\n typeof performance === 'object' && typeof performance?.now === 'function'\n ? performance.now()\n : Date.now()\n\n/** Set on the ctx by the first tracer of a chain: a second one (user + built-in) only passes on. */\nconst SEEN = 'tracer'\n\n/**\n * The `tracer` interceptor (§3.4): timings and per-step counts of the last pipeline run, read by\n * `table.explain()`. `createTable` installs it in development; in production it is off (§8.2) and\n * opt-in with `createTable({ interceptors: [tracer()] })`. Keeps the last run only: counts, never\n * rows.\n */\nexport function tracer(): Interceptor<Traced> {\n // Inside the factory: a bundle that never creates a tracer drops the definition (§9.1).\n keepDiagnostics(perfDiagnostics)\n return {\n name: 'tracer',\n on: ['pipeline:run', 'pipeline:step', 'state:dispatch'],\n intercept(ctx, next) {\n const table = ctx.table\n if (ctx.meta.has(SEEN) || typeof table !== 'object' || table === null) return next()\n ctx.meta.set(SEEN, true)\n const start = now()\n if (ctx.op === 'pipeline:run') {\n const trace: Trace = {\n run: ctx.run,\n trigger: ctx.trigger,\n ms: undefined,\n steps: new Map(),\n dispatch: readTrace(table)?.dispatch,\n }\n writeTrace(table, trace)\n try {\n return next()\n } finally {\n trace.ms = now() - start\n }\n }\n if (ctx.op === 'pipeline:step') {\n const out = next() as Uint32Array\n const
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/tracer.ts"],"sourcesContent":["import type { ContextBase } from '../intercept/context'\nimport type { Interceptor } from '../intercept/define'\nimport { perfDiagnostics } from '../pipeline/perf-codes'\nimport { keepDiagnostics } from './catalog'\nimport { readTrace, SLICED, sliceTimer, type Trace, writeTrace } from './trace-store'\nimport type { Emit } from './types'\n\n/**\n * The longest a single step may take (ms): §9.3's longest-task budget. A traced step over it\n * reports `DK_PERF_HINT` (§8.3) with its numbers.\n */\nexport const STEP_BUDGET_MS = 50\n\ntype Traced = 'pipeline:run' | 'pipeline:step' | 'state:dispatch' | 'column:batch'\n\nconst now = (): number =>\n typeof performance === 'object' && typeof performance?.now === 'function'\n ? performance.now()\n : Date.now()\n\n/** Set on the ctx by the first tracer of a chain: a second one (user + built-in) only passes on. */\nconst SEEN = 'tracer'\n\n/**\n * Per table, across runs (#61): a step waiting on time-sliced work, by feature: its stage, the\n * time of its runs so far plus the slices since, and how many runs. The run that gets the result\n * adds its own time and shows the total.\n */\ntype Job = { stage: string; ms: number; runs: number }\n\n/**\n * Per table: its steps waiting on sliced work, and the time of each `column:batch` slot\n * (`columns[0].format` → column id, ms) since the current step or slice began (#62).\n */\ntype Work = { jobs: Map<string, Job>; slots: Map<string, [string, number]> }\nconst work = new WeakMap<object, Work>()\n\nfunction workOf(table: object): Work {\n let w = work.get(table)\n if (!w) {\n w = { jobs: new Map(), slots: new Map() }\n work.set(table, w)\n }\n return w\n}\n\n/** DK_PERF_HINT for a task over the budget, with the slot most of its time went to (#62). */\nfunction hint(\n emit: Emit,\n w: Work,\n feature: string,\n stage: string,\n ms: number,\n rows: number,\n sliced: boolean,\n) {\n let slot: string | undefined\n let top: [string, number] = ['', 0]\n for (const [where, t] of w.slots) {\n if (t[1] > top[1]) {\n slot = where\n top = t\n }\n }\n w.slots.clear()\n if (ms <= STEP_BUDGET_MS) return\n emit('DK_PERF_HINT', {\n feature,\n stage,\n ms,\n rows,\n budget: STEP_BUDGET_MS,\n ...(sliced ? { sliced } : {}),\n // Named when it took at least half of the time.\n ...(slot && top[1] * 2 >= ms ? { slot, column: top[0], share: Math.min(1, top[1] / ms) } : {}),\n })\n}\n\n/** A slice of time-sliced work, run between pipeline runs (#61): its own task. */\nfunction onSlice(base: ContextBase, feature: string, ms: number) {\n const table = base.table as object\n const trace = readTrace(table)\n // Only for tables this tracer (or another) traces.\n if (!trace) return\n const w = workOf(table)\n const job = w.jobs.get(feature)\n if (job) job.ms += ms\n hint(base.emit, w, feature, job?.stage ?? '', ms, trace.steps.get(feature)?.input ?? 0, true)\n}\n\n/**\n * The `tracer` interceptor (§3.4): timings and per-step counts of the last pipeline run, read by\n * `table.explain()`. `createTable` installs it in development; in production it is off (§8.2) and\n * opt-in with `createTable({ interceptors: [tracer()] })`. Keeps the last run only: counts, never\n * rows. Time-sliced work (§9.1) is added to the step that waited on it (#61).\n */\nexport function tracer(): Interceptor<Traced> {\n // Inside the factory: a bundle that never creates a tracer drops the definition (§9.1).\n keepDiagnostics(perfDiagnostics)\n sliceTimer.current = onSlice\n return {\n name: 'tracer',\n on: ['pipeline:run', 'pipeline:step', 'state:dispatch', 'column:batch'],\n // column:batch: one timing per batch of a column, never per cell (§3.4).\n hot: true,\n intercept(ctx, next) {\n const table = ctx.table\n if (ctx.meta.has(SEEN) || typeof table !== 'object' || table === null) return next()\n ctx.meta.set(SEEN, true)\n const start = now()\n if (ctx.op === 'column:batch') {\n try {\n return next()\n } finally {\n const slots = workOf(table).slots\n const t = slots.get(ctx.where)\n const ms = now() - start\n if (t) t[1] += ms\n else slots.set(ctx.where, [ctx.input.column, ms])\n }\n }\n if (ctx.op === 'pipeline:run') {\n const trace: Trace = {\n run: ctx.run,\n trigger: ctx.trigger,\n ms: undefined,\n steps: new Map(),\n dispatch: readTrace(table)?.dispatch,\n }\n writeTrace(table, trace)\n try {\n return next()\n } finally {\n trace.ms = now() - start\n }\n }\n if (ctx.op === 'pipeline:step') {\n const w = workOf(table)\n w.slots.clear()\n const out = next() as Uint32Array\n const own = now() - start\n const { feature, stage, indices } = ctx.input\n const pending = w.jobs\n const job = pending.get(feature)\n let ms = own\n let runs: number | undefined\n if (ctx.meta.has(SLICED)) {\n // Its result comes in a later run: that run shows the total.\n pending.set(feature, { stage, ms: (job?.ms ?? 0) + own, runs: (job?.runs ?? 0) + 1 })\n } else if (job) {\n pending.delete(feature)\n ms += job.ms\n runs = job.runs + 1\n }\n const trace = readTrace(table)\n if (trace?.run === ctx.run) {\n trace.steps.set(feature, {\n input: indices.length,\n output: out.length,\n ms,\n ...(runs ? { runs } : {}),\n })\n }\n // One task: the step's own time.\n hint(ctx.emit, w, feature, stage, own, indices.length, false)\n return out\n }\n try {\n return next()\n } finally {\n const dispatch = { where: ctx.where, ms: now() - start }\n const trace = readTrace(table)\n // Before the first (lazy) run: kept on an empty trace until a run carries it over.\n if (trace) trace.dispatch = dispatch\n else {\n writeTrace(table, {\n run: ctx.run,\n trigger: ctx.trigger,\n ms: undefined,\n steps: new Map(),\n dispatch,\n })\n }\n }\n },\n } as Interceptor<Traced>\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,20 +1,74 @@
|
|
|
1
1
|
// src/diagnostics/tracer.ts
|
|
2
2
|
import { perfDiagnostics } from "../pipeline/perf-codes.js";
|
|
3
3
|
import { keepDiagnostics } from "./catalog.js";
|
|
4
|
-
import { readTrace, writeTrace } from "./trace-store.js";
|
|
4
|
+
import { readTrace, SLICED, sliceTimer, writeTrace } from "./trace-store.js";
|
|
5
5
|
var STEP_BUDGET_MS = 50;
|
|
6
6
|
var now = () => typeof performance === "object" && typeof performance?.now === "function" ? performance.now() : Date.now();
|
|
7
7
|
var SEEN = "tracer";
|
|
8
|
+
var work = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function workOf(table) {
|
|
10
|
+
let w = work.get(table);
|
|
11
|
+
if (!w) {
|
|
12
|
+
w = { jobs: /* @__PURE__ */ new Map(), slots: /* @__PURE__ */ new Map() };
|
|
13
|
+
work.set(table, w);
|
|
14
|
+
}
|
|
15
|
+
return w;
|
|
16
|
+
}
|
|
17
|
+
function hint(emit, w, feature, stage, ms, rows, sliced) {
|
|
18
|
+
let slot;
|
|
19
|
+
let top = ["", 0];
|
|
20
|
+
for (const [where, t] of w.slots) {
|
|
21
|
+
if (t[1] > top[1]) {
|
|
22
|
+
slot = where;
|
|
23
|
+
top = t;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
w.slots.clear();
|
|
27
|
+
if (ms <= STEP_BUDGET_MS) return;
|
|
28
|
+
emit("DK_PERF_HINT", {
|
|
29
|
+
feature,
|
|
30
|
+
stage,
|
|
31
|
+
ms,
|
|
32
|
+
rows,
|
|
33
|
+
budget: STEP_BUDGET_MS,
|
|
34
|
+
...sliced ? { sliced } : {},
|
|
35
|
+
// Named when it took at least half of the time.
|
|
36
|
+
...slot && top[1] * 2 >= ms ? { slot, column: top[0], share: Math.min(1, top[1] / ms) } : {}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function onSlice(base, feature, ms) {
|
|
40
|
+
const table = base.table;
|
|
41
|
+
const trace = readTrace(table);
|
|
42
|
+
if (!trace) return;
|
|
43
|
+
const w = workOf(table);
|
|
44
|
+
const job = w.jobs.get(feature);
|
|
45
|
+
if (job) job.ms += ms;
|
|
46
|
+
hint(base.emit, w, feature, job?.stage ?? "", ms, trace.steps.get(feature)?.input ?? 0, true);
|
|
47
|
+
}
|
|
8
48
|
function tracer() {
|
|
9
49
|
keepDiagnostics(perfDiagnostics);
|
|
50
|
+
sliceTimer.current = onSlice;
|
|
10
51
|
return {
|
|
11
52
|
name: "tracer",
|
|
12
|
-
on: ["pipeline:run", "pipeline:step", "state:dispatch"],
|
|
53
|
+
on: ["pipeline:run", "pipeline:step", "state:dispatch", "column:batch"],
|
|
54
|
+
// column:batch: one timing per batch of a column, never per cell (§3.4).
|
|
55
|
+
hot: true,
|
|
13
56
|
intercept(ctx, next) {
|
|
14
57
|
const table = ctx.table;
|
|
15
58
|
if (ctx.meta.has(SEEN) || typeof table !== "object" || table === null) return next();
|
|
16
59
|
ctx.meta.set(SEEN, true);
|
|
17
60
|
const start = now();
|
|
61
|
+
if (ctx.op === "column:batch") {
|
|
62
|
+
try {
|
|
63
|
+
return next();
|
|
64
|
+
} finally {
|
|
65
|
+
const slots = workOf(table).slots;
|
|
66
|
+
const t = slots.get(ctx.where);
|
|
67
|
+
const ms = now() - start;
|
|
68
|
+
if (t) t[1] += ms;
|
|
69
|
+
else slots.set(ctx.where, [ctx.input.column, ms]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
18
72
|
if (ctx.op === "pipeline:run") {
|
|
19
73
|
const trace = {
|
|
20
74
|
run: ctx.run,
|
|
@@ -31,25 +85,32 @@ function tracer() {
|
|
|
31
85
|
}
|
|
32
86
|
}
|
|
33
87
|
if (ctx.op === "pipeline:step") {
|
|
88
|
+
const w = workOf(table);
|
|
89
|
+
w.slots.clear();
|
|
34
90
|
const out = next();
|
|
35
|
-
const
|
|
91
|
+
const own = now() - start;
|
|
92
|
+
const { feature, stage, indices } = ctx.input;
|
|
93
|
+
const pending = w.jobs;
|
|
94
|
+
const job = pending.get(feature);
|
|
95
|
+
let ms = own;
|
|
96
|
+
let runs;
|
|
97
|
+
if (ctx.meta.has(SLICED)) {
|
|
98
|
+
pending.set(feature, { stage, ms: (job?.ms ?? 0) + own, runs: (job?.runs ?? 0) + 1 });
|
|
99
|
+
} else if (job) {
|
|
100
|
+
pending.delete(feature);
|
|
101
|
+
ms += job.ms;
|
|
102
|
+
runs = job.runs + 1;
|
|
103
|
+
}
|
|
36
104
|
const trace = readTrace(table);
|
|
37
105
|
if (trace?.run === ctx.run) {
|
|
38
|
-
trace.steps.set(
|
|
39
|
-
input:
|
|
106
|
+
trace.steps.set(feature, {
|
|
107
|
+
input: indices.length,
|
|
40
108
|
output: out.length,
|
|
41
|
-
ms
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
if (ms > STEP_BUDGET_MS) {
|
|
45
|
-
ctx.emit("DK_PERF_HINT", {
|
|
46
|
-
feature: ctx.input.feature,
|
|
47
|
-
stage: ctx.input.stage,
|
|
48
109
|
ms,
|
|
49
|
-
|
|
50
|
-
budget: STEP_BUDGET_MS
|
|
110
|
+
...runs ? { runs } : {}
|
|
51
111
|
});
|
|
52
112
|
}
|
|
113
|
+
hint(ctx.emit, w, feature, stage, own, indices.length, false);
|
|
53
114
|
return out;
|
|
54
115
|
}
|
|
55
116
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/tracer.ts"],"sourcesContent":["import type { Interceptor } from '../intercept/define'\nimport { perfDiagnostics } from '../pipeline/perf-codes'\nimport { keepDiagnostics } from './catalog'\nimport { readTrace, type Trace, writeTrace } from './trace-store'\n\n/**\n * The longest a single step may take (ms): §9.3's longest-task budget. A traced step over it\n * reports `DK_PERF_HINT` (§8.3) with its numbers.\n */\nexport const STEP_BUDGET_MS = 50\n\ntype Traced = 'pipeline:run' | 'pipeline:step' | 'state:dispatch'\n\nconst now = (): number =>\n typeof performance === 'object' && typeof performance?.now === 'function'\n ? performance.now()\n : Date.now()\n\n/** Set on the ctx by the first tracer of a chain: a second one (user + built-in) only passes on. */\nconst SEEN = 'tracer'\n\n/**\n * The `tracer` interceptor (§3.4): timings and per-step counts of the last pipeline run, read by\n * `table.explain()`. `createTable` installs it in development; in production it is off (§8.2) and\n * opt-in with `createTable({ interceptors: [tracer()] })`. Keeps the last run only: counts, never\n * rows.\n */\nexport function tracer(): Interceptor<Traced> {\n // Inside the factory: a bundle that never creates a tracer drops the definition (§9.1).\n keepDiagnostics(perfDiagnostics)\n return {\n name: 'tracer',\n on: ['pipeline:run', 'pipeline:step', 'state:dispatch'],\n intercept(ctx, next) {\n const table = ctx.table\n if (ctx.meta.has(SEEN) || typeof table !== 'object' || table === null) return next()\n ctx.meta.set(SEEN, true)\n const start = now()\n if (ctx.op === 'pipeline:run') {\n const trace: Trace = {\n run: ctx.run,\n trigger: ctx.trigger,\n ms: undefined,\n steps: new Map(),\n dispatch: readTrace(table)?.dispatch,\n }\n writeTrace(table, trace)\n try {\n return next()\n } finally {\n trace.ms = now() - start\n }\n }\n if (ctx.op === 'pipeline:step') {\n const out = next() as Uint32Array\n const
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/tracer.ts"],"sourcesContent":["import type { ContextBase } from '../intercept/context'\nimport type { Interceptor } from '../intercept/define'\nimport { perfDiagnostics } from '../pipeline/perf-codes'\nimport { keepDiagnostics } from './catalog'\nimport { readTrace, SLICED, sliceTimer, type Trace, writeTrace } from './trace-store'\nimport type { Emit } from './types'\n\n/**\n * The longest a single step may take (ms): §9.3's longest-task budget. A traced step over it\n * reports `DK_PERF_HINT` (§8.3) with its numbers.\n */\nexport const STEP_BUDGET_MS = 50\n\ntype Traced = 'pipeline:run' | 'pipeline:step' | 'state:dispatch' | 'column:batch'\n\nconst now = (): number =>\n typeof performance === 'object' && typeof performance?.now === 'function'\n ? performance.now()\n : Date.now()\n\n/** Set on the ctx by the first tracer of a chain: a second one (user + built-in) only passes on. */\nconst SEEN = 'tracer'\n\n/**\n * Per table, across runs (#61): a step waiting on time-sliced work, by feature: its stage, the\n * time of its runs so far plus the slices since, and how many runs. The run that gets the result\n * adds its own time and shows the total.\n */\ntype Job = { stage: string; ms: number; runs: number }\n\n/**\n * Per table: its steps waiting on sliced work, and the time of each `column:batch` slot\n * (`columns[0].format` → column id, ms) since the current step or slice began (#62).\n */\ntype Work = { jobs: Map<string, Job>; slots: Map<string, [string, number]> }\nconst work = new WeakMap<object, Work>()\n\nfunction workOf(table: object): Work {\n let w = work.get(table)\n if (!w) {\n w = { jobs: new Map(), slots: new Map() }\n work.set(table, w)\n }\n return w\n}\n\n/** DK_PERF_HINT for a task over the budget, with the slot most of its time went to (#62). */\nfunction hint(\n emit: Emit,\n w: Work,\n feature: string,\n stage: string,\n ms: number,\n rows: number,\n sliced: boolean,\n) {\n let slot: string | undefined\n let top: [string, number] = ['', 0]\n for (const [where, t] of w.slots) {\n if (t[1] > top[1]) {\n slot = where\n top = t\n }\n }\n w.slots.clear()\n if (ms <= STEP_BUDGET_MS) return\n emit('DK_PERF_HINT', {\n feature,\n stage,\n ms,\n rows,\n budget: STEP_BUDGET_MS,\n ...(sliced ? { sliced } : {}),\n // Named when it took at least half of the time.\n ...(slot && top[1] * 2 >= ms ? { slot, column: top[0], share: Math.min(1, top[1] / ms) } : {}),\n })\n}\n\n/** A slice of time-sliced work, run between pipeline runs (#61): its own task. */\nfunction onSlice(base: ContextBase, feature: string, ms: number) {\n const table = base.table as object\n const trace = readTrace(table)\n // Only for tables this tracer (or another) traces.\n if (!trace) return\n const w = workOf(table)\n const job = w.jobs.get(feature)\n if (job) job.ms += ms\n hint(base.emit, w, feature, job?.stage ?? '', ms, trace.steps.get(feature)?.input ?? 0, true)\n}\n\n/**\n * The `tracer` interceptor (§3.4): timings and per-step counts of the last pipeline run, read by\n * `table.explain()`. `createTable` installs it in development; in production it is off (§8.2) and\n * opt-in with `createTable({ interceptors: [tracer()] })`. Keeps the last run only: counts, never\n * rows. Time-sliced work (§9.1) is added to the step that waited on it (#61).\n */\nexport function tracer(): Interceptor<Traced> {\n // Inside the factory: a bundle that never creates a tracer drops the definition (§9.1).\n keepDiagnostics(perfDiagnostics)\n sliceTimer.current = onSlice\n return {\n name: 'tracer',\n on: ['pipeline:run', 'pipeline:step', 'state:dispatch', 'column:batch'],\n // column:batch: one timing per batch of a column, never per cell (§3.4).\n hot: true,\n intercept(ctx, next) {\n const table = ctx.table\n if (ctx.meta.has(SEEN) || typeof table !== 'object' || table === null) return next()\n ctx.meta.set(SEEN, true)\n const start = now()\n if (ctx.op === 'column:batch') {\n try {\n return next()\n } finally {\n const slots = workOf(table).slots\n const t = slots.get(ctx.where)\n const ms = now() - start\n if (t) t[1] += ms\n else slots.set(ctx.where, [ctx.input.column, ms])\n }\n }\n if (ctx.op === 'pipeline:run') {\n const trace: Trace = {\n run: ctx.run,\n trigger: ctx.trigger,\n ms: undefined,\n steps: new Map(),\n dispatch: readTrace(table)?.dispatch,\n }\n writeTrace(table, trace)\n try {\n return next()\n } finally {\n trace.ms = now() - start\n }\n }\n if (ctx.op === 'pipeline:step') {\n const w = workOf(table)\n w.slots.clear()\n const out = next() as Uint32Array\n const own = now() - start\n const { feature, stage, indices } = ctx.input\n const pending = w.jobs\n const job = pending.get(feature)\n let ms = own\n let runs: number | undefined\n if (ctx.meta.has(SLICED)) {\n // Its result comes in a later run: that run shows the total.\n pending.set(feature, { stage, ms: (job?.ms ?? 0) + own, runs: (job?.runs ?? 0) + 1 })\n } else if (job) {\n pending.delete(feature)\n ms += job.ms\n runs = job.runs + 1\n }\n const trace = readTrace(table)\n if (trace?.run === ctx.run) {\n trace.steps.set(feature, {\n input: indices.length,\n output: out.length,\n ms,\n ...(runs ? { runs } : {}),\n })\n }\n // One task: the step's own time.\n hint(ctx.emit, w, feature, stage, own, indices.length, false)\n return out\n }\n try {\n return next()\n } finally {\n const dispatch = { where: ctx.where, ms: now() - start }\n const trace = readTrace(table)\n // Before the first (lazy) run: kept on an empty trace until a run carries it over.\n if (trace) trace.dispatch = dispatch\n else {\n writeTrace(table, {\n run: ctx.run,\n trigger: ctx.trigger,\n ms: undefined,\n steps: new Map(),\n dispatch,\n })\n }\n }\n },\n } as Interceptor<Traced>\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/diagnostics/types.ts"],"sourcesContent":["import type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type DiagnosticLevel = 'error' | 'warn' | 'info'\n\nexport type Trigger = 'init' | 'data-change' | 'setOptions' | 'hydrate' | 'fetch' | `api:${string}`\n\nexport type Phase = 'ssr' | 'hydrate' | 'client'\n\n/**\n * Where in the developer's code or data (§8.1). `interceptor` and `operation` are additions for\n * plugin-level codes the spec table does not locate (interceptors, op registry).\n */\nexport type Location =\n | { kind: 'column'; index: number; id: string }\n | { kind: 'feature'; name: string }\n | { kind: 'option'; path: string }\n | { kind: 'row'; index: number; key?: string }\n | { kind: 'interceptor'; name: string; op: string }\n | { kind: 'operation'; op: string; slot?: string }\n\n/** A summary of table state — never raw input (§8.1). */\nexport interface DiagnosticStateSummary {\n searchLength: number\n activeFilters: string[]\n sort: string[]\n page: number\n}\n\nexport interface DiagnosticContext {\n /** `options.name`, else `table#n`. */\n table: string\n /** Dev: creation site, e.g. `UsersPage.tsx:42`. */\n createdAt?: string\n /** `@datakable/core@x.y.z` */\n package: string\n trigger: Trigger\n /** Pipeline run id — joins explain() and other diagnostics. */\n run: number\n phase: Phase\n mode: Record<string, 'client' | 'server'>\n state: DiagnosticStateSummary\n occurrences: { count: number; first: number; last: number }\n /** e.g. `DK_SCHEMA_INVALID_ROW#run57` */\n causedBy?: string\n /** Original error (ES2022 cause). Reaches `onDiagnostic` only; `toJSON` and the console leave it out. */\n cause?: unknown\n}\n\nexport interface Diagnostic {\n /** Stable public API; never reused or redefined. */\n code: DiagnosticCode\n level: DiagnosticLevel\n /** What happened, instance-specific. Empty in production unless messages are opted in. */\n problem: string\n where: Location\n /** Consequence + the fallback the system applied. */\n impact: string\n /** Concrete fixes, best first. */\n fix: string[]\n /** `docs/errors.md#<code>` (+ query params in production). */\n docs: string\n /** The catalog details. Redaction (T26) rewrites them before any sink sees them. */\n details: Record<string, unknown>\n context: DiagnosticContext\n /** `code@where` — grouping key (dedupe, Sentry). */\n fingerprint: string\n /** Safe serializer: bigint, cycles and Errors never throw; `context.cause` is left out. */\n toJSON(): object\n}\n\n/** Per-call additions to the live context. */\nexport interface EmitOptions {\n cause?: unknown\n causedBy?: string\n run?: number\n trigger?: Trigger\n phase?: Phase\n}\n\n/** Emits a catalog diagnostic; `details` is typed by `code` (see `DiagnosticCatalog`). */\nexport type Emit = <C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n) => void\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/diagnostics/types.ts"],"sourcesContent":["import type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type { DiagnosticCode, DiagnosticDetails } from './catalog'\n\nexport type DiagnosticLevel = 'error' | 'warn' | 'info'\n\nexport type Trigger = 'init' | 'data-change' | 'setOptions' | 'hydrate' | 'fetch' | `api:${string}`\n\nexport type Phase = 'ssr' | 'hydrate' | 'client'\n\n/**\n * Where in the developer's code or data (§8.1). `interceptor` and `operation` are additions for\n * plugin-level codes the spec table does not locate (interceptors, op registry).\n */\nexport type Location =\n | { kind: 'column'; index: number; id: string }\n | { kind: 'feature'; name: string }\n | { kind: 'option'; path: string }\n | { kind: 'row'; index: number; key?: string }\n | { kind: 'interceptor'; name: string; op: string }\n | { kind: 'operation'; op: string; slot?: string }\n\n/** A summary of table state — never raw input (§8.1). */\nexport interface DiagnosticStateSummary {\n searchLength: number\n activeFilters: string[]\n sort: string[]\n page: number\n}\n\nexport interface DiagnosticContext {\n /** `options.name`, else `table#n`. */\n table: string\n /** Dev: creation site, e.g. `UsersPage.tsx:42`. */\n createdAt?: string\n /** `@datakable/core@x.y.z` */\n package: string\n trigger: Trigger\n /** Pipeline run id — joins explain() and other diagnostics. */\n run: number\n phase: Phase\n mode: Record<string, 'client' | 'server'>\n state: DiagnosticStateSummary\n occurrences: { count: number; first: number; last: number }\n /** e.g. `DK_SCHEMA_INVALID_ROW#run57` */\n causedBy?: string\n /** Original error (ES2022 cause). Reaches `onDiagnostic` only; `toJSON` and the console leave it out. */\n cause?: unknown\n}\n\nexport interface Diagnostic {\n /** Stable public API; never reused or redefined. */\n code: DiagnosticCode\n level: DiagnosticLevel\n /** What happened, instance-specific. Empty in production unless messages are opted in. */\n problem: string\n where: Location\n /** Consequence + the fallback the system applied. */\n impact: string\n /** Concrete fixes, best first. */\n fix: string[]\n /** `docs/errors.md#<code>` (+ query params in production). */\n docs: string\n /** The catalog details. Redaction (T26) rewrites them before any sink sees them. */\n details: Record<string, unknown>\n context: DiagnosticContext\n /** `code@where` — grouping key (dedupe, Sentry). */\n fingerprint: string\n /** Safe serializer: bigint, cycles and Errors never throw; `context.cause` is left out. */\n toJSON(): object\n}\n\n/** Per-call additions to the live context. */\nexport interface EmitOptions {\n cause?: unknown\n causedBy?: string\n run?: number\n trigger?: Trigger\n phase?: Phase\n /**\n * Not printed by the default console sink (another table printed it in this process, #38). Still\n * stored and delivered to `onDiagnostic`.\n */\n quiet?: boolean\n}\n\n/** Emits a catalog diagnostic; `details` is typed by `code` (see `DiagnosticCatalog`). */\nexport type Emit = <C extends DiagnosticCode>(\n code: C,\n details: DiagnosticDetails<C>,\n options?: EmitOptions,\n) => void\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|