autotel-pact 0.2.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 (86) hide show
  1. package/README.md +385 -0
  2. package/dist/audit.cjs +412 -0
  3. package/dist/audit.cjs.map +1 -0
  4. package/dist/audit.d.cts +25 -0
  5. package/dist/audit.d.ts +25 -0
  6. package/dist/audit.js +403 -0
  7. package/dist/audit.js.map +1 -0
  8. package/dist/auto-wrap.cjs +255 -0
  9. package/dist/auto-wrap.cjs.map +1 -0
  10. package/dist/auto-wrap.d.cts +57 -0
  11. package/dist/auto-wrap.d.ts +57 -0
  12. package/dist/auto-wrap.js +248 -0
  13. package/dist/auto-wrap.js.map +1 -0
  14. package/dist/broker.cjs +84 -0
  15. package/dist/broker.cjs.map +1 -0
  16. package/dist/broker.d.cts +23 -0
  17. package/dist/broker.d.ts +23 -0
  18. package/dist/broker.js +80 -0
  19. package/dist/broker.js.map +1 -0
  20. package/dist/cli.cjs +662 -0
  21. package/dist/cli.cjs.map +1 -0
  22. package/dist/cli.d.cts +4 -0
  23. package/dist/cli.d.ts +4 -0
  24. package/dist/cli.js +656 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/index.cjs +967 -0
  27. package/dist/index.cjs.map +1 -0
  28. package/dist/index.d.cts +200 -0
  29. package/dist/index.d.ts +200 -0
  30. package/dist/index.js +932 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/ledger-BuBmfWNc.d.ts +22 -0
  33. package/dist/ledger-D88TzN1c.d.cts +22 -0
  34. package/dist/processor.cjs +200 -0
  35. package/dist/processor.cjs.map +1 -0
  36. package/dist/processor.d.cts +58 -0
  37. package/dist/processor.d.ts +58 -0
  38. package/dist/processor.js +193 -0
  39. package/dist/processor.js.map +1 -0
  40. package/dist/provider.cjs +219 -0
  41. package/dist/provider.cjs.map +1 -0
  42. package/dist/provider.d.cts +41 -0
  43. package/dist/provider.d.ts +41 -0
  44. package/dist/provider.js +213 -0
  45. package/dist/provider.js.map +1 -0
  46. package/dist/tag.cjs +50 -0
  47. package/dist/tag.cjs.map +1 -0
  48. package/dist/tag.d.cts +9 -0
  49. package/dist/tag.d.ts +9 -0
  50. package/dist/tag.js +48 -0
  51. package/dist/tag.js.map +1 -0
  52. package/dist/types-BHGiwqcp.d.cts +157 -0
  53. package/dist/types-BHGiwqcp.d.ts +157 -0
  54. package/package.json +108 -0
  55. package/schemas/README.md +24 -0
  56. package/schemas/audit-matrix-v0.2.0.json +78 -0
  57. package/schemas/ledger-entry-v0.2.0.json +77 -0
  58. package/src/attrs.test.ts +35 -0
  59. package/src/attrs.ts +53 -0
  60. package/src/audit.test.ts +189 -0
  61. package/src/audit.ts +251 -0
  62. package/src/auto-wrap.test.ts +149 -0
  63. package/src/auto-wrap.ts +283 -0
  64. package/src/broker.test.ts +175 -0
  65. package/src/broker.ts +118 -0
  66. package/src/cli.test.ts +148 -0
  67. package/src/cli.ts +287 -0
  68. package/src/index.ts +94 -0
  69. package/src/labels.ts +25 -0
  70. package/src/ledger-normalize.test.ts +141 -0
  71. package/src/ledger-normalize.ts +82 -0
  72. package/src/ledger.test.ts +92 -0
  73. package/src/ledger.ts +156 -0
  74. package/src/pact-file.test.ts +124 -0
  75. package/src/pact-file.ts +65 -0
  76. package/src/processor.test.ts +90 -0
  77. package/src/processor.ts +191 -0
  78. package/src/tag.test.ts +72 -0
  79. package/src/tag.ts +21 -0
  80. package/src/types.ts +169 -0
  81. package/src/wrapper-http.test.ts +133 -0
  82. package/src/wrapper-http.ts +194 -0
  83. package/src/wrapper-provider.test.ts +132 -0
  84. package/src/wrapper-provider.ts +163 -0
  85. package/src/wrapper.test.ts +176 -0
  86. package/src/wrapper.ts +221 -0
