@ultimat3/manifest 6.0.0 → 8.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 +10 -1
- package/README.md +1 -1
- package/package.json +6 -6
- package/src/build.ts +8 -18
- package/src/diff-operations.ts +6 -7
- package/src/diff-routes.ts +2 -2
- package/src/diff-work.ts +4 -4
- package/src/emit.ts +4 -2
- package/src/index.ts +2 -4
- package/src/schema.ts +5 -4
package/CLAUDE.md
CHANGED
|
@@ -33,7 +33,16 @@ by the CLI, not imported.
|
|
|
33
33
|
- **No nondeterminism.** No timestamp, git sha, hostname, counter, or unsorted iteration.
|
|
34
34
|
`buildManifest` is pure — it must never read a registry, a clock, or the filesystem.
|
|
35
35
|
- Top-level key order in the file is fixed by `KEY_ORDER` in `emit.ts`.
|
|
36
|
-
- `buildId` = sha256 of the
|
|
36
|
+
- `buildId` = sha256 of `@ultimat3/core`'s `canonicalJson` over the body — the framework's one
|
|
37
|
+
INJECTIVE form, and the same one every `diff-*.ts` equality is taken over. It was a local
|
|
38
|
+
`JSON.stringify(sortKeys(v))` until 2026-08-22, exported from `index.ts` as `canonical`
|
|
39
|
+
(**removed, breaking**): that form spells `-0` as `0` and `NaN`/`±Infinity` as `null`, so a
|
|
40
|
+
default a client is told to expect could move and the diff answered "no change". Ordinary JSON
|
|
41
|
+
is byte-identical between the two — both tracked apps' committed manifests hash to the same id
|
|
42
|
+
they already carried — so the swap is observable only where the old form folded. **The published
|
|
43
|
+
document is `manifestJson` and is still `JSON.stringify` with a fixed key order**: an injective
|
|
44
|
+
form emits tokens JSON cannot parse, which is why `@ultimat3/action`'s `stableStringify` exists
|
|
45
|
+
as a separate function and why this one may not be written to disk.
|
|
37
46
|
- Job `steps` keep declared order. Everything else sorts.
|
|
38
47
|
- `permissions` is derived, never a second declared list — and derived from each operation's own
|
|
39
48
|
`permissions`, **never from `policy`**. `policy` is a DISPLAY label: a composite renders as
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ bytes. Enforced, not hoped for:
|
|
|
45
45
|
filesystem.
|
|
46
46
|
- Object keys are written in a fixed order, not `JSON.stringify` order, so reordering a
|
|
47
47
|
struct literal produces no diff.
|
|
48
|
-
- `buildId` is a sha256 of the
|
|
48
|
+
- `buildId` is a sha256 of `canonicalJson` (`@ultimat3/core`) over the body, so it changes if and only if a fact changed —
|
|
49
49
|
and `verifyBuildId()` re-derives it from the file, catching a hand edit.
|
|
50
50
|
- Job `steps` keep declared order. A job's steps are a sequence, not a set.
|
|
51
51
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/manifest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
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": "
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/entity": "
|
|
37
|
-
"@ultimat3/jobs": "
|
|
38
|
-
"@ultimat3/query": "
|
|
34
|
+
"@ultimat3/action": "8.0.0",
|
|
35
|
+
"@ultimat3/core": "8.0.0",
|
|
36
|
+
"@ultimat3/entity": "8.0.0",
|
|
37
|
+
"@ultimat3/jobs": "8.0.0",
|
|
38
|
+
"@ultimat3/query": "8.0.0"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/build.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// assembled per app — both outside what this tier may import — so the CLI supplies them and
|
|
17
17
|
// this function stays pure and unit-testable.
|
|
18
18
|
|
|
19
|
+
import { canonicalJson } from '@ultimat3/core';
|
|
19
20
|
import type {
|
|
20
21
|
ActionFact,
|
|
21
22
|
EntityFact,
|
|
@@ -90,30 +91,19 @@ export function buildManifest(sources: ManifestSources): Manifest {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
|
-
* Content hash of the manifest body. Deliberately excludes `buildId` itself, and is
|
|
94
|
-
*
|
|
95
|
-
* the
|
|
94
|
+
* Content hash of the manifest body. Deliberately excludes `buildId` itself, and is taken over
|
|
95
|
+
* `@ultimat3/core`'s `canonicalJson` — the same INJECTIVE form the diff compares on, so a fact
|
|
96
|
+
* that changed cannot hash the same as the fact it replaced.
|
|
97
|
+
*
|
|
98
|
+
* This is a HASH, never the published document: `manifestJson` in `emit.ts` is what reaches disk,
|
|
99
|
+
* and it is `JSON.stringify` with a fixed key order for exactly that reason.
|
|
96
100
|
*/
|
|
97
101
|
export function contentHash(body: Omit<Manifest, 'buildId'>): string {
|
|
98
102
|
const hasher = new Bun.CryptoHasher('sha256');
|
|
99
|
-
hasher.update(
|
|
103
|
+
hasher.update(canonicalJson(body));
|
|
100
104
|
return hasher.digest('hex').slice(0, 16);
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
/** Sorted-key JSON. Never `JSON.stringify(value)` directly — key order is not a contract. */
|
|
104
|
-
export function canonical(value: unknown): string {
|
|
105
|
-
return JSON.stringify(sortKeys(value));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function sortKeys(value: unknown): unknown {
|
|
109
|
-
if (Array.isArray(value)) return value.map(sortKeys);
|
|
110
|
-
if (typeof value !== 'object' || value === null) return value;
|
|
111
|
-
const record = value as Record<string, unknown>;
|
|
112
|
-
const out: Record<string, unknown> = {};
|
|
113
|
-
for (const key of Object.keys(record).sort()) out[key] = sortKeys(record[key]);
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
107
|
function sortBy<T>(items: readonly T[], key: (item: T) => string): readonly T[] {
|
|
118
108
|
return [...items].sort((a, b) => {
|
|
119
109
|
const ka = key(a);
|
package/src/diff-operations.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// The two callable surfaces — actions and queries — and the permissions they require.
|
|
2
2
|
|
|
3
|
-
import { isMcpExposed } from '@ultimat3/core';
|
|
4
|
-
import { canonical } from './build';
|
|
3
|
+
import { canonicalJson, isMcpExposed } from '@ultimat3/core';
|
|
5
4
|
import type { ManifestChange } from './diff-change';
|
|
6
5
|
import { index } from './diff-change';
|
|
7
6
|
import { diffRateLimit } from './diff-rate-limit';
|
|
@@ -23,10 +22,10 @@ export function diffActions(
|
|
|
23
22
|
changes.push({ kind: 'breaking', path, detail: 'action removed' });
|
|
24
23
|
continue;
|
|
25
24
|
}
|
|
26
|
-
if (
|
|
25
|
+
if (canonicalJson(action.input) !== canonicalJson(next.input)) {
|
|
27
26
|
changes.push({ kind: 'breaking', path: `${path}.input`, detail: 'input schema changed' });
|
|
28
27
|
}
|
|
29
|
-
if (
|
|
28
|
+
if (canonicalJson(action.output) !== canonicalJson(next.output)) {
|
|
30
29
|
changes.push({ kind: 'breaking', path: `${path}.output`, detail: 'output schema changed' });
|
|
31
30
|
}
|
|
32
31
|
if (action.policy !== next.policy) {
|
|
@@ -52,7 +51,7 @@ export function diffActions(
|
|
|
52
51
|
}
|
|
53
52
|
changes.push(...diffPermissions(path, action, next));
|
|
54
53
|
changes.push(...diffRateLimit(path, action, next));
|
|
55
|
-
if (
|
|
54
|
+
if (canonicalJson(action.cacheInvalidates) !== canonicalJson(next.cacheInvalidates)) {
|
|
56
55
|
changes.push({
|
|
57
56
|
kind: 'internal',
|
|
58
57
|
path: `${path}.cacheInvalidates`,
|
|
@@ -83,7 +82,7 @@ export function diffQueries(
|
|
|
83
82
|
changes.push({ kind: 'breaking', path, detail: 'query removed' });
|
|
84
83
|
continue;
|
|
85
84
|
}
|
|
86
|
-
if (
|
|
85
|
+
if (canonicalJson(query.input) !== canonicalJson(next.input)) {
|
|
87
86
|
changes.push({ kind: 'breaking', path: `${path}.input`, detail: 'input schema changed' });
|
|
88
87
|
}
|
|
89
88
|
if (query.policy !== next.policy) {
|
|
@@ -104,7 +103,7 @@ export function diffQueries(
|
|
|
104
103
|
}
|
|
105
104
|
// The same fact as an action's `cacheInvalidates`, and the same class: which tag flushes a
|
|
106
105
|
// read is not a caller's contract, but a reviewer has to see it move.
|
|
107
|
-
if (
|
|
106
|
+
if (canonicalJson(query.cacheTags) !== canonicalJson(next.cacheTags)) {
|
|
108
107
|
changes.push({ kind: 'internal', path: `${path}.cacheTags`, detail: 'cache tags changed' });
|
|
109
108
|
}
|
|
110
109
|
}
|
package/src/diff-routes.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// now answers JSON breaks every link to it — while the delivery facts (`render`, `offline`,
|
|
3
3
|
// `hydrate`, `budget`, `revalidateTags`) are internal and reported.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { canonicalJson } from '@ultimat3/core';
|
|
6
6
|
import type { ManifestChange } from './diff-change';
|
|
7
7
|
import { diffScalar, index } from './diff-change';
|
|
8
8
|
import type { RouteFact } from './schema';
|
|
@@ -88,6 +88,6 @@ function diffJson(
|
|
|
88
88
|
detail: string,
|
|
89
89
|
): readonly ManifestChange[] {
|
|
90
90
|
if (before === undefined || after === undefined) return [];
|
|
91
|
-
if (
|
|
91
|
+
if (canonicalJson(before) === canonicalJson(after)) return [];
|
|
92
92
|
return [{ kind, path, detail }];
|
|
93
93
|
}
|
package/src/diff-work.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// nothing throws, the work simply stops happening — which is why a removal on either side is
|
|
3
3
|
// breaking rather than a note in the diff.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { canonicalJson } from '@ultimat3/core';
|
|
6
6
|
import type { ManifestChange } from './diff-change';
|
|
7
7
|
import { diffScalar, index } from './diff-change';
|
|
8
8
|
import type { JobFact, TaskFact } from './schema';
|
|
@@ -23,7 +23,7 @@ export function diffJobs(
|
|
|
23
23
|
changes.push({ kind: 'breaking', path, detail: 'job removed' });
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
|
-
if (
|
|
26
|
+
if (canonicalJson(job.input) !== canonicalJson(next.input)) {
|
|
27
27
|
changes.push({
|
|
28
28
|
kind: 'breaking',
|
|
29
29
|
path: `${path}.input`,
|
|
@@ -42,7 +42,7 @@ export function diffJobs(
|
|
|
42
42
|
),
|
|
43
43
|
);
|
|
44
44
|
changes.push(...diffRetry(path, job, next));
|
|
45
|
-
if (
|
|
45
|
+
if (canonicalJson(job.steps) !== canonicalJson(next.steps)) {
|
|
46
46
|
changes.push({
|
|
47
47
|
kind: 'internal',
|
|
48
48
|
path: `${path}.steps`,
|
|
@@ -118,7 +118,7 @@ export function diffTasks(
|
|
|
118
118
|
changes.push(
|
|
119
119
|
...diffScalar('internal', `${path}.tz`, task.tz, next.tz, (a, b) => `${a} -> ${b}`),
|
|
120
120
|
);
|
|
121
|
-
if (
|
|
121
|
+
if (canonicalJson(task.enqueues) !== canonicalJson(next.enqueues)) {
|
|
122
122
|
changes.push({ kind: 'internal', path: `${path}.enqueues`, detail: 'enqueued jobs changed' });
|
|
123
123
|
}
|
|
124
124
|
}
|
package/src/emit.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// so a refactor that reorders a struct literal does not produce a diff. Two-space indent and
|
|
5
5
|
// a trailing newline: the file is reviewed by humans and diffed by git.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { canonicalJson } from '@ultimat3/core';
|
|
8
|
+
import { contentHash } from './build';
|
|
8
9
|
import { ManifestDriftError } from './errors';
|
|
9
10
|
import type { Manifest } from './schema';
|
|
10
11
|
import { isManifest } from './schema';
|
|
@@ -127,7 +128,8 @@ function describeDrift(onDisk: Manifest, fresh: Manifest): readonly string[] {
|
|
|
127
128
|
const differences: string[] = [];
|
|
128
129
|
for (const key of KEY_ORDER) {
|
|
129
130
|
if (key === 'buildId') continue;
|
|
130
|
-
if (
|
|
131
|
+
if (canonicalJson(onDisk[key]) !== canonicalJson(fresh[key]))
|
|
132
|
+
differences.push(`${key} differs`);
|
|
131
133
|
}
|
|
132
134
|
return differences.length > 0 ? differences : ['buildId differs'];
|
|
133
135
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Public API of @ultimat3/manifest. Explicit — `x verify`, `x manifest`, and the MCP
|
|
2
2
|
// `manifest.read` resource are all built from exactly these exports.
|
|
3
3
|
|
|
4
|
+
export type { HydrateStrategy, OfflineStrategy, RenderMode } from '@ultimat3/core';
|
|
4
5
|
export type { AgentsMdCheck, CheckAgentsMdInput } from './agents-md';
|
|
5
6
|
export {
|
|
6
7
|
AGENTS_MD_FILENAME,
|
|
@@ -9,7 +10,7 @@ export {
|
|
|
9
10
|
checkAgentsMd,
|
|
10
11
|
} from './agents-md';
|
|
11
12
|
export type { ManifestSources } from './build';
|
|
12
|
-
export { buildManifest,
|
|
13
|
+
export { buildManifest, contentHash } from './build';
|
|
13
14
|
export type { ChangeKind, ManifestChange, ManifestDiff } from './diff';
|
|
14
15
|
export { diffManifest, formatDiff } from './diff';
|
|
15
16
|
export type { DocEntry, DocEntryKind } from './docs-scan';
|
|
@@ -46,15 +47,12 @@ export type {
|
|
|
46
47
|
ColumnFact,
|
|
47
48
|
EntityFact,
|
|
48
49
|
ErrorCodeFact,
|
|
49
|
-
HydrateStrategy,
|
|
50
50
|
JobFact,
|
|
51
51
|
JsonValue,
|
|
52
52
|
Manifest,
|
|
53
|
-
OfflineStrategy,
|
|
54
53
|
PolicyFact,
|
|
55
54
|
QueryFact,
|
|
56
55
|
RateLimitFact,
|
|
57
|
-
RenderMode,
|
|
58
56
|
RouteFact,
|
|
59
57
|
TaskFact,
|
|
60
58
|
} from './schema';
|
package/src/schema.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
// Every collection is `readonly` and every field is a plain JSON value: the manifest must
|
|
6
6
|
// round-trip through `JSON.stringify` without loss, because that is how it is stored.
|
|
7
7
|
|
|
8
|
+
// The route vocabulary is `@ultimat3/core`'s, at tier 0. It is IMPORTED rather than restated even
|
|
9
|
+
// though every other field here is a plain literal: the manifest's `render` field means the same
|
|
10
|
+
// thing as the route's, and two spellings of one closed set is what `'spa'` escaped through.
|
|
11
|
+
import type { HydrateStrategy, OfflineStrategy, RenderMode } from '@ultimat3/core';
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Bumped when a reader built for the previous version would be WRONG, not merely incomplete:
|
|
10
15
|
* a field removed, retyped, or given a new meaning.
|
|
@@ -25,10 +30,6 @@ export type JsonValue =
|
|
|
25
30
|
| readonly JsonValue[]
|
|
26
31
|
| { readonly [key: string]: JsonValue };
|
|
27
32
|
|
|
28
|
-
export type RenderMode = 'static' | 'isr' | 'ssr' | 'stream';
|
|
29
|
-
export type OfflineStrategy = 'precache' | 'runtime' | 'network-only';
|
|
30
|
-
export type HydrateStrategy = 'idle' | 'visible' | 'interaction' | 'never';
|
|
31
|
-
|
|
32
33
|
export interface RouteFact {
|
|
33
34
|
readonly url: string;
|
|
34
35
|
readonly render: RenderMode;
|