@ultimat3/manifest 19.2.0 → 19.3.2

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.
package/CLAUDE.md CHANGED
@@ -113,6 +113,15 @@ by the CLI, not imported.
113
113
  route's `surface`/`offline`/`hydrate`, and a query's `cacheTags` — and every one of them
114
114
  reported exactly `[{ kind: 'internal', path: 'buildId' }]` with `hasBreaking: false`. A new
115
115
  SECTION needs its own file beside the others; a new FIELD joins the file its section owns.
116
+ - **`ActionFact.mutator` is classified, and LOSING it is breaking** (`As of 2026-09`). It was
117
+ written by `sources.ts` and read by no `diff-*.ts` rule — two manifests differing only in an
118
+ action that stopped being a mutator answered `[{ kind: 'internal', path: 'buildId' }]`, the exact
119
+ failure the rule above exists for. A mutator is a client-contract CAPABILITY (it decides the HTTP
120
+ method and the idempotency the typed client and OpenAPI publish), so losing it refuses callers
121
+ written against it and gaining it refuses nobody. Folded with `=== true` rather than compared
122
+ through `diffScalar`: `sources.ts` writes the field only when true, so absence IS `false` here,
123
+ and an older manifest can therefore only under-report a mutator as newly GAINED, which is
124
+ additive. `mcp.description` joins it as `internal` — visible in the file, in no contract.
116
125
  - **The axis is what a change refuses, not how it reads.** Something that rejects input that was
117
126
  valid yesterday is breaking (an invariant added, a NOT NULL, a gained permission, a gained
118
127
  enforcement site, a lowered `retry.attempts`); something that accepts more is additive and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/manifest",
3
- "version": "19.2.0",
3
+ "version": "19.3.2",
4
4
  "description": "x.manifest.json: deterministic generated facts, contract diff, AGENTS.md budget",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,10 +31,10 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/action": "19.2.0",
35
- "@ultimat3/core": "19.2.0",
36
- "@ultimat3/entity": "19.2.0",
37
- "@ultimat3/jobs": "19.2.0",
38
- "@ultimat3/query": "19.2.0"
34
+ "@ultimat3/action": "19.3.2",
35
+ "@ultimat3/core": "19.3.2",
36
+ "@ultimat3/entity": "19.3.2",
37
+ "@ultimat3/jobs": "19.3.2",
38
+ "@ultimat3/query": "19.3.2"
39
39
  }
40
40
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { canonicalJson, isMcpExposed } from '@ultimat3/core';
4
4
  import type { ManifestChange } from './diff-change';
5
- import { index } from './diff-change';
5
+ import { diffScalar, index } from './diff-change';
6
6
  import { diffRateLimit } from './diff-rate-limit';
7
7
  import type { ActionFact, QueryFact } from './schema';
8
8
 
@@ -49,6 +49,35 @@ export function diffActions(
49
49
  detail: `mcp exposure ${String(exposed)} -> ${String(nextExposed)}`,
50
50
  });
51
51
  }
52
+ // `?.`, because `before` is a file parsed off DISK: a hand-trimmed or older manifest can carry
53
+ // no `mcp` block at all, and a description is not worth a throw out of the differ.
54
+ changes.push(
55
+ ...diffScalar(
56
+ 'internal',
57
+ `${path}.mcp.description`,
58
+ action.mcp?.description,
59
+ next.mcp?.description,
60
+ () => 'description changed',
61
+ ),
62
+ );
63
+ // Written only when TRUE (`sources.ts`), so absence IS `false` here — a fold, the way
64
+ // `isMcpExposed` folds `expose`, and deliberately not `diffScalar`'s "absence is no evidence":
65
+ // the value this field is absent FOR is the value it means. A manifest written before the
66
+ // field existed can therefore only under-report a mutator as newly gained, which is additive
67
+ // and not a wall of false breakings.
68
+ //
69
+ // The direction is the decision: a mutator is a client-contract CAPABILITY, not a label — it
70
+ // decides the HTTP method and the idempotency the typed client and the OpenAPI document
71
+ // publish — so an action that stops being one refuses callers written against it.
72
+ const mutator = action.mutator === true;
73
+ const nextMutator = next.mutator === true;
74
+ if (mutator !== nextMutator) {
75
+ changes.push({
76
+ kind: nextMutator ? 'additive' : 'breaking',
77
+ path: `${path}.mutator`,
78
+ detail: `mutator ${String(mutator)} -> ${String(nextMutator)}`,
79
+ });
80
+ }
52
81
  changes.push(...diffPermissions(path, action, next));
53
82
  changes.push(...diffRateLimit(path, action, next));
54
83
  if (canonicalJson(action.cacheInvalidates) !== canonicalJson(next.cacheInvalidates)) {