@fourier-labs/harbour-contracts 0.50.0 → 0.53.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.
@@ -2,7 +2,7 @@
2
2
  // Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
3
3
 
4
4
  /**
5
- * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It also carries validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run. An unsupported report carries only application.unsupported and no plan or fixtures.
5
+ * Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It may also carry validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run; the deployment plane reads the workspace fixture sidecar, so the report's copy is optional. An unsupported report carries only application.unsupported and no plan or fixtures.
6
6
  */
7
7
  export type TransformReport = {
8
8
  /**
@@ -11,23 +11,33 @@ export type TransformReport = {
11
11
  * @minItems 1
12
12
  */
13
13
  applied_playbooks?: string[];
14
+ /**
15
+ * Optional. The deployed value is a platform constant supplied to the build, not a report field, so a deployable report may omit it and one that still carries it stays valid.
16
+ */
14
17
  static_rebuild_required?: boolean;
15
18
  analysis?: {
16
19
  deployable: boolean;
17
- needsTransformation: boolean;
20
+ /**
21
+ * Whether the attempt changed the tree. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
22
+ */
23
+ needsTransformation?: boolean;
18
24
  /**
19
25
  * @maxItems 64
20
26
  */
21
27
  blockers: string[];
22
28
  /**
29
+ * Paths the attempt touched. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
30
+ *
23
31
  * @minItems 1
24
32
  * @maxItems 1024
25
33
  */
26
- surfaces: string[];
34
+ surfaces?: string[];
27
35
  /**
36
+ * Optional file-and-line citations for why the transformation was needed. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
37
+ *
28
38
  * @maxItems 1024
29
39
  */
30
- evidence: {
40
+ evidence?: {
31
41
  path: string;
32
42
  line: number;
33
43
  reason: string;
@@ -35,29 +45,46 @@ export type TransformReport = {
35
45
  };
36
46
  database?: {
37
47
  mode: 'none' | 'sql';
38
- changeType: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
39
- requiresCodeChanges: boolean;
40
- requiresSchemaMigration: boolean;
41
48
  /**
49
+ * Classification of the changed paths. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
50
+ */
51
+ changeType?: 'none' | 'code_only' | 'schema_only' | 'code_and_schema';
52
+ /**
53
+ * Whether any changed path sits outside the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
54
+ */
55
+ requiresCodeChanges?: boolean;
56
+ /**
57
+ * Whether the Aurora bundle grew past its seeded prefix. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
58
+ */
59
+ requiresSchemaMigration?: boolean;
60
+ /**
61
+ * Migration files in the Aurora bundle. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
62
+ *
42
63
  * @maxItems 128
43
64
  */
44
- migrationPaths: string[];
65
+ migrationPaths?: string[];
45
66
  /**
67
+ * Changed paths on the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.
68
+ *
46
69
  * @maxItems 512
47
70
  */
48
- changedFiles: string[];
71
+ changedFiles?: string[];
49
72
  /**
73
+ * Optional file-and-line citations for the database findings. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.
74
+ *
50
75
  * @maxItems 128
51
76
  */
52
- evidence: {
77
+ evidence?: {
53
78
  path: string;
54
79
  line: number;
55
80
  reason: string;
56
81
  }[];
57
82
  /**
83
+ * Optional. The platform's only structural decline is application.unsupported; nothing reads this list, so a deployable report may omit it and one that still carries it stays valid.
84
+ *
58
85
  * @maxItems 64
59
86
  */
60
- unsupportedFeatures: string[];
87
+ unsupportedFeatures?: string[];
61
88
  };
62
89
  /**
63
90
  * Agent-authored preservation (smoke) plan for this attempt. Required whenever the report is deployable; forbidden on an unsupported report. Every test must be grounded in files the agent actually read or changed (evidence) and expressed as tenant-relative browser or HTTP steps the validation cell can execute without secrets. Structural bounds live here; per-action required fields, at least one assert* step per test, one to three happy_path tests, unique ids, the {{runId}} substitution and path safety are enforced semantically by the deployment plane.
@@ -75,12 +102,12 @@ export type TransformReport = {
75
102
  kind: 'happy_path' | 'boundary_check';
76
103
  name: string;
77
104
  /**
78
- * Transformation boundaries this test claims to cover.
105
+ * Transformation boundaries this test claims to cover. Optional: Harbour derives them from the test's own evidence paths, so a deployable report may omit them and one that still carries them stays valid.
79
106
  *
80
107
  * @minItems 1
81
108
  * @maxItems 8
82
109
  */
83
- boundaries: (
110
+ boundaries?: (
84
111
  | 'application'
85
112
  | 'routing'
86
113
  | 'database'
@@ -159,7 +186,10 @@ export type TransformReport = {
159
186
  * @maxItems 8
160
187
  */
161
188
  uncovered: {
162
- boundary:
189
+ /**
190
+ * Optional: Harbour derives the uncovered boundaries from the changed paths and the covered set, so a deployable report may omit this and one that still carries it stays valid.
191
+ */
192
+ boundary?:
163
193
  | 'application'
164
194
  | 'routing'
165
195
  | 'database'
@@ -247,7 +277,13 @@ export type TransformReport = {
247
277
  kind: 'sql';
248
278
  path: string;
249
279
  } | null;
280
+ /**
281
+ * Optional: determined by topology (static topology iff static build), so a deployable report may omit it and one that still carries it stays valid.
282
+ */
250
283
  buildKind?: 'container' | 'static';
284
+ /**
285
+ * Optional: determined by the build output kinds, so a deployable report may omit it and one that still carries it stays valid.
286
+ */
251
287
  topology?: 'static' | 'backend' | 'frontend_and_backend';
252
288
  entrypoint?: string;
253
289
  build?: {
@@ -271,6 +307,9 @@ export type TransformReport = {
271
307
  route: string;
272
308
  }[];
273
309
  };
310
+ /**
311
+ * Optional: both members are platform constants, so a deployable report may omit this object and one that still carries it stays valid.
312
+ */
274
313
  basePath?: {
275
314
  required: boolean;
276
315
  environmentVariable: 'HARBOUR_BASE_PATH';
@@ -285,6 +324,8 @@ export type TransformReport = {
285
324
  reason: string;
286
325
  }[];
287
326
  /**
327
+ * Optional: a deployable plan requires this empty, so a deployable report may omit it and one that still carries it (empty) stays valid.
328
+ *
288
329
  * @maxItems 64
289
330
  */
290
331
  unsupportedFeatures?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour-contracts",
3
- "version": "0.50.0",
3
+ "version": "0.53.0",
4
4
  "description": "Single source of truth for every data shape that crosses a boundary between Harbour components.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/app-integrations-declaration.schema.json",
4
4
  "title": "AppIntegrationsDeclaration",
5
- "description": "Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (five operations) and each operation carries a fixed identity mode: slack.channel.history=user, slack.message.post=app, gmail.thread.list=user, gmail.message.read=user, warehouse.view.read=app. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read. Gmail is read-only and user-mode only; its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind enums referenced by the other integration schemas.",
5
+ "description": "Committed `.harbour/integrations.json`: the app's declared use of tenant-wide integration connections. Keys are logical connection aliases (the governance sourceId); resources are logical names (Slack channel alias, warehouse view name), never IDs, tokens or URLs. The operation set is closed (six operations). Each operation carries a fixed identity mode — slack.channel.history=user, gmail.thread.list=user, gmail.message.read=user, gmail.message.send=user, warehouse.view.read=app — except slack.message.post, whose mode the app DECLARES: `app` posts as the company's Slack app (the one audited bot identity, under the IT-approved per-app display name when the connection declares `presentation`), `user` posts as the signed-in person after their own consent. The mode is never chosen at call time: governance runs the call under the approved grant of the declared mode. A `saas` connection may declare only the Slack and Gmail operations and a `database` connection only warehouse.view.read; `presentation` is a `saas` connection's optional per-app display name and icon for app-mode Slack posts, shown to IT at approval time and applied only from the approved grant. Gmail is user-mode only (reads plus one send); its single resource is the fixed `inbox` alias. This file also owns the shared IntegrationOperation / IntegrationIdentityMode / IntegrationEnvironment / IntegrationConnectionKind / IntegrationPresentation definitions referenced by the other integration schemas.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema", "connections"],
@@ -26,18 +26,34 @@
26
26
  "IntegrationOperation": {
27
27
  "type": "string",
28
28
  "enum": ["slack.channel.history", "slack.message.post", "gmail.thread.list", "gmail.message.read", "gmail.message.send", "warehouse.view.read"],
29
- "description": "Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity."
29
+ "description": "Closed operation set. Extending it is a contracts change. gmail.message.send (0.50.0) sends one plain-text email as the signed-in person: a send like slack.message.post (explicit Send, idempotencyKey, write receipt) under user identity. slack.message.post (0.53.0) runs in the mode the app declares: app (the company's Slack app, under the IT-approved per-app name) or user (the signed-in person, after their consent)."
30
30
  },
31
31
  "IntegrationIdentityMode": {
32
32
  "type": "string",
33
33
  "enum": ["app", "user"],
34
- "description": "Whose authority an operation runs under. Fixed per operation; callers never choose it."
34
+ "description": "Whose authority an operation runs under. Fixed per operation (slack.message.post: declared per app); never chosen at call time."
35
35
  },
36
36
  "IntegrationEnvironment": {
37
37
  "type": "string",
38
38
  "enum": ["development", "preview", "production"],
39
39
  "description": "development is the CLI/local kit; preview and production are hosted."
40
40
  },
41
+ "IntegrationPresentation": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["displayName"],
45
+ "description": "How an app's app-mode Slack posts are presented (0.53.0): the display name and optional icon the platform bot posts under, so each app's posts are distinguishable while the bot stays the one audited identity. Declared by the app, shown to IT on the access request (\"posts as\"), copied onto the approved grant and applied only from there — a call can never name itself. Needs the Slack app scope chat:write.customize on the tenant's installation; without it the post goes out under the bot's own name and the receipt says so.",
46
+ "properties": {
47
+ "displayName": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "maxLength": 80,
51
+ "pattern": "^(?!\\s*$)(?!.*(^|[\\s(\\[<\"'])@[A-Za-z0-9_])(?!.*<@)[^\\u0000-\\u001F\\u007F]*$",
52
+ "description": "1..80 characters of plain text: not blank, no control characters, no @-mentions (`@name`, `<@id>`). Must not impersonate a person: IT sees it before approving."
53
+ },
54
+ "iconEmoji": { "type": "string", "pattern": "^:[a-z0-9_+-]{1,60}:$", "description": "A Slack emoji shortcode such as `:sandwich:`; absent keeps the bot's own icon." }
55
+ }
56
+ },
41
57
  "ColumnName": {
42
58
  "type": "string",
43
59
  "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,62}$",
@@ -71,12 +87,13 @@
71
87
  "resources": { "$ref": "#/definitions/ResourceNames" }
72
88
  }
73
89
  },
74
- "AppModeChannelOperation": {
90
+ "DeclaredModeChannelOperation": {
75
91
  "type": "object",
76
92
  "additionalProperties": false,
77
93
  "required": ["identity", "resources"],
94
+ "description": "slack.message.post: the app declares the mode — app (posts as the company's Slack app) or user (posts as the signed-in person).",
78
95
  "properties": {
79
- "identity": { "type": "string", "const": "app" },
96
+ "identity": { "$ref": "#/definitions/IntegrationIdentityMode" },
80
97
  "resources": { "$ref": "#/definitions/ResourceNames" }
81
98
  }
82
99
  },
@@ -101,13 +118,14 @@
101
118
  "required": ["kind", "operations"],
102
119
  "properties": {
103
120
  "kind": { "$ref": "#/definitions/IntegrationConnectionKind" },
121
+ "presentation": { "$ref": "#/definitions/IntegrationPresentation", "description": "saas connections only (Slack): how this app's app-mode posts are presented; meaningless for every other operation." },
104
122
  "operations": {
105
123
  "type": "object",
106
124
  "additionalProperties": false,
107
125
  "minProperties": 1,
108
126
  "properties": {
109
127
  "slack.channel.history": { "$ref": "#/definitions/UserModeChannelOperation" },
110
- "slack.message.post": { "$ref": "#/definitions/AppModeChannelOperation" },
128
+ "slack.message.post": { "$ref": "#/definitions/DeclaredModeChannelOperation" },
111
129
  "gmail.thread.list": { "$ref": "#/definitions/UserModeChannelOperation" },
112
130
  "gmail.message.read": { "$ref": "#/definitions/UserModeChannelOperation" },
113
131
  "gmail.message.send": { "$ref": "#/definitions/UserModeChannelOperation" },
@@ -118,6 +136,7 @@
118
136
  "if": { "properties": { "kind": { "const": "saas" } } },
119
137
  "then": { "properties": { "operations": { "not": { "required": ["warehouse.view.read"] } } } },
120
138
  "else": {
139
+ "not": { "required": ["presentation"] },
121
140
  "properties": {
122
141
  "operations": {
123
142
  "not": { "anyOf": [{ "required": ["slack.channel.history"] }, { "required": ["slack.message.post"] }, { "required": ["gmail.thread.list"] }, { "required": ["gmail.message.read"] }, { "required": ["gmail.message.send"] }] }
@@ -132,6 +151,7 @@
132
151
  "connections": {
133
152
  "company-slack": {
134
153
  "kind": "saas",
154
+ "presentation": { "displayName": "Lunch Vote", "iconEmoji": ":sandwich:" },
135
155
  "operations": {
136
156
  "slack.channel.history": { "identity": "user", "resources": ["team-updates"] },
137
157
  "slack.message.post": { "identity": "app", "resources": ["team-updates"] }
@@ -155,6 +175,17 @@
155
175
  }
156
176
  }
157
177
  }
178
+ },
179
+ {
180
+ "schema": "harbour.app-integrations/2.0",
181
+ "connections": {
182
+ "company-slack": {
183
+ "kind": "saas",
184
+ "operations": {
185
+ "slack.message.post": { "identity": "user", "resources": ["team-updates"] }
186
+ }
187
+ }
188
+ }
158
189
  }
159
190
  ]
160
191
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/data-grant-v2.schema.json",
4
4
  "title": "DataGrantV2",
5
- "description": "An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON [\"v2\",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.",
5
+ "description": "An app's integration grant for one connection, environment and identity mode (control table sk APP#<appId>#IGRANT#<environment>#<sourceId>#<mode>; grantId = base64url(JSON [\"v2\",appId,environment,sourceId,mode])). Additive to the legacy DataGrant (data-grant.schema.json), which is never consulted by v2 paths. A fresh REQUESTED record carries empty `operations`/`resources` until the attached request is approved; approved resources are structured `{name, columns?}` entries (a Slack channel alias, the fixed Gmail `inbox` alias, or a warehouse view alias with its approved columns). Expiry is enforced from UTC time on every call and SDK calls never extend it; `expiredAt` is stamped by the recovery sweep afterwards. Status edges live in x-transitions; DENIED and REVOKED are terminal (a regrant after revocation is a new REQUESTED record once cleanup is DONE). Database grants add credentialRef/mintedPrincipal; both hold references only. An app-mode Slack grant may carry `presentation` (0.53.0): the IT-approved display name and icon the app's posts go out under, copied from the approved request and read by every post — never from the call. `identityMode` and `installation` are the deprecated 0.45.0 spellings: the record carries `mode`, and a SaaS grant runs on the connection's single installation.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema", "tenantId", "appId", "environment", "sourceId", "mode", "kind", "status", "operations", "resources", "version", "provisioning", "createdAt", "updatedAt"],
@@ -35,6 +35,7 @@
35
35
  "approvedAt": { "type": "string", "format": "date-time" },
36
36
  "version": { "type": "integer", "minimum": 1, "description": "Optimistic-concurrency version; approvals compare it with the reviewed value." },
37
37
  "pendingRequestId": { "type": "string", "minLength": 1, "maxLength": 128, "description": "The one open access request attached to this grant, if any." },
38
+ "presentation": { "$ref": "app-integrations-declaration.schema.json#/definitions/IntegrationPresentation", "description": "App-mode Slack grants only: how the app's posts are presented, as IT approved it. Absent means the posts carry the bot's own name." },
38
39
  "provisioning": { "$ref": "#/definitions/DataGrantV2Provisioning" },
39
40
  "credentialRef": {
40
41
  "type": "string",
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/integration-execute-request.schema.json",
4
4
  "title": "IntegrationExecuteRequest",
5
- "description": "Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is NOT a caller field: it is fixed by the operation and enforced from the approved grant; any identity/userId/scope field is rejected by additionalProperties. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.",
5
+ "description": "Body of POST /_harbour/integrations/execute (app SDK), forwarded unchanged to the governance development and runtime execute routes. Exactly one declared operation against one declared resource. The identity mode is NOT a caller field: it is fixed by the operation (declared per app for slack.message.post) and enforced from the approved grant; any identity/userId/scope field is rejected by additionalProperties. A slack.message.post runs as the app (the IT-approved per-app name from the grant, plus a \"Posted by <app> on Harbour\" footer) or as the signed-in person (no name, no footer), whichever mode the app declared and IT approved — the same input either way. Per-operation input bounds: slack.channel.history {limit? 1..15, default 15}; slack.message.post {text 1..4000 chars} and a required idempotencyKey retained across retries of one explicit Send; gmail.thread.list {limit? 1..15, default 15}; gmail.message.read {messageId}; gmail.message.send {to 1..50 addresses (to+cc+bcc <= 50 in total), cc?, bcc?, subject 1..256 chars, text 1..4000 chars, replyToMessageId?} and a required idempotencyKey like slack.message.post (plain text only, no attachments); warehouse.view.read {columns 1..32, limit? 1..1000, default 100}. Each operation's then-block is the one statement of its input rules: propertyNames.enum names the fields it accepts, required the ones it needs, and per-field overrides (maximum, default) narrow the shared property. Codegen derives IntegrationExecuteInputRules and validateIntegrationExecuteInput (gen-ts/integration-execute-input-rules.ts, gen-go/integration_execute_input_rules.go) from exactly these blocks, so the governance execute route, the app SDK and the kit's local check refuse the same request with the same message. Transport limits (not expressible here): request <= 16 KiB, total deadline 10 s.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["connection", "operation", "resource", "input"],
@@ -14,8 +14,8 @@
14
14
  "type": "object",
15
15
  "additionalProperties": false,
16
16
  "properties": {
17
- "limit": { "type": "integer", "minimum": 1, "maximum": 1000 },
18
- "text": { "type": "string", "minLength": 1, "maxLength": 4000 },
17
+ "limit": { "type": "integer", "minimum": 1, "maximum": 1000, "description": "Read operations: how many items to return; the per-operation then-block sets the real ceiling (15 for slack.channel.history and gmail.thread.list, 1000 for warehouse.view.read) and the default." },
18
+ "text": { "type": "string", "minLength": 1, "maxLength": 4000, "description": "slack.message.post / gmail.message.send: the plain-text body, 1..4000 characters." },
19
19
  "messageId": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "description": "gmail.message.read: the Gmail message id a gmail.thread.list result named." },
20
20
  "to": { "$ref": "#/definitions/EmailAddressList", "description": "gmail.message.send: primary recipients." },
21
21
  "cc": { "$ref": "#/definitions/EmailAddressList", "description": "gmail.message.send: carbon-copy recipients." },
@@ -42,7 +42,7 @@
42
42
  "then": {
43
43
  "not": { "required": ["idempotencyKey"] },
44
44
  "properties": {
45
- "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
45
+ "input": { "propertyNames": { "enum": ["limit"] }, "properties": { "limit": { "maximum": 15, "default": 15 } } }
46
46
  }
47
47
  }
48
48
  },
@@ -51,7 +51,7 @@
51
51
  "then": {
52
52
  "required": ["idempotencyKey"],
53
53
  "properties": {
54
- "input": { "required": ["text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
54
+ "input": { "propertyNames": { "enum": ["text"] }, "required": ["text"] }
55
55
  }
56
56
  }
57
57
  },
@@ -60,7 +60,7 @@
60
60
  "then": {
61
61
  "not": { "required": ["idempotencyKey"] },
62
62
  "properties": {
63
- "input": { "required": ["columns"], "not": { "anyOf": [{ "required": ["text"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
63
+ "input": { "propertyNames": { "enum": ["columns", "limit"] }, "required": ["columns"], "properties": { "limit": { "maximum": 1000, "default": 100 } } }
64
64
  }
65
65
  }
66
66
  },
@@ -69,7 +69,7 @@
69
69
  "then": {
70
70
  "not": { "required": ["idempotencyKey"] },
71
71
  "properties": {
72
- "input": { "properties": { "limit": { "maximum": 15 } }, "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["messageId"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
72
+ "input": { "propertyNames": { "enum": ["limit"] }, "properties": { "limit": { "maximum": 15, "default": 15 } } }
73
73
  }
74
74
  }
75
75
  },
@@ -78,7 +78,7 @@
78
78
  "then": {
79
79
  "not": { "required": ["idempotencyKey"] },
80
80
  "properties": {
81
- "input": { "required": ["messageId"], "not": { "anyOf": [{ "required": ["text"] }, { "required": ["columns"] }, { "required": ["limit"] }, { "required": ["to"] }, { "required": ["subject"] }] } }
81
+ "input": { "propertyNames": { "enum": ["messageId"] }, "required": ["messageId"] }
82
82
  }
83
83
  }
84
84
  },
@@ -87,7 +87,7 @@
87
87
  "then": {
88
88
  "required": ["idempotencyKey"],
89
89
  "properties": {
90
- "input": { "required": ["to", "subject", "text"], "not": { "anyOf": [{ "required": ["limit"] }, { "required": ["columns"] }, { "required": ["messageId"] }] } }
90
+ "input": { "propertyNames": { "enum": ["to", "cc", "bcc", "subject", "text", "replyToMessageId"] }, "required": ["to", "subject", "text"] }
91
91
  }
92
92
  }
93
93
  }
@@ -94,7 +94,8 @@
94
94
  "workspaceId": { "type": "string", "minLength": 1, "maxLength": 128 },
95
95
  "botUserId": { "type": "string", "maxLength": 128, "description": "Empty only for a legacy record normalised without one." },
96
96
  "status": { "$ref": "#/definitions/SaasInstallationStatus" },
97
- "installedAt": { "type": "string", "format": "date-time" }
97
+ "installedAt": { "type": "string", "format": "date-time" },
98
+ "scopes": { "type": "array", "maxItems": 64, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 64 }, "description": "The bot scopes Slack granted at installation (0.53.0). Absent on installations recorded before it, which therefore lack chat:write.customize: per-app display names need a reinstall." }
98
99
  }
99
100
  },
100
101
  "DevelopmentPreapproval": {
@@ -144,7 +145,7 @@
144
145
  "displayName": "Company Slack",
145
146
  "status": "ACTIVE",
146
147
  "version": 3,
147
- "installation": { "tokenRecordId": "tok_workspace", "workspaceId": "T0WORKSPACE", "botUserId": "U0BOT", "status": "installed", "installedAt": "2026-09-09T08:00:00Z" },
148
+ "installation": { "tokenRecordId": "tok_workspace", "workspaceId": "T0WORKSPACE", "botUserId": "U0BOT", "status": "installed", "installedAt": "2026-09-09T08:00:00Z", "scopes": ["channels:read", "chat:write", "chat:write.customize"] },
148
149
  "channels": { "team-updates": "C0123TEAM" },
149
150
  "operations": ["slack.channel.history", "slack.message.post"],
150
151
  "developmentPreapproval": { "enabled": true, "hours": 24 },
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://schemas.harbour.dev/transform-report.schema.json",
4
4
  "title": "TransformReport",
5
- "description": "Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It also carries validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run. An unsupported report carries only application.unsupported and no plan or fixtures.",
5
+ "description": "Structured output an agent must return from one Harbour transformation attempt. Handed to the agent as its output schema, then decoded and semantically re-validated by the control plane, which remains the authority. The enum on applied_playbooks.items is the one dynamic part: the adapter injects the names of the playbooks supplied for that invocation before writing the document, so the model is structurally prevented from naming a playbook it was not given. Application-secret handling was removed platform-wide, so this document carries no redactions and no secrets fields. A deployable report must also carry preservation_plan: the agent's own grounded smoke tests (one to three happy_path user journeys plus a boundary_check per behaviour its playbooks or edits could regress) that the validation cell executes after transformation, and an uncovered list naming every boundary it could not test and why. It may also carry validation_fixtures: null when no external fixture is needed, or a bounded generic HTTP/WebSocket fixture declaration for that one run; the deployment plane reads the workspace fixture sidecar, so the report's copy is optional. An unsupported report carries only application.unsupported and no plan or fixtures.",
6
6
  "x-go-embed": true,
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -14,26 +14,34 @@
14
14
  "minItems": 1,
15
15
  "items": { "type": "string", "minLength": 1 }
16
16
  },
17
- "static_rebuild_required": { "type": "boolean" },
17
+ "static_rebuild_required": {
18
+ "description": "Optional. The deployed value is a platform constant supplied to the build, not a report field, so a deployable report may omit it and one that still carries it stays valid.",
19
+ "type": "boolean"
20
+ },
18
21
  "analysis": {
19
22
  "type": "object",
20
23
  "additionalProperties": false,
21
- "required": ["deployable", "needsTransformation", "blockers", "surfaces", "evidence"],
24
+ "required": ["deployable", "blockers"],
22
25
  "properties": {
23
26
  "deployable": { "type": "boolean" },
24
- "needsTransformation": { "type": "boolean" },
27
+ "needsTransformation": {
28
+ "description": "Whether the attempt changed the tree. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
29
+ "type": "boolean"
30
+ },
25
31
  "blockers": {
26
32
  "type": "array",
27
33
  "maxItems": 64,
28
34
  "items": { "type": "string", "minLength": 1, "maxLength": 1024 }
29
35
  },
30
36
  "surfaces": {
37
+ "description": "Paths the attempt touched. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
31
38
  "type": "array",
32
39
  "minItems": 1,
33
40
  "maxItems": 1024,
34
41
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
35
42
  },
36
43
  "evidence": {
44
+ "description": "Optional file-and-line citations for why the transformation was needed. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.",
37
45
  "type": "array",
38
46
  "maxItems": 1024,
39
47
  "items": {
@@ -52,35 +60,36 @@
52
60
  "database": {
53
61
  "type": "object",
54
62
  "additionalProperties": false,
55
- "required": [
56
- "mode",
57
- "changeType",
58
- "requiresCodeChanges",
59
- "requiresSchemaMigration",
60
- "migrationPaths",
61
- "changedFiles",
62
- "evidence",
63
- "unsupportedFeatures"
64
- ],
63
+ "required": ["mode"],
65
64
  "properties": {
66
65
  "mode": { "type": "string", "enum": ["none", "sql"] },
67
66
  "changeType": {
67
+ "description": "Classification of the changed paths. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
68
68
  "type": "string",
69
69
  "enum": ["none", "code_only", "schema_only", "code_and_schema"]
70
70
  },
71
- "requiresCodeChanges": { "type": "boolean" },
72
- "requiresSchemaMigration": { "type": "boolean" },
71
+ "requiresCodeChanges": {
72
+ "description": "Whether any changed path sits outside the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
73
+ "type": "boolean"
74
+ },
75
+ "requiresSchemaMigration": {
76
+ "description": "Whether the Aurora bundle grew past its seeded prefix. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
77
+ "type": "boolean"
78
+ },
73
79
  "migrationPaths": {
80
+ "description": "Migration files in the Aurora bundle. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
74
81
  "type": "array",
75
82
  "maxItems": 128,
76
83
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
77
84
  },
78
85
  "changedFiles": {
86
+ "description": "Changed paths on the database boundary. Optional: Harbour derives this from the transformed workspace, so a deployable report may omit it and one that still carries it stays valid.",
79
87
  "type": "array",
80
88
  "maxItems": 512,
81
89
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
82
90
  },
83
91
  "evidence": {
92
+ "description": "Optional file-and-line citations for the database findings. Nothing downstream reads them, so a deployable report may omit them and one that still carries them stays valid.",
84
93
  "type": "array",
85
94
  "maxItems": 128,
86
95
  "items": {
@@ -95,6 +104,7 @@
95
104
  }
96
105
  },
97
106
  "unsupportedFeatures": {
107
+ "description": "Optional. The platform's only structural decline is application.unsupported; nothing reads this list, so a deployable report may omit it and one that still carries it stays valid.",
98
108
  "type": "array",
99
109
  "maxItems": 64,
100
110
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
@@ -116,13 +126,13 @@
116
126
  "items": {
117
127
  "type": "object",
118
128
  "additionalProperties": false,
119
- "required": ["id", "kind", "name", "boundaries", "evidence", "confidence", "steps"],
129
+ "required": ["id", "kind", "name", "evidence", "confidence", "steps"],
120
130
  "properties": {
121
131
  "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$" },
122
132
  "kind": { "type": "string", "enum": ["happy_path", "boundary_check"] },
123
133
  "name": { "type": "string", "minLength": 1, "maxLength": 120 },
124
134
  "boundaries": {
125
- "description": "Transformation boundaries this test claims to cover.",
135
+ "description": "Transformation boundaries this test claims to cover. Optional: Harbour derives them from the test's own evidence paths, so a deployable report may omit them and one that still carries them stays valid.",
126
136
  "type": "array",
127
137
  "minItems": 1,
128
138
  "maxItems": 8,
@@ -202,9 +212,10 @@
202
212
  "items": {
203
213
  "type": "object",
204
214
  "additionalProperties": false,
205
- "required": ["boundary", "reason"],
215
+ "required": ["reason"],
206
216
  "properties": {
207
217
  "boundary": {
218
+ "description": "Optional: Harbour derives the uncovered boundaries from the changed paths and the covered set, so a deployable report may omit this and one that still carries it stays valid.",
208
219
  "type": "string",
209
220
  "enum": ["application", "routing", "database", "authentication", "runtime_dependency", "files", "provider", "background_work"]
210
221
  },
@@ -390,8 +401,13 @@
390
401
  { "type": "null" }
391
402
  ]
392
403
  },
393
- "buildKind": { "type": "string", "enum": ["container", "static"] },
404
+ "buildKind": {
405
+ "description": "Optional: determined by topology (static topology iff static build), so a deployable report may omit it and one that still carries it stays valid.",
406
+ "type": "string",
407
+ "enum": ["container", "static"]
408
+ },
394
409
  "topology": {
410
+ "description": "Optional: determined by the build output kinds, so a deployable report may omit it and one that still carries it stays valid.",
395
411
  "type": "string",
396
412
  "enum": ["static", "backend", "frontend_and_backend"]
397
413
  },
@@ -434,6 +450,7 @@
434
450
  }
435
451
  },
436
452
  "basePath": {
453
+ "description": "Optional: both members are platform constants, so a deployable report may omit this object and one that still carries it stays valid.",
437
454
  "type": "object",
438
455
  "additionalProperties": false,
439
456
  "required": ["required", "environmentVariable"],
@@ -458,6 +475,7 @@
458
475
  }
459
476
  },
460
477
  "unsupportedFeatures": {
478
+ "description": "Optional: a deployable plan requires this empty, so a deployable report may omit it and one that still carries it (empty) stays valid.",
461
479
  "type": "array",
462
480
  "maxItems": 64,
463
481
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
@@ -667,10 +685,10 @@
667
685
  ],
668
686
  "oneOf": [
669
687
  {
670
- "required": ["static_rebuild_required", "analysis", "database", "preservation_plan", "validation_fixtures", "application"],
688
+ "required": ["analysis", "database", "preservation_plan", "application"],
671
689
  "properties": {
672
690
  "application": {
673
- "required": ["runtime", "buildKind", "topology", "entrypoint", "build", "basePath", "evidence", "unsupportedFeatures"],
691
+ "required": ["runtime", "entrypoint", "build", "evidence"],
674
692
  "not": { "required": ["unsupported"] }
675
693
  }
676
694
  }