@@ -0,0 +1,148 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { main } from './cli.js';
6
+ import type { InteractionLedgerEntry, PactFile } from './types.js';
7
+ import { LEDGER_ENTRY_SPEC } from './types.js';
8
+
9
+ let workDir: string;
10
+ let originalCwd: string;
11
+ let stdoutChunks: string[];
12
+ let stderrChunks: string[];
13
+ let stdoutSpy: ReturnType<typeof vi.spyOn>;
14
+ let stderrSpy: ReturnType<typeof vi.spyOn>;
15
+
16
+ beforeEach(() => {
17
+ originalCwd = process.cwd();
18
+ workDir = mkdtempSync(path.join(tmpdir(), 'autotel-pact-cli-'));
19
+ process.chdir(workDir);
20
+ stdoutChunks = [];
21
+ stderrChunks = [];
22
+ stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
23
+ stdoutChunks.push(String(chunk));
24
+ return true;
25
+ });
26
+ stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation((chunk: unknown) => {
27
+ stderrChunks.push(String(chunk));
28
+ return true;
29
+ });
30
+ });
31
+
32
+ afterEach(() => {
33
+ stdoutSpy.mockRestore();
34
+ stderrSpy.mockRestore();
35
+ process.chdir(originalCwd);
36
+ rmSync(workDir, { recursive: true, force: true });
37
+ });
38
+
39
+ function writePact(filename: string, file: PactFile): void {
40
+ mkdirSync('pacts', { recursive: true });
41
+ writeFileSync(path.join('pacts', filename), JSON.stringify(file));
42
+ }
43
+
44
+ function writeLedger(entries: InteractionLedgerEntry[]): void {
45
+ mkdirSync('.autotel-pact', { recursive: true });
46
+ writeFileSync(
47
+ '.autotel-pact/ledger-x.jsonl',
48
+ entries.map((e) => JSON.stringify(e)).join('\n') + '\n',
49
+ );
50
+ }
51
+
52
+ function freshEntry(overrides: Partial<InteractionLedgerEntry> = {}): InteractionLedgerEntry {
53
+ return {
54
+ type: 'interaction',
55
+ spec: LEDGER_ENTRY_SPEC,
56
+ consumer: 'A',
57
+ provider: 'B',
58
+ interaction: 'evt',
59
+ states: [],
60
+ kind: 'message',
61
+ source: 'test',
62
+ role: 'consumer',
63
+ outcome: 'passed',
64
+ duration_ms: 1,
65
+ observed_at: new Date().toISOString(),
66
+ ...overrides,
67
+ };
68
+ }
69
+
70
+ describe('cli', () => {
71
+ it('--help exits 0 and prints help', async () => {
72
+ const code = await main(['--help']);
73
+ expect(code).toBe(0);
74
+ expect(stdoutChunks.join('')).toContain('autotel-pact audit');
75
+ });
76
+
77
+ it('default mode prints a table and exits 0 even with stale confidence', async () => {
78
+ writePact('A-B.json', { consumer: { name: 'A' }, provider: { name: 'B' }, messages: [{ description: 'evt' }] });
79
+ const code = await main([]);
80
+ expect(code).toBe(0);
81
+ expect(stdoutChunks.join('')).toMatch(/STALE/);
82
+ expect(stdoutChunks.join('')).toContain('TEST_SEEN');
83
+ });
84
+
85
+ it('--gate exits 1 when contracted interactions are not seen in test', async () => {
86
+ writePact('A-B.json', { consumer: { name: 'A' }, provider: { name: 'B' }, messages: [{ description: 'evt' }] });
87
+ const code = await main(['--gate']);
88
+ expect(code).toBe(1);
89
+ });
90
+
91
+ it('--gate=strict also fails on observed-but-not-contracted', async () => {
92
+ writeLedger([freshEntry()]);
93
+ const code = await main(['--gate=strict']);
94
+ expect(code).toBe(1);
95
+ });
96
+
97
+ it('--gate=strict succeeds when all interactions are contracted and seen in test', async () => {
98
+ writePact('A-B.json', { consumer: { name: 'A' }, provider: { name: 'B' }, messages: [{ description: 'evt' }] });
99
+ writeLedger([freshEntry()]);
100
+ const code = await main(['--gate=strict']);
101
+ expect(code).toBe(0);
102
+ });
103
+
104
+ it('--json emits parseable JSON', async () => {
105
+ writePact('A-B.json', { consumer: { name: 'A' }, provider: { name: 'B' }, messages: [{ description: 'evt' }] });
106
+ const code = await main(['--json']);
107
+ expect(code).toBe(0);
108
+ const parsed = JSON.parse(stdoutChunks.join(''));
109
+ expect(parsed.counts.contracted_not_test_seen).toBe(1);
110
+ expect(parsed.spec).toContain('v0.2.0');
111
+ });
112
+
113
+ it('rejects unknown args with exit 2', async () => {
114
+ const code = await main(['--bogus']);
115
+ expect(code).toBe(2);
116
+ expect(stderrChunks.join('')).toMatch(/Unknown argument/);
117
+ });
118
+
119
+ it('exits 2 when --pacts is given with no value', async () => {
120
+ const code = await main(['--pacts']);
121
+ expect(code).toBe(2);
122
+ expect(stderrChunks.join('')).toMatch(/Missing value for --pacts/);
123
+ });
124
+
125
+ it('exits 2 when --ledger is given with no value', async () => {
126
+ const code = await main(['--ledger']);
127
+ expect(code).toBe(2);
128
+ expect(stderrChunks.join('')).toMatch(/Missing value for --ledger/);
129
+ });
130
+
131
+ it('exits 2 when --pacts= is given with empty value', async () => {
132
+ const code = await main(['--pacts=']);
133
+ expect(code).toBe(2);
134
+ expect(stderrChunks.join('')).toMatch(/Missing value for --pacts/);
135
+ });
136
+
137
+ it('exits 2 when --ledger= is given with empty value', async () => {
138
+ const code = await main(['--ledger=']);
139
+ expect(code).toBe(2);
140
+ expect(stderrChunks.join('')).toMatch(/Missing value for --ledger/);
141
+ });
142
+
143
+ it('exits 2 when --window= is given with empty value', async () => {
144
+ const code = await main(['--window=']);
145
+ expect(code).toBe(2);
146
+ expect(stderrChunks.join('')).toMatch(/Missing value for --window/);
147
+ });
148
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ import { runAudit } from './audit.js';
3
+ import { brokerConfigFromEnv } from './broker.js';
4
+ import { CLI_COLUMNS, formatYesNo } from './labels.js';
5
+ import type { AuditMatrix, AuditRow } from './types.js';
6
+
7
+ interface CliArgs {
8
+ pactsDir?: string;
9
+ ledgerDir?: string;
10
+ windowDays?: number;
11
+ gate: 'off' | 'on' | 'strict' | 'broker';
12
+ json: boolean;
13
+ help: boolean;
14
+ brokerUrl?: string;
15
+ brokerToken?: string;
16
+ }
17
+
18
+ const HELP_TEXT = `autotel-pact audit — runtime evidence for Pact contracts
19
+
20
+ USAGE
21
+ autotel-pact audit [options]
22
+
23
+ OPTIONS
24
+ --pacts <dir> Directory containing pact files (default: ./pacts)
25
+ --ledger <dir> Directory containing ledger files (default: .autotel-pact)
26
+ --window <days> How many days back to consider observations recent (default: 14)
27
+ --gate Exit non-zero if any contracted interaction was not seen in test
28
+ --gate=strict Also exit non-zero on observations with no matching contract
29
+ --gate=broker Exit non-zero if broker is configured and any row lacks broker verification
30
+ --broker-url <url> Pact Broker base URL (or PACT_BROKER_BASE_URL)
31
+ --broker-token <tok> Pact Broker bearer token (or PACT_BROKER_TOKEN)
32
+ --json Emit machine-readable JSON instead of a table
33
+ --help, -h Show this help
34
+
35
+ ENVIRONMENT
36
+ AUTOTEL_PACT_RUN_ID Tag ledger entries with a run id
37
+ AUTOTEL_PACT_LEDGER_DIR Override default ledger directory
38
+ PACT_BROKER_BASE_URL Broker base URL
39
+ PACT_BROKER_TOKEN Broker bearer token
40
+ PACT_BROKER_USERNAME Broker basic auth username
41
+ PACT_BROKER_PASSWORD Broker basic auth password
42
+ `;
43
+
44
+ function parseDuration(value: string): number {
45
+ const m = /^(\d+)(d?)$/.exec(value);
46
+ if (!m) throw new Error(`Invalid --window value: ${value}`);
47
+ return Number.parseInt(m[1]!, 10);
48
+ }
49
+
50
+ function requireValue(argv: string[], i: number, flag: string): string {
51
+ const value = argv[i];
52
+ if (value === undefined || value.startsWith('--')) {
53
+ throw new Error(`Missing value for ${flag}`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function parseArgs(argv: string[]): CliArgs {
59
+ const args: CliArgs = { gate: 'off', json: false, help: false };
60
+ for (let i = 0; i < argv.length; i++) {
61
+ const a = argv[i]!;
62
+ switch (a) {
63
+ case '--help':
64
+ case '-h': {
65
+ args.help = true;
66
+ break;
67
+ }
68
+ case '--json': {
69
+ args.json = true;
70
+ break;
71
+ }
72
+ case '--gate': {
73
+ args.gate = 'on';
74
+ break;
75
+ }
76
+ case '--gate=strict': {
77
+ args.gate = 'strict';
78
+ break;
79
+ }
80
+ case '--gate=broker': {
81
+ args.gate = 'broker';
82
+ break;
83
+ }
84
+ case '--pacts': {
85
+ args.pactsDir = requireValue(argv, ++i, '--pacts');
86
+ break;
87
+ }
88
+ case '--ledger': {
89
+ args.ledgerDir = requireValue(argv, ++i, '--ledger');
90
+ break;
91
+ }
92
+ case '--window': {
93
+ args.windowDays = parseDuration(requireValue(argv, ++i, '--window'));
94
+ break;
95
+ }
96
+ case '--broker-url': {
97
+ args.brokerUrl = requireValue(argv, ++i, '--broker-url');
98
+ break;
99
+ }
100
+ case '--broker-token': {
101
+ args.brokerToken = requireValue(argv, ++i, '--broker-token');
102
+ break;
103
+ }
104
+ default: {
105
+ if (a.startsWith('--pacts=')) {
106
+ const value = a.slice('--pacts='.length);
107
+ if (!value) throw new Error('Missing value for --pacts');
108
+ args.pactsDir = value;
109
+ } else if (a.startsWith('--ledger=')) {
110
+ const value = a.slice('--ledger='.length);
111
+ if (!value) throw new Error('Missing value for --ledger');
112
+ args.ledgerDir = value;
113
+ } else if (a.startsWith('--window=')) {
114
+ const value = a.slice('--window='.length);
115
+ if (!value) throw new Error('Missing value for --window');
116
+ args.windowDays = parseDuration(value);
117
+ } else if (a.startsWith('--broker-url=')) {
118
+ const value = a.slice('--broker-url='.length);
119
+ if (!value) throw new Error('Missing value for --broker-url');
120
+ args.brokerUrl = value;
121
+ } else if (a.startsWith('--broker-token=')) {
122
+ const value = a.slice('--broker-token='.length);
123
+ if (!value) throw new Error('Missing value for --broker-token');
124
+ args.brokerToken = value;
125
+ } else if (a === 'audit') continue;
126
+ else throw new Error(`Unknown argument: ${a}`);
127
+ }
128
+ }
129
+ }
130
+ return args;
131
+ }
132
+
133
+ function statusLabel(r: AuditRow): string {
134
+ if (r.contracted && r.test_seen) return 'OK';
135
+ if (r.contracted && !r.test_seen) return 'STALE';
136
+ return 'SHADOW';
137
+ }
138
+
139
+ function formatTable(matrix: AuditMatrix): string {
140
+ const lines: string[] = [
141
+ `Window: last ${matrix.window_days} day(s)`,
142
+ `Generated: ${matrix.generated_at}`,
143
+ '',
144
+ ];
145
+
146
+ if (matrix.rows.length === 0) {
147
+ lines.push('No contracts or observations found.');
148
+ return lines.join('\n');
149
+ }
150
+
151
+ const header = [
152
+ CLI_COLUMNS.STATUS,
153
+ CLI_COLUMNS.CONTRACTED,
154
+ CLI_COLUMNS.TEST_SEEN,
155
+ CLI_COLUMNS.PROD_SEEN,
156
+ CLI_COLUMNS.PROVIDER_VERIFIED,
157
+ CLI_COLUMNS.BROKER_VERIFIED,
158
+ CLI_COLUMNS.PAIR,
159
+ CLI_COLUMNS.KIND,
160
+ CLI_COLUMNS.INTERACTION,
161
+ ];
162
+ const rows = matrix.rows.map((r) => [
163
+ statusLabel(r),
164
+ formatYesNo(r.contracted),
165
+ formatYesNo(r.test_seen),
166
+ formatYesNo(r.prod_seen),
167
+ formatYesNo(r.provider_verified),
168
+ formatYesNo(r.broker_verified),
169
+ `${r.consumer} → ${r.provider}`,
170
+ r.kind,
171
+ r.interaction,
172
+ ]);
173
+ const widths = header.map((h, i) =>
174
+ Math.max(h.length, ...rows.map((row) => row[i]!.length)),
175
+ );
176
+ const fmt = (cells: string[]): string =>
177
+ cells.map((c, i) => c.padEnd(widths[i]!)).join(' ');
178
+
179
+ lines.push(
180
+ fmt(header),
181
+ widths.map((w) => '─'.repeat(w)).join(' '),
182
+ ...rows.map((r) => fmt(r)),
183
+ '',
184
+ 'Summary',
185
+ ` Total interactions: ${matrix.counts.total}`,
186
+ ` Contracted: ${matrix.counts.contracted}`,
187
+ ` Seen in test: ${matrix.counts.test_seen}`,
188
+ ` Seen in production: ${matrix.counts.prod_seen}`,
189
+ ` Provider verified (interaction): ${matrix.counts.provider_verified}`,
190
+ ` Broker verified (pact-pair): ${matrix.counts.broker_verified}`,
191
+ ` Contracted AND seen in test: ${matrix.counts.contracted_and_test_seen}`,
192
+ ` Contracted, NOT seen in test: ${matrix.counts.contracted_not_test_seen} ← stale confidence`,
193
+ ` Seen, NOT contracted: ${matrix.counts.test_or_prod_seen_not_contracted} ← ungoverned flow`,
194
+ );
195
+
196
+ if (matrix.verification_failures && matrix.verification_failures.length > 0) {
197
+ lines.push('', 'Provider verification failures (run-level):');
198
+ for (const f of matrix.verification_failures) {
199
+ lines.push(` ${f.consumer} → ${f.provider}: ${f.error}`);
200
+ }
201
+ }
202
+
203
+ const brokerErrors = new Map<string, string>();
204
+ for (const r of matrix.rows) {
205
+ if (r.broker_error) {
206
+ brokerErrors.set(`${r.consumer} → ${r.provider}`, r.broker_error);
207
+ }
208
+ }
209
+ if (brokerErrors.size > 0) {
210
+ lines.push('', 'Broker unreachable / errored (verification status unknown):');
211
+ for (const [pair, err] of brokerErrors) {
212
+ lines.push(` ${pair}: ${err}`);
213
+ }
214
+ }
215
+
216
+ return lines.join('\n');
217
+ }
218
+
219
+ function shouldFail(matrix: AuditMatrix, gate: CliArgs['gate']): boolean {
220
+ if (gate === 'off') return false;
221
+ if (matrix.counts.contracted_not_test_seen > 0) return true;
222
+ if (gate === 'strict' && matrix.counts.test_or_prod_seen_not_contracted > 0) return true;
223
+ if (gate === 'broker') {
224
+ const contractedRows = matrix.rows.filter((r) => r.contracted);
225
+ // Fail loudly on unreachable broker — a transient outage should not be
226
+ // silently treated as "not verified" because that would mask broker health
227
+ // problems behind a contract-verification message.
228
+ if (contractedRows.some((r) => r.broker_error)) return true;
229
+ if (contractedRows.some((r) => !r.broker_verified)) return true;
230
+ }
231
+ return false;
232
+ }
233
+
234
+ export async function main(argv: string[]): Promise<number> {
235
+ let args: CliArgs;
236
+ try {
237
+ args = parseArgs(argv);
238
+ } catch (error) {
239
+ process.stderr.write(`${(error as Error).message}\n\n${HELP_TEXT}`);
240
+ return 2;
241
+ }
242
+
243
+ if (args.help) {
244
+ process.stdout.write(HELP_TEXT);
245
+ return 0;
246
+ }
247
+
248
+ const envBroker = brokerConfigFromEnv();
249
+ const broker =
250
+ args.brokerUrl || envBroker
251
+ ? {
252
+ baseUrl: args.brokerUrl ?? envBroker!.baseUrl,
253
+ token: args.brokerToken ?? envBroker?.token,
254
+ username: envBroker?.username,
255
+ password: envBroker?.password,
256
+ }
257
+ : undefined;
258
+
259
+ const matrix = await runAudit({
260
+ pactsDir: args.pactsDir,
261
+ dir: args.ledgerDir,
262
+ windowDays: args.windowDays,
263
+ broker,
264
+ });
265
+
266
+ if (args.json) {
267
+ process.stdout.write(JSON.stringify(matrix, null, 2) + '\n');
268
+ } else {
269
+ process.stdout.write(formatTable(matrix) + '\n');
270
+ }
271
+
272
+ return shouldFail(matrix, args.gate) ? 1 : 0;
273
+ }
274
+
275
+ const isDirectInvocation =
276
+ typeof process !== 'undefined' &&
277
+ process.argv[1] &&
278
+ (process.argv[1].endsWith('cli.js') || process.argv[1].endsWith('autotel-pact'));
279
+
280
+ if (isDirectInvocation) {
281
+ main(process.argv.slice(2))
282
+ .then((code) => process.exit(code))
283
+ .catch((error) => {
284
+ console.error(error);
285
+ process.exit(1);
286
+ });
287
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ export { withPactInteraction } from './wrapper.js';
2
+ export type {
3
+ WithPactInteractionOptions,
4
+ MessageConsumerPactLike,
5
+ PactMessageHandler,
6
+ ReifiedMessage,
7
+ } from './wrapper.js';
8
+
9
+ export { withHttpPactInteraction } from './wrapper-http.js';
10
+ export type {
11
+ WithHttpPactInteractionOptions,
12
+ HttpPactLike,
13
+ HttpInteraction,
14
+ HttpMockServer,
15
+ HttpPactTestFn,
16
+ } from './wrapper-http.js';
17
+
18
+ export { withProviderVerification } from './wrapper-provider.js';
19
+ export type {
20
+ WithProviderVerificationOptions,
21
+ VerifierOptionsLike,
22
+ VerifierLike,
23
+ VerifierConstructor,
24
+ } from './wrapper-provider.js';
25
+
26
+ export {
27
+ appendLedgerEntry,
28
+ /**
29
+ * Append a ledger record without blocking the caller. Shares an internal
30
+ * serialized write chain with `PactLedgerSpanProcessor`; call
31
+ * `flushLedgerWrites()` to wait for all pending writes to land.
32
+ */
33
+ appendLedgerEntryAsync,
34
+ appendProviderVerificationFailure,
35
+ readLedger,
36
+ ledgerPath,
37
+ /** Awaits the shared async write chain used by `appendLedgerEntryAsync`. */
38
+ flushLedgerWrites,
39
+ } from './ledger.js';
40
+ export type { LedgerOptions } from './ledger.js';
41
+
42
+ export { buildPactAttributes, outcomeAttribute, PACT_ATTRS } from './attrs.js';
43
+ export type { PactAttributeKey } from './attrs.js';
44
+
45
+ export { tagPactInteraction } from './tag.js';
46
+
47
+ export { PactLedgerSpanProcessor, createPactLedgerProcessor } from './processor.js';
48
+ export type {
49
+ PactLedgerProcessorOptions,
50
+ ReadableSpanLike,
51
+ SpanProcessorLike,
52
+ } from './processor.js';
53
+
54
+ export { runAudit, runAuditSync, computeAuditMatrix, keyOf } from './audit.js';
55
+ export type { AuditOptions } from './audit.js';
56
+
57
+ export {
58
+ fetchBrokerVerifications,
59
+ parseBrokerVerificationResult,
60
+ brokerConfigFromEnv,
61
+ } from './broker.js';
62
+ export type { BrokerConfig, ConsumerProviderPair } from './broker.js';
63
+
64
+ export {
65
+ interactionsFromPactFile,
66
+ listPactFiles,
67
+ parsePactFile,
68
+ } from './pact-file.js';
69
+
70
+ export {
71
+ LEDGER_ENTRY_SPEC,
72
+ AUDIT_MATRIX_SPEC,
73
+ isInteractionLedgerEntry,
74
+ isProviderVerificationRun,
75
+ } from './types.js';
76
+ export type {
77
+ PactKind,
78
+ PactOutcome,
79
+ PactInteractionMeta,
80
+ InteractionLedgerEntry,
81
+ LedgerRecord,
82
+ ProviderVerificationRunEntry,
83
+ LedgerSource,
84
+ LedgerRole,
85
+ PactFile,
86
+ AuditRow,
87
+ AuditMatrix,
88
+ BrokerVerification,
89
+ PactInteractionKey,
90
+ } from './types.js';
91
+
92
+ // Downstream consumers building dashboards that explain why a broker
93
+ // verification is pair-level (not interaction-level) can surface this warning.
94
+ export { BROKER_GRANULARITY_WARNING } from './labels.js';
package/src/labels.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Human-facing labels for CLI and docs. Internal JSON keeps machine keys.
3
+ */
4
+
5
+ export const CLI_COLUMNS = {
6
+ STATUS: 'STATUS',
7
+ CONTRACTED: 'CONTRACTED',
8
+ TEST_SEEN: 'TEST_SEEN',
9
+ PROD_SEEN: 'PROD_SEEN',
10
+ PROVIDER_VERIFIED: 'PROVIDER_VERIFIED',
11
+ BROKER_VERIFIED: 'BROKER_VERIFIED',
12
+ PAIR: 'CONSUMER → PROVIDER',
13
+ KIND: 'KIND',
14
+ INTERACTION: 'INTERACTION',
15
+ } as const;
16
+
17
+ export function formatYesNo(value: boolean): string {
18
+ return value ? 'yes' : 'no';
19
+ }
20
+
21
+ export const EVIDENCE_THEME =
22
+ 'We do not guess. We record evidence.';
23
+
24
+ export const BROKER_GRANULARITY_WARNING =
25
+ 'Broker verification proves the latest pact between a consumer and provider was verified. It does not prove each interaction was individually observed by autotel-pact.';
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeLedgerRecord } from './ledger-normalize.js';
3
+ import { AUDIT_MATRIX_SPEC, LEDGER_ENTRY_SPEC } from './types.js';
4
+
5
+ describe('normalizeLedgerRecord', () => {
6
+ it('accepts a current v0.2 interaction entry unchanged', () => {
7
+ const result = normalizeLedgerRecord({
8
+ type: 'interaction',
9
+ spec: LEDGER_ENTRY_SPEC,
10
+ consumer: 'A',
11
+ provider: 'B',
12
+ interaction: 'evt',
13
+ states: ['ready'],
14
+ kind: 'message',
15
+ outcome: 'passed',
16
+ source: 'test',
17
+ role: 'consumer',
18
+ duration_ms: 12,
19
+ observed_at: '2026-05-28T00:00:00Z',
20
+ });
21
+ expect(result).toMatchObject({
22
+ type: 'interaction',
23
+ spec: LEDGER_ENTRY_SPEC,
24
+ source: 'test',
25
+ role: 'consumer',
26
+ kind: 'message',
27
+ });
28
+ });
29
+
30
+ it('rejects v0.1 spec strings (legacy support removed)', () => {
31
+ expect(
32
+ normalizeLedgerRecord({
33
+ spec: 'autotel-pact-ledger-entry/v0.1.0',
34
+ consumer: 'A',
35
+ provider: 'B',
36
+ interaction: 'evt',
37
+ kind: 'message',
38
+ outcome: 'passed',
39
+ duration_ms: 0,
40
+ observed_at: '2026-05-28T00:00:00Z',
41
+ }),
42
+ ).toBeNull();
43
+ });
44
+
45
+ it('rejects schema_version: 1 legacy rows', () => {
46
+ expect(
47
+ normalizeLedgerRecord({
48
+ schema_version: 1,
49
+ consumer: 'A',
50
+ provider: 'B',
51
+ interaction: 'evt',
52
+ observed_at: '2026-05-28T00:00:00Z',
53
+ }),
54
+ ).toBeNull();
55
+ });
56
+
57
+ it('defaults missing states to [] and missing duration_ms to 0', () => {
58
+ const result = normalizeLedgerRecord({
59
+ spec: LEDGER_ENTRY_SPEC,
60
+ consumer: 'A',
61
+ provider: 'B',
62
+ interaction: 'evt',
63
+ outcome: 'passed',
64
+ observed_at: '2026-05-28T00:00:00Z',
65
+ });
66
+ expect(result).not.toBeNull();
67
+ if (result && result.type !== 'provider_verification_run') {
68
+ expect(result.states).toEqual([]);
69
+ expect(result.duration_ms).toBe(0);
70
+ expect(result.kind).toBe('message');
71
+ }
72
+ });
73
+
74
+ it('rejects unknown spec strings', () => {
75
+ expect(
76
+ normalizeLedgerRecord({
77
+ spec: 'autotel-pact-ledger-entry/v9.9.9',
78
+ consumer: 'A',
79
+ provider: 'B',
80
+ interaction: 'evt',
81
+ }),
82
+ ).toBeNull();
83
+ });
84
+
85
+ it('rejects entries with missing required fields', () => {
86
+ expect(
87
+ normalizeLedgerRecord({
88
+ spec: LEDGER_ENTRY_SPEC,
89
+ consumer: 'A',
90
+ // provider missing
91
+ interaction: 'evt',
92
+ }),
93
+ ).toBeNull();
94
+ });
95
+
96
+ it('returns null for non-object input', () => {
97
+ expect(normalizeLedgerRecord(null)).toBeNull();
98
+ expect(normalizeLedgerRecord('string')).toBeNull();
99
+ expect(normalizeLedgerRecord(42)).toBeNull();
100
+ });
101
+
102
+ it('preserves a provider_verification_run entry', () => {
103
+ const result = normalizeLedgerRecord({
104
+ type: 'provider_verification_run',
105
+ spec: LEDGER_ENTRY_SPEC,
106
+ consumer: 'A',
107
+ provider: 'B',
108
+ error: 'verifier crashed',
109
+ observed_at: '2026-05-28T00:00:00Z',
110
+ });
111
+ expect(result).toMatchObject({
112
+ type: 'provider_verification_run',
113
+ outcome: 'failed',
114
+ role: 'provider',
115
+ error: 'verifier crashed',
116
+ });
117
+ });
118
+
119
+ it('rejects provider_verification_run with missing error', () => {
120
+ expect(
121
+ normalizeLedgerRecord({
122
+ type: 'provider_verification_run',
123
+ spec: LEDGER_ENTRY_SPEC,
124
+ consumer: 'A',
125
+ provider: 'B',
126
+ observed_at: '2026-05-28T00:00:00Z',
127
+ }),
128
+ ).toBeNull();
129
+ });
130
+
131
+ it('does not migrate spec when audit-matrix spec given (rejects)', () => {
132
+ expect(
133
+ normalizeLedgerRecord({
134
+ spec: AUDIT_MATRIX_SPEC,
135
+ consumer: 'A',
136
+ provider: 'B',
137
+ interaction: 'evt',
138
+ }),
139
+ ).toBeNull();
140
+ });
141
+ });