@ultimat3/manifest 17.0.0 → 18.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.
package/CLAUDE.md CHANGED
@@ -132,6 +132,14 @@ by the CLI, not imported.
132
132
  **breaking**, so it also demands a major release of every APP that regenerates. Charging every
133
133
  app a major for a field their readers never had to look at is a fix line that is not true.
134
134
  `build.test.ts`'s `shape compatibility` case is the assertion — if it fails, the bump is earned.
135
+ - **`QueryFact.subscribes` is the one fact a tier-1 generator cannot reach any other way** (`As of
136
+ 2026-08-26`). `@ultimat3/db` needs the tables a live query subscribes to so `x db gen` can grant
137
+ them `REPLICA IDENTITY FULL`, and `@ultimat3/cli` (tier 5) cannot import `@ultimat3/query`'s
138
+ runtime to ask — the manifest is the crossing. Written only when the read declared some:
139
+ `QueryDescriptor.subscribes` is `null` for a read that named none and an empty list is refused at
140
+ `query()`, so absence carries the whole meaning and no plain read pays a key for it. Classified
141
+ in `diff-operations.ts` as **internal** — no caller's contract moved — but reported, because the
142
+ list is what a migration is keyed on.
135
143
  - `diff.ts` reads `mcp.expose` through `isMcpExposed` from `@ultimat3/core`, on **both** sides.
136
144
  `before` is a file parsed off disk, so an older or hand-trimmed manifest can carry an absent or
137
145
  non-boolean value that `!==` would classify from; and the fact `sources.ts` publishes has to be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/manifest",
3
- "version": "17.0.0",
3
+ "version": "18.0.0",
4
4
  "description": "x.manifest.json: deterministic generated facts, contract diff, AGENTS.md budget",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,17 +24,17 @@
24
24
  "LICENSE"
25
25
  ],
26
26
  "engines": {
27
- "bun": ">=1.3.0"
27
+ "bun": ">=1.4.0"
28
28
  },
29
29
  "scripts": {
30
30
  "typecheck": "tsc --noEmit -p tsconfig.json",
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/action": "17.0.0",
35
- "@ultimat3/core": "17.0.0",
36
- "@ultimat3/entity": "17.0.0",
37
- "@ultimat3/jobs": "17.0.0",
38
- "@ultimat3/query": "17.0.0"
34
+ "@ultimat3/action": "18.0.0",
35
+ "@ultimat3/core": "18.0.0",
36
+ "@ultimat3/entity": "18.0.0",
37
+ "@ultimat3/jobs": "18.0.0",
38
+ "@ultimat3/query": "18.0.0"
39
39
  }
40
40
  }
package/src/build.ts CHANGED
@@ -147,6 +147,9 @@ const normalizeQuery = (query: QueryFact): QueryFact => ({
147
147
  ...query,
148
148
  permissions: [...query.permissions].sort(),
149
149
  cacheTags: [...query.cacheTags].sort(),
150
+ // Conditional, not `?? []`: the fact is absent when the read declared nothing, and writing an
151
+ // empty array would make every plain read carry a key that means the same as no key.
152
+ ...(query.subscribes === undefined ? {} : { subscribes: [...query.subscribes].sort() }),
150
153
  });
151
154
 
152
155
  // Job steps keep their DECLARED order — a job's steps are a sequence, not a set, and
@@ -27,6 +27,7 @@ export const fixtureQuery = (name: string, policy: string, permissions = [policy
27
27
  policy,
28
28
  permissions,
29
29
  live: true,
30
+ subscribes: ['posts'],
30
31
  cacheTags: ['post'],
31
32
  });
32
33
 
@@ -106,6 +106,15 @@ export function diffQueries(
106
106
  if (canonicalJson(query.cacheTags) !== canonicalJson(next.cacheTags)) {
107
107
  changes.push({ kind: 'internal', path: `${path}.cacheTags`, detail: 'cache tags changed' });
108
108
  }
109
+ // Internal for the same reason, and reported for a sharper one: this list is what `x db gen`
110
+ // grants REPLICA IDENTITY FULL to, so a move here is a migration the release owes.
111
+ if (canonicalJson(query.subscribes ?? null) !== canonicalJson(next.subscribes ?? null)) {
112
+ changes.push({
113
+ kind: 'internal',
114
+ path: `${path}.subscribes`,
115
+ detail: 'subscribed relations changed',
116
+ });
117
+ }
109
118
  }
110
119
  for (const query of after) {
111
120
  if (!beforeByName.has(query.name)) {
package/src/schema.ts CHANGED
@@ -96,6 +96,17 @@ export interface QueryFact {
96
96
  /** Every permission the policy asserts, flattened through the combinators, deduped and sorted. */
97
97
  readonly permissions: readonly string[];
98
98
  readonly live: boolean;
99
+ /**
100
+ * The relations a live read is patched from, as the query DECLARED them — absent when it
101
+ * declared none, exactly like `ActionFact.rateLimit`, because an empty array on every plain
102
+ * read is bytes in a hand-reviewed file for no fact gained.
103
+ *
104
+ * The reason this fact exists: `x db gen` has to grant `REPLICA IDENTITY FULL` to those tables
105
+ * or `@ultimat3/realtime` refuses the subscription, and `@ultimat3/cli` cannot derive them —
106
+ * the relation name lives inside the query's `sql:` callback, which no generator can invoke
107
+ * without valid input. So the manifest is the one place a tier-1 generator can read it.
108
+ */
109
+ readonly subscribes?: readonly string[];
99
110
  readonly cacheTags: readonly string[];
100
111
  }
101
112
 
package/src/sources.ts CHANGED
@@ -81,6 +81,9 @@ export function frameworkSources(input: FrameworkSourcesInput): ManifestSources
81
81
  policy: query.capability,
82
82
  permissions: query.permissions,
83
83
  live: query.live,
84
+ // Written only when declared — `null` is the descriptor's "the read named none", and an
85
+ // empty list is refused at `query()`, so absence carries the whole meaning.
86
+ ...(query.subscribes === null ? {} : { subscribes: query.subscribes }),
84
87
  cacheTags: query.tags,
85
88
  })),
86
89
  jobs: describeJobs().map((job) => ({