Skip to content

feat(core): .inspect() redact (ADR 0018) + .report() result object (ADR 0019)#342

Merged
rejifald merged 5 commits into
mainfrom
feat/adr-0018-inspect-raw-redaction
Jun 30, 2026
Merged

feat(core): .inspect() redact (ADR 0018) + .report() result object (ADR 0019)#342
rejifald merged 5 commits into
mainfrom
feat/adr-0018-inspect-raw-redaction

Conversation

@rejifald

@rejifald rejifald commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Implements ADR 0018 and ADR 0019 — two of the four deferrals from ADR 0016 (.inspect()/raw/findings, merged in #334). ADR 0019's branch (#343) was stacked on this one and merged into it, so both land together here; #343 is not in main independently. Supersedes the docs-only #337.

Scope note (updated): the original PR described only ADR 0018. It now also carries ADR 0019 because #343 was merged into this branch rather than retargeted to main. The two ADRs are designed to interlock (0019's RunReport inherits 0018's redacted/non-enumerable raw), so they ship as one.

ADR 0018 — opt-in redact for .inspect()'s raw

Adds InspectOptions.redact (default off) to scrub secret-named fields from raw before logging/forwarding. Opt-in by design: raw exists to catch a stray token in an undeclared field, and a name-based redactor can't catch a secret in an innocuously-named field — so redaction is a safe-sharing convenience, never a leak guarantee. Defaulting it on would create false confidence.

  • rename the secret-key predicate isSecretQueryKeyisSecretKey, keep isSecretQueryKey as an alias (URL scrubbers/auth unchanged)
  • add redactSecretsDeep(value, extra?): deep-clone, replace keys matching the shared denylist (or caller extra patterns via matchPath) with 'REDACTED'; never mutates input
  • wire into .inspect() after findings are computed (findings run on the unredacted body — safe, detailFor emits kinds only, never values)
  • export isSecretKey + redactSecretsDeep. No engine change.

ADR 0019 — enhanced result object (.report()) + the source discriminator

  • source: 'live' | 'cache' | 'stream' on Inspection<T> — the interpretant of raw (explains why raw is null). Free: derived from the existing event spine.
  • .report()RunReport<T> extends Inspection<T> — the inspection plus run diagnostics: attempts, timing ({ ms, waited? }), the resolved+redacted config (the __config projection, never __rawConfig), and a fine-grained cache outcome. Never throws; a network probe like .inspect(). Built entirely off the spine — no new engine events.

Merged with main + verification

Rebased onto current main and resolved its freshly-landed contract renames against this work:

  • P5 (valuedata on result envelopes): Inspection keeps canonical data + @deprecated value; drainRun reads ev.data.
  • P17 (waited/elapsed duration de-suffix): drainRun reads canonical ev.waited / ev.elapsed (the merge would otherwise have silently read now-deprecated ev.waitedMs / ev.ms); new specs switched off deprecated .value to .data.

Green locally: typecheck (src+tests), 1153 core tests, contract ratchet, lint, exports, type-decls (tsd), format, bundle-size, size-docs.

Bundle budget (deliberate bump)

Both features attach to the core Stitch surface (.report() can't tree-shake), adding ~0.45 KB to the core path → 23.77 / 19.25 KB gzip. Budget raised 23.55→23.95 / 19.0→19.45 KB (tight ~0.2 KB headroom, per the gate's convention), with the rationale recorded in bundle-size.mjs. The whole-entry advertised figure moves ~23 → ~24 kB across the 5 drift-gated files; import { stitch } stays ~19 kB.

🤖 Generated with Claude Code

Add `InspectOptions.redact` (default off) so a consumer can scrub
secret-named fields from `raw` before logging or forwarding it. Opt-in by
design: `raw` exists to catch a stray token in an *undeclared* field, and a
name-based redactor can't catch a secret in an innocuously-named field — so
redaction is a safe-sharing convenience, never a leak guarantee, and must
not default on (that would create false confidence).

- rename the secret-key predicate isSecretQueryKey -> isSecretKey, keep
  isSecretQueryKey as an alias (URL scrubbers/auth unchanged)
- add redactSecretsDeep(value, extra?) in util.ts: deep-clone, replace keys
  matching the shared denylist (or caller `extra` patterns via matchPath)
  with 'REDACTED'; never mutates input
- wire it into the .inspect() consumer in stitch.ts, AFTER findings are
  computed (findings run on the unredacted body — safe, detailFor emits
  kinds only, never values); no engine change
- export isSecretKey + redactSecretsDeep

Flips ADR 0018 to Accepted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rejifald and others added 4 commits June 29, 2026 09:21
…ADR 0019) (#343)

Add the run-diagnostics surface ADR 0016 held the line against, plus the
`source` discriminator (folds in two of 0016's four deferrals).

- `Inspection<T>` gains `source: 'live' | 'cache' | 'stream'` — the
  interpretant of `raw` (why it is/isn't null). It lands on the minimal
  object (not the report) because it explains an Inspection field; revises
  0016's guess that source belonged on the enhanced object.
- new `.report()` -> `RunReport<T> extends Inspection<T>` adding `attempts`,
  `timing { ms, waited? }`, the redacted `config`, and the `cache` outcome.
  `.inspect()` stays the minimal "raw + drift" probe.

Built entirely from the existing event spine — `attempts`/`done.ms`/
`Σ progress.waitedMs`/the `phase:'cache'` detail — so NO new engine events
(per-attempt latency is deferred). A shared `drainRun` feeds both consumers;
`source` precedence is stream > cache > live. `config` is the already-redacted
`__config`, never `__rawConfig`. Never throws.

Flips ADR 0019 to Accepted.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#343 merged into this branch added the .report() instance method but the
generated playground-completions.generated.ts was not re-run, so the
verify gate's `git diff --exit-code` on the generated file failed.
Pure regeneration via `pnpm --filter @stitchapi/docs gen:completions`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…et for ADR 0018+0019

Resolve main's P5 (value→data) / P17 (waited/elapsed de-suffix) renames
against the ADR 0018 (.inspect redact) + ADR 0019 (.report/source) work:

- Inspection<T>: keep main's canonical `data` (+ @deprecated `value`)
  alongside the ADR 0018 raw-redaction JSDoc and ADR 0019's `source`.
- drainRun/makeInspection: read canonical `ev.data`/`ev.waited`/`ev.elapsed`
  (the *Ms / value aliases are @deprecated post-merge); co-set data+value
  and carry `source`. New specs switch off deprecated `.value` to `.data`.

Bundle: the two features add ~0.45 KB to the core path (both attach to the
core Stitch surface — `.report()` can't tree-shake), reaching 23.77/19.25 KB
gzip. Raise the gate 23.55→23.95 / 19.0→19.45 KB (tight ~0.2 KB headroom per
convention) and move the advertised whole-entry figure ~23→~24 kB across the
5 drift-gated files. import{stitch} stays ~19 kB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rejifald rejifald changed the title feat(core): opt-in redact option for .inspect()'s raw (ADR 0018) feat(core): .inspect() redact (ADR 0018) + .report() result object (ADR 0019) Jun 30, 2026
@rejifald rejifald merged commit f9a99d4 into main Jun 30, 2026
9 checks passed
@rejifald rejifald deleted the feat/adr-0018-inspect-raw-redaction branch June 30, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant