autotel-eventcatalog 1.0.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 (54) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/CONTRIBUTING.md +212 -0
  3. package/README.md +307 -0
  4. package/action.yml +155 -0
  5. package/dist/cli.cjs +1071 -0
  6. package/dist/cli.cjs.map +1 -0
  7. package/dist/cli.d.cts +2 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +1065 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.cjs +794 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +267 -0
  14. package/dist/index.d.ts +267 -0
  15. package/dist/index.js +764 -0
  16. package/dist/index.js.map +1 -0
  17. package/docs/CONTRACT.md +280 -0
  18. package/docs/EXTENDING.md +248 -0
  19. package/docs/TROUBLESHOOTING.md +220 -0
  20. package/docs/UPGRADING.md +202 -0
  21. package/package.json +78 -0
  22. package/schemas/README.md +44 -0
  23. package/schemas/drift-report-v0.1.0.json +107 -0
  24. package/schemas/drift-report-v0.2.0.json +137 -0
  25. package/schemas/drift-summary-v0.1.0.json +74 -0
  26. package/schemas/drift-summary-v0.2.0.json +74 -0
  27. package/schemas/stamp-summary-v0.1.0.json +54 -0
  28. package/src/__fixtures__/drift-report-all.golden.json +33 -0
  29. package/src/__fixtures__/drift-summary-clean.golden.json +17 -0
  30. package/src/__fixtures__/drift-summary-drifty.golden.json +17 -0
  31. package/src/__fixtures__/stamp-summary-noop.golden.json +10 -0
  32. package/src/catalog.test.ts +63 -0
  33. package/src/catalog.ts +169 -0
  34. package/src/cli.e2e.test.ts +310 -0
  35. package/src/cli.ts +402 -0
  36. package/src/contract.test.ts +395 -0
  37. package/src/diff-vs-base.test.ts +145 -0
  38. package/src/diff-vs-base.ts +242 -0
  39. package/src/diff.test.ts +384 -0
  40. package/src/diff.ts +296 -0
  41. package/src/index.ts +73 -0
  42. package/src/policy.test.ts +75 -0
  43. package/src/policy.ts +41 -0
  44. package/src/renderers/index.ts +35 -0
  45. package/src/renderers/json.ts +33 -0
  46. package/src/renderers/markdown.ts +223 -0
  47. package/src/renderers/renderers.test.ts +79 -0
  48. package/src/renderers/terminal.ts +30 -0
  49. package/src/renderers/types.ts +26 -0
  50. package/src/report.test.ts +205 -0
  51. package/src/report.ts +27 -0
  52. package/src/snapshot.ts +25 -0
  53. package/src/stamp.test.ts +283 -0
  54. package/src/stamp.ts +232 -0
