@jinn-network/core 0.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.
Files changed (111) hide show
  1. package/dist/canonical-json.d.ts +1 -0
  2. package/dist/canonical-json.js +67 -0
  3. package/dist/captured-task.d.ts +140 -0
  4. package/dist/captured-task.js +122 -0
  5. package/dist/contribution-store.d.ts +97 -0
  6. package/dist/contribution-store.js +770 -0
  7. package/dist/corpus-read/acquire.d.ts +39 -0
  8. package/dist/corpus-read/acquire.js +212 -0
  9. package/dist/corpus-read/cache.d.ts +14 -0
  10. package/dist/corpus-read/cache.js +17 -0
  11. package/dist/corpus-read/capture-meta.d.ts +16 -0
  12. package/dist/corpus-read/capture-meta.js +18 -0
  13. package/dist/corpus-read/create-corpus.d.ts +2 -0
  14. package/dist/corpus-read/create-corpus.js +97 -0
  15. package/dist/corpus-read/fetch-artifact.d.ts +21 -0
  16. package/dist/corpus-read/fetch-artifact.js +32 -0
  17. package/dist/corpus-read/fetch.d.ts +12 -0
  18. package/dist/corpus-read/fetch.js +24 -0
  19. package/dist/corpus-read/http-discovery.d.ts +9 -0
  20. package/dist/corpus-read/http-discovery.js +128 -0
  21. package/dist/corpus-read/index.d.ts +10 -0
  22. package/dist/corpus-read/index.js +10 -0
  23. package/dist/corpus-read/ipfs.d.ts +5 -0
  24. package/dist/corpus-read/ipfs.js +72 -0
  25. package/dist/corpus-read/route-resolver.d.ts +16 -0
  26. package/dist/corpus-read/route-resolver.js +19 -0
  27. package/dist/corpus-read/types.d.ts +177 -0
  28. package/dist/corpus-read/types.js +42 -0
  29. package/dist/envelope.d.ts +134 -0
  30. package/dist/envelope.js +162 -0
  31. package/dist/evidence-adapter.d.ts +26 -0
  32. package/dist/evidence-adapter.js +321 -0
  33. package/dist/evidence-filesystem.d.ts +40 -0
  34. package/dist/evidence-filesystem.js +267 -0
  35. package/dist/evidence-index.d.ts +117 -0
  36. package/dist/evidence-index.js +1083 -0
  37. package/dist/evidence-store-lock.d.ts +2 -0
  38. package/dist/evidence-store-lock.js +208 -0
  39. package/dist/execution-envelope.d.ts +3142 -0
  40. package/dist/execution-envelope.js +203 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.js +12 -0
  43. package/dist/manifest.d.ts +75 -0
  44. package/dist/manifest.js +184 -0
  45. package/dist/paired.d.ts +68 -0
  46. package/dist/paired.js +92 -0
  47. package/dist/scrub/build.d.ts +52 -0
  48. package/dist/scrub/build.js +84 -0
  49. package/dist/scrub/emit-scrub.d.ts +41 -0
  50. package/dist/scrub/emit-scrub.js +87 -0
  51. package/dist/scrub/index.d.ts +12 -0
  52. package/dist/scrub/index.js +12 -0
  53. package/dist/scrub/key-policy.d.ts +20 -0
  54. package/dist/scrub/key-policy.js +42 -0
  55. package/dist/scrub/layer2.d.ts +23 -0
  56. package/dist/scrub/layer2.js +32 -0
  57. package/dist/scrub/ml-pii-stage.d.ts +24 -0
  58. package/dist/scrub/ml-pii-stage.js +42 -0
  59. package/dist/scrub/openredaction-stage.d.ts +196 -0
  60. package/dist/scrub/openredaction-stage.js +270 -0
  61. package/dist/scrub/pii-build.d.ts +38 -0
  62. package/dist/scrub/pii-build.js +67 -0
  63. package/dist/scrub/pipeline.d.ts +38 -0
  64. package/dist/scrub/pipeline.js +83 -0
  65. package/dist/scrub/plain-patterns-stage.d.ts +31 -0
  66. package/dist/scrub/plain-patterns-stage.js +84 -0
  67. package/dist/scrub/secretlint-stage.d.ts +36 -0
  68. package/dist/scrub/secretlint-stage.js +249 -0
  69. package/dist/scrub/transformers-detector.d.ts +35 -0
  70. package/dist/scrub/transformers-detector.js +67 -0
  71. package/dist/scrub/types.d.ts +25 -0
  72. package/dist/scrub/types.js +1 -0
  73. package/dist/session-provenance.d.ts +75 -0
  74. package/dist/session-provenance.js +20 -0
  75. package/dist/skill-artifact.d.ts +442 -0
  76. package/dist/skill-artifact.js +136 -0
  77. package/dist/trajectory/hash-chain.d.ts +18 -0
  78. package/dist/trajectory/hash-chain.js +47 -0
  79. package/dist/trajectory/index.d.ts +4 -0
  80. package/dist/trajectory/index.js +4 -0
  81. package/dist/trajectory/schema.d.ts +606 -0
  82. package/dist/trajectory/schema.js +110 -0
  83. package/dist/trajectory/transcript-parsers/aider-history.d.ts +42 -0
  84. package/dist/trajectory/transcript-parsers/aider-history.js +181 -0
  85. package/dist/trajectory/transcript-parsers/claude-code-jsonl.d.ts +37 -0
  86. package/dist/trajectory/transcript-parsers/claude-code-jsonl.js +142 -0
  87. package/dist/trajectory/transcript-parsers/codex-session.d.ts +42 -0
  88. package/dist/trajectory/transcript-parsers/codex-session.js +235 -0
  89. package/dist/trajectory/transcript-parsers/continue-devdata.d.ts +41 -0
  90. package/dist/trajectory/transcript-parsers/continue-devdata.js +190 -0
  91. package/dist/trajectory/transcript-parsers/cursor-sqlite.d.ts +38 -0
  92. package/dist/trajectory/transcript-parsers/cursor-sqlite.js +118 -0
  93. package/dist/trajectory/transcript-parsers/gemini-session.d.ts +35 -0
  94. package/dist/trajectory/transcript-parsers/gemini-session.js +137 -0
  95. package/dist/trajectory/transcript-parsers/index.d.ts +7 -0
  96. package/dist/trajectory/transcript-parsers/index.js +7 -0
  97. package/dist/trajectory/transcript-parsers/types.d.ts +101 -0
  98. package/dist/trajectory/transcript-parsers/types.js +32 -0
  99. package/dist/trajectory/transcript-to-spans/attrs.d.ts +4 -0
  100. package/dist/trajectory/transcript-to-spans/attrs.js +30 -0
  101. package/dist/trajectory/transcript-to-spans/claude-code-stream-json.d.ts +8 -0
  102. package/dist/trajectory/transcript-to-spans/claude-code-stream-json.js +162 -0
  103. package/dist/trajectory/transcript-to-spans/codex-exec-json.d.ts +8 -0
  104. package/dist/trajectory/transcript-to-spans/codex-exec-json.js +253 -0
  105. package/dist/trajectory/transcript-to-spans/index.d.ts +4 -0
  106. package/dist/trajectory/transcript-to-spans/index.js +4 -0
  107. package/dist/trajectory/transcript-to-spans/types.d.ts +18 -0
  108. package/dist/trajectory/transcript-to-spans/types.js +1 -0
  109. package/dist/window.d.ts +12 -0
  110. package/dist/window.js +5 -0
  111. package/package.json +67 -0
