@sitecoreai-labs/sitecoreai-cli 0.2.0 → 0.2.1

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.
@@ -43,6 +43,14 @@ export type CreateBriefInput = {
43
43
  fields?: Record<string, unknown>;
44
44
  isTemplate?: boolean;
45
45
  };
46
+ /**
47
+ * Validate an unknown value as a `CreateBriefInput`. Throws a typed
48
+ * `ScaiError` (`INPUT_INVALID`) on failure; returns the narrowed input
49
+ * on success. `createBrief` calls this itself, so direct SDK consumers
50
+ * are guarded without invoking it explicitly. Mirrors
51
+ * `assertCreateBriefTypeInput` in `./brief-types.ts`.
52
+ */
53
+ export declare const assertCreateBriefInput: (value: unknown) => CreateBriefInput;
46
54
  /** Create a brief. Returns the persisted record (201). */
47
55
  export declare const createBrief: (options: BriefApiClientOptions, input: CreateBriefInput) => Promise<Brief>;
48
56
  /**
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deleteBrief = exports.setBriefStatus = exports.updateBrief = exports.createBrief = exports.getBrief = exports.listBriefs = void 0;
3
+ exports.deleteBrief = exports.setBriefStatus = exports.updateBrief = exports.createBrief = exports.assertCreateBriefInput = exports.getBrief = exports.listBriefs = void 0;
4
+ const errors_1 = require("../../shared/errors");
4
5
  const request_1 = require("./request");
5
6
  /** List all briefs in the tenant (paged). */
