Two additive capabilities so a consumer's richer redactor can de-fork without
regressing:
- `stringMode: 'mask-spans'` replaces only matched substrings (each →
`[REDACTED:<kind>]`), preserving surrounding non-PII context — for an
analyst loop that reads prose. Default stays 'collapse' (whole-string),
byte-identical to before. Exposed standalone as `maskSpans(text, patterns)`,
built on detectSpans so matching/validate/non-overlap are shared.
- cycle guard in the object walk: a circular payload previously recursed
forever; now a re-encountered object/array is returned untouched. Fixes a
latent hang for every consumer.