@ultimat3/manifest 16.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
@@ -30,6 +30,13 @@ by the CLI, not imported.
30
30
 
31
31
  ## Invariants
32
32
 
33
+ - **`checkAgentsMd`'s `maxBytes` is a whole number of 0 or more** (`As of 2026-08-26`),
34
+ `finiteCount('checkAgentsMd', 'maxBytes', …)`, and it is screened in `checkAgentsMd` rather than
35
+ in `assertAgentsMd` because that is the function that READS the option — "inspect without
36
+ throwing" is a promise about the repository's state, not about a caller passing a budget that is
37
+ not a number. `bytes > maxBytes` is false when `maxBytes` is `NaN`, so `assertAgentsMd` passed an
38
+ `AGENTS.md` of any size while the `AgentsMdCheck` it returned reported `ok: false`: the gate's
39
+ throw and the gate's report disagreeing about one file, with nothing raising.
33
40
  - **No nondeterminism.** No timestamp, git sha, hostname, counter, or unsorted iteration.
34
41
  `buildManifest` is pure — it must never read a registry, a clock, or the filesystem.
35
42
  - Top-level key order in the file is fixed by `KEY_ORDER` in `emit.ts` — `as const satisfies
@@ -125,6 +132,14 @@ by the CLI, not imported.
125
132
  **breaking**, so it also demands a major release of every APP that regenerates. Charging every
126
133
  app a major for a field their readers never had to look at is a fix line that is not true.
127
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.
128
143
  - `diff.ts` reads `mcp.expose` through `isMcpExposed` from `@ultimat3/core`, on **both** sides.
129
144
  `before` is a file parsed off disk, so an older or hand-trimmed manifest can carry an absent or
130
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": "16.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": "16.0.0",
35
- "@ultimat3/core": "16.0.0",
36
- "@ultimat3/entity": "16.0.0",
37
- "@ultimat3/jobs": "16.0.0",
38
- "@ultimat3/query": "16.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/agents-md.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  // This module therefore checks exactly two things: that the file exists, and that it has not
16
16
  // grown into a document nobody reads. It never writes.
17
17
 
18
+ import { finiteCount } from '@ultimat3/core';
18
19
  import { AgentsMdMissingError, AgentsMdTooLargeError } from './errors';
19
20
 
20
21
  export const AGENTS_MD_FILENAME = 'AGENTS.md';
@@ -43,7 +44,12 @@ export interface CheckAgentsMdInput {
43
44
  /** Inspect without throwing — `x verify --json` reports, `assertAgentsMd` enforces. */
44
45
  export async function checkAgentsMd(input: CheckAgentsMdInput = {}): Promise<AgentsMdCheck> {
45
46
  const path = input.path ?? `./${AGENTS_MD_FILENAME}`;
46
- const maxBytes = input.maxBytes ?? AGENTS_MD_MAX_BYTES;
47
+ // `bytes > maxBytes` is false when `maxBytes` is `NaN`, so `assertAgentsMd` passed a file of any
48
+ // size while the check it returned reported `ok: false` — the gate's throw and the gate's report
49
+ // disagreeing about one file, with nothing raising. Screened here rather than in `assertAgentsMd`
50
+ // because this is the function that reads the option; "inspect without throwing" is about the
51
+ // file's state, and a budget that is not a number is the caller's bug, not the repository's.
52
+ const maxBytes = finiteCount('checkAgentsMd', 'maxBytes', input.maxBytes ?? AGENTS_MD_MAX_BYTES);
47
53
  const file = Bun.file(path);
48
54
 
49
55
  if (!(await file.exists())) {
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) => ({