@aplaytest/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 (112) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/config/schema.d.ts +247 -0
  3. package/dist/config/schema.d.ts.map +1 -0
  4. package/dist/config/schema.js +279 -0
  5. package/dist/config/schema.js.map +1 -0
  6. package/dist/evidence/id.d.ts +17 -0
  7. package/dist/evidence/id.d.ts.map +1 -0
  8. package/dist/evidence/id.js +20 -0
  9. package/dist/evidence/id.js.map +1 -0
  10. package/dist/evidence/redact.d.ts +22 -0
  11. package/dist/evidence/redact.d.ts.map +1 -0
  12. package/dist/evidence/redact.js +97 -0
  13. package/dist/evidence/redact.js.map +1 -0
  14. package/dist/evidence/store.d.ts +51 -0
  15. package/dist/evidence/store.d.ts.map +1 -0
  16. package/dist/evidence/store.js +148 -0
  17. package/dist/evidence/store.js.map +1 -0
  18. package/dist/evidence/types.d.ts +183 -0
  19. package/dist/evidence/types.d.ts.map +1 -0
  20. package/dist/evidence/types.js +21 -0
  21. package/dist/evidence/types.js.map +1 -0
  22. package/dist/history/ingest-playwright.d.ts +26 -0
  23. package/dist/history/ingest-playwright.d.ts.map +1 -0
  24. package/dist/history/ingest-playwright.js +73 -0
  25. package/dist/history/ingest-playwright.js.map +1 -0
  26. package/dist/history/ingest.d.ts +29 -0
  27. package/dist/history/ingest.d.ts.map +1 -0
  28. package/dist/history/ingest.js +86 -0
  29. package/dist/history/ingest.js.map +1 -0
  30. package/dist/history/memory-index.d.ts +88 -0
  31. package/dist/history/memory-index.d.ts.map +1 -0
  32. package/dist/history/memory-index.js +236 -0
  33. package/dist/history/memory-index.js.map +1 -0
  34. package/dist/history/playwright-report.d.ts +102 -0
  35. package/dist/history/playwright-report.d.ts.map +1 -0
  36. package/dist/history/playwright-report.js +274 -0
  37. package/dist/history/playwright-report.js.map +1 -0
  38. package/dist/history/store.d.ts +73 -0
  39. package/dist/history/store.d.ts.map +1 -0
  40. package/dist/history/store.js +243 -0
  41. package/dist/history/store.js.map +1 -0
  42. package/dist/history/types.d.ts +69 -0
  43. package/dist/history/types.d.ts.map +1 -0
  44. package/dist/history/types.js +18 -0
  45. package/dist/history/types.js.map +1 -0
  46. package/dist/history/url.d.ts +79 -0
  47. package/dist/history/url.d.ts.map +1 -0
  48. package/dist/history/url.js +121 -0
  49. package/dist/history/url.js.map +1 -0
  50. package/dist/index.d.ts +39 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +31 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/locator/stability.d.ts +63 -0
  55. package/dist/locator/stability.d.ts.map +1 -0
  56. package/dist/locator/stability.js +127 -0
  57. package/dist/locator/stability.js.map +1 -0
  58. package/dist/taxonomy/classify.d.ts +63 -0
  59. package/dist/taxonomy/classify.d.ts.map +1 -0
  60. package/dist/taxonomy/classify.js +344 -0
  61. package/dist/taxonomy/classify.js.map +1 -0
  62. package/dist/taxonomy/kinds.d.ts +49 -0
  63. package/dist/taxonomy/kinds.d.ts.map +1 -0
  64. package/dist/taxonomy/kinds.js +139 -0
  65. package/dist/taxonomy/kinds.js.map +1 -0
  66. package/dist/taxonomy/parse-error.d.ts +85 -0
  67. package/dist/taxonomy/parse-error.d.ts.map +1 -0
  68. package/dist/taxonomy/parse-error.js +179 -0
  69. package/dist/taxonomy/parse-error.js.map +1 -0
  70. package/dist/version.d.ts +10 -0
  71. package/dist/version.d.ts.map +1 -0
  72. package/dist/version.js +10 -0
  73. package/dist/version.js.map +1 -0
  74. package/dist-cjs/.tsbuildinfo +1 -0
  75. package/dist-cjs/config/schema.js +283 -0
  76. package/dist-cjs/config/schema.js.map +1 -0
  77. package/dist-cjs/evidence/id.js +24 -0
  78. package/dist-cjs/evidence/id.js.map +1 -0
  79. package/dist-cjs/evidence/redact.js +103 -0
  80. package/dist-cjs/evidence/redact.js.map +1 -0
  81. package/dist-cjs/evidence/store.js +156 -0
  82. package/dist-cjs/evidence/store.js.map +1 -0
  83. package/dist-cjs/evidence/types.js +25 -0
  84. package/dist-cjs/evidence/types.js.map +1 -0
  85. package/dist-cjs/history/ingest-playwright.js +76 -0
  86. package/dist-cjs/history/ingest-playwright.js.map +1 -0
  87. package/dist-cjs/history/ingest.js +89 -0
  88. package/dist-cjs/history/ingest.js.map +1 -0
  89. package/dist-cjs/history/memory-index.js +242 -0
  90. package/dist-cjs/history/memory-index.js.map +1 -0
  91. package/dist-cjs/history/playwright-report.js +277 -0
  92. package/dist-cjs/history/playwright-report.js.map +1 -0
  93. package/dist-cjs/history/store.js +247 -0
  94. package/dist-cjs/history/store.js.map +1 -0
  95. package/dist-cjs/history/types.js +23 -0
  96. package/dist-cjs/history/types.js.map +1 -0
  97. package/dist-cjs/history/url.js +127 -0
  98. package/dist-cjs/history/url.js.map +1 -0
  99. package/dist-cjs/index.js +88 -0
  100. package/dist-cjs/index.js.map +1 -0
  101. package/dist-cjs/locator/stability.js +134 -0
  102. package/dist-cjs/locator/stability.js.map +1 -0
  103. package/dist-cjs/package.json +3 -0
  104. package/dist-cjs/taxonomy/classify.js +348 -0
  105. package/dist-cjs/taxonomy/classify.js.map +1 -0
  106. package/dist-cjs/taxonomy/kinds.js +145 -0
  107. package/dist-cjs/taxonomy/kinds.js.map +1 -0
  108. package/dist-cjs/taxonomy/parse-error.js +186 -0
  109. package/dist-cjs/taxonomy/parse-error.js.map +1 -0
  110. package/dist-cjs/version.js +13 -0
  111. package/dist-cjs/version.js.map +1 -0
  112. package/package.json +39 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Configuration: one file, fully typed, Zod-validated at load, every field