@@ -0,0 +1,52 @@
1
+ import { ScrubPipeline } from './pipeline.js';
2
+ import { type KeyPolicy } from './key-policy.js';
3
+ import { type PiiDetector } from './ml-pii-stage.js';
4
+ /**
5
+ * Default key policy. `jinn.*` identity/chain attributes are structural and pass
6
+ * raw. The `drop` tier deletes the spec's stage-1 high-confidence keys outright
7
+ * (auth headers, cookies, env dumps — see `spec/2026-06-15-ts-trajectory-scrub-stack.md`):
8
+ * these never carry sellable content and are never safe to publish. Globs use the
9
+ * trailing-`*` prefix form `classifyKey` supports; the HTTP header keys are listed
10
+ * per request/response direction (a leading `*.header.…` glob is not matched, so
11
+ * we enumerate the concrete keys). Everything else is `content` and flows through
12
+ * the value-scrubbing stages.
13
+ */
14
+ export declare const DEFAULT_KEY_POLICY: KeyPolicy;
15
+ export interface BuildScrubPipelineOptions {
16
+ policy?: KeyPolicy;
17
+ /** When provided, the ML PII (GLiNER) stage is appended. */
18
+ piiDetector?: PiiDetector;
19
+ }
20
+ /**
21
+ * Assembles the seller-side scrub pipeline (cost-ascending): structural key
22
+ * policy → openredaction (structured PII) → secretlint + entropy (secrets) →
23
+ * optional GLiNER (ML PII). The GLiNER stage is added only when a detector is
24
+ * supplied, so the daemon degrades gracefully (regex + secrets still scrub) when
25
+ * the ML model is unavailable.
26
+ */
27
+ export declare function buildScrubPipeline(opts?: BuildScrubPipelineOptions): ScrubPipeline;
28
+ /**
29
+ * Seed-profile scrub pipeline (#1409). Seeds are public, transformed or
30
+ * otherwise human-curated content — not operator trace data — so the
31
+ * probabilistic stages (openredaction, secretlint's pass-2 entropy fallback,
32
+ * ML PII) that exist to catch unknown-shape PII/secrets in private traces are
33
+ * dropped: on prose they false-positive (trigger words, dated env-var slugs,
34
+ * long camelCase identifiers) and deface the corpus. The deterministic
35
+ * detectors stay:
36
+ * structural key policy, plain-patterns (emails, home paths, and — seed-only,
37
+ * #1415 — bare AWS access-key IDs and GCP `AIza…` API keys, deterministic
38
+ * prefix shapes secretlint pass-1 does not cover), and secretlint's pass-1
39
+ * preset rules (AWS secret-key assignments, GitHub / Slack / npm token
40
+ * shapes, GCP service-account JSON). Accepted residual: JWTs and unprefixed
41
+ * high-entropy blobs (trace profile catches them via the entropy fallback),
42
+ * plus every structured identifier or PII class detected only by the omitted
43
+ * openredaction stage, pass unredacted. The latter is a 570+ pattern surface;
44
+ * payment cards, phone numbers, SSNs, medical or health-plan identifiers,
45
+ * government identity documents, and financial account references are
46
+ * examples, not an exhaustive allowlist. This is acceptable only for public
47
+ * seed material that a curator has transformed and reviewed for those classes.
48
+ * The reduced stage list is inspectable locally through the pipeline's
49
+ * `components` surface. `TraceEnvelopeV0` does not publish that list, so a
50
+ * fetched envelope cannot by itself prove which scrub profile ran.
51
+ */
52
+ export declare function buildSeedScrubPipeline(policy?: KeyPolicy): ScrubPipeline;
@@ -0,0 +1,84 @@
1
+ import { ScrubPipeline } from './pipeline.js';
2
+ import { keyPolicyStage } from './key-policy.js';
3
+ import { openredactionStage } from './openredaction-stage.js';
4
+ import { plainPatternsStage } from './plain-patterns-stage.js';
5
+ import { secretlintStage } from './secretlint-stage.js';
6
+ import { mlPiiStage } from './ml-pii-stage.js';
7
+ /**
8
+ * Default key policy. `jinn.*` identity/chain attributes are structural and pass
9
+ * raw. The `drop` tier deletes the spec's stage-1 high-confidence keys outright
10
+ * (auth headers, cookies, env dumps — see `spec/2026-06-15-ts-trajectory-scrub-stack.md`):
11
+ * these never carry sellable content and are never safe to publish. Globs use the
12
+ * trailing-`*` prefix form `classifyKey` supports; the HTTP header keys are listed
13
+ * per request/response direction (a leading `*.header.…` glob is not matched, so
14
+ * we enumerate the concrete keys). Everything else is `content` and flows through
15
+ * the value-scrubbing stages.
16
+ */
17
+ export const DEFAULT_KEY_POLICY = {
18
+ safe: ['jinn.*'],
19
+ drop: [
20
+ 'http.request.header.authorization',
21
+ 'http.response.header.authorization',
22
+ 'http.request.header.cookie',
23
+ 'http.response.header.cookie',
24
+ 'http.request.header.set-cookie',
25
+ 'http.response.header.set-cookie',
26
+ 'env.*',
27
+ ],
28
+ };
29
+ /**
30
+ * Assembles the seller-side scrub pipeline (cost-ascending): structural key
31
+ * policy → openredaction (structured PII) → secretlint + entropy (secrets) →
32
+ * optional GLiNER (ML PII). The GLiNER stage is added only when a detector is
33
+ * supplied, so the daemon degrades gracefully (regex + secrets still scrub) when
34
+ * the ML model is unavailable.
35
+ */
36
+ export function buildScrubPipeline(opts = {}) {
37
+ const policy = opts.policy ?? DEFAULT_KEY_POLICY;
38
+ return new ScrubPipeline(assembleScrubStages(policy, opts.piiDetector));
39
+ }
40
+ function assembleScrubStages(policy, piiDetector) {
41
+ // plain-patterns runs after openredaction: deterministic email/home-path
42
+ // regexes closing gaps the probabilistic stage demonstrably has (#1330).
43
+ const stages = [
44
+ keyPolicyStage(policy),
45
+ openredactionStage(policy),
46
+ plainPatternsStage(policy),
47
+ secretlintStage(policy),
48
+ ];
49
+ if (piiDetector) {
50
+ stages.push(mlPiiStage(policy, piiDetector));
51
+ }
52
+ return stages;
53
+ }
54
+ /**
55
+ * Seed-profile scrub pipeline (#1409). Seeds are public, transformed or
56
+ * otherwise human-curated content — not operator trace data — so the
57
+ * probabilistic stages (openredaction, secretlint's pass-2 entropy fallback,
58
+ * ML PII) that exist to catch unknown-shape PII/secrets in private traces are
59
+ * dropped: on prose they false-positive (trigger words, dated env-var slugs,
60
+ * long camelCase identifiers) and deface the corpus. The deterministic
61
+ * detectors stay:
62
+ * structural key policy, plain-patterns (emails, home paths, and — seed-only,
63
+ * #1415 — bare AWS access-key IDs and GCP `AIza…` API keys, deterministic
64
+ * prefix shapes secretlint pass-1 does not cover), and secretlint's pass-1
65
+ * preset rules (AWS secret-key assignments, GitHub / Slack / npm token
66
+ * shapes, GCP service-account JSON). Accepted residual: JWTs and unprefixed
67
+ * high-entropy blobs (trace profile catches them via the entropy fallback),
68
+ * plus every structured identifier or PII class detected only by the omitted
69
+ * openredaction stage, pass unredacted. The latter is a 570+ pattern surface;
70
+ * payment cards, phone numbers, SSNs, medical or health-plan identifiers,
71
+ * government identity documents, and financial account references are
72
+ * examples, not an exhaustive allowlist. This is acceptable only for public
73
+ * seed material that a curator has transformed and reviewed for those classes.
74
+ * The reduced stage list is inspectable locally through the pipeline's
75
+ * `components` surface. `TraceEnvelopeV0` does not publish that list, so a
76
+ * fetched envelope cannot by itself prove which scrub profile ran.
77
+ */
78
+ export function buildSeedScrubPipeline(policy = DEFAULT_KEY_POLICY) {
79
+ return new ScrubPipeline([
80
+ keyPolicyStage(policy),
81
+ plainPatternsStage(policy, { credentialIds: true }),
82
+ secretlintStage(policy, { entropyFallback: false }),
83
+ ]);
84
+ }
@@ -0,0 +1,41 @@
1
+ import type { RedactionManifest, Span } from '../trajectory/schema.js';
2
+ import type { ScrubPipeline } from './pipeline.js';
3
+ /**
4
+ * Seller-side scrub applied at emit time, before a trajectory is signed and
5
+ * published. Runs every span's attributes (and each event's attributes) through
6
+ * the pipeline, rebuilds the prev-span hash chain over the scrubbed spans, and
7
+ * produces the redaction manifest (one entry per span listing the keys touched;
8
+ * `event.<key>` for event-attribute redactions).
9
+ */
10
+ export declare function scrubSpansForEmit(spans: Span[], pipeline: ScrubPipeline, taskCid: string): Promise<{
11
+ spans: Span[];
12
+ redactionManifest: RedactionManifest;
13
+ }>;
14
+ /**
15
+ * Minimal structural shape of a capture span (jinn.capture-trajectory.v1): a
16
+ * flat attribute bag plus the keys already redacted at ingest. Captures have no
17
+ * events array and no in-run hash chain, so they cannot go through
18
+ * {@link scrubSpansForEmit}; this is their dedicated seller-side scrub.
19
+ */
20
+ export interface ScrubbableCaptureSpan {
21
+ attributes: Record<string, unknown>;
22
+ redactedKeys: string[];
23
+ }
24
+ /**
25
+ * Seller-side scrub for capture spans, applied at publish time before the
26
+ * capture trajectory is uploaded. Runs each span's attributes through the
27
+ * pipeline, replacing them with the scrubbed values, and unions the pipeline's
28
+ * redactions into the span's existing `redactedKeys`. Unlike
29
+ * {@link scrubSpansForEmit} it does NOT touch events or inject a hash chain —
30
+ * capture spans carry neither. Generic over the span type so the concrete
31
+ * `SpanRow` (with its extra columns) is returned unchanged apart from
32
+ * `attributes` and `redactedKeys`.
33
+ */
34
+ export declare function scrubCaptureSpans<T extends ScrubbableCaptureSpan>(spans: T[], pipeline: ScrubPipeline): Promise<T[]>;
35
+ /**
36
+ * Unions two redaction manifests by spanId (dedup redactedKeys per span). Used
37
+ * when an ingestion-time manifest (e.g. a capture's stored redactedKeys) is
38
+ * combined with the emit-time pipeline manifest. Keeps the
39
+ * `totalRedactions === sum(redactedKeys)` invariant the schema enforces.
40
+ */
41
+ export declare function mergeRedactionManifests(a: RedactionManifest, b: RedactionManifest): RedactionManifest;
@@ -0,0 +1,87 @@
1
+ import { computeGenesisHash, computePrevSpanHash, } from '../trajectory/hash-chain.js';
2
+ /**
3
+ * Seller-side scrub applied at emit time, before a trajectory is signed and
4
+ * published. Runs every span's attributes (and each event's attributes) through
5
+ * the pipeline, rebuilds the prev-span hash chain over the scrubbed spans, and
6
+ * produces the redaction manifest (one entry per span listing the keys touched;
7
+ * `event.<key>` for event-attribute redactions).
8
+ */
9
+ export async function scrubSpansForEmit(spans, pipeline, taskCid) {
10
+ const out = [];
11
+ const manifestSpans = [];
12
+ let total = 0;
13
+ let prev = computeGenesisHash(taskCid);
14
+ for (const span of spans) {
15
+ const redactedKeys = new Set();
16
+ const attrResult = await pipeline.run(span.attributes ?? {});
17
+ for (const r of attrResult.redactions)
18
+ redactedKeys.add(r.key);
19
+ const events = [];
20
+ for (const event of span.events) {
21
+ if (event.attributes && Object.keys(event.attributes).length > 0) {
22
+ const evResult = await pipeline.run(event.attributes);
23
+ events.push({ ...event, attributes: evResult.attributes });
24
+ for (const r of evResult.redactions)
25
+ redactedKeys.add(`event.${r.key}`);
26
+ }
27
+ else {
28
+ events.push(event);
29
+ }
30
+ }
31
+ const scrubbed = {
32
+ ...span,
33
+ attributes: { ...attrResult.attributes, 'jinn.prevSpanHash': prev },
34
+ events,
35
+ };
36
+ out.push(scrubbed);
37
+ prev = computePrevSpanHash(scrubbed);
38
+ if (redactedKeys.size > 0) {
39
+ const keys = [...redactedKeys];
40
+ manifestSpans.push({ spanId: span.spanId, redactedKeys: keys });
41
+ total += keys.length;
42
+ }
43
+ }
44
+ return { spans: out, redactionManifest: { spans: manifestSpans, totalRedactions: total } };
45
+ }
46
+ /**
47
+ * Seller-side scrub for capture spans, applied at publish time before the
48
+ * capture trajectory is uploaded. Runs each span's attributes through the
49
+ * pipeline, replacing them with the scrubbed values, and unions the pipeline's
50
+ * redactions into the span's existing `redactedKeys`. Unlike
51
+ * {@link scrubSpansForEmit} it does NOT touch events or inject a hash chain —
52
+ * capture spans carry neither. Generic over the span type so the concrete
53
+ * `SpanRow` (with its extra columns) is returned unchanged apart from
54
+ * `attributes` and `redactedKeys`.
55
+ */
56
+ export async function scrubCaptureSpans(spans, pipeline) {
57
+ const out = [];
58
+ for (const span of spans) {
59
+ const result = await pipeline.run(span.attributes ?? {});
60
+ const redactedKeys = new Set(span.redactedKeys);
61
+ for (const r of result.redactions)
62
+ redactedKeys.add(r.key);
63
+ out.push({ ...span, attributes: result.attributes, redactedKeys: [...redactedKeys] });
64
+ }
65
+ return out;
66
+ }
67
+ /**
68
+ * Unions two redaction manifests by spanId (dedup redactedKeys per span). Used
69
+ * when an ingestion-time manifest (e.g. a capture's stored redactedKeys) is
70
+ * combined with the emit-time pipeline manifest. Keeps the
71
+ * `totalRedactions === sum(redactedKeys)` invariant the schema enforces.
72
+ */
73
+ export function mergeRedactionManifests(a, b) {
74
+ const bySpan = new Map();
75
+ for (const m of [a, b]) {
76
+ for (const s of m.spans) {
77
+ const set = bySpan.get(s.spanId) ?? new Set();
78
+ for (const k of s.redactedKeys)
79
+ set.add(k);
80
+ bySpan.set(s.spanId, set);
81
+ }
82
+ }
83
+ const spans = [...bySpan]
84
+ .map(([spanId, keys]) => ({ spanId, redactedKeys: [...keys] }))
85
+ .filter((s) => s.redactedKeys.length > 0);
86
+ return { spans, totalRedactions: spans.reduce((n, s) => n + s.redactedKeys.length, 0) };
87
+ }
@@ -0,0 +1,12 @@
1
+ export * from './build.js';
2
+ export * from './emit-scrub.js';
3
+ export * from './key-policy.js';
4
+ export * from './layer2.js';
5
+ export * from './ml-pii-stage.js';
6
+ export * from './openredaction-stage.js';
7
+ export * from './pii-build.js';
8
+ export * from './pipeline.js';
9
+ export * from './plain-patterns-stage.js';
10
+ export * from './secretlint-stage.js';
11
+ export * from './transformers-detector.js';
12
+ export * from './types.js';
@@ -0,0 +1,12 @@
1
+ export * from './build.js';
2
+ export * from './emit-scrub.js';
3
+ export * from './key-policy.js';
4
+ export * from './layer2.js';
5
+ export * from './ml-pii-stage.js';
6
+ export * from './openredaction-stage.js';
7
+ export * from './pii-build.js';
8
+ export * from './pipeline.js';
9
+ export * from './plain-patterns-stage.js';
10
+ export * from './secretlint-stage.js';
11
+ export * from './transformers-detector.js';
12
+ export * from './types.js';
@@ -0,0 +1,20 @@
1
+ import type { ScrubStage } from './types.js';
2
+ export interface KeyPolicy {
3
+ /** keys whose values are structural and safe to publish raw */
4
+ safe: string[];
5
+ /** keys to delete entirely (never published) */
6
+ drop: string[];
7
+ }
8
+ export type KeyClass = 'safe' | 'content' | 'drop';
9
+ /**
10
+ * Classify an attribute key against the policy. `drop` wins over `safe` (a key
11
+ * matching both is dropped — fail safe). Unmatched keys are `content` and flow
12
+ * to the value-scrubbing stages.
13
+ */
14
+ export declare function classifyKey(key: string, policy: KeyPolicy): KeyClass;
15
+ /**
16
+ * Structural key-policy stage: deletes `drop` keys outright (recording a
17
+ * redaction) and passes `safe` + `content` keys through unchanged. Value-level
18
+ * scrubbing of `content` keys is handled by later stages.
19
+ */
20
+ export declare function keyPolicyStage(policy: KeyPolicy): ScrubStage;
@@ -0,0 +1,42 @@
1
+ const VERSION = '0.1.0';
2
+ /** Exact match, or prefix match when the pattern ends with `*`. */
3
+ function matches(key, pattern) {
4
+ if (pattern.endsWith('*'))
5
+ return key.startsWith(pattern.slice(0, -1));
6
+ return key === pattern;
7
+ }
8
+ /**
9
+ * Classify an attribute key against the policy. `drop` wins over `safe` (a key
10
+ * matching both is dropped — fail safe). Unmatched keys are `content` and flow
11
+ * to the value-scrubbing stages.
12
+ */
13
+ export function classifyKey(key, policy) {
14
+ if (policy.drop.some((p) => matches(key, p)))
15
+ return 'drop';
16
+ if (policy.safe.some((p) => matches(key, p)))
17
+ return 'safe';
18
+ return 'content';
19
+ }
20
+ /**
21
+ * Structural key-policy stage: deletes `drop` keys outright (recording a
22
+ * redaction) and passes `safe` + `content` keys through unchanged. Value-level
23
+ * scrubbing of `content` keys is handled by later stages.
24
+ */
25
+ export function keyPolicyStage(policy) {
26
+ return {
27
+ name: 'key-policy',
28
+ version: VERSION,
29
+ scrub(attributes) {
30
+ const out = {};
31
+ const redactions = [];
32
+ for (const [key, value] of Object.entries(attributes)) {
33
+ if (classifyKey(key, policy) === 'drop') {
34
+ redactions.push({ key, stage: 'key-policy', kind: 'dropped-key' });
35
+ continue;
36
+ }
37
+ out[key] = value;
38
+ }
39
+ return { attributes: out, redactions };
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,23 @@
1
+ import { ScrubPipeline } from './pipeline.js';
2
+ import { type KeyPolicy } from './key-policy.js';
3
+ /**
4
+ * The layer-2 / public / derived-content scrub altitude
5
+ * (spec/2026-07-06-distillation-v1.md §10, D6; reconciled 2026-07-07 with the
6
+ * shipped #1409 seed profile).
7
+ *
8
+ * Secret-only: the structural key policy + deterministic plain-patterns
9
+ * (emails, home-dir paths, credential-ID prefixes — #1330/#1415) + the FULL
10
+ * secretlint stage (including its Pass-2 entropy secret-shape fallback). It
11
+ * deliberately DROPS openredaction and ML-PII — the stages whose PII
12
+ * shape-matching and trigger-word behaviour deface ordinary public prose
13
+ * (#1409).
14
+ *
15
+ * Mode note vs the seed profile (`buildSeedScrubPipeline`): seeds are
16
+ * REDACT-mode (the scrubbed output is published, so prose must survive →
17
+ * entropy fallback OFF there). This pipeline serves CHECK-mode consumers —
18
+ * `distillClusters` REJECTS a skill on any scrub change rather than publishing
19
+ * a redacted body — so a false positive costs one re-distill, never defaces
20
+ * published content, and the entropy fallback stays ON as the stricter net for
21
+ * a fresh secret surface (DR-2026-07-06 decision 3).
22
+ */
23
+ export declare function buildLayer2ScrubPipeline(policy?: KeyPolicy): ScrubPipeline;
@@ -0,0 +1,32 @@
1
+ import { ScrubPipeline } from './pipeline.js';
2
+ import { keyPolicyStage } from './key-policy.js';
3
+ import { secretlintStage } from './secretlint-stage.js';
4
+ import { plainPatternsStage } from './plain-patterns-stage.js';
5
+ import { DEFAULT_KEY_POLICY } from './build.js';
6
+ /**
7
+ * The layer-2 / public / derived-content scrub altitude
8
+ * (spec/2026-07-06-distillation-v1.md §10, D6; reconciled 2026-07-07 with the
9
+ * shipped #1409 seed profile).
10
+ *
11
+ * Secret-only: the structural key policy + deterministic plain-patterns
12
+ * (emails, home-dir paths, credential-ID prefixes — #1330/#1415) + the FULL
13
+ * secretlint stage (including its Pass-2 entropy secret-shape fallback). It
14
+ * deliberately DROPS openredaction and ML-PII — the stages whose PII
15
+ * shape-matching and trigger-word behaviour deface ordinary public prose
16
+ * (#1409).
17
+ *
18
+ * Mode note vs the seed profile (`buildSeedScrubPipeline`): seeds are
19
+ * REDACT-mode (the scrubbed output is published, so prose must survive →
20
+ * entropy fallback OFF there). This pipeline serves CHECK-mode consumers —
21
+ * `distillClusters` REJECTS a skill on any scrub change rather than publishing
22
+ * a redacted body — so a false positive costs one re-distill, never defaces
23
+ * published content, and the entropy fallback stays ON as the stricter net for
24
+ * a fresh secret surface (DR-2026-07-06 decision 3).
25
+ */
26
+ export function buildLayer2ScrubPipeline(policy = DEFAULT_KEY_POLICY) {
27
+ return new ScrubPipeline([
28
+ keyPolicyStage(policy),
29
+ plainPatternsStage(policy, { credentialIds: true }),
30
+ secretlintStage(policy),
31
+ ]);
32
+ }
@@ -0,0 +1,24 @@
1
+ import { type KeyPolicy } from './key-policy.js';
2
+ import type { ScrubStage } from './types.js';
3
+ /** A PII entity detected in text: a label (PER/ORG/LOC/…) and the matched span text. */
4
+ export interface PiiEntity {
5
+ label: string;
6
+ text: string;
7
+ }
8
+ /**
9
+ * ML PII detector seam. The production implementation wraps a Transformers.js
10
+ * token-classification (NER) model; tests inject a deterministic fake. Kept
11
+ * behind an interface so the stage logic is unit-tested without loading a model.
12
+ */
13
+ export interface PiiDetector {
14
+ detect(text: string): Promise<PiiEntity[]>;
15
+ }
16
+ /**
17
+ * ML PII stage. Runs the injected NER detector over each `content`-classified
18
+ * string value and replaces whole-word occurrences of each detected entity with
19
+ * `[PII:<label>]` (word-boundary matching so substrings aren't corrupted — e.g.
20
+ * detecting "London" leaves "Londoner" intact). `safe` and non-string values
21
+ * pass through untouched. The detector returns matched text rather than offsets
22
+ * because the Transformers.js NER pipeline does not surface character offsets.
23
+ */
24
+ export declare function mlPiiStage(policy: KeyPolicy, detector: PiiDetector): ScrubStage;
@@ -0,0 +1,42 @@
1
+ import { classifyKey } from './key-policy.js';
2
+ const VERSION = '0.1.0';
3
+ function escapeRegExp(s) {
4
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
+ }
6
+ /**
7
+ * ML PII stage. Runs the injected NER detector over each `content`-classified
8
+ * string value and replaces whole-word occurrences of each detected entity with
9
+ * `[PII:<label>]` (word-boundary matching so substrings aren't corrupted — e.g.
10
+ * detecting "London" leaves "Londoner" intact). `safe` and non-string values
11
+ * pass through untouched. The detector returns matched text rather than offsets
12
+ * because the Transformers.js NER pipeline does not surface character offsets.
13
+ */
14
+ export function mlPiiStage(policy, detector) {
15
+ return {
16
+ name: 'ml-pii',
17
+ version: VERSION,
18
+ async scrub(attributes) {
19
+ const out = {};
20
+ const redactions = [];
21
+ for (const [key, value] of Object.entries(attributes)) {
22
+ if (typeof value !== 'string' || classifyKey(key, policy) !== 'content') {
23
+ out[key] = value;
24
+ continue;
25
+ }
26
+ let text = value;
27
+ for (const ent of await detector.detect(value)) {
28
+ if (!ent.text)
29
+ continue;
30
+ const re = new RegExp(`\\b${escapeRegExp(ent.text)}\\b`, 'g');
31
+ const replaced = text.replace(re, `[PII:${ent.label}]`);
32
+ if (replaced !== text) {
33
+ redactions.push({ key, stage: 'ml-pii', kind: 'pii', detail: ent.label });
34
+ text = replaced;
35
+ }
36
+ }
37
+ out[key] = text;
38
+ }
39
+ return { attributes: out, redactions };
40
+ },
41
+ };
42
+ }