@anchrd/intel-contract 0.20.0 → 0.22.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/dist/contract/bundle.d.ts +62 -0
- package/dist/contract/bundle.js +84 -8
- package/dist/contract/contract.d.ts +70 -0
- package/dist/contract/contract.js +68 -0
- package/dist/contract/flow.d.ts +2 -1
- package/dist/contract/flow.js +21 -3
- package/dist/contract/node.js +8 -26
- package/dist/contract/share.d.ts +8 -0
- package/dist/contract/share.js +32 -11
- package/package.json +1 -1
|
@@ -24,6 +24,19 @@ export declare const BundleEntryKind: z.ZodEnum<{
|
|
|
24
24
|
flow: "flow";
|
|
25
25
|
}>;
|
|
26
26
|
export type BundleEntryKind = z.infer<typeof BundleEntryKind>;
|
|
27
|
+
/**
|
|
28
|
+
* Why an entry carries no bytes although its kind normally does (#534).
|
|
29
|
+
*
|
|
30
|
+
* `no-content` is a node that has never been given any: a table created but never defined has no
|
|
31
|
+
* version, and an attachment created but never uploaded has none either, so there is nothing to
|
|
32
|
+
* write. That is a different sentence from "the content is gone", which the export refuses as
|
|
33
|
+
* `content_missing` — and a file of zero bytes cannot say which of the two it is, which is exactly
|
|
34
|
+
* what it used to do.
|
|
35
|
+
*/
|
|
36
|
+
export declare const BundleEntryAbsence: z.ZodEnum<{
|
|
37
|
+
"no-content": "no-content";
|
|
38
|
+
}>;
|
|
39
|
+
export type BundleEntryAbsence = z.infer<typeof BundleEntryAbsence>;
|
|
27
40
|
export declare const BundleManifestEntry: z.ZodObject<{
|
|
28
41
|
id: z.ZodString;
|
|
29
42
|
kind: z.ZodEnum<{
|
|
@@ -39,13 +52,35 @@ export declare const BundleManifestEntry: z.ZodObject<{
|
|
|
39
52
|
description: z.ZodNullable<z.ZodString>;
|
|
40
53
|
mediaType: z.ZodNullable<z.ZodString>;
|
|
41
54
|
path: z.ZodString;
|
|
55
|
+
absence: z.ZodOptional<z.ZodEnum<{
|
|
56
|
+
"no-content": "no-content";
|
|
57
|
+
}>>;
|
|
42
58
|
}, z.core.$strict>;
|
|
43
59
|
export type BundleManifestEntry = z.infer<typeof BundleManifestEntry>;
|
|
60
|
+
/**
|
|
61
|
+
* What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
|
|
62
|
+
* version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ `unpublished-flow-changes` is the FIFTH, and it is the one exception to ADR-0006's own
|
|
65
|
+
* headline (#585). A bundle carries the current state of everything — except a flow that has been
|
|
66
|
+
* published, which travels as its PUBLISHED graph. Edits made after that publication are in no
|
|
67
|
+
* bundle, and they are not history: they are the newest state there is. The word exists because the
|
|
68
|
+
* loss is only acceptable while it is named, and `version-history` does not name it — a reader who
|
|
69
|
+
* saw only that word would expect the newest graph and get one that is deliberately older.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ These are WIRE values: an older reader refuses a bundle naming an exclusion it does not know,
|
|
72
|
+
* the same way it would refuse an unknown `kind`. The enum grows only for something an installation
|
|
73
|
+
* genuinely leaves behind, never as a place to note a nicety (ADR-0006, #433). Growing it costs the
|
|
74
|
+
* direction nobody migrates in — a bundle written HERE is refused by an installation older than
|
|
75
|
+
* this line, while every bundle written before it still parses, which is the direction 0.22.0's
|
|
76
|
+
* rebuild actually runs.
|
|
77
|
+
*/
|
|
44
78
|
export declare const BundleExclusion: z.ZodEnum<{
|
|
45
79
|
"version-history": "version-history";
|
|
46
80
|
grants: "grants";
|
|
47
81
|
"flow-runs": "flow-runs";
|
|
48
82
|
"archived-nodes": "archived-nodes";
|
|
83
|
+
"unpublished-flow-changes": "unpublished-flow-changes";
|
|
49
84
|
}>;
|
|
50
85
|
export type BundleExclusion = z.infer<typeof BundleExclusion>;
|
|
51
86
|
export declare const BundleManifest: z.ZodObject<{
|
|
@@ -67,19 +102,46 @@ export declare const BundleManifest: z.ZodObject<{
|
|
|
67
102
|
description: z.ZodNullable<z.ZodString>;
|
|
68
103
|
mediaType: z.ZodNullable<z.ZodString>;
|
|
69
104
|
path: z.ZodString;
|
|
105
|
+
absence: z.ZodOptional<z.ZodEnum<{
|
|
106
|
+
"no-content": "no-content";
|
|
107
|
+
}>>;
|
|
70
108
|
}, z.core.$strict>>;
|
|
71
109
|
excluded: z.ZodArray<z.ZodEnum<{
|
|
72
110
|
"version-history": "version-history";
|
|
73
111
|
grants: "grants";
|
|
74
112
|
"flow-runs": "flow-runs";
|
|
75
113
|
"archived-nodes": "archived-nodes";
|
|
114
|
+
"unpublished-flow-changes": "unpublished-flow-changes";
|
|
76
115
|
}>>;
|
|
77
116
|
}, z.core.$strict>;
|
|
78
117
|
export type BundleManifest = z.infer<typeof BundleManifest>;
|
|
118
|
+
/**
|
|
119
|
+
* What one import made. Import always creates new nodes — no merge, no overwrite, no restored IDs
|
|
120
|
+
* (#137, phase 1) — so the answer is counts and the new roots, never a diff. `replayed` marks the
|
|
121
|
+
* idempotent second answer to the same key: nothing was created twice.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ `excluded` is what the IMPORT knows about itself, never what the bundle claimed (ADR-0006,
|
|
124
|
+
* #433). A subtree moved between installations arrives with one version per node, no grants and no
|
|
125
|
+
* runs, and until now nothing said so at the moment somebody could still act on it — the word stood
|
|
126
|
+
* only in a `manifest.json` inside the zip that had already been written.
|
|
127
|
+
*
|
|
128
|
+
* It is deliberately NOT read back out of that manifest. A hand-written bundle saying `excluded: []`
|
|
129
|
+
* would then make the import report that nothing was left behind, and the import would launder
|
|
130
|
+
* somebody else's claim into an answer that sounds like its own — the trap `absence` is bound
|
|
131
|
+
* against in `BundleManifestEntry` above (#534, #560). The five exclusions hold for every import
|
|
132
|
+
* this build performs, with a manifest or from a naked folder, so this is a constant of the code.
|
|
133
|
+
*/
|
|
79
134
|
export declare const BundleImportResult: z.ZodObject<{
|
|
80
135
|
nodes: z.ZodNumber;
|
|
81
136
|
flows: z.ZodNumber;
|
|
82
137
|
rootNodeIds: z.ZodArray<z.ZodString>;
|
|
83
138
|
replayed: z.ZodBoolean;
|
|
139
|
+
excluded: z.ZodArray<z.ZodEnum<{
|
|
140
|
+
"version-history": "version-history";
|
|
141
|
+
grants: "grants";
|
|
142
|
+
"flow-runs": "flow-runs";
|
|
143
|
+
"archived-nodes": "archived-nodes";
|
|
144
|
+
"unpublished-flow-changes": "unpublished-flow-changes";
|
|
145
|
+
}>>;
|
|
84
146
|
}, z.core.$strict>;
|
|
85
147
|
export type BundleImportResult = z.infer<typeof BundleImportResult>;
|
package/dist/contract/bundle.js
CHANGED
|
@@ -27,21 +27,83 @@ export const BundleEntryKind = z.enum([
|
|
|
27
27
|
"board",
|
|
28
28
|
"flow",
|
|
29
29
|
]);
|
|
30
|
+
/**
|
|
31
|
+
* Why an entry carries no bytes although its kind normally does (#534).
|
|
32
|
+
*
|
|
33
|
+
* `no-content` is a node that has never been given any: a table created but never defined has no
|
|
34
|
+
* version, and an attachment created but never uploaded has none either, so there is nothing to
|
|
35
|
+
* write. That is a different sentence from "the content is gone", which the export refuses as
|
|
36
|
+
* `content_missing` — and a file of zero bytes cannot say which of the two it is, which is exactly
|
|
37
|
+
* what it used to do.
|
|
38
|
+
*/
|
|
39
|
+
export const BundleEntryAbsence = z.enum(["no-content"]);
|
|
30
40
|
// One entry of the manifest: the identity a re-import needs, next to the relative path where the
|
|
31
41
|
// bytes sit in the zip. A folder carries no media type — it has no bytes.
|
|
32
|
-
export const BundleManifestEntry = z
|
|
42
|
+
export const BundleManifestEntry = z
|
|
43
|
+
.strictObject({
|
|
33
44
|
id: IntelId,
|
|
34
45
|
kind: BundleEntryKind,
|
|
35
46
|
title: z.string().min(1).max(240),
|
|
36
47
|
description: z.string().max(2_000).nullable(),
|
|
37
48
|
mediaType: z.string().min(1).max(160).nullable(),
|
|
38
49
|
// Relative to the zip root, forward slashes, no leading slash. Folders end with a slash so an
|
|
39
|
-
// empty folder still has an address.
|
|
50
|
+
// empty folder still has an address. An entry with an `absence` keeps its path even though no
|
|
51
|
+
// file sits there: the path is what puts the node in the tree on the way back in.
|
|
40
52
|
path: z.string().min(1).max(4_000),
|
|
53
|
+
// ⚠️ OPTIONAL rather than nullable, and that is about files that already exist. This object is
|
|
54
|
+
// strict, so a required key would refuse every bundle written before #534 with
|
|
55
|
+
// `import_invalid_manifest` — a whole format broken over a word that only ever describes a node
|
|
56
|
+
// with nothing in it. Absent means the entry has its file, which is what every older bundle
|
|
57
|
+
// means.
|
|
58
|
+
absence: BundleEntryAbsence.optional(),
|
|
59
|
+
})
|
|
60
|
+
/**
|
|
61
|
+
* ⚠️ Only a table or an attachment may declare an absence, and the guard belongs HERE rather
|
|
62
|
+
* than at the import.
|
|
63
|
+
*
|
|
64
|
+
* A bundle is somebody else's file, and the import reads this word as permission to skip the
|
|
65
|
+
* check that a manifest entry has its file in the zip. Left unrestricted, an entry saying
|
|
66
|
+
* `kind: "document"` with `absence: "no-content"` would walk past that check even when the file
|
|
67
|
+
* IS in the zip — and the document would be created empty, silently, with nothing refused.
|
|
68
|
+
*
|
|
69
|
+
* The two kinds named here are the ones whose canonical content is the whole of them: a table
|
|
70
|
+
* IS its CSV and an attachment IS its bytes, so for them "no content" is a state of the node
|
|
71
|
+
* rather than a claim about a file. A document without a version is a different case and stays
|
|
72
|
+
* out — an empty markdown file says the same thing about it without ambiguity (#534, #560).
|
|
73
|
+
*
|
|
74
|
+
* Refusing at the boundary is the difference between a named refusal and lost content: the
|
|
75
|
+
* import validates this schema before it plans anything, so a manifest like that fails as
|
|
76
|
+
* `import_invalid_manifest` instead of importing something the bundle does not say.
|
|
77
|
+
*/
|
|
78
|
+
.refine((entry) => entry.absence === undefined || entry.kind === "table" || entry.kind === "attachment", {
|
|
79
|
+
message: "Only a table or attachment entry may declare an absence",
|
|
80
|
+
path: ["absence"],
|
|
41
81
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
82
|
+
/**
|
|
83
|
+
* What an export leaves out on purpose, named so a bundle says it rather than a reader guessing:
|
|
84
|
+
* version history, grants/shares, flow runs, and archived nodes are not in any bundle (#136).
|
|
85
|
+
*
|
|
86
|
+
* ⚠️ `unpublished-flow-changes` is the FIFTH, and it is the one exception to ADR-0006's own
|
|
87
|
+
* headline (#585). A bundle carries the current state of everything — except a flow that has been
|
|
88
|
+
* published, which travels as its PUBLISHED graph. Edits made after that publication are in no
|
|
89
|
+
* bundle, and they are not history: they are the newest state there is. The word exists because the
|
|
90
|
+
* loss is only acceptable while it is named, and `version-history` does not name it — a reader who
|
|
91
|
+
* saw only that word would expect the newest graph and get one that is deliberately older.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ These are WIRE values: an older reader refuses a bundle naming an exclusion it does not know,
|
|
94
|
+
* the same way it would refuse an unknown `kind`. The enum grows only for something an installation
|
|
95
|
+
* genuinely leaves behind, never as a place to note a nicety (ADR-0006, #433). Growing it costs the
|
|
96
|
+
* direction nobody migrates in — a bundle written HERE is refused by an installation older than
|
|
97
|
+
* this line, while every bundle written before it still parses, which is the direction 0.22.0's
|
|
98
|
+
* rebuild actually runs.
|
|
99
|
+
*/
|
|
100
|
+
export const BundleExclusion = z.enum([
|
|
101
|
+
"version-history",
|
|
102
|
+
"grants",
|
|
103
|
+
"flow-runs",
|
|
104
|
+
"archived-nodes",
|
|
105
|
+
"unpublished-flow-changes",
|
|
106
|
+
]);
|
|
45
107
|
export const BundleManifest = z.strictObject({
|
|
46
108
|
version: z.literal(1),
|
|
47
109
|
exportedAt: IsoDateTime,
|
|
@@ -52,12 +114,26 @@ export const BundleManifest = z.strictObject({
|
|
|
52
114
|
excluded: z.array(BundleExclusion),
|
|
53
115
|
});
|
|
54
116
|
// ── Bundle import (#137) ────────────────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
117
|
+
/**
|
|
118
|
+
* What one import made. Import always creates new nodes — no merge, no overwrite, no restored IDs
|
|
119
|
+
* (#137, phase 1) — so the answer is counts and the new roots, never a diff. `replayed` marks the
|
|
120
|
+
* idempotent second answer to the same key: nothing was created twice.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ `excluded` is what the IMPORT knows about itself, never what the bundle claimed (ADR-0006,
|
|
123
|
+
* #433). A subtree moved between installations arrives with one version per node, no grants and no
|
|
124
|
+
* runs, and until now nothing said so at the moment somebody could still act on it — the word stood
|
|
125
|
+
* only in a `manifest.json` inside the zip that had already been written.
|
|
126
|
+
*
|
|
127
|
+
* It is deliberately NOT read back out of that manifest. A hand-written bundle saying `excluded: []`
|
|
128
|
+
* would then make the import report that nothing was left behind, and the import would launder
|
|
129
|
+
* somebody else's claim into an answer that sounds like its own — the trap `absence` is bound
|
|
130
|
+
* against in `BundleManifestEntry` above (#534, #560). The five exclusions hold for every import
|
|
131
|
+
* this build performs, with a manifest or from a naked folder, so this is a constant of the code.
|
|
132
|
+
*/
|
|
58
133
|
export const BundleImportResult = z.strictObject({
|
|
59
134
|
nodes: z.number().int().nonnegative(),
|
|
60
135
|
flows: z.number().int().nonnegative(),
|
|
61
136
|
rootNodeIds: z.array(IntelId),
|
|
62
137
|
replayed: z.boolean(),
|
|
138
|
+
excluded: z.array(BundleExclusion),
|
|
63
139
|
});
|
|
@@ -2,8 +2,74 @@ import { z } from "zod";
|
|
|
2
2
|
export declare const IntelId: z.ZodString;
|
|
3
3
|
export declare const IsoDateTime: z.ZodISODateTime;
|
|
4
4
|
export declare const IdempotencyKey: z.ZodString;
|
|
5
|
+
/**
|
|
6
|
+
* The version a write is made against, in the one form every writer of versioned content takes it.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ It lived beside the two node writers until #459 and moved here when the flow graph became the
|
|
9
|
+
* third — the point at which the repository stops copying and names the thing. It could not simply
|
|
10
|
+
* be imported: `flow.ts` may not depend on `node.ts` (the graph is `table → node`, `flow-run →
|
|
11
|
+
* flow`, and everything → here), and a copy would have been a second place for one sentence to
|
|
12
|
+
* drift, which is exactly what `IdempotencyKey` above exists to avoid.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ Nullable and required are not the same thing, and here they stand together (#437). `null` is
|
|
15
|
+
* how something that has no version yet is addressed, and it is the only way to write its first
|
|
16
|
+
* content; leaving the field out says nothing at all and is refused. Whoever leaves it out is
|
|
17
|
+
* almost always holding a node or a flow created one call earlier, and the plain type error they
|
|
18
|
+
* used to get — "expected string, received undefined" — reads as "this field must be an id", about
|
|
19
|
+
* a field that cannot have one yet. Both refusals therefore name both ways instead.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ It stays required once a version exists, and that is what it is for: a mismatch is answered
|
|
22
|
+
* with a `409` rather than by overwriting somebody else's newer version. Making it `optional()` to
|
|
23
|
+
* smooth the first write is the repair that breaks the guard — an absent field would then have to
|
|
24
|
+
* mean "there is none", and the forgetful caller silently replaces the newest one.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ The bounds remain `IntelId`'s. The repeated `min(1)` adds no rule — it only puts the sentence
|
|
27
|
+
* on the second value that gets tried after `undefined`, the empty string.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ The sentence says what the field TAKES, not what the caller did wrong. It answers a wrong type
|
|
30
|
+
* as well as an absent field, and "is required" would be false about the first — which would be
|
|
31
|
+
* this same ticket one corner further on. A value that is merely too long keeps Zod's own `Too
|
|
32
|
+
* big`: that one is already a true statement about what was sent.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ The JSON Schema it produces is the one `IntelId.nullable()` produced — an `anyOf` of the
|
|
35
|
+
* bounded string and `null`, still `required` — so nothing on the MCP surface moves when a field
|
|
36
|
+
* changes over to it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function baseVersion(rule: string): z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
|
5
39
|
export declare const UI_LANGUAGES: readonly ["en", "de", "es"];
|
|
6
40
|
export type UiLanguage = (typeof UI_LANGUAGES)[number];
|
|
41
|
+
/**
|
|
42
|
+
* A set of things the reader is only partly entitled to see: the ones they may see BY NAME, the
|
|
43
|
+
* rest as a NUMBER (ADR-0004 §3). Never the titles of the rest, in any field, ever — a warning or a
|
|
44
|
+
* refusal must not become a way of reading the tree.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ It lives here rather than in `share.ts` because two domains state it: a grant says what it
|
|
47
|
+
* does not reach (`UnreadableNodes`), and a refusal says who is still calling in
|
|
48
|
+
* (`ProblemDetails.callers`, #448). `contract.ts` may not import from `share.ts` — the graph
|
|
49
|
+
* between these files has to stay acyclic — and a copy would be a second place for one rule to
|
|
50
|
+
* drift, which is what `IdempotencyKey` and `baseVersion` above exist against.
|
|
51
|
+
*/
|
|
52
|
+
export declare const NamedOrCounted: z.ZodObject<{
|
|
53
|
+
titles: z.ZodArray<z.ZodString>;
|
|
54
|
+
hidden: z.ZodNumber;
|
|
55
|
+
}, z.core.$strict>;
|
|
56
|
+
export type NamedOrCounted = z.infer<typeof NamedOrCounted>;
|
|
57
|
+
/**
|
|
58
|
+
* ⚠️ `callers` is the one member here that is not about every problem, and it is deliberate (#448).
|
|
59
|
+
* RFC 9457 allows extension members for exactly this: `folder_execute_in_use` refuses to narrow a
|
|
60
|
+
* library folder while flows from outside still call into it, and the CALLERS are the only thing
|
|
61
|
+
* that makes the refusal actionable. They used to exist only inside `detail` — an English sentence
|
|
62
|
+
* built on the server — so a German or Spanish surface could do nothing but print it verbatim, and
|
|
63
|
+
* no surface could shorten it or link the titles.
|
|
64
|
+
*
|
|
65
|
+
* The split is the point: the server decides WHAT is named and what is only counted, because that
|
|
66
|
+
* is an authorization answer nobody else can give; the surface decides how the sentence READS,
|
|
67
|
+
* because that is a language answer no server can give.
|
|
68
|
+
*
|
|
69
|
+
* `detail` keeps its sentence rather than emptying out — MCP hands a model prose and has nothing to
|
|
70
|
+
* formulate with, and an empty `detail` beside a new field would be the worst of the three states.
|
|
71
|
+
* It is absent on every other problem, so a reader must not treat its presence as guaranteed.
|
|
72
|
+
*/
|
|
7
73
|
export declare const ProblemDetails: z.ZodObject<{
|
|
8
74
|
type: z.ZodString;
|
|
9
75
|
status: z.ZodNumber;
|
|
@@ -11,6 +77,10 @@ export declare const ProblemDetails: z.ZodObject<{
|
|
|
11
77
|
detail: z.ZodOptional<z.ZodString>;
|
|
12
78
|
instance: z.ZodOptional<z.ZodString>;
|
|
13
79
|
code: z.ZodOptional<z.ZodString>;
|
|
80
|
+
callers: z.ZodOptional<z.ZodObject<{
|
|
81
|
+
titles: z.ZodArray<z.ZodString>;
|
|
82
|
+
hidden: z.ZodNumber;
|
|
83
|
+
}, z.core.$strict>>;
|
|
14
84
|
}, z.core.$strict>;
|
|
15
85
|
export type ProblemDetails = z.infer<typeof ProblemDetails>;
|
|
16
86
|
export declare const SessionUser: z.ZodObject<{
|
|
@@ -13,6 +13,42 @@ export const IdempotencyKey = z
|
|
|
13
13
|
.min(8)
|
|
14
14
|
.max(200)
|
|
15
15
|
.describe("A caller-chosen key that makes this call safe to retry: repeating the same key returns the first result instead of doing the work twice. Reuse the SAME key when retrying one attempt, and a new one for a genuinely new call.");
|
|
16
|
+
/**
|
|
17
|
+
* The version a write is made against, in the one form every writer of versioned content takes it.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ It lived beside the two node writers until #459 and moved here when the flow graph became the
|
|
20
|
+
* third — the point at which the repository stops copying and names the thing. It could not simply
|
|
21
|
+
* be imported: `flow.ts` may not depend on `node.ts` (the graph is `table → node`, `flow-run →
|
|
22
|
+
* flow`, and everything → here), and a copy would have been a second place for one sentence to
|
|
23
|
+
* drift, which is exactly what `IdempotencyKey` above exists to avoid.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ Nullable and required are not the same thing, and here they stand together (#437). `null` is
|
|
26
|
+
* how something that has no version yet is addressed, and it is the only way to write its first
|
|
27
|
+
* content; leaving the field out says nothing at all and is refused. Whoever leaves it out is
|
|
28
|
+
* almost always holding a node or a flow created one call earlier, and the plain type error they
|
|
29
|
+
* used to get — "expected string, received undefined" — reads as "this field must be an id", about
|
|
30
|
+
* a field that cannot have one yet. Both refusals therefore name both ways instead.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ It stays required once a version exists, and that is what it is for: a mismatch is answered
|
|
33
|
+
* with a `409` rather than by overwriting somebody else's newer version. Making it `optional()` to
|
|
34
|
+
* smooth the first write is the repair that breaks the guard — an absent field would then have to
|
|
35
|
+
* mean "there is none", and the forgetful caller silently replaces the newest one.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ The bounds remain `IntelId`'s. The repeated `min(1)` adds no rule — it only puts the sentence
|
|
38
|
+
* on the second value that gets tried after `undefined`, the empty string.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ The sentence says what the field TAKES, not what the caller did wrong. It answers a wrong type
|
|
41
|
+
* as well as an absent field, and "is required" would be false about the first — which would be
|
|
42
|
+
* this same ticket one corner further on. A value that is merely too long keeps Zod's own `Too
|
|
43
|
+
* big`: that one is already a true statement about what was sent.
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ The JSON Schema it produces is the one `IntelId.nullable()` produced — an `anyOf` of the
|
|
46
|
+
* bounded string and `null`, still `required` — so nothing on the MCP surface moves when a field
|
|
47
|
+
* changes over to it.
|
|
48
|
+
*/
|
|
49
|
+
export function baseVersion(rule) {
|
|
50
|
+
return z.union([IntelId.min(1, { error: rule }), z.null()], { error: rule });
|
|
51
|
+
}
|
|
16
52
|
// The languages whose UI catalog ships inside intel-ui. They live here rather than only in the UI
|
|
17
53
|
// because `intel build` has to know them: a built-in language may be chosen as `ui.defaultLanguage`
|
|
18
54
|
// without the customer listing a copy under `ui.languages` that would rot at every UI update.
|
|
@@ -21,6 +57,37 @@ export const IdempotencyKey = z
|
|
|
21
57
|
// `i18n.unit.ts` compares it against the catalogs actually built in; a language listed here without
|
|
22
58
|
// a file gives a red run there instead of a UI that starts on a catalog which does not exist.
|
|
23
59
|
export const UI_LANGUAGES = ["en", "de", "es"];
|
|
60
|
+
/**
|
|
61
|
+
* A set of things the reader is only partly entitled to see: the ones they may see BY NAME, the
|
|
62
|
+
* rest as a NUMBER (ADR-0004 §3). Never the titles of the rest, in any field, ever — a warning or a
|
|
63
|
+
* refusal must not become a way of reading the tree.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ It lives here rather than in `share.ts` because two domains state it: a grant says what it
|
|
66
|
+
* does not reach (`UnreadableNodes`), and a refusal says who is still calling in
|
|
67
|
+
* (`ProblemDetails.callers`, #448). `contract.ts` may not import from `share.ts` — the graph
|
|
68
|
+
* between these files has to stay acyclic — and a copy would be a second place for one rule to
|
|
69
|
+
* drift, which is what `IdempotencyKey` and `baseVersion` above exist against.
|
|
70
|
+
*/
|
|
71
|
+
export const NamedOrCounted = z.strictObject({
|
|
72
|
+
titles: z.array(z.string().min(1).max(240)),
|
|
73
|
+
hidden: z.number().int().nonnegative(),
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* ⚠️ `callers` is the one member here that is not about every problem, and it is deliberate (#448).
|
|
77
|
+
* RFC 9457 allows extension members for exactly this: `folder_execute_in_use` refuses to narrow a
|
|
78
|
+
* library folder while flows from outside still call into it, and the CALLERS are the only thing
|
|
79
|
+
* that makes the refusal actionable. They used to exist only inside `detail` — an English sentence
|
|
80
|
+
* built on the server — so a German or Spanish surface could do nothing but print it verbatim, and
|
|
81
|
+
* no surface could shorten it or link the titles.
|
|
82
|
+
*
|
|
83
|
+
* The split is the point: the server decides WHAT is named and what is only counted, because that
|
|
84
|
+
* is an authorization answer nobody else can give; the surface decides how the sentence READS,
|
|
85
|
+
* because that is a language answer no server can give.
|
|
86
|
+
*
|
|
87
|
+
* `detail` keeps its sentence rather than emptying out — MCP hands a model prose and has nothing to
|
|
88
|
+
* formulate with, and an empty `detail` beside a new field would be the worst of the three states.
|
|
89
|
+
* It is absent on every other problem, so a reader must not treat its presence as guaranteed.
|
|
90
|
+
*/
|
|
24
91
|
export const ProblemDetails = z.strictObject({
|
|
25
92
|
type: z.string(),
|
|
26
93
|
status: z.number().int().min(400).max(599),
|
|
@@ -28,6 +95,7 @@ export const ProblemDetails = z.strictObject({
|
|
|
28
95
|
detail: z.string().optional(),
|
|
29
96
|
instance: z.string().optional(),
|
|
30
97
|
code: z.string().optional(),
|
|
98
|
+
callers: NamedOrCounted.optional(),
|
|
31
99
|
});
|
|
32
100
|
// Who the caller is, as Gate resolved it from the bearer: no token, and no capability list.
|
|
33
101
|
//
|
package/dist/contract/flow.d.ts
CHANGED
|
@@ -672,6 +672,7 @@ export declare const FlowRequirements: z.ZodObject<{
|
|
|
672
672
|
id: z.ZodString;
|
|
673
673
|
title: z.ZodString;
|
|
674
674
|
}, z.core.$strict>>;
|
|
675
|
+
invalidNodes: z.ZodArray<z.ZodString>;
|
|
675
676
|
hiddenNodes: z.ZodNumber;
|
|
676
677
|
servers: z.ZodArray<z.ZodString>;
|
|
677
678
|
}, z.core.$strict>;
|
|
@@ -730,7 +731,7 @@ export declare const GetFlowInput: z.ZodObject<{
|
|
|
730
731
|
export type GetFlowInput = z.infer<typeof GetFlowInput>;
|
|
731
732
|
export declare const SaveFlowVersionInput: z.ZodObject<{
|
|
732
733
|
flowId: z.ZodString;
|
|
733
|
-
baseVersionId: z.
|
|
734
|
+
baseVersionId: z.ZodUnion<readonly [z.ZodString, z.ZodNull]>;
|
|
734
735
|
graph: z.ZodObject<{
|
|
735
736
|
nodes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
736
737
|
kind: z.ZodLiteral<"trigger">;
|
package/dist/contract/flow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { IdempotencyKey, IntelId, IsoDateTime, LinkConfiguration } from "./contract.js";
|
|
2
|
+
import { baseVersion, IdempotencyKey, IntelId, IsoDateTime, LinkConfiguration, } from "./contract.js";
|
|
3
3
|
import { serverOf, ToolName, ToolServerHandle } from "./tool.js";
|
|
4
4
|
export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
|
|
5
5
|
export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
|
|
@@ -259,11 +259,24 @@ export const ReferencedNode = z.strictObject({
|
|
|
259
259
|
// (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
|
|
260
260
|
// the requesting user's own token (ADR-0003).
|
|
261
261
|
//
|
|
262
|
-
// ⚠️ `nodes` names only what the asking user may see. The rest
|
|
262
|
+
// ⚠️ `nodes` names only what the asking user may see. The rest falls into TWO fields, and keeping
|
|
263
|
+
// them apart is the whole of #509: `invalidNodes` are references that name nothing at all any more,
|
|
264
|
+
// `hiddenNodes` is a count of the ones that exist and are out of this reader's reach.
|
|
265
|
+
//
|
|
266
|
+
// ⚠️ They used to be one number, and the number was read out as "you cannot see it" — a sentence
|
|
267
|
+
// about a PERMISSION, in front of a broken link. The two states suggest opposite actions and only
|
|
268
|
+
// one of them helps: with a permission one asks for access and waits, with a dead reference the step
|
|
269
|
+
// has to be replaced before the flow can run at all. #492 decided that such a reference is marked
|
|
270
|
+
// visibly invalid rather than left silently pointed at an id nothing answers.
|
|
271
|
+
//
|
|
272
|
+
// ⚠️ `invalidNodes` carries IDS, never titles, and that is not the same concession `hiddenNodes`
|
|
273
|
+
// makes. There is nothing left to name — the row is gone — and the id is one the caller sent us out
|
|
274
|
+
// of a graph they may already read, so it discloses nothing they did not write themselves.
|
|
263
275
|
export const FlowRequirements = z.strictObject({
|
|
264
276
|
flowId: IntelId,
|
|
265
277
|
versionId: IntelId.nullable(),
|
|
266
278
|
nodes: z.array(ReferencedNode),
|
|
279
|
+
invalidNodes: z.array(IntelId),
|
|
267
280
|
hiddenNodes: z.number().int().nonnegative(),
|
|
268
281
|
servers: z.array(ToolServerHandle),
|
|
269
282
|
});
|
|
@@ -375,7 +388,12 @@ export const GetFlowInput = z.strictObject({
|
|
|
375
388
|
});
|
|
376
389
|
export const SaveFlowVersionInput = z.strictObject({
|
|
377
390
|
flowId: IntelId.describe("Flow to append a version to."),
|
|
378
|
-
|
|
391
|
+
// ⚠️ `baseVersion`, not `IntelId.nullable()` (#459). The field is required-nullable either way;
|
|
392
|
+
// what changes is what a caller who left it out is told. Zod's own `expected string, received
|
|
393
|
+
// undefined` reads as "this needs an id" to somebody holding a flow created one call earlier —
|
|
394
|
+
// whose `currentVersionId` is `null` and can be nothing else. That reading cost a session at
|
|
395
|
+
// #437, and this was the third field carrying it.
|
|
396
|
+
baseVersionId: baseVersion("baseVersionId takes the `currentVersionId` from flow_get, or null when the flow has no version yet — and it has to be sent.").describe("The version this edit was made against, from flow_get, or `null` for a flow that has none yet. If the flow has moved on since, the call is refused rather than overwriting the newer version."),
|
|
379
397
|
graph: FlowGraph.describe("The complete graph — nodes and edges — not a patch. It is validated before it is stored, so an unreachable node or a dangling edge is refused here rather than at run time."),
|
|
380
398
|
idempotencyKey: IdempotencyKey,
|
|
381
399
|
});
|
package/dist/contract/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { IdempotencyKey, IntelId, IsoDateTime } from "./contract.js";
|
|
2
|
+
import { baseVersion, IdempotencyKey, IntelId, IsoDateTime } from "./contract.js";
|
|
3
3
|
// What this installation is equipped to do — deployment facts, never the caller's permissions
|
|
4
4
|
// (those stay behind each door, where `/session` deliberately does not carry them). `agentRuntime`
|
|
5
5
|
// says whether an agent Worker is bound at all (#190): without it the UI offers no "New agent" and
|
|
@@ -95,30 +95,6 @@ export const CreateNodeInput = z.strictObject({
|
|
|
95
95
|
.describe("Optional sentence about the node, for readers rather than for search ranking."),
|
|
96
96
|
idempotencyKey: IdempotencyKey,
|
|
97
97
|
});
|
|
98
|
-
/**
|
|
99
|
-
* The version a write is made against, in the one form both writers of node content take it.
|
|
100
|
-
*
|
|
101
|
-
* ⚠️ Nullable and required are not the same thing, and here they stand together (#437). `null` is
|
|
102
|
-
* how a node that has no version yet is addressed, and it is the only way to write its first
|
|
103
|
-
* content; leaving the field out says nothing at all and is refused. Whoever leaves it out is
|
|
104
|
-
* almost always holding a node created one call earlier, and the plain type error they used to get
|
|
105
|
-
* — "expected string, received undefined" — reads as "this field must be an id", about a field that
|
|
106
|
-
* cannot have one yet. Both refusals therefore name both ways instead.
|
|
107
|
-
*
|
|
108
|
-
* ⚠️ It stays required once a version exists, and that is what it is for: a mismatch is answered
|
|
109
|
-
* with a `409` rather than by overwriting somebody else's newer version.
|
|
110
|
-
*
|
|
111
|
-
* ⚠️ The bounds remain `IntelId`'s. The repeated `min(1)` adds no rule — it only puts the sentence
|
|
112
|
-
* on the second value that gets tried after `undefined`, the empty string.
|
|
113
|
-
*
|
|
114
|
-
* ⚠️ The sentence says what the field TAKES, not what the caller did wrong. It answers a wrong type
|
|
115
|
-
* as well as an absent field, and "is required" would be false about the first — which would be
|
|
116
|
-
* this same ticket one corner further on. A value that is merely too long keeps Zod's own `Too
|
|
117
|
-
* big`: that one is already a true statement about what was sent.
|
|
118
|
-
*/
|
|
119
|
-
function baseVersion(rule) {
|
|
120
|
-
return z.union([IntelId.min(1, { error: rule }), z.null()], { error: rule });
|
|
121
|
-
}
|
|
122
98
|
export const SaveNodeVersionInput = z.strictObject({
|
|
123
99
|
nodeId: IntelId.describe("Document node to append a version to."),
|
|
124
100
|
baseVersionId: baseVersion("baseVersionId takes the `currentVersionId` from node_get, or null when the node has no version yet — and it has to be sent.").describe("The version this edit was made against — `currentVersionId` from node_get. If the node has moved on since, the call is refused rather than overwriting the newer version; pass `null` only for a node that has no version yet."),
|
|
@@ -194,7 +170,13 @@ export const PurgeNodeInput = z.strictObject({
|
|
|
194
170
|
nodeId: IntelId.describe("Node to delete for good. It must already be archived — a living node has no path into nothing."),
|
|
195
171
|
idempotencyKey: IdempotencyKey,
|
|
196
172
|
});
|
|
197
|
-
|
|
173
|
+
// The question `PurgeNodeInput` cannot be asked afterwards: what goes with it, while it is all
|
|
174
|
+
// still there to be counted. Described since #586, because the count reached MCP that day — and a
|
|
175
|
+
// surface that may delete without being able to look first is the state `destructive.md` calls a
|
|
176
|
+
// confirmation that cannot name what disappears.
|
|
177
|
+
export const PurgeNodePreviewInput = z.strictObject({
|
|
178
|
+
nodeId: IntelId.describe("Archived node a purge would delete. Answers how many items would go with it and how many documents link to it from outside, and changes nothing. A node that is not archived is refused here for the same reason node_purge refuses it."),
|
|
179
|
+
});
|
|
198
180
|
export const PurgeNodePreview = z.strictObject({
|
|
199
181
|
inboundLinks: z.number().int().nonnegative(),
|
|
200
182
|
totalItems: z.number().int().positive(),
|
package/dist/contract/share.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ export declare const ListFlowGrantsInput: z.ZodObject<{
|
|
|
24
24
|
flowId: z.ZodString;
|
|
25
25
|
}, z.core.$strict>;
|
|
26
26
|
export type ListFlowGrantsInput = z.infer<typeof ListFlowGrantsInput>;
|
|
27
|
+
export declare const ListEffectiveAccessInput: z.ZodObject<{
|
|
28
|
+
resourceId: z.ZodString;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
export type ListEffectiveAccessInput = z.infer<typeof ListEffectiveAccessInput>;
|
|
31
|
+
export declare const ListFlowEffectiveAccessInput: z.ZodObject<{
|
|
32
|
+
flowId: z.ZodString;
|
|
33
|
+
}, z.core.$strict>;
|
|
34
|
+
export type ListFlowEffectiveAccessInput = z.infer<typeof ListFlowEffectiveAccessInput>;
|
|
27
35
|
export declare const ResourceGrant: z.ZodObject<{
|
|
28
36
|
id: z.ZodString;
|
|
29
37
|
resourceId: z.ZodString;
|
package/dist/contract/share.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { IdempotencyKey, IntelId, IsoDateTime } from "./contract.js";
|
|
2
|
+
import { IdempotencyKey, IntelId, IsoDateTime, NamedOrCounted } from "./contract.js";
|
|
3
3
|
// ⚠️ There is no `ContextPolicy`, and it is not coming back in this shape (#76). It said whether a
|
|
4
4
|
// document should be pinned into a context, be found by relevance, or be named explicitly — an
|
|
5
5
|
// instruction to a retrieval Intel does not perform. Intel hands out references and the agent
|
|
@@ -23,6 +23,21 @@ export const ListGrantsInput = z.strictObject({
|
|
|
23
23
|
export const ListFlowGrantsInput = z.strictObject({
|
|
24
24
|
flowId: IntelId.describe("Flow whose direct grants to list. Access inherited from the folder it is filed in is not a grant on this flow and is not listed here."),
|
|
25
25
|
});
|
|
26
|
+
// ⚠️ A DIFFERENT question from the two above, not a fuller answer to them (#586). The `*GrantsInput`
|
|
27
|
+
// pair asks what sits ON this one resource; these two ask who REACHES it once inheritance is
|
|
28
|
+
// resolved — every grant along the folders above plus the owners, who need no grant row at all. On
|
|
29
|
+
// the everyday resource, whose access comes from the folder it is filed in, the first answer is
|
|
30
|
+
// empty and this one is not, so a caller that reaches for the wrong one is told "nobody" about
|
|
31
|
+
// something half the organization can open.
|
|
32
|
+
//
|
|
33
|
+
// Two inputs rather than one widened `resourceId`, for the reason the pair above gives: the id
|
|
34
|
+
// names a different table, and the queries behind them are two statements over two sets of tables.
|
|
35
|
+
export const ListEffectiveAccessInput = z.strictObject({
|
|
36
|
+
resourceId: IntelId.describe("Node to answer for. The answer crosses the folders above it and names owners as well as grants, so it is not the list node_grant_list gives."),
|
|
37
|
+
});
|
|
38
|
+
export const ListFlowEffectiveAccessInput = z.strictObject({
|
|
39
|
+
flowId: IntelId.describe("Flow to answer for. The answer includes what the folder it is filed in passes down and names owners as well as grants, so it is not the list flow_grant_list gives."),
|
|
40
|
+
});
|
|
26
41
|
export const ResourceGrant = z.strictObject({
|
|
27
42
|
id: IntelId,
|
|
28
43
|
resourceId: IntelId,
|
|
@@ -32,13 +47,20 @@ export const ResourceGrant = z.strictObject({
|
|
|
32
47
|
createdBy: IntelId,
|
|
33
48
|
createdAt: IsoDateTime,
|
|
34
49
|
});
|
|
50
|
+
// The word means exactly the same thing on a node grant and on a flow grant, so it is described
|
|
51
|
+
// once — the rule this package states about `IdempotencyKey`, applied within one file rather than
|
|
52
|
+
// across the package. Two copies are two places to drift, and both of them ship in `tools/list`.
|
|
53
|
+
//
|
|
54
|
+
// ⚠️ What is refused is decided by `deps.now()` in the application layer and cannot be expressed
|
|
55
|
+
// here (#442); this sentence names the answer a caller gets, it does not produce it.
|
|
56
|
+
const GrantExpiresAt = IsoDateTime.nullable()
|
|
57
|
+
.default(null)
|
|
58
|
+
.describe("When the grant stops working, or `null` for one that does not expire on its own. A time that is not in the future is refused with `grant_already_expired`: a grant that could never work is not created.");
|
|
35
59
|
export const ShareInput = z.strictObject({
|
|
36
60
|
resourceId: IntelId.describe("Node to grant access to. A grant on a folder is inherited by everything beneath it, which is the usual way to share a whole area."),
|
|
37
61
|
principal: SharePrincipal.describe("Who gets the access: a Gate user by id, someone by verified email address, or the whole organization."),
|
|
38
62
|
verb: ResourceVerb.describe("What they may do. Each verb is granted on its own and none implies another: `read` reads, `write` writes, `execute` runs a flow, `share` passes access on. Seeing a process is deliberately separable from being allowed to start it."),
|
|
39
|
-
expiresAt:
|
|
40
|
-
.default(null)
|
|
41
|
-
.describe("When the grant stops working, or `null` for one that does not expire on its own."),
|
|
63
|
+
expiresAt: GrantExpiresAt,
|
|
42
64
|
idempotencyKey: IdempotencyKey,
|
|
43
65
|
});
|
|
44
66
|
export const RevokeGrantInput = z.strictObject({
|
|
@@ -50,9 +72,7 @@ export const ShareFlowInput = z.strictObject({
|
|
|
50
72
|
flowId: IntelId.describe("Flow to grant access to. A grant here reaches this flow and nothing else — not the folder it is filed in, and not the flows it calls."),
|
|
51
73
|
principal: SharePrincipal.describe("Who gets the access: a Gate user by id, someone by verified email address, or the whole organization."),
|
|
52
74
|
verb: ResourceVerb.describe("What they may do. Each verb is granted on its own and none implies another: `read` opens the flow, `write` edits it, `execute` runs it, `share` passes access on."),
|
|
53
|
-
expiresAt:
|
|
54
|
-
.default(null)
|
|
55
|
-
.describe("When the grant stops working, or `null` for one that does not expire on its own."),
|
|
75
|
+
expiresAt: GrantExpiresAt,
|
|
56
76
|
idempotencyKey: IdempotencyKey,
|
|
57
77
|
});
|
|
58
78
|
export const RevokeFlowGrantInput = z.strictObject({
|
|
@@ -66,10 +86,11 @@ export const RevokeFlowGrantInput = z.strictObject({
|
|
|
66
86
|
//
|
|
67
87
|
// ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
|
|
68
88
|
// number. A warning must not become a way of reading titles out of the tree.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
//
|
|
90
|
+
// It IS `NamedOrCounted` — one schema, under the name this domain calls it by. The shape moved to
|
|
91
|
+
// `contract.ts` when a refusal started carrying it too (#448); it is not restated here, because two
|
|
92
|
+
// declarations of one rule are two places for it to drift.
|
|
93
|
+
export const UnreadableNodes = NamedOrCounted;
|
|
73
94
|
// The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
|
|
74
95
|
// and described afterwards. Blocking would force everyone who uses a central policy document to
|
|
75
96
|
// duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
|