3
+ * optional with a documented default.
4
+ *
5
+ * The defaults encode the project's core stance — `mode: 'strict'`,
6
+ * `heal.apply: 'propose'`, quarantine expiry ON — so that a consumer who
7
+ * writes `export default defineAtestConfig({})` gets the safe system rather
8
+ * than an unconfigured one.
9
+ */
10
+ import { z } from 'zod';
11
+ import { LOCATOR_STRATEGIES, MAX_STABILITY_RANK } from '../locator/stability.js';
12
+ export const ExecutionModeSchema = z.enum(['strict', 'assisted', 'agentic']);
13
+ export const AggressivenessSchema = z.enum(['off', 'conservative', 'balanced', 'aggressive']);
14
+ const PlaywrightSchema = z.object({
15
+ /** Named Playwright configs this project can drive. */
16
+ configs: z.record(z.string(), z.string()).prefault({}),
17
+ defaultConfig: z.string().default('playwright.config.ts'),
18
+ });
19
+ const HistorySchema = z.object({
20
+ /**
21
+ * Chosen from `url`, not configured separately. Two fields naming one thing
22
+ * can disagree, and `driver: 'sqlite'` beside an `azblob://` url has no
23
+ * correct interpretation — so this is derived, and left here only because
24
+ * `aplaytest doctor` and the report header print it.
25
+ */
26
+ driver: z.enum(['sqlite', 'azure-blob', 'memory']).default('sqlite'),
27
+ /**
28
+ * Where history lives. A path is a local SQLite file; `azblob://<account>/
29
+ * <container>[/<prefix>]` is Azure Blob Storage; `:memory:` is a throwaway.
30
+ *
31
+ * `:memory:` is a footgun in CI and is deliberately NOT the default here —
32
+ * with it, every run sees one attempt per test and flake scoring reports
33
+ * "insufficient data" forever, which reads as the engine working.
34
+ */
35
+ url: z.string().default('.atest/history.sqlite'),
36
+ /** Attempts older than this are pruned by `aplaytest history prune`. */
37
+ retainDays: z.number().int().positive().default(90),
38
+ /**
39
+ * Days the blob driver downloads before scoring. Bounded on purpose: an
40
+ * unbounded read gets slower every week until someone turns the feature off.
41
+ * Keep it at or below `retainDays` — reading further back than prune keeps
42
+ * only buys empty listings.
43
+ */
44
+ windowDays: z.number().int().positive().default(90),
45
+ });
46
+ const EvidenceSchema = z.object({
47
+ dir: z.string().default('.atest/evidence'),
48
+ retainRuns: z.number().int().positive().default(50),
49
+ /**
50
+ * Header/body/console keys scrubbed before anything is written to disk or
51
+ * sent to a model. A suite with MSAL auth WILL capture bearer tokens.
52
+ */
53
+ redact: z
54
+ .array(z.string())
55
+ .default(['password', 'token', 'authorization', 'cookie', 'secret', 'api-key']),
56
+ /** Token ceiling for the ARIA snapshot before truncation (marked explicitly). */
57
+ ariaMaxTokens: z.number().int().positive().default(6_000),
58
+ });
59
+ const LlmSchema = z.object({
60
+ /**
61
+ * Model provider. Only `anthropic` is implemented; `none` disables the
62
+ * model tier entirely. OpenAI and Ollama are not advertised here because
63
+ * a config that parses and then silently uses a different provider is
64
+ * worse than a config that fails at load.
65
+ */
66
+ provider: z.enum(['anthropic', 'none']).default('anthropic'),
67
+ models: z
68
+ .object({
69
+ classify: z.string().default('claude-haiku-4-5'),
70
+ heal: z.string().default('claude-sonnet-5'),
71
+ author: z.string().default('claude-opus-5'),
72
+ vision: z.string().default('claude-sonnet-5'),
73
+ })
74
+ .prefault({}),
75
+ /**
76
+ * Effort replaces temperature: sampling parameters are rejected outright by
77
+ * current Opus/Sonnet models. See docs/10-recommendations.md.
78
+ */
79
+ effort: z
80
+ .object({
81
+ classify: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).default('low'),
82
+ heal: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).default('medium'),
83
+ author: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).default('xhigh'),
84
+ })
85
+ .prefault({}),
86
+ budget: z
87
+ .object({
88
+ perFailureUsd: z.number().positive().default(0.05),
89
+ perRunUsd: z.number().positive().default(2.0),
90
+ })
91
+ .prefault({}),
92
+ });
93
+ const HealSchema = z.object({
94
+ aggressiveness: AggressivenessSchema.default('balanced'),
95
+ strategies: z.array(z.enum(['selector', 'assertion', 'flow'])).default(['selector']),
96
+ /** Re-runs required before a proposal is considered validated. */
97
+ validationRuns: z.number().int().min(1).default(3),
98
+ /** Also re-run the rest of the spec file, to catch collateral damage. */
99
+ validateCollateral: z.boolean().default(true),
100
+ /** Reject candidates weaker than this rank. 4 = text; excludes css/xpath. */
101
+ minStabilityRank: z.number().int().min(0).max(MAX_STABILITY_RANK).default(4),
102
+ allowedStrategies: z.array(z.enum(LOCATOR_STRATEGIES)).default(['testid', 'role', 'label', 'text']),
103
+ apply: z.enum(['propose', 'pr', 'local']).default('propose'),
104
+ /** Glob patterns a heal patch is permitted to touch. */
105
+ targets: z
106
+ .array(z.string())
107
+ .default([
108
+ 'src/**/*.constants.ts',
109
+ 'src/**/*.page.ts',
110
+ 'src/**/*.section.ts',
111
+ 'tests/**/*.spec.ts',
112
+ 'tests/**/*.test.ts',
113
+ ]),
114
+ });
115
+ const FlakySchema = z.object({
116
+ window: z
117
+ .object({
118
+ runs: z.number().int().positive().default(50),
119
+ days: z.number().int().positive().default(14),
120
+ })
121
+ .prefault({}),
122
+ /** Recency weighting: a failure this old counts half as much as one today. */
123
+ halfLifeDays: z.number().positive().default(7),
124
+ threshold: z.number().min(0).max(1).default(0.15),
125
+ minRuns: z.number().int().positive().default(10),
126
+ quarantine: z
127
+ .object({
128
+ policy: z.enum(['off', 'propose', 'auto']).default('propose'),
129
+ expiryDays: z.number().int().positive().default(14),
130
+ maxTests: z.number().int().nonnegative().default(5),
131
+ maxRatio: z.number().min(0).max(1).default(0.02),
132
+ tag: z.string().default('@quarantine'),
133
+ })
134
+ .prefault({}),
135
+ });
136
+ const ImpactSchema = z.object({
137
+ enabled: z.boolean().default(true),
138
+ /** Changing any of these runs the whole suite, no questions asked. */
139
+ fullSuiteTriggers: z
140
+ .array(z.string())
141
+ .default([
142
+ 'package.json',
143
+ 'package-lock.json',
144
+ 'pnpm-lock.yaml',
145
+ 'playwright*.config.ts',
146
+ 'src/shared/**',
147
+ 'atest.config.ts',
148
+ '.github/workflows/**',
149
+ 'Dockerfile',
150
+ ]),
151
+ /** Tags that always run regardless of impact selection. */
152
+ alwaysRunTags: z.array(z.string()).default(['@smoke']),
153
+ /** Above this share of the suite, just run everything. */
154
+ fullSuiteThreshold: z.number().min(0).max(1).default(0.6),
155
+ });
156
+ const ConventionsSchema = z.object({
157
+ titlePattern: z.string().default('^Given .+, when .+, then .+$'),
158
+ requiredTags: z.array(z.string()).default([]),
159
+ seededDataDir: z.string().default('tests/testdata/seeded'),
160
+ /**
161
+ * Paths no agent may write to, ever. The seeded data is the oracle: an
162
+ * agent that can edit expected values can make any test pass.
163
+ */
164
+ forbidWriteTo: z
165
+ .array(z.string())
166
+ .default([
167
+ 'tests/testdata/seeded/**',
168
+ '**/__screenshots__/**',
169
+ '**/__aria__/**',
170
+ '.env*',
171
+ '.github/workflows/**',
172
+ 'atest.config.ts',
173
+ ]),
174
+ pageObjectDir: z.string().default('src/ui/pages'),
175
+ /** Run against generated code before it is ever proposed. */
176
+ verifyCommands: z.array(z.string()).default(['npm run typecheck', 'npm run lint']),
177
+ });
178
+ const IntegrationsSchema = z.object({
179
+ github: z
180
+ .object({
181
+ autoComment: z.boolean().default(true),
182
+ autoIssue: z.boolean().default(false),
183
+ healBranchPrefix: z.string().default('atest/heal/'),
184
+ })
185
+ .prefault({}),
186
+ otel: z
187
+ .object({
188
+ endpoint: z.string().nullable().default(null),
189
+ queryUrl: z.string().nullable().default(null),
190
+ })
191
+ .prefault({}),
192
+ });
193
+ export const AtestConfigSchema = z.object({
194
+ mode: ExecutionModeSchema.default('strict'),
195
+ playwright: PlaywrightSchema.prefault({}),
196
+ history: HistorySchema.prefault({}),
197
+ evidence: EvidenceSchema.prefault({}),
198
+ llm: LlmSchema.prefault({}),
199
+ heal: HealSchema.prefault({}),
200
+ flaky: FlakySchema.prefault({}),
201
+ impact: ImpactSchema.prefault({}),
202
+ conventions: ConventionsSchema.prefault({}),
203
+ integrations: IntegrationsSchema.prefault({}),
204
+ });
205
+ /**
206
+ * Validate user config and apply the safe defaults.
207
+ *
208
+ * An empty object yields `mode: 'strict'`, propose-only healing, and
209
+ * quarantine expiry — a consumer who configures nothing must not get an
210
+ * unguarded system. A misconfigured run fails here, at startup, not
211
+ * halfway through a suite.
212
+ *
213
+ * @param input - Partial config plus optional identity hooks for run/trace ids.
214
+ * @returns Every field filled, with `identity` attached (empty object if omitted).
215
+ * @throws {Error} When a field fails Zod validation. The message is prefixed
216
+ * with `Invalid atest.config.ts:` and lists every issue.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * // atest.config.ts
221
+ * import { defineAtestConfig } from '@aplaytest/core';
222
+ *
223
+ * export default defineAtestConfig({
224
+ * mode: 'strict',
225
+ * heal: { apply: 'propose' },
226
+ * });
227
+ * ```
228
+ */
229
+ export function defineAtestConfig(input = {}) {
230
+ const { identity, ...rest } = input;
231
+ // Presets apply only when the user named an aggressiveness. Empty config
232
+ // must stay propose-only — the balanced preset's `apply: 'pr'` would
233
+ // otherwise override the safe default the rest of this function documents.
234
+ const named = rest.heal?.aggressiveness;
235
+ const preset = named === undefined ? undefined : AGGRESSIVENESS_PRESETS[named];
236
+ const merged = preset === undefined
237
+ ? rest
238
+ : {
239
+ ...rest,
240
+ heal: { ...preset, ...rest.heal },
241
+ };
242
+ const parsed = AtestConfigSchema.safeParse(merged);
243
+ if (!parsed.success) {
244
+ const issues = parsed.error.issues
245
+ .map(i => ` • ${i.path.join('.') || '(root)'}: ${i.message}`)
246
+ .join('\n');
247
+ throw new Error(`Invalid atest.config.ts:\n${issues}`);
248
+ }
249
+ return { ...parsed.data, identity: identity ?? {} };
250
+ }
251
+ /**
252
+ * Aggressiveness presets. Explicit fields in `heal` always win — the preset
253
+ * only fills what the user did not state.
254
+ */
255
+ export const AGGRESSIVENESS_PRESETS = {
256
+ off: { strategies: [], apply: 'propose' },
257
+ conservative: {
258
+ strategies: ['selector'],
259
+ allowedStrategies: ['testid', 'role'],
260
+ minStabilityRank: 1,
261
+ validationRuns: 5,
262
+ apply: 'propose',
263
+ },
264
+ balanced: {
265
+ strategies: ['selector'],
266
+ allowedStrategies: ['testid', 'role', 'label', 'text'],
267
+ minStabilityRank: 4,
268
+ validationRuns: 3,
269
+ apply: 'pr',
270
+ },
271
+ aggressive: {
272
+ strategies: ['selector', 'flow'],
273
+ allowedStrategies: ['testid', 'role', 'label', 'text'],
274
+ minStabilityRank: 4,
275
+ validationRuns: 3,
276
+ apply: 'local',
277
+ },
278
+ };
279
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Evidence ids are DERIVED, never random.
3
+ *
4
+ * The same failure, re-analysed on another machine or in another shard, must
5
+ * produce the same id — otherwise the heal ledger, the history store, and CI
6
+ * artifacts cannot be joined after the fact.
7
+ */
8
+ import type { EvidenceId } from './types.js';
9
+ export interface EvidenceIdParts {
10
+ readonly runId: string;
11
+ readonly testId: string;
12
+ readonly project: string;
13
+ readonly retry: number;
14
+ }
15
+ export declare function evidenceId(parts: EvidenceIdParts): EvidenceId;
16
+ export declare function isEvidenceId(value: string): value is EvidenceId;
17
+ //# sourceMappingURL=id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/evidence/id.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK7C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,UAAU,CAK7D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Evidence ids are DERIVED, never random.
3
+ *
4
+ * The same failure, re-analysed on another machine or in another shard, must
5
+ * produce the same id — otherwise the heal ledger, the history store, and CI
6
+ * artifacts cannot be joined after the fact.
7
+ */
8
+ import { createHash } from 'node:crypto';
9
+ const ID_PREFIX = 'ev_';
10
+ const ID_LENGTH = 12; // 48 bits — ample for per-run uniqueness, short enough to type
11
+ export function evidenceId(parts) {
12
+ const digest = createHash('sha256')
13
+ .update([parts.runId, parts.testId, parts.project, String(parts.retry)].join('\x00'))
14
+ .digest('hex');
15
+ return `${ID_PREFIX}${digest.slice(0, ID_LENGTH)}`;
16
+ }
17
+ export function isEvidenceId(value) {
18
+ return new RegExp(`^${ID_PREFIX}[0-9a-f]{${ID_LENGTH}}$`).test(value);
19
+ }
20
+ //# sourceMappingURL=id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/evidence/id.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,MAAM,SAAS,GAAG,KAAK,CAAC;AACxB,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC,+DAA+D;AASrF,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC;SAChC,MAAM,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;SACpF,MAAM,CAAC,KAAK,CAAC,CAAC;IACjB,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,EAAgB,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,IAAI,MAAM,CAAC,IAAI,SAAS,YAAY,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxE,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Redaction.
3
+ *
4
+ * Evidence bundles are written to disk, uploaded as CI artifacts, and sent to
5
+ * a model. A suite that authenticates — MSAL, OAuth, an API key header — WILL
6
+ * capture bearer tokens in its request ledger. Scrubbing happens once, at the
7
+ * boundary where a bundle is created, so that no downstream consumer has to
8
+ * remember to do it.
9
+ *
10
+ * The rule is fail-safe: an unrecognised structure is traversed, not skipped.
11
+ */
12
+ export declare const REDACTED = "[redacted]";
13
+ export declare function redactString(input: string, keys: readonly string[]): string;
14
+ /**
15
+ * Deep-redact any JSON-shaped value. Keys matching `keys` have their values
16
+ * replaced wholesale; every string is additionally scanned for embedded
17
+ * credentials, because secrets do not always arrive under an obvious key.
18
+ */
19
+ export declare function redact<T>(value: T, keys: readonly string[]): T;
20
+ /** Redact a URL's query string while keeping the path readable. */
21
+ export declare function redactUrl(rawUrl: string, keys: readonly string[]): string;
22
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/evidence/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,QAAQ,eAAe,CAAC;AAiCrC,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAmB3E;AAOD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAE9D;AAmBD,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAWzE"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Redaction.
3
+ *
4
+ * Evidence bundles are written to disk, uploaded as CI artifacts, and sent to
5
+ * a model. A suite that authenticates — MSAL, OAuth, an API key header — WILL
6
+ * capture bearer tokens in its request ledger. Scrubbing happens once, at the
7
+ * boundary where a bundle is created, so that no downstream consumer has to
8
+ * remember to do it.
9
+ *
10
+ * The rule is fail-safe: an unrecognised structure is traversed, not skipped.
11
+ */
12
+ export const REDACTED = '[redacted]';
13
+ /**
14
+ * Header and field names arrive in every casing and separator style —
15
+ * `api-key`, `API_KEY`, `apiKey`, `x-api-key-header`. Comparing them
16
+ * literally means a real credential slips through on a spelling difference,
17
+ * so both sides are normalised to letters and digits before matching.
18
+ */
19
+ function normaliseKey(key) {
20
+ return key.toLowerCase().replace(/[-_\s.]/g, '');
21
+ }
22
+ /** Matches `authorization: Bearer xyz`, `"token": "xyz"`, `API_KEY=xyz`. */
23
+ function keyValuePattern(key) {
24
+ // Allow any separator style between the key's own word parts.
25
+ const flexible = key
26
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
27
+ .replace(/[-_\s]+/g, '[-_\\s]?');
28
+ // The value group optionally swallows an auth scheme, so
29
+ // `Authorization: Bearer <token>` redacts the token rather than the word
30
+ // "Bearer".
31
+ return new RegExp(`(["']?${flexible}["']?\\s*[:=]\\s*["']?)((?:Bearer|Basic|Token)\\s+)?([^"'\\s,;&}]+)`, 'gi');
32
+ }
33
+ /** Matches a bearer token anywhere, even without a recognisable key. */
34
+ const BEARER_PATTERN = /\b(Bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/gi;
35
+ /** Matches a JWT anywhere — three base64url segments. */
36
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g;
37
+ export function redactString(input, keys) {
38
+ // ORDER IS LOAD-BEARING: value patterns run BEFORE key patterns.
39
+ //
40
+ // Running keys first leaks. On `Authorization: Bearer <token>` the key
41
+ // pattern matches `Authorization: ` and captures only `Bearer` — its value
42
+ // group stops at whitespace — so it rewrites the string to
43
+ // `Authorization: [redacted] <token>`. The bearer pattern then finds no
44
+ // `Bearer ` to anchor on, and the credential survives redaction entirely.
45
+ let out = input;
46
+ out = out.replace(BEARER_PATTERN, (_m, prefix) => `${prefix}${REDACTED}`);
47
+ out = out.replace(JWT_PATTERN, REDACTED);
48
+ for (const key of keys) {
49
+ out = out.replace(keyValuePattern(key), (_m, prefix, scheme) => `${prefix}${scheme ?? ''}${REDACTED}`);
50
+ }
51
+ return out;
52
+ }
53
+ function keyMatches(key, keys) {
54
+ const normalised = normaliseKey(key);
55
+ return keys.some(k => normalised.includes(normaliseKey(k)));
56
+ }
57
+ /**
58
+ * Deep-redact any JSON-shaped value. Keys matching `keys` have their values
59
+ * replaced wholesale; every string is additionally scanned for embedded
60
+ * credentials, because secrets do not always arrive under an obvious key.
61
+ */
62
+ export function redact(value, keys) {
63
+ return redactValue(value, keys, new WeakSet());
64
+ }
65
+ function redactValue(value, keys, seen) {
66
+ if (typeof value === 'string')
67
+ return redactString(value, keys);
68
+ if (value === null || typeof value !== 'object')
69
+ return value;
70
+ // Cycles are possible in captured objects; degrade rather than blow the stack.
71
+ if (seen.has(value))
72
+ return '[circular]';
73
+ seen.add(value);
74
+ if (Array.isArray(value))
75
+ return value.map(v => redactValue(v, keys, seen));
76
+ const out = {};
77
+ for (const [k, v] of Object.entries(value)) {
78
+ out[k] = keyMatches(k, keys) ? REDACTED : redactValue(v, keys, seen);
79
+ }
80
+ return out;
81
+ }
82
+ /** Redact a URL's query string while keeping the path readable. */
83
+ export function redactUrl(rawUrl, keys) {
84
+ try {
85
+ const url = new URL(rawUrl);
86
+ for (const key of [...url.searchParams.keys()]) {
87
+ if (keyMatches(key, keys))
88
+ url.searchParams.set(key, REDACTED);
89
+ }
90
+ return url.toString();
91
+ }
92
+ catch {
93
+ // Not a parseable URL — fall back to string scrubbing rather than dropping it.
94
+ return redactString(rawUrl, keys);
95
+ }
96
+ }
97
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/evidence/redact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The evidence store: plain JSON on disk, one directory per run.
3
+ *
4
+ * <dir>/<runId>/<evidenceId>.json
5
+ *
6
+ * Deliberately not a binary format and not a database. A human with `jq` must
7
+ * be able to read a bundle, and a CI job must be able to upload the directory
8
+ * as an artifact with no export step. Every bundle carries `schemaVersion` so
9
+ * a reader can refuse politely rather than mis-parse.
10
+ */
11
+ import { type EvidenceBundle, type EvidenceId } from './types.js';
12
+ export interface EvidenceStoreOptions {
13
+ readonly dir: string;
14
+ readonly redactKeys: readonly string[];
15
+ readonly retainRuns: number;
16
+ }
17
+ export interface SkippedBundle {
18
+ readonly file: string;
19
+ readonly reason: string;
20
+ }
21
+ export interface LoadRunBundlesResult {
22
+ readonly runId: string | null;
23
+ readonly bundles: readonly EvidenceBundle[];
24
+ readonly skipped: readonly SkippedBundle[];
25
+ }
26
+ export declare function isEvidenceBundle(value: unknown): value is EvidenceBundle;
27
+ export declare function parseEvidenceBundle(value: unknown): EvidenceBundle | null;
28
+ export declare function loadRunBundles(evidenceDir: string, runId?: string | undefined): Promise<LoadRunBundlesResult>;
29
+ export declare class SchemaVersionError extends Error {
30
+ readonly found: number;
31
+ readonly expected: number;
32
+ readonly path: string;
33
+ constructor(found: number, expected: number, path: string);
34
+ }
35
+ export declare class EvidenceStore {
36
+ private readonly options;
37
+ constructor(options: EvidenceStoreOptions);
38
+ private bundlePath;
39
+ /**
40
+ * Redaction happens HERE, on the write path, so there is exactly one place
41
+ * that can forget to do it.
42
+ */
43
+ write(bundle: EvidenceBundle): Promise<string>;
44
+ read(runId: string, id: EvidenceId): Promise<EvidenceBundle>;
45
+ /** Run ids, newest first by directory mtime. */
46
+ listRuns(): Promise<string[]>;
47
+ listBundles(runId: string): Promise<EvidenceId[]>;
48
+ /** Drop the oldest runs beyond the retention limit. Returns what it removed. */
49
+ prune(): Promise<string[]>;
50
+ }
51
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/evidence/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,EAA2B,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAI3F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAMD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAOxE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAEzE;AAYD,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAyC/B;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAFZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM;CAQxB;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,oBAAoB;IAE1D,OAAO,CAAC,UAAU;IAIlB;;;OAGG;IACG,KAAK,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ9C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC;IAWlE,gDAAgD;IAC1C,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAc7B,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAOvD,gFAAgF;IAC1E,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;CAMjC"}