@@ -0,0 +1,248 @@
1
+ # Extending autotel-eventcatalog
2
+
3
+ The package is designed to be extended in **one** direction: new
4
+ renderers. Everything else (the diff engine, the policy layer, the
5
+ stamper) is deliberately stable. See [CONTRIBUTING.md](../CONTRIBUTING.md#3-no-domain-specific-extensions-to-the-core)
6
+ for why.
7
+
8
+ ## Writing a custom renderer
9
+
10
+ A renderer is a small adapter that turns a drift result into output
11
+ text. The built-ins are `markdown`, `terminal`, and `json`. To add a new
12
+ one (e.g. SARIF, Slack-flavoured markdown, GitHub Check Runs API JSON,
13
+ your in-house dashboard payload), implement the `Renderer` interface and
14
+ register it.
15
+
16
+ ### Step 1: implement the interface
17
+
18
+ ```typescript
19
+ // src/renderers/sarif.ts
20
+ import type { DriftReport } from '../diff';
21
+ import type { DriftDelta } from '../diff-vs-base';
22
+ import type { Renderer } from './types';
23
+
24
+ function renderReport(report: DriftReport): string {
25
+ // SARIF (https://sarifweb.azurewebsites.net) wants a fixed envelope
26
+ // with `runs[].results[]`. Each drift finding becomes one result.
27
+ const results = [
28
+ ...report.events.observedButUndocumented.map((name) => ({
29
+ ruleId: 'autotel/event-undocumented',
30
+ level: 'warning',
31
+ message: { text: `Event \`${name}\` is emitted but not documented.` },
32
+ locations: [
33
+ {
34
+ physicalLocation: {
35
+ artifactLocation: { uri: `events/${name}` },
36
+ },
37
+ },
38
+ ],
39
+ })),
40
+ // ... documented-but-unseen, field drift, services, channels
41
+ ];
42
+
43
+ return JSON.stringify(
44
+ {
45
+ version: '2.1.0',
46
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
47
+ runs: [
48
+ {
49
+ tool: {
50
+ driver: {
51
+ name: 'autotel-eventcatalog',
52
+ informationUri: 'https://github.com/jagreehal/autotel',
53
+ },
54
+ },
55
+ results,
56
+ },
57
+ ],
58
+ },
59
+ null,
60
+ 2,
61
+ );
62
+ }
63
+
64
+ function renderDelta(delta: DriftDelta): string {
65
+ // For PR-mode runs, emit results only for `delta.introduced`.
66
+ // ... similar shape ...
67
+ return JSON.stringify({
68
+ /* ... */
69
+ });
70
+ }
71
+
72
+ export const sarifRenderer: Renderer = {
73
+ name: 'sarif',
74
+ description:
75
+ 'Static Analysis Results Interchange Format (GitHub Code Scanning).',
76
+ renderReport,
77
+ renderDelta,
78
+ };
79
+ ```
80
+
81
+ ### Step 2: register it
82
+
83
+ Add the renderer to the registry in `src/renderers/index.ts`:
84
+
85
+ ```typescript
86
+ import { sarifRenderer } from './sarif';
87
+
88
+ export const RENDERERS: readonly Renderer[] = [
89
+ markdownRenderer,
90
+ terminalRenderer,
91
+ jsonRenderer,
92
+ sarifRenderer, // ← new
93
+ ];
94
+ ```
95
+
96
+ That's it. The CLI's `--format sarif` will automatically work, the help
97
+ text will mention it, and the validator that catches bad `--format`
98
+ values will accept it.
99
+
100
+ ### Step 3: write the tests
101
+
102
+ ```typescript
103
+ // src/renderers/sarif.test.ts
104
+ import { describe, it, expect } from 'vitest';
105
+ import { sarifRenderer } from './sarif';
106
+ import type { DriftReport } from '../diff';
107
+
108
+ const driftyReport: DriftReport = {
109
+ snapshotGeneratedAt: '2026-05-22T00:00:00.000Z',
110
+ snapshotService: 'fixture',
111
+ events: {
112
+ observedButUndocumented: ['order.cancelled'],
113
+ documentedButUnseen: [],
114
+ fieldDrift: [],
115
+ },
116
+ services: { observedButUndocumented: [] },
117
+ channels: { observedButUndocumented: [] },
118
+ };
119
+
120
+ describe('sarifRenderer', () => {
121
+ it('emits a valid SARIF v2.1.0 envelope', () => {
122
+ const out = JSON.parse(sarifRenderer.renderReport(driftyReport));
123
+ expect(out.version).toBe('2.1.0');
124
+ expect(out.runs[0].tool.driver.name).toBe('autotel-eventcatalog');
125
+ expect(out.runs[0].results).toHaveLength(1);
126
+ expect(out.runs[0].results[0].ruleId).toBe('autotel/event-undocumented');
127
+ });
128
+ });
129
+ ```
130
+
131
+ ### Step 4 (optional): document it
132
+
133
+ If the renderer is going to be a first-class citizen, add a row to the
134
+ "Renderers" section in the README and bump the changeset (`pnpm
135
+ changeset`).
136
+
137
+ ## What a good renderer looks like
138
+
139
+ - **Pure function from input to string.** No I/O, no globals, no side
140
+ effects. The CLI is responsible for writing output; the renderer is
141
+ responsible for shaping it.
142
+ - **Handles both `renderReport` and `renderDelta`.** Even if you don't
143
+ care about the delta mode, return _something_. Clean delta output is
144
+ often "no new findings" plus the resolved section.
145
+ - **Self-contained.** A renderer should not import from `cli.ts`, from
146
+ `policy.ts`, or from another renderer's internals. The core types
147
+ (`DriftReport`, `DriftDelta`) are the only contract.
148
+ - **Deterministic.** Same input, same output. No timestamps from
149
+ `Date.now()`, no random IDs. (The snapshot already carries
150
+ `snapshotGeneratedAt` if you need a timestamp.)
151
+ - **Compact.** The Markdown renderer is ~120 lines. If yours is
152
+ significantly longer, you're probably doing logic that belongs in
153
+ the core. Push back to the renderer interface.
154
+
155
+ ## When NOT to write a renderer
156
+
157
+ - **"I want to send drift to my dashboard."** Don't write a renderer for
158
+ that. Your dashboard should poll the snapshot/drift endpoints (or
159
+ parse the JSON envelope on its own). Renderers are for tools that
160
+ consume the _output_, not the _event stream_.
161
+ - **"I want to gate CI differently."** That's a policy concern, not a
162
+ rendering concern. See `policy.ts` and `evaluatePolicy`.
163
+ - **"I want field-level severity (P0/P1/P2)."** Severity classification
164
+ is a policy decision that the renderer applies. A SARIF renderer can
165
+ map every drift category to a SARIF level; that mapping lives in the
166
+ renderer, not in the core types.
167
+
168
+ ## A larger example: Slack Block Kit
169
+
170
+ ```typescript
171
+ // src/renderers/slack.ts
172
+ import type { DriftReport } from '../diff';
173
+ import type { DriftDelta } from '../diff-vs-base';
174
+ import { countDriftReport } from '../diff';
175
+ import type { Renderer } from './types';
176
+
177
+ export const slackRenderer: Renderer = {
178
+ name: 'slack',
179
+ description: 'Slack Block Kit JSON. Post directly to a webhook.',
180
+ renderReport(report) {
181
+ const counts = countDriftReport(report);
182
+ return JSON.stringify({
183
+ blocks: [
184
+ {
185
+ type: 'header',
186
+ text: { type: 'plain_text', text: `${counts.total} drift findings` },
187
+ },
188
+ {
189
+ type: 'section',
190
+ text: {
191
+ type: 'mrkdwn',
192
+ text:
193
+ report.events.observedButUndocumented.length > 0
194
+ ? `*Undocumented:* ${report.events.observedButUndocumented.map((n) => `\`${n}\``).join(', ')}`
195
+ : '_No new events to document._',
196
+ },
197
+ },
198
+ ],
199
+ });
200
+ },
201
+ renderDelta(delta) {
202
+ /* ... */
203
+ return JSON.stringify({ blocks: [] });
204
+ },
205
+ };
206
+ ```
207
+
208
+ Register, test, ship. Now `--format slack` produces JSON you can `curl`
209
+ straight to a Slack webhook URL.
210
+
211
+ ## Library-mode renderers (out-of-tree)
212
+
213
+ If you don't want to upstream your renderer, the library API lets you
214
+ plug one in at runtime in your own code:
215
+
216
+ ```typescript
217
+ import {
218
+ diffCatalogAgainstSnapshot,
219
+ readCatalogState,
220
+ loadSnapshot,
221
+ } from 'autotel-eventcatalog';
222
+ import { myRenderer } from './my-renderer';
223
+
224
+ const snapshot = await loadSnapshot('./snapshot.json');
225
+ const catalog = await readCatalogState('./catalog');
226
+ const report = diffCatalogAgainstSnapshot(snapshot, catalog);
227
+
228
+ console.log(myRenderer.renderReport(report));
229
+ ```
230
+
231
+ You don't have to modify the package to use your own renderer. Upstreaming
232
+ is for when the renderer has general value to other users.
233
+
234
+ ## What's NOT extendable (and why)
235
+
236
+ | Surface | Extension allowed? | Why |
237
+ | ------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------- |
238
+ | Renderers | Yes (registry pattern) | Output targets vary; core data does not |
239
+ | New CLI commands | No (without a user) | See [CONTRIBUTING.md invariant #1](../CONTRIBUTING.md#1-no-new-top-level-commands-without-a-user) |
240
+ | New diff categories | No (without ecosystem buy-in) | Each category cascades through diff/delta/counts/renderers/schemas |
241
+ | Custom policies | Yes, but file an issue first | `evaluatePolicy` is small; an extension might earn its place but probably wants a new policy mode in core |
242
+ | Snapshot format | No | Owned by `autotel-subscribers`; this package only consumes |
243
+ | Stamp marker syntax | No | Backwards compatibility with previously-stamped catalogs |
244
+
245
+ If you find yourself wanting to extend something marked "no", the answer
246
+ is almost always: file an issue describing the use case, and we'll figure
247
+ out whether it belongs in this package, in `autotel-subscribers`, in a
248
+ new sister package, or in your own downstream code.
@@ -0,0 +1,220 @@
1
+ # Troubleshooting
2
+
3
+ ## Exit codes
4
+
5
+ The `drift` CLI exits with a documented set of codes:
6
+
7
+ | Code | Meaning | When |
8
+ | ----- | -------------- | ------------------------------------------------------------------ |
9
+ | `0` | Clean | No drift, OR drift exists but `--fail-on-drift` was not set |
10
+ | `1` | Drift detected | `--fail-on-drift` was set AND the policy decided this is a failure |
11
+ | `2` | Bad arguments | Missing required flag, unknown flag, invalid value |
12
+ | other | Hard error | The CLI itself crashed; surface the stderr |
13
+
14
+ The `stamp` CLI exits `0` on success, `2` on bad arguments. It does not
15
+ have a drift-style failure mode; it either writes or it doesn't.
16
+
17
+ **If you wrap the CLI in a script:** check for codes 0/1 as expected
18
+ behaviour and treat anything else as a hard error to surface. The
19
+ [bundled action](../action.yml) does exactly this.
20
+
21
+ ## Common errors
22
+
23
+ ### "Both --snapshot and --catalog are required."
24
+
25
+ You omitted one of the two required arguments. Either is fine to forget;
26
+ the message says which.
27
+
28
+ ### "--policy new-only requires --base-snapshot."
29
+
30
+ `--policy new-only` compares your current snapshot against a baseline.
31
+ Without a baseline, there's no "new" to compute. Either:
32
+
33
+ - Drop `--policy new-only` (defaults to `all` without a baseline), or
34
+ - Add `--base-snapshot <path>` pointing to the baseline snapshot.
35
+
36
+ In the GitHub Action, the baseline is fetched automatically from the PR's
37
+ base branch; supply `base-ref: origin/${{ github.base_ref }}`.
38
+
39
+ ### "Invalid --format value: foo. Available renderers: markdown, terminal, json."
40
+
41
+ `--format` accepts a renderer name registered in
42
+ `src/renderers/index.ts`. If you added a custom renderer, it needs to be
43
+ in `RENDERERS`. See [EXTENDING.md](EXTENDING.md).
44
+
45
+ ### "autotel-eventcatalog did not produce a summary output."
46
+
47
+ The action's drift step expected `--summary-output` to write a file but
48
+ couldn't find it after the CLI exited. The CLI itself probably crashed
49
+ _before_ writing the summary. Check stderr in the job log; the error
50
+ is up there.
51
+
52
+ ### "Not an autotel architecture snapshot (missing spec marker)"
53
+
54
+ The file you passed to `--snapshot` doesn't look like a snapshot. It
55
+ needs to:
56
+
57
+ - Be valid JSON
58
+ - Have a `spec` field starting with `autotel-architecture/`
59
+
60
+ If the file is empty or zero-bytes, the snapshot subscriber probably
61
+ didn't run. Verify your test suite (or whatever produces the snapshot)
62
+ finished and wrote to disk before the CLI ran.
63
+
64
+ ### Drift CLI reports events I expect to be in the catalog
65
+
66
+ The catalog reader walks `<catalog>/**/index.mdx` looking for an `id:`
67
+ field in the frontmatter. If your event mdx files use a different
68
+ filename or don't have proper frontmatter, they're invisible.
69
+
70
+ Check:
71
+
72
+ 1. The event mdx file is named `index.mdx` (not `event.mdx`, not
73
+ `OrderPlaced.mdx`).
74
+ 2. The path matches `.../events/<X>/index.mdx`.
75
+ 3. The frontmatter has an `id:` (matched case-insensitively, with dots
76
+ and underscores normalised).
77
+
78
+ ### Drift CLI doesn't see field-path drift even though I know it exists
79
+
80
+ Field-path drift is only computed for events with a `schemaPath:` in
81
+ their frontmatter. If your event mdx doesn't declare a schema, the
82
+ field-path check is skipped (you'll only get existence checks).
83
+
84
+ To enable field-path drift:
85
+
86
+ ```yaml
87
+ ---
88
+ id: OrderPlaced
89
+ schemaPath: schema.json # ← relative to the event mdx
90
+ ---
91
+ ```
92
+
93
+ The schema file needs to be a JSON Schema with `properties` keys and
94
+ (optionally) nested `items` for arrays.
95
+
96
+ ### Stamp output looks weird / duplicated
97
+
98
+ Stamps are scoped to the markers:
99
+
100
+ ```
101
+ <!-- autotel:stamp-start -->
102
+ ...
103
+ <!-- autotel:stamp-end -->
104
+ ```
105
+
106
+ If you see duplicate stamp blocks, either:
107
+
108
+ - Someone manually copied the marker pair without realising they're
109
+ load-bearing, or
110
+ - A previous run was interrupted before completing.
111
+
112
+ Fix: delete the malformed marker pair manually, then re-run `stamp`. The
113
+ stamp command will detect the first `<!-- autotel:stamp-start -->` and
114
+ the first `<!-- autotel:stamp-end -->` and replace between them; manual
115
+ cleanup is only needed if those don't bracket the block correctly.
116
+
117
+ ### `pnpm catalog:stamp` failed but my catalog wasn't updated
118
+
119
+ `stamp` is mostly read-only (it reads the snapshot, reads the catalog,
120
+ plans what to write, then writes). If it fails mid-flight, you may
121
+ have a few files updated and the rest not.
122
+
123
+ The stamp summary JSON (`--summary-output`) tells you exactly which files
124
+ were inserts vs. replaces vs. skipped. Compare against your git status to
125
+ see what landed.
126
+
127
+ ### The action posted no comment on my PR
128
+
129
+ Three possibilities:
130
+
131
+ 1. **The repository doesn't allow comments from Actions.** Check
132
+ `Settings > Actions > General > Workflow permissions`. Needs
133
+ "Read and write permissions" or at least the `pull-requests: write`
134
+ permission in the workflow file.
135
+ 2. **The sticky-comment action failed.** With
136
+ `continue-on-comment-failure: true` (the default), this is a
137
+ warning, not a failure. Look for the warning in the job log.
138
+ 3. **You're not running on a `pull_request` event.** The action only
139
+ comments on PRs. On push events, it'll still run and write the
140
+ summary file, but the comment step is skipped.
141
+
142
+ ### "autotel-eventcatalog drift CLI failed with exit $STATUS"
143
+
144
+ Any non-0, non-1 exit code from the CLI surfaces as a hard error in the
145
+ action. Look for the CLI output above this line in the job log; the
146
+ CLI's stderr will explain.
147
+
148
+ ## Debugging tips
149
+
150
+ ### See exactly what the catalog reader found
151
+
152
+ Add this to your local script:
153
+
154
+ ```typescript
155
+ import { readCatalogState } from 'autotel-eventcatalog';
156
+
157
+ const state = await readCatalogState('./catalog');
158
+ console.log('events:', [...state.events.keys()]);
159
+ console.log('services:', [...state.services.keys()]);
160
+ console.log('channels:', [...state.channels.keys()]);
161
+ ```
162
+
163
+ If your event isn't listed, the reader didn't pick it up. Check the
164
+ filename, the frontmatter, and (on Windows) make sure the path
165
+ contains valid `services/.../events/` segments.
166
+
167
+ ### See what the snapshot looked like
168
+
169
+ ```bash
170
+ # Pretty-print, restricted to the event you care about:
171
+ node -e "const s=JSON.parse(require('fs').readFileSync('snapshot.json','utf8'));console.log(JSON.stringify(s.events['order.placed'],null,2))"
172
+ ```
173
+
174
+ ### Reproduce a CI failure locally
175
+
176
+ ```bash
177
+ # Same flags the bundled action passes internally:
178
+ pnpm autotel-eventcatalog drift \
179
+ --snapshot ./services/test/snapshot.json \
180
+ --catalog ./catalog \
181
+ --policy all \
182
+ --output ./drift.md \
183
+ --summary-output ./drift-summary.json \
184
+ --fail-on-drift
185
+ echo "exit: $?"
186
+ cat drift-summary.json
187
+ ```
188
+
189
+ If this reproduces locally with the same exit code and the same
190
+ summary, you've isolated the issue from CI-specific state.
191
+
192
+ ### Reset to a known clean state
193
+
194
+ If you suspect the working tree has gone weird (e.g. partial stamp
195
+ runs), the surest reset is:
196
+
197
+ ```bash
198
+ git restore apps/example-eventcatalog/catalog
199
+ git clean -fd apps/example-eventcatalog/catalog
200
+ pnpm services:snapshot
201
+ pnpm catalog:stamp
202
+ ```
203
+
204
+ After that, `git status` should show the catalog unchanged. If it
205
+ doesn't, your snapshot has changed since the last commit; that's an
206
+ intended diff, not a tooling problem.
207
+
208
+ ## Still stuck?
209
+
210
+ Open an issue with:
211
+
212
+ - The exact command you ran
213
+ - The stderr output (full)
214
+ - The exit code
215
+ - The `spec:` field from your snapshot, drift report, or stamp summary
216
+ - Your Node version and OS
217
+
218
+ Most issues turn out to be one of: missing frontmatter, missing
219
+ schemaPath, paths not normalised on Windows, or the action being asked
220
+ to comment without `pull-requests: write` permission.
@@ -0,0 +1,202 @@
1
+ # Upgrading
2
+
3
+ The package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
4
+ for the **npm package version** and for the **JSON contract version**
5
+ (the `vX.Y.Z` segment in each schema's `$id`).
6
+
7
+ These two versions usually move together but are conceptually
8
+ independent. The npm version changes when _any_ user-facing thing
9
+ changes (CLI flags, library exports, action inputs). The contract
10
+ version changes only when one of the **three published JSON shapes**
11
+ changes.
12
+
13
+ ## Quick reference
14
+
15
+ | Change | npm version | Contract version |
16
+ | ----------------------------------------------- | ----------- | -------------------------------- |
17
+ | Bug fix, internal refactor | patch | unchanged |
18
+ | New optional CLI flag, new library export | minor | unchanged |
19
+ | New optional field in a JSON output | minor | minor |
20
+ | Renamed / removed JSON field, changed JSON type | major | major |
21
+ | Removed CLI flag, removed library export | major | unchanged (if JSON shape stable) |
22
+ | Changed CLI exit code semantics | major | unchanged |
23
+
24
+ ## Reading the version
25
+
26
+ Three places to look:
27
+
28
+ 1. **`package.json`**: the npm version. `pnpm list autotel-eventcatalog`
29
+ from your project root.
30
+ 2. **The `spec:` field on every JSON output**: the contract version.
31
+ 3. **The schema URLs** in `schemas/`: the contract version, with
32
+ `https://autotel.dev/schemas/...` so downstream tooling can validate
33
+ without depending on the npm package.
34
+
35
+ ## Patch upgrade (e.g. `0.1.0 → 0.1.1`)
36
+
37
+ Bug fixes, internal refactors, dependency updates.
38
+
39
+ **What you do:** `pnpm update autotel-eventcatalog`. Nothing else.
40
+
41
+ **What might happen:** Behaviour changes that you weren't relying on
42
+ (e.g. an error message wording, the order of items in stderr logs).
43
+ Real public-API behaviour is preserved.
44
+
45
+ ## Minor upgrade (e.g. `0.1.0 → 0.2.0`)
46
+
47
+ New optional CLI flags, new optional fields in JSON outputs, new
48
+ renderers in the registry, new exported types or functions.
49
+
50
+ **What you do:** `pnpm update autotel-eventcatalog`.
51
+
52
+ **What might happen:**
53
+
54
+ - A new field shows up in the JSON output. Existing parsers that ignore
55
+ unknown fields keep working. Parsers that explicitly reject unknown
56
+ fields (some strict schema validators) need to bump the schema they
57
+ validate against.
58
+ - A new renderer becomes available via `--format`. Existing `--format
59
+ markdown` / `--format json` calls keep working.
60
+ - A new library export exists. Existing imports keep working.
61
+
62
+ **Validating after a minor JSON upgrade**
63
+
64
+ If you're using a strict validator (`additionalProperties: false` in
65
+ your own schema, for example), you'll want to point it at the new
66
+ schema file:
67
+
68
+ ```diff
69
+ - import schema from 'autotel-eventcatalog/schemas/drift-summary-v0.1.0.json';
70
+ + import schema from 'autotel-eventcatalog/schemas/drift-summary-v0.2.0.json';
71
+ ```
72
+
73
+ If you're using `oneOf` on the `spec:` field, add the new value:
74
+
75
+ ```diff
76
+ if (![
77
+ 'autotel-eventcatalog-drift-summary/v0.1.0',
78
+ + 'autotel-eventcatalog-drift-summary/v0.2.0',
79
+ ].includes(summary.spec)) {
80
+ throw new Error(`Unknown drift summary version: ${summary.spec}`);
81
+ }
82
+ ```
83
+
84
+ ## Major upgrade (e.g. `0.x → 1.0.0` or `1.x → 2.0.0`)
85
+
86
+ Breaking changes: renamed JSON fields, removed CLI flags, changed
87
+ behaviour.
88
+
89
+ **What you do:** read the CHANGELOG, update your code or workflow, then
90
+ `pnpm update autotel-eventcatalog`.
91
+
92
+ Major upgrades within `0.x` (e.g. `0.1.0 → 0.2.0` if it ever ships as a
93
+ breaking change) are signalled by the **JSON contract spec version**,
94
+ not just by the npm version. The package version may bump in lockstep,
95
+ but the contract version is what your downstream consumers need to know
96
+ about.
97
+
98
+ **A worked example.** Imagine v1.0.0 renames `counts.total` to
99
+ `counts.findings` in the drift summary. Steps:
100
+
101
+ 1. **Detect the mismatch.** Your consumer's `spec:` check catches it:
102
+
103
+ ```typescript
104
+ if (summary.spec !== 'autotel-eventcatalog-drift-summary/v0.1.0') {
105
+ throw new Error(
106
+ `Drift summary spec ${summary.spec} is not v0.1.0; refusing to parse.`,
107
+ );
108
+ }
109
+ ```
110
+
111
+ The throw is the signal to read the upgrade notes.
112
+
113
+ 2. **Read the CHANGELOG** for the new version.
114
+
115
+ 3. **Update your consumer code:**
116
+
117
+ ```diff
118
+ - if (summary.counts.total > 0) { ... }
119
+ + if (summary.counts.findings > 0) { ... }
120
+ ```
121
+
122
+ 4. **Update the spec check** to allow the new version.
123
+
124
+ 5. **Update the schema file you validate against**, if any.
125
+
126
+ 6. **Bump the npm package** in your `package.json`.
127
+
128
+ ## Detecting an unexpected version
129
+
130
+ The `spec:` field is your defence against silent shape changes. Every
131
+ JSON envelope this package produces carries it. Downstream tooling
132
+ should always check it before parsing.
133
+
134
+ Pattern: explicit allowlist of accepted spec values, with a clear
135
+ error message when something unexpected lands.
136
+
137
+ ```typescript
138
+ const ACCEPTED_SPECS = new Set([
139
+ 'autotel-eventcatalog-drift-summary/v0.1.0',
140
+ // Add new minors here as you update; reject anything else.
141
+ ]);
142
+
143
+ function parseSummary(json: string): DriftSummary {
144
+ const parsed = JSON.parse(json);
145
+ if (!ACCEPTED_SPECS.has(parsed.spec)) {
146
+ throw new Error(
147
+ `Drift summary spec "${parsed.spec}" is not in the accepted set. ` +
148
+ `Read the autotel-eventcatalog CHANGELOG and update this consumer.`,
149
+ );
150
+ }
151
+ return parsed as DriftSummary;
152
+ }
153
+ ```
154
+
155
+ This is intentionally noisy. A silent fallback to "best effort parse"
156
+ is exactly how downstream consumers lose track of contracts.
157
+
158
+ ## Renderer compatibility
159
+
160
+ The CLI's `--format` flag accepts any name registered in the renderer
161
+ registry. Adding a new renderer is a **minor** bump (it doesn't break
162
+ existing callers). Removing a renderer would be a **major** bump
163
+ (callers using `--format <removed>` would fail).
164
+
165
+ The built-in renderers (`markdown`, `terminal`, `json`) are intended to
166
+ stay forever. If a future major version retires one of them, the
167
+ CHANGELOG will name the migration target.
168
+
169
+ ## Action upgrade
170
+
171
+ The GitHub Action uses semver via the `package-version` input. Default
172
+ is `^0`, major-version-pinned to v0.
173
+
174
+ When v1.0.0 ships, your workflow keeps running on the latest v0 release
175
+ until you explicitly opt in:
176
+
177
+ ```yaml
178
+ - uses: jagreehal/autotel-eventcatalog@v0 # always latest v0
179
+ - uses: jagreehal/autotel-eventcatalog@v1 # opt into v1
180
+ - uses: jagreehal/autotel-eventcatalog@v0.1.0 # pin exactly
181
+ ```
182
+
183
+ Inside the action itself, the `package-version` input controls which
184
+ npm release runs:
185
+
186
+ ```yaml
187
+ - uses: jagreehal/autotel-eventcatalog@v0
188
+ with:
189
+ package-version: '0.1.0' # exact pin
190
+ # OR
191
+ package-version: '^0.1' # minor-range
192
+ ```
193
+
194
+ ## When in doubt
195
+
196
+ - The CHANGELOG is the source of truth for what changed.
197
+ - The schemas in `schemas/` are the source of truth for the JSON shape.
198
+ - The `spec:` field is the source of truth for which schema a given
199
+ output matches.
200
+
201
+ If something looks like an undocumented breaking change, file an
202
+ issue; that's a bug in the release process, not in your code.