6
7
  const listBriefs = (options, query) => {
@@ -19,17 +20,54 @@ exports.listBriefs = listBriefs;
19
20
  /** Read a single brief by id. 404 surfaces as `BRIEF_API_FAILED` with `"Brief not found"`. */
20
21
  const getBrief = (options, briefId) => (0, request_1.briefRequest)(options, `/api/brief/v1/briefs/${encodeURIComponent(briefId)}`);
21
22
  exports.getBrief = getBrief;
23
+ /** Required keys on a `CreateBriefInput` body. */
24
+ const BRIEF_REQUIRED_KEYS = ["name", "briefTypeId"];
25
+ /**
26
+ * Validate an unknown value as a `CreateBriefInput`. Throws a typed
27
+ * `ScaiError` (`INPUT_INVALID`) on failure; returns the narrowed input
28
+ * on success. `createBrief` calls this itself, so direct SDK consumers
29
+ * are guarded without invoking it explicitly. Mirrors
30
+ * `assertCreateBriefTypeInput` in `./brief-types.ts`.
31
+ */
32
+ const assertCreateBriefInput = (value) => {
33
+ if (!value || typeof value !== "object") {
34
+ throw (0, errors_1.createScaiError)("Brief body must be a JSON object.", "INPUT_INVALID");
35
+ }
36
+ const obj = value;
37
+ const missing = BRIEF_REQUIRED_KEYS.filter((key) => obj[key] === undefined);
38
+ if (missing.length > 0) {
39
+ throw (0, errors_1.createScaiError)(`Brief body is missing required fields: ${missing.join(", ")}.`, "INPUT_INVALID", { hint: "Required: name (string), briefTypeId (UUID of the brief type)." });
40
+ }
41
+ if (typeof obj.name !== "string" || obj.name.length === 0) {
42
+ throw (0, errors_1.createScaiError)(`Invalid 'name': ${JSON.stringify(obj.name)}.`, "INPUT_INVALID", {
43
+ hint: "Brief name must be a non-empty string.",
44
+ });
45
+ }
46
+ if (typeof obj.briefTypeId !== "string" || obj.briefTypeId.length === 0) {
47
+ throw (0, errors_1.createScaiError)(`Invalid 'briefTypeId': ${JSON.stringify(obj.briefTypeId)}.`, "INPUT_INVALID", {
48
+ hint: "Pass the brief type's UUID. List types with `scai ops brief types list`.",
49
+ });
50
+ }
51
+ if (obj.fields !== undefined && (typeof obj.fields !== "object" || Array.isArray(obj.fields))) {
52
+ throw (0, errors_1.createScaiError)("'fields' must be an object keyed by field name.", "INPUT_INVALID");
53
+ }
54
+ return obj;
55
+ };
56
+ exports.assertCreateBriefInput = assertCreateBriefInput;
22
57
  /** Create a brief. Returns the persisted record (201). */
23
- const createBrief = (options, input) => (0, request_1.briefRequest)(options, "/api/brief/v1/briefs", {
24
- method: "POST",
25
- body: {
26
- name: input.name,
27
- briefTypeId: input.briefTypeId,
28
- locale: input.locale,
29
- fields: input.fields,
30
- isTemplate: input.isTemplate,
31
- },
32
- });
58
+ const createBrief = (options, input) => {
59
+ (0, exports.assertCreateBriefInput)(input);
60
+ return (0, request_1.briefRequest)(options, "/api/brief/v1/briefs", {
61
+ method: "POST",
62
+ body: {
63
+ name: input.name,
64
+ briefTypeId: input.briefTypeId,
65
+ locale: input.locale,
66
+ fields: input.fields,
67
+ isTemplate: input.isTemplate,
68
+ },
69
+ });
70
+ };
33
71
  exports.createBrief = createBrief;
34
72
  /**
35
73
  * Partial update of a brief (`PUT /api/brief/v1/briefs/{id}` — 204 No
@@ -23,7 +23,7 @@ export type { BriefApiClientOptions, BriefRequestInit, BriefQueryValue, BriefQue
23
23
  export { BRIEF_API_HOST_TEMPLATE, DEFAULT_BRIEF_API_BASE } from "./api/types";
24
24
  export { briefRequest } from "./api/request";
25
25
  export type { PagedResult, LocalizedString, Link, ExternalLink, Reference, BriefField, BriefFieldBase, RichTextField, DateTimeField, TimelineField, BudgetField, BriefType, Brief, BriefStatus, BriefTask, BriefComment, ExternalMapping, } from "./api/schema";
26
- export { listBriefs, getBrief, createBrief, updateBrief, setBriefStatus, deleteBrief, type CreateBriefInput, type ListBriefsQuery, } from "./api/briefs";
26
+ export { listBriefs, getBrief, createBrief, updateBrief, setBriefStatus, deleteBrief, assertCreateBriefInput, type CreateBriefInput, type ListBriefsQuery, } from "./api/briefs";
27
27
  export { listBriefTypes, getBriefType, createBriefType, updateBriefType, deleteBriefType, type CreateBriefTypeInput, } from "./api/brief-types";
28
28
  export { listBriefTasks, getBriefTask, type BriefTaskMetadata, type ListBriefTasksQuery, } from "./api/tasks";
29
29
  export { listBriefComments, createBriefComment, type ListBriefCommentsQuery, type CreateBriefCommentInput, } from "./api/comments";
@@ -21,7 +21,7 @@
21
21
  * (read+write). The Agents env client carries both by default.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.resolveBriefClient = exports.BRIEF_SCOPES_REQUESTED = exports.acquireBriefToken = exports.createBriefComment = exports.listBriefComments = exports.getBriefTask = exports.listBriefTasks = exports.deleteBriefType = exports.updateBriefType = exports.createBriefType = exports.getBriefType = exports.listBriefTypes = exports.deleteBrief = exports.setBriefStatus = exports.updateBrief = exports.createBrief = exports.getBrief = exports.listBriefs = exports.briefRequest = exports.DEFAULT_BRIEF_API_BASE = exports.BRIEF_API_HOST_TEMPLATE = void 0;
24
+ exports.resolveBriefClient = exports.BRIEF_SCOPES_REQUESTED = exports.acquireBriefToken = exports.createBriefComment = exports.listBriefComments = exports.getBriefTask = exports.listBriefTasks = exports.deleteBriefType = exports.updateBriefType = exports.createBriefType = exports.getBriefType = exports.listBriefTypes = exports.assertCreateBriefInput = exports.deleteBrief = exports.setBriefStatus = exports.updateBrief = exports.createBrief = exports.getBrief = exports.listBriefs = exports.briefRequest = exports.DEFAULT_BRIEF_API_BASE = exports.BRIEF_API_HOST_TEMPLATE = void 0;
25
25
  var types_1 = require("./api/types");
26
26
  Object.defineProperty(exports, "BRIEF_API_HOST_TEMPLATE", { enumerable: true, get: function () { return types_1.BRIEF_API_HOST_TEMPLATE; } });
27
27
  Object.defineProperty(exports, "DEFAULT_BRIEF_API_BASE", { enumerable: true, get: function () { return types_1.DEFAULT_BRIEF_API_BASE; } });
@@ -34,6 +34,7 @@ Object.defineProperty(exports, "createBrief", { enumerable: true, get: function
34
34
  Object.defineProperty(exports, "updateBrief", { enumerable: true, get: function () { return briefs_1.updateBrief; } });
35
35
  Object.defineProperty(exports, "setBriefStatus", { enumerable: true, get: function () { return briefs_1.setBriefStatus; } });
36
36
  Object.defineProperty(exports, "deleteBrief", { enumerable: true, get: function () { return briefs_1.deleteBrief; } });
37
+ Object.defineProperty(exports, "assertCreateBriefInput", { enumerable: true, get: function () { return briefs_1.assertCreateBriefInput; } });
37
38
  var brief_types_1 = require("./api/brief-types");
38
39
  Object.defineProperty(exports, "listBriefTypes", { enumerable: true, get: function () { return brief_types_1.listBriefTypes; } });
39
40
  Object.defineProperty(exports, "getBriefType", { enumerable: true, get: function () { return brief_types_1.getBriefType; } });
@@ -1,6 +1,12 @@
1
1
  /**
2
- * The `brief-type` recipe kind — declarative definition + `sync` support
3
- * for Sitecore Content Operations brief types.
2
+ * The brief recipe surface — declarative definitions + `sync` support
3
+ * for Sitecore Content Operations brief types AND brief instances.
4
+ *
5
+ * Two kinds:
6
+ * - `briefTypeKind` ("brief-type") — the schema template a brief is
7
+ * built against. Identified by stable codename.
8
+ * - `briefInstanceKind` ("brief") — a populated brief instance.
9
+ * Identified by display name; references its type by codename.
4
10
  *
5
11
  * See docs/recipe-sync-architecture.md.
6
12
  */
@@ -8,3 +14,6 @@ export { BriefTypeRecipeSchema, BriefFieldSchema, RichTextFieldSchema, DateTimeF
8
14
  export { diffBriefType } from "./diff";
9
15
  export { resolveBriefClient } from "./client";
10
16
  export { briefTypeKind } from "./kind";
17
+ export { BriefInstanceRecipeSchema, BriefInstanceStatusSchema, BriefInstanceFieldsSchema, type BriefInstanceRecipe, } from "./instance-schema";
18
+ export { diffBriefInstance } from "./instance-diff";
19
+ export { briefInstanceKind } from "./instance-kind";
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.briefTypeKind = exports.resolveBriefClient = exports.diffBriefType = exports.LocalizedStringSchema = exports.BudgetFieldSchema = exports.TimelineFieldSchema = exports.DateTimeFieldSchema = exports.RichTextFieldSchema = exports.BriefFieldSchema = exports.BriefTypeRecipeSchema = void 0;
3
+ exports.briefInstanceKind = exports.diffBriefInstance = exports.BriefInstanceFieldsSchema = exports.BriefInstanceStatusSchema = exports.BriefInstanceRecipeSchema = exports.briefTypeKind = exports.resolveBriefClient = exports.diffBriefType = exports.LocalizedStringSchema = exports.BudgetFieldSchema = exports.TimelineFieldSchema = exports.DateTimeFieldSchema = exports.RichTextFieldSchema = exports.BriefFieldSchema = exports.BriefTypeRecipeSchema = void 0;
4
4
  /**
5
- * The `brief-type` recipe kind — declarative definition + `sync` support
6
- * for Sitecore Content Operations brief types.
5
+ * The brief recipe surface — declarative definitions + `sync` support
6
+ * for Sitecore Content Operations brief types AND brief instances.
7
+ *
8
+ * Two kinds:
9
+ * - `briefTypeKind` ("brief-type") — the schema template a brief is
10
+ * built against. Identified by stable codename.
11
+ * - `briefInstanceKind` ("brief") — a populated brief instance.
12
+ * Identified by display name; references its type by codename.
7
13
  *
8
14
  * See docs/recipe-sync-architecture.md.
9
15
  */
@@ -21,3 +27,11 @@ var client_1 = require("./client");
21
27
  Object.defineProperty(exports, "resolveBriefClient", { enumerable: true, get: function () { return client_1.resolveBriefClient; } });
22
28
  var kind_1 = require("./kind");
23
29
  Object.defineProperty(exports, "briefTypeKind", { enumerable: true, get: function () { return kind_1.briefTypeKind; } });
30
+ var instance_schema_1 = require("./instance-schema");
31
+ Object.defineProperty(exports, "BriefInstanceRecipeSchema", { enumerable: true, get: function () { return instance_schema_1.BriefInstanceRecipeSchema; } });
32
+ Object.defineProperty(exports, "BriefInstanceStatusSchema", { enumerable: true, get: function () { return instance_schema_1.BriefInstanceStatusSchema; } });
33
+ Object.defineProperty(exports, "BriefInstanceFieldsSchema", { enumerable: true, get: function () { return instance_schema_1.BriefInstanceFieldsSchema; } });
34
+ var instance_diff_1 = require("./instance-diff");
35
+ Object.defineProperty(exports, "diffBriefInstance", { enumerable: true, get: function () { return instance_diff_1.diffBriefInstance; } });
36
+ var instance_kind_1 = require("./instance-kind");
37
+ Object.defineProperty(exports, "briefInstanceKind", { enumerable: true, get: function () { return instance_kind_1.briefInstanceKind; } });
@@ -0,0 +1,4 @@
1
+ import type { RecipePlan } from "../../sync";
2
+ import type { BriefInstanceRecipe } from "./instance-schema";
3
+ /** Diff a desired brief-instance recipe against captured current state. */
4
+ export declare const diffBriefInstance: (desired: BriefInstanceRecipe, current: BriefInstanceRecipe | null) => RecipePlan;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffBriefInstance = void 0;
4
+ /**
5
+ * The pure diff for the `brief` recipe kind — desired recipe vs.
6
+ * captured current state → a `RecipePlan`. No I/O, so it is cheap to
7
+ * unit-test and is the testable core of the kind.
8
+ *
9
+ * Unlike `brief-type` (which is a single full-replacement PUT), brief
10
+ * instances support a partial PUT: the diff therefore emits two stages
11
+ * of changes:
12
+ *
13
+ * - One `stage: "instance"` change carries the whole desired recipe.
14
+ * `apply` consumes it to drive `createBrief` or `updateBrief`.
15
+ * - Per-element `stage: "field"` changes describe the convergence
16
+ * element-by-element so the rendered plan reads cleanly. They are
17
+ * descriptive only — the single `stage: "instance"` change is what
18
+ * `apply` writes.
19
+ *
20
+ * Identification is by `name` (the brief's display name). Brief
21
+ * instances are not enforced unique by name on the server; the diff and
22
+ * apply both treat "first match wins", matching the campaign-instance
23
+ * precedent. See docs/recipe-sync-architecture.md.
24
+ */
25
+ const node_util_1 = require("node:util");
26
+ /**
27
+ * The top-level recipe elements compared one-by-one when the brief
28
+ * exists. `briefTypeName` is included because the diff surfaces a
29
+ * type-change attempt — `apply` then refuses it (the Brief API has no
30
+ * verified path to repoint an existing brief at a different type).
31
+ */
32
+ const COMPARED_ELEMENTS = ["briefTypeName", "locale", "status", "isTemplate", "fields"];
33
+ /** Diff a desired brief-instance recipe against captured current state. */
34
+ const diffBriefInstance = (desired, current) => {
35
+ const changes = [];
36
+ if (current === null) {
37
+ changes.push({
38
+ kind: "create",
39
+ path: "brief",
40
+ summary: `Create brief "${desired.name}"`,
41
+ after: desired.name,
42
+ meta: { stage: "instance", recipe: desired },
43
+ });
44
+ return { changes };
45
+ }
46
+ // The brief exists — compare each element so the plan reads
47
+ // element-by-element. `apply` acts on the lead `stage: "instance"`
48
+ // change, not on these per-element entries.
49
+ let anyElementChanged = false;
50
+ for (const element of COMPARED_ELEMENTS) {
51
+ const before = current[element];
52
+ const after = desired[element];
53
+ const path = `brief.${element}`;
54
+ const meta = { stage: "field", element };
55
+ if ((0, node_util_1.isDeepStrictEqual)(before, after)) {
56
+ changes.push({ kind: "noop", path, summary: `${element} unchanged`, meta });
57
+ }
58
+ else {
59
+ anyElementChanged = true;
60
+ changes.push({ kind: "update", path, summary: `${element}`, before, after, meta });
61
+ }
62
+ }
63
+ // A single PUT converges every changed element at once. Emitted as
64
+ // the lead change so `apply` finds it without scanning.
65
+ if (anyElementChanged) {
66
+ changes.unshift({
67
+ kind: "update",
68
+ path: "brief",
69
+ summary: `Update brief "${desired.name}"`,
70
+ before: current.name,
71
+ after: desired.name,
72
+ meta: { stage: "instance", recipe: desired },
73
+ });
74
+ }
75
+ return { changes };
76
+ };
77
+ exports.diffBriefInstance = diffBriefInstance;
@@ -0,0 +1,4 @@
1
+ import type { RecipeKind } from "../../sync";
2
+ import { type BriefInstanceRecipe } from "./instance-schema";
3
+ /** The `brief` recipe kind. */
4
+ export declare const briefInstanceKind: RecipeKind<BriefInstanceRecipe>;
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.briefInstanceKind = void 0;
4
+ /**
5
+ * The `brief` recipe kind — wires the Sitecore Content Operations
6
+ * Brief instance API into the `sync` engine.
7
+ *
8
+ * `ref.id` is the brief's display NAME — recipes identify a brief by
9
+ * name, not UUID, mirroring how `campaign` identifies a project. The
10
+ * brief type is referenced by its stable codename (`briefTypeName`)
11
+ * and resolved to a server id at push-time via `listBriefTypes`.
12
+ *
13
+ * `apply` is straight CRUD: `createBrief` when the brief is absent,
14
+ * `updateBrief` (partial PUT) to converge an existing one. Repointing
15
+ * an existing brief at a different brief type is refused — the Brief
16
+ * API has no verified path for that.
17
+ *
18
+ * See docs/recipe-sync-architecture.md.
19
+ */
20
+ const brief_1 = require("../../brief");
21
+ const errors_1 = require("../../shared/errors");
22
+ const client_1 = require("./client");
23
+ const instance_diff_1 = require("./instance-diff");
24
+ const instance_schema_1 = require("./instance-schema");
25
+ /**
26
+ * Find a brief by its display name, paging the list endpoint until a
27
+ * match is found or the cursor is exhausted. The Brief list endpoint
28
+ * supports no server-side name filter (see `ListBriefsQuery`), so the
29
+ * walk is unavoidable.
30
+ */
31
+ const findBriefByName = async (client, name) => {
32
+ let cursor;
33
+ for (;;) {
34
+ const page = await (0, brief_1.listBriefs)(client, cursor ? { next: cursor } : undefined);
35
+ const match = page.data.find((brief) => brief.name === name);
36
+ if (match)
37
+ return match;
38
+ if (!page.next || page.data.length === 0)
39
+ return null;
40
+ cursor = page.next;
41
+ }
42
+ };
43
+ /** Find a brief type by its codename (mirrors `briefTypeKind`'s helper). */
44
+ const findTypeByName = async (client, name) => {
45
+ const page = await (0, brief_1.listBriefTypes)(client);
46
+ return page.data.find((type) => type.name === name) ?? null;
47
+ };
48
+ /** Enumerate every brief on the remote — fans out into the aggregate sync. */
49
+ const list = async (ctx) => {
50
+ const client = await (0, client_1.resolveBriefClient)(ctx);
51
+ const refs = [];
52
+ let cursor;
53
+ for (;;) {
54
+ const page = await (0, brief_1.listBriefs)(client, cursor ? { next: cursor } : undefined);
55
+ for (const brief of page.data) {
56
+ refs.push({ kind: "brief", id: brief.name });
57
+ }
58
+ if (!page.next || page.data.length === 0)
59
+ return refs;
60
+ cursor = page.next;
61
+ }
62
+ };
63
+ /** Project a live brief into the recipe shape, dropping server ids/metadata. */
64
+ const toRecipe = (brief, briefTypeName) => ({
65
+ name: brief.name,
66
+ briefTypeName,
67
+ locale: brief.locale || undefined,
68
+ status: brief.status,
69
+ isTemplate: brief.isTemplate,
70
+ fields: brief.fields ?? {},
71
+ });
72
+ /**
73
+ * Capture a live brief as a recipe. `null` when no brief has the name.
74
+ *
75
+ * The list endpoint omits the full nested `fields` map on some
76
+ * envelopes, so the matched id is re-read via `getBrief` for fidelity.
77
+ */
78
+ const readCurrent = async (ref, ctx) => {
79
+ const client = await (0, client_1.resolveBriefClient)(ctx);
80
+ const found = await findBriefByName(client, ref.id);
81
+ if (!found)
82
+ return null;
83
+ const brief = await (0, brief_1.getBrief)(client, found.id);
84
+ // Resolve the brief-type codename. The brief carries a `Link` to its
85
+ // type with the id but not the codename, so a follow-up listTypes
86
+ // resolves it. If the type has been deleted (orphan brief), surface
87
+ // the id verbatim so the pulled recipe still round-trips.
88
+ const types = await (0, brief_1.listBriefTypes)(client);
89
+ const briefTypeName = types.data.find((type) => type.id === brief.briefType.id)?.name ?? brief.briefType.id;
90
+ return toRecipe(brief, briefTypeName);
91
+ };
92
+ /** Resolve a recipe's `briefTypeName` to its server id, or fail with a hint. */
93
+ const resolveBriefTypeId = async (client, briefTypeName) => {
94
+ const type = await findTypeByName(client, briefTypeName);
95
+ if (!type) {
96
+ throw (0, errors_1.createScaiError)(`Brief type "${briefTypeName}" not found.`, "INPUT_INVALID", {
97
+ hint: "Push the brief-type recipe first, or check the `briefTypeName` codename with `scai ops brief types list`.",
98
+ });
99
+ }
100
+ return type.id;
101
+ };
102
+ /** Apply a plan — create the brief, or PUT-patch it onto the desired state. */
103
+ const apply = async (plan, ref, ctx) => {
104
+ const client = await (0, client_1.resolveBriefClient)(ctx);
105
+ const applied = [];
106
+ const skipped = [];
107
+ // The single `stage: "instance"` change carries the full desired
108
+ // recipe; per-element `stage: "field"` changes are descriptive only.
109
+ const instanceChange = plan.changes.find((change) => change.meta?.stage === "instance");
110
+ if (!instanceChange) {
111
+ // Nothing to write — an all-noop plan.
112
+ for (const change of plan.changes)
113
+ skipped.push(change);
114
+ return { applied, skipped };
115
+ }
116
+ const recipe = instanceChange.meta?.recipe;
117
+ if (!recipe) {
118
+ throw (0, errors_1.createScaiError)("Brief plan change is missing its recipe payload.", "INPUT_INVALID", {
119
+ hint: "This is an internal diff error — change.meta.recipe was not set.",
120
+ });
121
+ }
122
+ if (instanceChange.kind === "create") {
123
+ const briefTypeId = await resolveBriefTypeId(client, recipe.briefTypeName);
124
+ const input = {
125
+ name: recipe.name,
126
+ briefTypeId,
127
+ ...(recipe.locale !== undefined && { locale: recipe.locale }),
128
+ ...(Object.keys(recipe.fields ?? {}).length > 0 && { fields: recipe.fields }),
129
+ ...(recipe.isTemplate !== undefined && { isTemplate: recipe.isTemplate }),
130
+ };
131
+ ctx.logger?.info(`Creating brief "${recipe.name}".`);
132
+ const created = await (0, brief_1.createBrief)(client, input);
133
+ // `createBrief` accepts no `status` field — POSTs land in the
134
+ // server default ("Draft"). If the recipe pins a different status,
135
+ // converge with a follow-up PUT so the post-apply state matches.
136
+ if (recipe.status && recipe.status !== created.status) {
137
+ ctx.logger?.info(`Setting brief "${recipe.name}" status to "${recipe.status}".`);
138
+ await (0, brief_1.updateBrief)(client, created.id, { status: recipe.status });
139
+ }
140
+ }
141
+ else {
142
+ const existing = await findBriefByName(client, ref.id);
143
+ if (!existing) {
144
+ throw (0, errors_1.createScaiError)(`Brief "${ref.id}" not found.`, "INPUT_INVALID", {
145
+ hint: "The brief was expected to exist for an update — check the name or push a create.",
146
+ });
147
+ }
148
+ // The Brief API has no verified path to repoint a brief at a
149
+ // different type. Surface that loudly instead of silently dropping
150
+ // the change.
151
+ const types = await (0, brief_1.listBriefTypes)(client);
152
+ const existingTypeName = types.data.find((type) => type.id === existing.briefType.id)?.name ?? existing.briefType.id;
153
+ if (existingTypeName !== recipe.briefTypeName) {
154
+ throw (0, errors_1.createScaiError)(`Cannot repoint brief "${recipe.name}" from type "${existingTypeName}" to "${recipe.briefTypeName}".`, "INPUT_INVALID", {
155
+ hint: "The Brief API has no verified path to change a brief's type. Delete and recreate the brief, or correct the recipe's briefTypeName.",
156
+ });
157
+ }
158
+ const patch = {
159
+ name: recipe.name,
160
+ ...(recipe.locale !== undefined && { locale: recipe.locale }),
161
+ ...(recipe.isTemplate !== undefined && { isTemplate: recipe.isTemplate }),
162
+ ...(recipe.status !== undefined && { status: recipe.status }),
163
+ fields: recipe.fields ?? {},
164
+ };
165
+ ctx.logger?.info(`Updating brief "${recipe.name}" (${existing.id}).`);
166
+ await (0, brief_1.updateBrief)(client, existing.id, patch);
167
+ }
168
+ applied.push(instanceChange);
169
+ // Per-element changes are converged by the single PUT (or POST).
170
+ for (const change of plan.changes) {
171
+ if (change === instanceChange)
172
+ continue;
173
+ if (change.kind === "noop")
174
+ skipped.push(change);
175
+ else
176
+ applied.push(change);
177
+ }
178
+ return { applied, skipped };
179
+ };
180
+ /** Compute the plan to converge a brief onto `desired`. */
181
+ const plan = async (desired, ref, ctx) => (0, instance_diff_1.diffBriefInstance)(desired, await readCurrent(ref, ctx));
182
+ /** The `brief` recipe kind. */
183
+ exports.briefInstanceKind = {
184
+ name: "brief",
185
+ schema: instance_schema_1.BriefInstanceRecipeSchema,
186
+ readCurrent,
187
+ plan,
188
+ apply,
189
+ list,
190
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `BriefInstanceRecipe` — the declarative definition of a Sitecore
3
+ * Content Operations *brief instance* (the unit of work — a populated
4
+ * brief built against a `BriefType` schema).
5
+ *
6
+ * Companion to `BriefTypeRecipeSchema` (this file's sibling `schema.ts`,
7
+ * which models the schema template). Brief instances reference their
8
+ * type by stable codename (`briefTypeName`) rather than UUID, exactly as
9
+ * a `campaign` recipe references nothing by id and identifies itself by
10
+ * `name`. See docs/recipe-sync-architecture.md.
11
+ *
12
+ * The schema captures the fields `createBrief` / `updateBrief` accept
13
+ * on the Brief API — `name`, `briefTypeName`, `locale`, `status`,
14
+ * `isTemplate`, and the per-field `fields` map. Sub-resources (tasks,
15
+ * comments, references, contributors, external mappings) are returned
16
+ * by the read endpoints but are not part of the brief write surface,
17
+ * so they stay out of the recipe.
18
+ */
19
+ import { z } from "zod";
20
+ /**
21
+ * Brief workflow status — the values the Brief API accepts on a write.
22
+ * Mirrors the `BriefStatus` wire type in `../api/schema.ts`; surfacing
23
+ * the literal set here means the model reads the enum directly off the
24
+ * recipe schema (and bad values fail at parse-time, not push-time).
25
+ *
26
+ * `InReview` is the wire form for the "In Review" UI label.
27
+ */
28
+ export declare const BriefInstanceStatusSchema: z.ZodEnum<{
29
+ Draft: "Draft";
30
+ InReview: "InReview";
31
+ Approved: "Approved";
32
+ Canceled: "Canceled";
33
+ Archived: "Archived";
34
+ }>;
35
+ /**
36
+ * Field values on a brief instance, keyed by `BriefField.name`. The
37
+ * per-field shape varies by field `type` on the brief's type (RichText
38
+ * values are ProseMirror docs; DateTime is an ISO string; Timeline and
39
+ * Budget carry their own object shape). The shape is left as
40
+ * `z.unknown()` so any field type round-trips losslessly between
41
+ * `recipe pull` and `recipe push` without the recipe schema chasing
42
+ * the per-field encoding. Validation is deferred to the server.
43
+ */
44
+ export declare const BriefInstanceFieldsSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
45
+ /** The full brief-instance recipe. */
46
+ export declare const BriefInstanceRecipeSchema: z.ZodObject<{
47
+ name: z.ZodString;
48
+ briefTypeName: z.ZodString;
49
+ locale: z.ZodOptional<z.ZodString>;
50
+ status: z.ZodOptional<z.ZodEnum<{
51
+ Draft: "Draft";
52
+ InReview: "InReview";
53
+ Approved: "Approved";
54
+ Canceled: "Canceled";
55
+ Archived: "Archived";
56
+ }>>;
57
+ isTemplate: z.ZodOptional<z.ZodBoolean>;
58
+ fields: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
59
+ }, z.core.$strip>;
60
+ /** A validated brief-instance recipe. */
61
+ export type BriefInstanceRecipe = z.infer<typeof BriefInstanceRecipeSchema>;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BriefInstanceRecipeSchema = exports.BriefInstanceFieldsSchema = exports.BriefInstanceStatusSchema = void 0;
4
+ /**
5
+ * `BriefInstanceRecipe` — the declarative definition of a Sitecore
6
+ * Content Operations *brief instance* (the unit of work — a populated
7
+ * brief built against a `BriefType` schema).
8
+ *
9
+ * Companion to `BriefTypeRecipeSchema` (this file's sibling `schema.ts`,
10
+ * which models the schema template). Brief instances reference their
11
+ * type by stable codename (`briefTypeName`) rather than UUID, exactly as
12
+ * a `campaign` recipe references nothing by id and identifies itself by
13
+ * `name`. See docs/recipe-sync-architecture.md.
14
+ *
15
+ * The schema captures the fields `createBrief` / `updateBrief` accept
16
+ * on the Brief API — `name`, `briefTypeName`, `locale`, `status`,
17
+ * `isTemplate`, and the per-field `fields` map. Sub-resources (tasks,
18
+ * comments, references, contributors, external mappings) are returned
19
+ * by the read endpoints but are not part of the brief write surface,
20
+ * so they stay out of the recipe.
21
+ */
22
+ const zod_1 = require("zod");
23
+ /**
24
+ * Brief workflow status — the values the Brief API accepts on a write.
25
+ * Mirrors the `BriefStatus` wire type in `../api/schema.ts`; surfacing
26
+ * the literal set here means the model reads the enum directly off the
27
+ * recipe schema (and bad values fail at parse-time, not push-time).
28
+ *
29
+ * `InReview` is the wire form for the "In Review" UI label.
30
+ */
31
+ exports.BriefInstanceStatusSchema = zod_1.z
32
+ .enum(["Draft", "InReview", "Approved", "Canceled", "Archived"])
33
+ .describe('Brief workflow status. Wire form — "InReview" is the "In Review" UI label. A brief must leave "Draft" before it can be linked to a campaign.');
34
+ /**
35
+ * Field values on a brief instance, keyed by `BriefField.name`. The
36
+ * per-field shape varies by field `type` on the brief's type (RichText
37
+ * values are ProseMirror docs; DateTime is an ISO string; Timeline and
38
+ * Budget carry their own object shape). The shape is left as
39
+ * `z.unknown()` so any field type round-trips losslessly between
40
+ * `recipe pull` and `recipe push` without the recipe schema chasing
41
+ * the per-field encoding. Validation is deferred to the server.
42
+ */
43
+ exports.BriefInstanceFieldsSchema = zod_1.z
44
+ .record(zod_1.z.string(), zod_1.z.unknown())
45
+ .default({})
46
+ .describe("Field values keyed by BriefField.name. Per-field value shape follows the brief type's field definitions (e.g. RichText is a ProseMirror doc node).");
47
+ /** The full brief-instance recipe. */
48
+ exports.BriefInstanceRecipeSchema = zod_1.z.object({
49
+ name: zod_1.z
50
+ .string()
51
+ .min(1)
52
+ .describe("Display name of the brief. Identifies the brief when pushing. Briefs are matched by name; the recipe pushes through to `createBrief` / `updateBrief`."),
53
+ briefTypeName: zod_1.z
54
+ .string()
55
+ .min(1)
56
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/, "Must start with a letter and contain only letters, digits, or underscores.")
57
+ .describe("Codename of the brief type this brief is built against — looked up at push-time and resolved to its server id. The brief type must already exist (push the type's recipe first if it doesn't)."),
58
+ locale: zod_1.z
59
+ .string()
60
+ .optional()
61
+ .describe('Brief locale — BCP-47-ish, e.g. "en-us". Omit to let the server default apply.'),
62
+ status: exports.BriefInstanceStatusSchema.optional().describe('Brief workflow status. Omit to leave at the server default ("Draft" on creation).'),
63
+ isTemplate: zod_1.z
64
+ .boolean()
65
+ .optional()
66
+ .describe("Whether the brief is a template (omit to leave at the server default)."),
67
+ fields: exports.BriefInstanceFieldsSchema,
68
+ });
@@ -1,3 +1,4 @@
1
+ import { type CreateBriefInput } from "../api/briefs";
1
2
  import { type CreateBriefTypeInput } from "../api/brief-types";
2
3
  import type { Brief, BriefComment, BriefStatus, BriefTask, BriefType, PagedResult } from "../api/schema";
3
4
  /**
@@ -100,6 +101,40 @@ export declare const runBriefCommentsList: (options: RunBriefBaseOptions & {
100
101
  briefId?: string;
101
102
  limit?: number;
102
103
  }) => Promise<PagedResult<BriefComment>>;
104
+ /**
105
+ * Create a brief instance from a `CreateBriefInput`. Mirrors
106
+ * `runBriefTypeCreate` — honours `whatIf` for a plan-only dry run. The
107
+ * SDK `createBrief` is verified against the Agents tenant.
108
+ */
109
+ export declare const runBriefCreate: (options: RunBriefBaseOptions & {
110
+ input: CreateBriefInput;
111
+ whatIf?: boolean;
112
+ }) => Promise<Brief | {
113
+ plan: CreateBriefInput;
114
+ }>;
115
+ /**
116
+ * Update a brief instance by id with a partial patch (`PUT`). Accepts
117
+ * any subset of `CreateBriefInput` plus an optional `status`. Mirrors
118
+ * `runBriefTypeUpdate` — honours `whatIf` for a plan-only dry run. The
119
+ * status-only PUT path is verified (2026-05-15); other partial fields
120
+ * are wired the same way but not smoke-tested.
121
+ */
122
+ export declare const runBriefUpdate: (options: RunBriefBaseOptions & {
123
+ briefId: string;
124
+ patch: Partial<CreateBriefInput> & {
125
+ status?: BriefStatus;
126
+ };
127
+ whatIf?: boolean;
128
+ }) => Promise<{
129
+ id: string;
130
+ } | {
131
+ plan: {
132
+ id: string;
133
+ patch: Partial<CreateBriefInput> & {
134
+ status?: BriefStatus;
135
+ };
136
+ };
137
+ }>;
103
138
  /**
104
139
  * Delete a brief instance. Mirrors `runBriefTypeDelete` — honours
105
140
  * `whatIf` for a plan-only dry run. SDK `deleteBrief` is verified