@sitecoreai-labs/sitecoreai-cli 0.1.2 → 0.2.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/agents/tasks/agent.js +2 -2
- package/dist/agents/tasks/resources.js +2 -2
- package/dist/agents/tasks/space.js +1 -1
- package/dist/brand/api/auth.d.ts +16 -9
- package/dist/brand/api/auth.js +29 -19
- package/dist/brand/credential.d.ts +71 -1
- package/dist/brand/credential.js +119 -2
- package/dist/brand/recipe/diff.js +7 -2
- package/dist/brand/recipe/kind.js +0 -0
- package/dist/brand/recipe/schema.d.ts +113 -7
- package/dist/brand/recipe/schema.js +137 -8
- package/dist/brand/seed.d.ts +9 -5
- package/dist/brand/seed.js +30 -5
- package/dist/brief/recipe/schema.js +4 -1
- package/dist/campaigns/recipe/schema.d.ts +39 -8
- package/dist/campaigns/recipe/schema.js +40 -10
- package/dist/commands/agents/sync.js +2 -2
- package/dist/commands/brand/seed.js +1 -1
- package/dist/commands/brand/sync.js +2 -2
- package/dist/commands/brief/sync.js +2 -2
- package/dist/commands/campaign/sync.js +2 -2
- package/dist/recipe/compile/design-parameters-template.js +5 -3
- package/dist/recipe/compile/enumeration.js +6 -11
- package/dist/recipe/compile/shared.js +12 -12
- package/dist/recipe/compile.js +4 -4
- package/dist/recipe/io.d.ts +8 -3
- package/dist/recipe/io.js +11 -81
- package/dist/recipe/items/read-current.js +31 -24
- package/dist/recipe/schema/recipe.d.ts +167 -84
- package/dist/recipe/schema/recipe.js +130 -46
- package/dist/recipe/schema/source-fields.d.ts +20 -0
- package/dist/recipe/schema/source-fields.js +25 -1
- package/dist/recipe/validate.d.ts +3 -3
- package/dist/recipe/validate.js +20 -10
- package/dist/sync/aggregate.js +2 -2
- package/dist/sync/io.d.ts +13 -3
- package/dist/sync/io.js +43 -17
- package/dist/sync/typescript-recipe.d.ts +30 -0
- package/dist/sync/typescript-recipe.js +112 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BrandKitRecipeSchema = exports.BrandDocumentSchema = exports.BrandFieldValueSchema = exports.BrandRichEntrySchema = void 0;
|
|
3
|
+
exports.BrandKitRecipeSchema = exports.BrandDocumentSchema = exports.BrandRegistryFileDocumentSchema = exports.BrandUrlDocumentSchema = exports.BRAND_KIT_CANONICAL_SECTIONS = exports.BrandFieldValueSchema = exports.BrandRichEntrySchema = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `BrandKitRecipe` — the declarative definition of a Sitecore Brand
|
|
6
6
|
* brand kit.
|
|
@@ -8,9 +8,26 @@ exports.BrandKitRecipeSchema = exports.BrandDocumentSchema = exports.BrandFieldV
|
|
|
8
8
|
* This schema is the single source of truth for the `brand-kit` recipe
|
|
9
9
|
* kind: it validates recipe files, drives the `sync` CLI, and becomes
|
|
10
10
|
* the MCP tool input schema. Keep the `.describe()` text accurate — the
|
|
11
|
-
* model reads it.
|
|
11
|
+
* model reads it.
|
|
12
|
+
*
|
|
13
|
+
* Two authoring shapes share this schema:
|
|
14
|
+
*
|
|
15
|
+
* - **scai-native** — flat object with `name`, `documents`, `sections`.
|
|
16
|
+
* No `kind` / `schemaVersion` / `handle`. Used by hand-authored
|
|
17
|
+
* `.brandkit.yaml` files and the `scai brand sync pull` capture
|
|
18
|
+
* path. Stays valid: the discriminator fields are optional here.
|
|
19
|
+
* - **registry-superset** — the richer shape the `@registry`
|
|
20
|
+
* `sitecore-recipes.ts` exports use: adds `kind: "brandkit"`,
|
|
21
|
+
* `schemaVersion: "1"`, `handle`, `displayName`, and a
|
|
22
|
+
* discriminated `documents[]` shape (`url` | `registry-file`)
|
|
23
|
+
* with `tags` / `sections` hints per document. The orchestrator
|
|
24
|
+
* passes recipes through unchanged from the registry to scai;
|
|
25
|
+
* scai's parser accepts the extra fields without stripping them.
|
|
26
|
+
*
|
|
27
|
+
* See docs/recipe-sync-architecture.md.
|
|
12
28
|
*/
|
|
13
29
|
const zod_1 = require("zod");
|
|
30
|
+
const HANDLE_PATTERN = /^[a-z][a-z0-9-]*@\d+$/;
|
|
14
31
|
/** A `richArray`-field entry: text plus optional tags and a constraint. */
|
|
15
32
|
exports.BrandRichEntrySchema = zod_1.z.object({
|
|
16
33
|
name: zod_1.z.string().min(1).describe("The entry's text."),
|
|
@@ -31,29 +48,141 @@ exports.BrandRichEntrySchema = zod_1.z.object({
|
|
|
31
48
|
exports.BrandFieldValueSchema = zod_1.z
|
|
32
49
|
.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string()), zod_1.z.array(exports.BrandRichEntrySchema)])
|
|
33
50
|
.describe("A text value, a list of names, or a list of rich entries.");
|
|
34
|
-
/**
|
|
35
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Canonical Sitecore AI brand-kit section names — the seven buckets
|
|
53
|
+
* the EnrichSections pipeline produces. Mirrors
|
|
54
|
+
* `BRAND_KIT_CANONICAL_SECTIONS` in the registry's recipe definitions.
|
|
55
|
+
* Exported so callers building recipes have a stable list to bias
|
|
56
|
+
* `documents[].sections` against.
|
|
57
|
+
*/
|
|
58
|
+
exports.BRAND_KIT_CANONICAL_SECTIONS = [
|
|
59
|
+
"Brand Context",
|
|
60
|
+
"Global Goals",
|
|
61
|
+
"Tone of Voice",
|
|
62
|
+
"Glossary and Localization",
|
|
63
|
+
"Do's and Don'ts",
|
|
64
|
+
"Grammar Checklists",
|
|
65
|
+
"Visual Guidelines",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Optional ingestion hints — `tags` and `sections` — shared by both
|
|
69
|
+
* document variants. Hoisted into a shape so the URL and registry-file
|
|
70
|
+
* shapes stay in lockstep without duplicating field-by-field.
|
|
71
|
+
*/
|
|
72
|
+
const DocumentHints = {
|
|
73
|
+
title: zod_1.z.string().optional().describe('Document title. Defaults to "brand guidelines".'),
|
|
74
|
+
summary: zod_1.z.string().optional().describe("Short document summary."),
|
|
75
|
+
tags: zod_1.z
|
|
76
|
+
.array(zod_1.z.string())
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Free-form tags surfaced to the EnrichSections pipeline (e.g. "voice").'),
|
|
79
|
+
sections: zod_1.z
|
|
80
|
+
.array(zod_1.z.string())
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Canonical section names to bias ingestion toward (e.g. ["Tone of Voice"]). Prefer values from BRAND_KIT_CANONICAL_SECTIONS.'),
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A brand document referenced by URL. Sitecore's Documents API fetches
|
|
86
|
+
* the URL server-side and copies the bytes into MMS. The default
|
|
87
|
+
* variant — what `scai brand sync pull` emits when capturing a live
|
|
88
|
+
* kit.
|
|
89
|
+
*/
|
|
90
|
+
exports.BrandUrlDocumentSchema = zod_1.z.object({
|
|
91
|
+
kind: zod_1.z.literal("url"),
|
|
36
92
|
url: zod_1.z
|
|
37
93
|
.string()
|
|
38
94
|
.min(1)
|
|
39
95
|
.describe("Publicly reachable URL of a brand document (PDF). Sitecore fetches it server-side."),
|
|
40
|
-
|
|
41
|
-
summary: zod_1.z.string().optional().describe("Short document summary."),
|
|
96
|
+
...DocumentHints,
|
|
42
97
|
});
|
|
98
|
+
/**
|
|
99
|
+
* A brand document stored alongside the recipe in the registry repo.
|
|
100
|
+
* The `path` is relative to the recipe file's directory. scai itself
|
|
101
|
+
* does **not** upload these — the Sitecore Documents API has no
|
|
102
|
+
* working bytes-upload path (see
|
|
103
|
+
* `src/brand/documents/upload.ts::LOCAL_UPLOAD_UNSUPPORTED_MESSAGE`),
|
|
104
|
+
* so the orchestrator (or whoever owns the recipe before scai sees
|
|
105
|
+
* it) MUST translate `registry-file` entries to `url` entries
|
|
106
|
+
* pointing at an HTTP-reachable host before invoking `scai brand
|
|
107
|
+
* sync push`. The seed runner rejects unresolved `registry-file`
|
|
108
|
+
* documents with a clear hint; this stays in the schema as an
|
|
109
|
+
* authoring-time shape so registry-side recipes round-trip cleanly.
|
|
110
|
+
*/
|
|
111
|
+
exports.BrandRegistryFileDocumentSchema = zod_1.z.object({
|
|
112
|
+
kind: zod_1.z.literal("registry-file"),
|
|
113
|
+
path: zod_1.z.string().min(1).describe("Path to the document, relative to the recipe file."),
|
|
114
|
+
...DocumentHints,
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* A brand document to ingest. Two variants — `{ kind: "url", url }` and
|
|
118
|
+
* `{ kind: "registry-file", path }` — sharing optional `title` /
|
|
119
|
+
* `summary` / `tags` / `sections` ingestion hints.
|
|
120
|
+
*
|
|
121
|
+
* Back-compat: scai-native recipes that pre-date the discriminator wrote
|
|
122
|
+
* documents as the flat `{ url, title?, summary? }` shape. The
|
|
123
|
+
* `z.preprocess` step defaults a missing `kind` to `"url"` whenever
|
|
124
|
+
* `url` is present, so legacy recipes keep parsing without a migration
|
|
125
|
+
* step. Zod 4's discriminated unions require the discriminator to be
|
|
126
|
+
* present BEFORE routing, so `.default("url")` inline on the literal
|
|
127
|
+
* doesn't work — preprocess is the correct seam.
|
|
128
|
+
*/
|
|
129
|
+
exports.BrandDocumentSchema = zod_1.z.preprocess((raw) => {
|
|
130
|
+
if (raw !== null &&
|
|
131
|
+
typeof raw === "object" &&
|
|
132
|
+
!Array.isArray(raw) &&
|
|
133
|
+
!("kind" in raw) &&
|
|
134
|
+
"url" in raw) {
|
|
135
|
+
return { kind: "url", ...raw };
|
|
136
|
+
}
|
|
137
|
+
return raw;
|
|
138
|
+
}, zod_1.z.discriminatedUnion("kind", [exports.BrandUrlDocumentSchema, exports.BrandRegistryFileDocumentSchema]));
|
|
43
139
|
/** The full brand-kit recipe. */
|
|
44
140
|
exports.BrandKitRecipeSchema = zod_1.z.object({
|
|
141
|
+
/**
|
|
142
|
+
* Registry-superset discriminator — `"brandkit"`. Optional: scai-
|
|
143
|
+
* native YAML/JSON recipes pre-date this field and don't carry it.
|
|
144
|
+
* Defaulting in (rather than requiring) keeps the legacy parse
|
|
145
|
+
* path intact. Registry-authored recipes set this explicitly.
|
|
146
|
+
*/
|
|
147
|
+
kind: zod_1.z.literal("brandkit").optional(),
|
|
148
|
+
/**
|
|
149
|
+
* Registry-superset schema version pin — `"1"`. Optional for the
|
|
150
|
+
* same back-compat reason as `kind`. When set, the registry's
|
|
151
|
+
* discriminated-union loader picks this recipe up.
|
|
152
|
+
*/
|
|
153
|
+
schemaVersion: zod_1.z.literal("1").optional(),
|
|
154
|
+
/**
|
|
155
|
+
* Stable handle of the form `<kebab-name>@<major>` (e.g.
|
|
156
|
+
* `acme-brand@1`). Registry-side identifier used to wire brand
|
|
157
|
+
* kits to themes. Optional in scai because scai-native recipes
|
|
158
|
+
* identify kits by `name`, not handle.
|
|
159
|
+
*/
|
|
160
|
+
handle: zod_1.z
|
|
161
|
+
.string()
|
|
162
|
+
.regex(HANDLE_PATTERN, {
|
|
163
|
+
message: "handle must match `<kebab-name>@<major>` (e.g. acme-brand@1)",
|
|
164
|
+
})
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Stable handle `<kebab-name>@<major>`. Required for registry-authored recipes."),
|
|
45
167
|
name: zod_1.z
|
|
46
168
|
.string()
|
|
47
169
|
.min(1)
|
|
48
170
|
.describe("Display name of the brand kit. Identifies the kit when pushing."),
|
|
171
|
+
/**
|
|
172
|
+
* Author-facing label, mirrors the registry's superset. When absent
|
|
173
|
+
* the registry's loader treats it as identical to `name`; scai's
|
|
174
|
+
* `apply()` likewise ignores it (the API has no separate display
|
|
175
|
+
* name slot beyond `name`).
|
|
176
|
+
*/
|
|
177
|
+
displayName: zod_1.z.string().min(1).optional().describe("Author-facing label; defaults to `name`."),
|
|
49
178
|
description: zod_1.z.string().optional().describe("Human description of the kit."),
|
|
50
179
|
industry: zod_1.z.string().optional().describe('Industry label, e.g. "retail" or "developer-tools".'),
|
|
51
180
|
documents: zod_1.z
|
|
52
181
|
.array(exports.BrandDocumentSchema)
|
|
53
182
|
.default([])
|
|
54
|
-
.describe("Brand documents to ingest. On push, when the kit has no populated sections, each is uploaded and run through the ingestion + enrichment pipelines."),
|
|
183
|
+
.describe("Brand documents to ingest. On push, when the kit has no populated sections, each is uploaded and run through the ingestion + enrichment pipelines. `registry-file` variants must be translated to `url` variants before scai sees the recipe — the Sitecore Documents API has no working bytes-upload path."),
|
|
55
184
|
sections: zod_1.z
|
|
56
185
|
.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), exports.BrandFieldValueSchema))
|
|
57
186
|
.default({})
|
|
58
|
-
.describe("Desired field values, keyed by section name then field name. Sections and fields are created by enrichment; push converges their values."),
|
|
187
|
+
.describe("Desired field values, keyed by section name then field name. Prefer canonical names from BRAND_KIT_CANONICAL_SECTIONS for the outer key. Sections and fields are created by enrichment; push converges their values."),
|
|
59
188
|
});
|
package/dist/brand/seed.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { BrandApiClientOptions } from "./api/client";
|
|
|
2
2
|
import { type BrandKitSectionSummary } from "./kits/sections";
|
|
3
3
|
import { type UploadDocumentSource, type UploadedDocument } from "./documents/upload";
|
|
4
4
|
import type { BrandKitSummary } from "./kits/list";
|
|
5
|
+
import type { BrandDocument } from "./recipe/schema";
|
|
5
6
|
/**
|
|
6
7
|
* Stage labels emitted via `onProgress`. Useful for callers that
|
|
7
8
|
* surface progress (CLI streaming output, MCP progress events).
|
|
@@ -30,12 +31,15 @@ export interface SeedBrandKitOptions {
|
|
|
30
31
|
/**
|
|
31
32
|
* Multiple source documents — uploaded before a single ingestion +
|
|
32
33
|
* enrichment pass. Takes precedence over `source` when non-empty.
|
|
34
|
+
* Accepts the recipe-shaped `BrandDocument` union (URL or
|
|
35
|
+
* registry-file). The seed runner rejects `registry-file` entries
|
|
36
|
+
* with `INPUT_INVALID` and a clear hint — see the
|
|
37
|
+
* `LOCAL_UPLOAD_UNSUPPORTED_MESSAGE` rationale in
|
|
38
|
+
* `documents/upload.ts` for why bytes uploads don't work end-to-end.
|
|
39
|
+
* Callers (orchestrator's `brandkit_deploy` handler) must translate
|
|
40
|
+
* registry-file paths to public URLs before invoking scai.
|
|
33
41
|
*/
|
|
34
|
-
documents?:
|
|
35
|
-
url: string;
|
|
36
|
-
title?: string;
|
|
37
|
-
summary?: string;
|
|
38
|
-
}[];
|
|
42
|
+
documents?: BrandDocument[];
|
|
39
43
|
/** Optional kit metadata. */
|
|
40
44
|
description?: string;
|
|
41
45
|
industry?: string;
|
package/dist/brand/seed.js
CHANGED
|
@@ -49,12 +49,37 @@ const seedBrandKit = async (options) => {
|
|
|
49
49
|
const kit = await (0, create_1.createBrandKit)(createInput);
|
|
50
50
|
emit({ stage: "createKit", message: `Created kit ${kit.id}` });
|
|
51
51
|
// 2. UPLOAD doc(s) — one or many, before a single ingestion pass.
|
|
52
|
+
//
|
|
53
|
+
// The `documents` list is the recipe-shaped discriminated union.
|
|
54
|
+
// `registry-file` entries arrive unresolved (path relative to the
|
|
55
|
+
// recipe file) and CANNOT be uploaded as bytes — the Sitecore
|
|
56
|
+
// Documents API has no working local-upload path (verified
|
|
57
|
+
// 2026-05-15; see documents/upload.ts). The caller (orchestrator's
|
|
58
|
+
// `brandkit_deploy` handler) must translate them to URL entries
|
|
59
|
+
// first. Failing fast here — with the path in the hint — beats
|
|
60
|
+
// a confusing 400 from `uploadDocument` minutes later.
|
|
61
|
+
if (options.documents) {
|
|
62
|
+
for (const doc of options.documents) {
|
|
63
|
+
if (doc.kind === "registry-file") {
|
|
64
|
+
throw (0, errors_1.createScaiError)(`Brand document "${doc.path}" is a registry-file path; scai cannot upload local bytes.`, "INPUT_INVALID", {
|
|
65
|
+
hint: 'Host the file at an HTTPS URL Sitecore\'s edge can reach (S3, GitHub raw, a CDN) and pass it as a `{ kind: "url", url }` document. The Sitecore Documents API rejects local-file uploads — translation has to happen upstream of scai.',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
52
70
|
const uploadList = options.documents && options.documents.length > 0
|
|
53
|
-
? options.documents.map((doc) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
? options.documents.map((doc) => {
|
|
72
|
+
// After the registry-file guard above, every doc is a URL
|
|
73
|
+
// variant; the narrow keeps the TS surface clean.
|
|
74
|
+
if (doc.kind !== "url") {
|
|
75
|
+
throw (0, errors_1.createScaiError)("Unreachable: non-URL document survived the registry-file guard.", "INPUT_INVALID");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
source: { url: doc.url },
|
|
79
|
+
title: doc.title,
|
|
80
|
+
summary: doc.summary,
|
|
81
|
+
};
|
|
82
|
+
})
|
|
58
83
|
: options.source
|
|
59
84
|
? [
|
|
60
85
|
{
|
|
@@ -69,7 +69,10 @@ exports.BudgetFieldSchema = zod_1.z
|
|
|
69
69
|
type: zod_1.z.literal("Budget").describe("Discriminator — a budget/currency field."),
|
|
70
70
|
...briefFieldBaseShape,
|
|
71
71
|
currencies: zod_1.z
|
|
72
|
-
.array(zod_1.z
|
|
72
|
+
.array(zod_1.z
|
|
73
|
+
.string()
|
|
74
|
+
.length(3)
|
|
75
|
+
.regex(/^[A-Z]{3}$/, "ISO-4217 currency code must be 3 uppercase letters (e.g. `USD`)."))
|
|
73
76
|
.describe('ISO-4217 currency codes the field accepts, e.g. ["USD", "EUR"].'),
|
|
74
77
|
})
|
|
75
78
|
.describe("A budget/currency field.");
|
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
* model reads it. See docs/recipe-sync-architecture.md.
|
|
10
10
|
*/
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
/**
|
|
13
|
+
* Server enum values **confirmed by observation** in HAR captures of the
|
|
14
|
+
* Sitecore Content Operations UI driving the Orchestrate API. Other
|
|
15
|
+
* values likely exist in the server's enum — `recipe pull` from a tenant
|
|
16
|
+
* with a live `IN_PROGRESS` campaign must still round-trip cleanly — so
|
|
17
|
+
* the schema accepts the known set as a strong hint and falls back to
|
|
18
|
+
* `z.string()`. JSON Schema renders this as `anyOf: [{ enum: [...] }, { type:
|
|
19
|
+
* "string" }]`, which the model reads as "prefer one of these values; new
|
|
20
|
+
* uppercase enums are acceptable too".
|
|
21
|
+
*
|
|
22
|
+
* Update the lists when more values are observed; the trailing
|
|
23
|
+
* `z.string()` keeps the schema permissive in the meantime.
|
|
24
|
+
*/
|
|
25
|
+
export declare const KNOWN_CAMPAIGN_STATUSES: readonly ["NOT_STARTED"];
|
|
26
|
+
export declare const KNOWN_CAMPAIGN_FUNNEL_STAGES: readonly ["TOP"];
|
|
12
27
|
/**
|
|
13
28
|
* A task — the leaf work item of a campaign. Owned by a deliverable.
|
|
14
29
|
* Identified within its deliverable by `name`; server ids are dropped
|
|
@@ -16,7 +31,9 @@ import { z } from "zod";
|
|
|
16
31
|
*/
|
|
17
32
|
export declare const CampaignTaskSchema: z.ZodObject<{
|
|
18
33
|
name: z.ZodString;
|
|
19
|
-
status: z.ZodOptional<z.
|
|
34
|
+
status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
35
|
+
NOT_STARTED: "NOT_STARTED";
|
|
36
|
+
}>, z.ZodString]>>;
|
|
20
37
|
dueDate: z.ZodOptional<z.ZodString>;
|
|
21
38
|
priority: z.ZodOptional<z.ZodString>;
|
|
22
39
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -29,14 +46,20 @@ export declare const CampaignTaskSchema: z.ZodObject<{
|
|
|
29
46
|
*/
|
|
30
47
|
export declare const CampaignDeliverableSchema: z.ZodObject<{
|
|
31
48
|
name: z.ZodString;
|
|
32
|
-
status: z.ZodOptional<z.
|
|
49
|
+
status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
50
|
+
NOT_STARTED: "NOT_STARTED";
|
|
51
|
+
}>, z.ZodString]>>;
|
|
33
52
|
dueDate: z.ZodOptional<z.ZodString>;
|
|
34
|
-
funnelStage: z.ZodOptional<z.
|
|
53
|
+
funnelStage: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
54
|
+
TOP: "TOP";
|
|
55
|
+
}>, z.ZodString]>>;
|
|
35
56
|
funnelTactics: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
36
57
|
labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
37
58
|
tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
38
59
|
name: z.ZodString;
|
|
39
|
-
status: z.ZodOptional<z.
|
|
60
|
+
status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
61
|
+
NOT_STARTED: "NOT_STARTED";
|
|
62
|
+
}>, z.ZodString]>>;
|
|
40
63
|
dueDate: z.ZodOptional<z.ZodString>;
|
|
41
64
|
priority: z.ZodOptional<z.ZodString>;
|
|
42
65
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -48,21 +71,29 @@ export declare const CampaignDeliverableSchema: z.ZodObject<{
|
|
|
48
71
|
export declare const CampaignRecipeSchema: z.ZodObject<{
|
|
49
72
|
name: z.ZodString;
|
|
50
73
|
description: z.ZodOptional<z.ZodString>;
|
|
51
|
-
status: z.ZodOptional<z.
|
|
74
|
+
status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
75
|
+
NOT_STARTED: "NOT_STARTED";
|
|
76
|
+
}>, z.ZodString]>>;
|
|
52
77
|
startDate: z.ZodOptional<z.ZodString>;
|
|
53
78
|
dueDate: z.ZodOptional<z.ZodString>;
|
|
54
79
|
brandKitId: z.ZodOptional<z.ZodString>;
|
|
55
80
|
labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
56
81
|
deliverables: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
57
82
|
name: z.ZodString;
|
|
58
|
-
status: z.ZodOptional<z.
|
|
83
|
+
status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
84
|
+
NOT_STARTED: "NOT_STARTED";
|
|
85
|
+
}>, z.ZodString]>>;
|
|
59
86
|
dueDate: z.ZodOptional<z.ZodString>;
|
|
60
|
-
funnelStage: z.ZodOptional<z.
|
|
87
|
+
funnelStage: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
88
|
+
TOP: "TOP";
|
|
89
|
+
}>, z.ZodString]>>;
|
|
61
90
|
funnelTactics: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
62
91
|
labels: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
63
92
|
tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
64
93
|
name: z.ZodString;
|
|
65
|
-
status: z.ZodOptional<z.
|
|
94
|
+
status: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
95
|
+
NOT_STARTED: "NOT_STARTED";
|
|
96
|
+
}>, z.ZodString]>>;
|
|
66
97
|
dueDate: z.ZodOptional<z.ZodString>;
|
|
67
98
|
priority: z.ZodOptional<z.ZodString>;
|
|
68
99
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CampaignRecipeSchema = exports.CampaignDeliverableSchema = exports.CampaignTaskSchema = void 0;
|
|
3
|
+
exports.CampaignRecipeSchema = exports.CampaignDeliverableSchema = exports.CampaignTaskSchema = exports.KNOWN_CAMPAIGN_FUNNEL_STAGES = exports.KNOWN_CAMPAIGN_STATUSES = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `CampaignRecipe` — the declarative definition of a Sitecore
|
|
6
6
|
* Orchestrate campaign (a `project`, with its nested deliverables and
|
|
@@ -12,6 +12,33 @@ exports.CampaignRecipeSchema = exports.CampaignDeliverableSchema = exports.Campa
|
|
|
12
12
|
* model reads it. See docs/recipe-sync-architecture.md.
|
|
13
13
|
*/
|
|
14
14
|
const zod_1 = require("zod");
|
|
15
|
+
/**
|
|
16
|
+
* ISO-8601 date-or-datetime — accepts `2026-05-26`, `2026-05-26T15:00:00Z`,
|
|
17
|
+
* or `2026-05-26T15:00:00.123+02:00`. Less strict than `z.string().datetime()`
|
|
18
|
+
* because the campaign API returns date-only values for some fields.
|
|
19
|
+
*/
|
|
20
|
+
const Iso8601 = zod_1.z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2}))?$/, {
|
|
23
|
+
message: "must be an ISO-8601 date or datetime (e.g. `2026-05-26` or `2026-05-26T15:00:00Z`)",
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Server enum values **confirmed by observation** in HAR captures of the
|
|
27
|
+
* Sitecore Content Operations UI driving the Orchestrate API. Other
|
|
28
|
+
* values likely exist in the server's enum — `recipe pull` from a tenant
|
|
29
|
+
* with a live `IN_PROGRESS` campaign must still round-trip cleanly — so
|
|
30
|
+
* the schema accepts the known set as a strong hint and falls back to
|
|
31
|
+
* `z.string()`. JSON Schema renders this as `anyOf: [{ enum: [...] }, { type:
|
|
32
|
+
* "string" }]`, which the model reads as "prefer one of these values; new
|
|
33
|
+
* uppercase enums are acceptable too".
|
|
34
|
+
*
|
|
35
|
+
* Update the lists when more values are observed; the trailing
|
|
36
|
+
* `z.string()` keeps the schema permissive in the meantime.
|
|
37
|
+
*/
|
|
38
|
+
exports.KNOWN_CAMPAIGN_STATUSES = ["NOT_STARTED"];
|
|
39
|
+
exports.KNOWN_CAMPAIGN_FUNNEL_STAGES = ["TOP"];
|
|
40
|
+
const CampaignStatusSchema = zod_1.z.union([zod_1.z.enum(exports.KNOWN_CAMPAIGN_STATUSES), zod_1.z.string()]);
|
|
41
|
+
const CampaignFunnelStageSchema = zod_1.z.union([zod_1.z.enum(exports.KNOWN_CAMPAIGN_FUNNEL_STAGES), zod_1.z.string()]);
|
|
15
42
|
/**
|
|
16
43
|
* A task — the leaf work item of a campaign. Owned by a deliverable.
|
|
17
44
|
* Identified within its deliverable by `name`; server ids are dropped
|
|
@@ -19,9 +46,12 @@ const zod_1 = require("zod");
|
|
|
19
46
|
*/
|
|
20
47
|
exports.CampaignTaskSchema = zod_1.z.object({
|
|
21
48
|
name: zod_1.z.string().min(1).describe("Task name. Identifies the task within its deliverable."),
|
|
22
|
-
status:
|
|
23
|
-
dueDate:
|
|
24
|
-
priority: zod_1.z
|
|
49
|
+
status: CampaignStatusSchema.optional().describe('Task status — a server enum. Confirmed values: "NOT_STARTED". Other UPPER_SNAKE values may exist on the server; the schema accepts them but agents should prefer the confirmed set.'),
|
|
50
|
+
dueDate: Iso8601.optional().describe("Task due date (ISO-8601 date or datetime)."),
|
|
51
|
+
priority: zod_1.z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Task priority — a server enum. No values have been observed yet; convention follows project-management norms (e.g. UPPERCASE strings). Schema is `string` until the enum set is captured."),
|
|
25
55
|
description: zod_1.z.string().optional().describe("Task description. HTML."),
|
|
26
56
|
assignee: zod_1.z.string().optional().describe('Assignee — an Auth0 user subject (e.g. "auth0|...").'),
|
|
27
57
|
labels: zod_1.z.array(zod_1.z.string()).default([]).describe("Free-form labels on the task."),
|
|
@@ -35,9 +65,9 @@ exports.CampaignDeliverableSchema = zod_1.z.object({
|
|
|
35
65
|
.string()
|
|
36
66
|
.min(1)
|
|
37
67
|
.describe("Deliverable name. Identifies the deliverable within its campaign."),
|
|
38
|
-
status:
|
|
39
|
-
dueDate:
|
|
40
|
-
funnelStage:
|
|
68
|
+
status: CampaignStatusSchema.optional().describe('Deliverable status — a server enum. Confirmed values: "NOT_STARTED". Other UPPER_SNAKE values may exist on the server; the schema accepts them but agents should prefer the confirmed set.'),
|
|
69
|
+
dueDate: Iso8601.optional().describe("Deliverable due date (ISO-8601 date or datetime)."),
|
|
70
|
+
funnelStage: CampaignFunnelStageSchema.optional().describe('Funnel stage — a server enum. Confirmed values: "TOP". Other values likely exist (e.g. middle / bottom of funnel); the schema accepts them but agents should prefer the confirmed set.'),
|
|
41
71
|
funnelTactics: zod_1.z.array(zod_1.z.string()).default([]).describe("Funnel tactics for the deliverable."),
|
|
42
72
|
labels: zod_1.z.array(zod_1.z.string()).default([]).describe("Free-form labels on the deliverable."),
|
|
43
73
|
tasks: zod_1.z
|
|
@@ -52,9 +82,9 @@ exports.CampaignRecipeSchema = zod_1.z.object({
|
|
|
52
82
|
.min(1)
|
|
53
83
|
.describe("Display name of the campaign (project). Identifies the campaign when pushing."),
|
|
54
84
|
description: zod_1.z.string().optional().describe("Human description of the campaign."),
|
|
55
|
-
status:
|
|
56
|
-
startDate:
|
|
57
|
-
dueDate:
|
|
85
|
+
status: CampaignStatusSchema.optional().describe('Campaign status — a server enum. Confirmed values: "NOT_STARTED". Other UPPER_SNAKE values may exist on the server; the schema accepts them but agents should prefer the confirmed set.'),
|
|
86
|
+
startDate: Iso8601.optional().describe("Campaign start date (ISO-8601 date or datetime)."),
|
|
87
|
+
dueDate: Iso8601.optional().describe("Campaign due date (ISO-8601 date or datetime)."),
|
|
58
88
|
brandKitId: zod_1.z
|
|
59
89
|
.string()
|
|
60
90
|
.optional()
|
|
@@ -91,7 +91,7 @@ const createDiffCommand = () => {
|
|
|
91
91
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
92
92
|
const kind = resolveKind(options.kind);
|
|
93
93
|
const ctx = buildContext(options, logger);
|
|
94
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
|
|
94
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
|
|
95
95
|
const plan = await (0, sync_1.syncDiff)(kind, recipe, { kind: kind.name, id: recipeName(recipe) }, ctx);
|
|
96
96
|
printPlan(logger, plan);
|
|
97
97
|
});
|
|
@@ -110,7 +110,7 @@ const createPushCommand = () => {
|
|
|
110
110
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
111
111
|
const kind = resolveKind(options.kind);
|
|
112
112
|
const ctx = buildContext(options, logger);
|
|
113
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
|
|
113
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", kind.schema);
|
|
114
114
|
const mode = options.allowWrite ? "apply" : "what-if";
|
|
115
115
|
const outcome = await (0, sync_1.syncPush)(kind, recipe, { kind: kind.name, id: recipeName(recipe) }, ctx, {
|
|
116
116
|
mode,
|
|
@@ -70,7 +70,7 @@ const seedFromFile = async (options, logger) => {
|
|
|
70
70
|
configPath,
|
|
71
71
|
logger,
|
|
72
72
|
};
|
|
73
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
|
|
73
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
|
|
74
74
|
const outcome = await (0, sync_1.syncPush)(recipe_1.brandKitKind, recipe, { kind: recipe_1.brandKitKind.name, id: recipe.name }, ctx, { mode: "apply", prune: false });
|
|
75
75
|
if (options.format === "json") {
|
|
76
76
|
process.stdout.write(JSON.stringify({ recipe: recipe.name, plan: outcome.plan, result: outcome.result }, null, 2) +
|
|
@@ -77,7 +77,7 @@ const createDiffCommand = () => {
|
|
|
77
77
|
command.action(async (options) => {
|
|
78
78
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
79
79
|
const ctx = buildContext(options, logger);
|
|
80
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
|
|
80
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
|
|
81
81
|
const plan = await (0, sync_1.syncDiff)(recipe_1.brandKitKind, recipe, { kind: recipe_1.brandKitKind.name, id: recipe.name }, ctx);
|
|
82
82
|
printPlan(logger, plan);
|
|
83
83
|
if (hasPaidPipeline(plan)) {
|
|
@@ -98,7 +98,7 @@ const createPushCommand = () => {
|
|
|
98
98
|
command.action(async (options) => {
|
|
99
99
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
100
100
|
const ctx = buildContext(options, logger);
|
|
101
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
|
|
101
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.brandKitKind.schema);
|
|
102
102
|
const mode = options.allowWrite ? "apply" : "what-if";
|
|
103
103
|
const outcome = await (0, sync_1.syncPush)(recipe_1.brandKitKind, recipe, { kind: recipe_1.brandKitKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
|
|
104
104
|
printPlan(logger, outcome.plan);
|
|
@@ -76,7 +76,7 @@ const createDiffCommand = () => {
|
|
|
76
76
|
command.action(async (options) => {
|
|
77
77
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
78
78
|
const ctx = buildContext(options, logger);
|
|
79
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.briefTypeKind.schema);
|
|
79
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.briefTypeKind.schema);
|
|
80
80
|
const plan = await (0, sync_1.syncDiff)(recipe_1.briefTypeKind, recipe, { kind: recipe_1.briefTypeKind.name, id: recipe.name }, ctx);
|
|
81
81
|
printPlan(logger, plan);
|
|
82
82
|
});
|
|
@@ -94,7 +94,7 @@ const createPushCommand = () => {
|
|
|
94
94
|
command.action(async (options) => {
|
|
95
95
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
96
96
|
const ctx = buildContext(options, logger);
|
|
97
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.briefTypeKind.schema);
|
|
97
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.briefTypeKind.schema);
|
|
98
98
|
const mode = options.allowWrite ? "apply" : "what-if";
|
|
99
99
|
const outcome = await (0, sync_1.syncPush)(recipe_1.briefTypeKind, recipe, { kind: recipe_1.briefTypeKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
|
|
100
100
|
printPlan(logger, outcome.plan);
|
|
@@ -76,7 +76,7 @@ const createDiffCommand = () => {
|
|
|
76
76
|
command.action(async (options) => {
|
|
77
77
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
78
78
|
const ctx = buildContext(options, logger);
|
|
79
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
|
|
79
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
|
|
80
80
|
const plan = await (0, sync_1.syncDiff)(recipe_1.campaignKind, recipe, { kind: recipe_1.campaignKind.name, id: recipe.name }, ctx);
|
|
81
81
|
printPlan(logger, plan);
|
|
82
82
|
});
|
|
@@ -94,7 +94,7 @@ const createPushCommand = () => {
|
|
|
94
94
|
command.action(async (options) => {
|
|
95
95
|
const logger = (0, cli_tasks_1.toLogger)(options);
|
|
96
96
|
const ctx = buildContext(options, logger);
|
|
97
|
-
const recipe = (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
|
|
97
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file ?? "", recipe_1.campaignKind.schema);
|
|
98
98
|
const mode = options.allowWrite ? "apply" : "what-if";
|
|
99
99
|
const outcome = await (0, sync_1.syncPush)(recipe_1.campaignKind, recipe, { kind: recipe_1.campaignKind.name, id: recipe.name }, ctx, { mode, prune: options.prune });
|
|
100
100
|
printPlan(logger, outcome.plan);
|
|
@@ -6,6 +6,7 @@ const operations_1 = require("../ir/operations");
|
|
|
6
6
|
const policy_1 = require("../runtime/policy");
|
|
7
7
|
const sitecore_templates_1 = require("../ir/sitecore-templates");
|
|
8
8
|
const recipe_1 = require("../schema/recipe");
|
|
9
|
+
const component_section_1 = require("./component-section");
|
|
9
10
|
const shared_1 = require("./shared");
|
|
10
11
|
/**
|
|
11
12
|
* Compile a standalone `DesignParametersTemplateRecipe` to an Operation IR.
|
|
@@ -22,9 +23,10 @@ function compileDesignParametersTemplateRecipe(input, context, emittedFolders =
|
|
|
22
23
|
const policy = (0, policy_1.defaultPolicyForRecipe)(recipe.kind);
|
|
23
24
|
const icon = recipe.icon ?? sitecore_templates_1.DEFAULT_ICON;
|
|
24
25
|
const site = (0, shared_1.siteOf)(context);
|
|
25
|
-
(0,
|
|
26
|
-
|
|
27
|
-
const
|
|
26
|
+
const sectionName = (0, component_section_1.resolveSectionRecipe)(recipe.handle, recipe.section.handle, context.sectionsByHandle).name;
|
|
27
|
+
(0, shared_1.ensureSectionFolder)(operations, context, sectionName, emittedFolders);
|
|
28
|
+
const bucketRefKey = (0, shared_1.ensurePresentationDesignParametersBucket)(operations, context, sectionName, emittedFolders);
|
|
29
|
+
const parentPath = (0, shared_1.resolvePresentationDesignParametersBucketPath)(context, sectionName);
|
|
28
30
|
// The standalone parameters template lands at the same identity
|
|
29
31
|
// (designParametersTemplateId) as inline-hoisted ones — keeps re-pushes
|
|
30
32
|
// idempotent if a recipe migrates from inline to standalone.
|
|
@@ -118,17 +118,12 @@ function compileEnumerationRecipe(input, context, emittedFolders = new Set()) {
|
|
|
118
118
|
kind: "ref-path",
|
|
119
119
|
value: context.enumerationsRoot,
|
|
120
120
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (folderSegments.length === 0) {
|
|
128
|
-
throw (0, errors_1.createScaiError)(`Recipe '${recipe.handle}' declares location.folder that is empty after trimming.`, "INPUT_INVALID", {
|
|
129
|
-
hint: "Use a non-empty folder string like 'Theme' or 'Components/Card'.",
|
|
130
|
-
});
|
|
131
|
-
}
|
|
121
|
+
// `recipe.location.folder` is normalized to `string[]` by the schema
|
|
122
|
+
// (`FolderPath` in schema/recipe.ts) — both `"Theme/Color"` and
|
|
123
|
+
// `["Theme", "Color"]` input shapes land here as `["Theme", "Color"]`,
|
|
124
|
+
// already trimmed and non-empty.
|
|
125
|
+
const folderSegments = recipe.location?.folder;
|
|
126
|
+
if (folderSegments && folderSegments.length > 0) {
|
|
132
127
|
const cumulativeSegments = [];
|
|
133
128
|
for (const segment of folderSegments) {
|
|
134
129
|
cumulativeSegments.push(segment);
|
|
@@ -50,9 +50,12 @@ const resolveEnumFolderPath = (context, enumHandle, consumerHandle) => {
|
|
|
50
50
|
hint: "Add an `EnumerationRecipe` (kind: 'enumeration') with the matching handle to the recipe set, or change the field's `sitecore.enumHandle` to point at an existing one.",
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// `location.folder` is a `string[]` of grouping segments after the
|
|
54
|
+
// schema's `FolderPath` normalisation — join with `/` here to build
|
|
55
|
+
// the cumulative path under enumerationsRoot.
|
|
56
|
+
const folderSegments = enumRecipe.location?.folder;
|
|
57
|
+
return folderSegments && folderSegments.length > 0
|
|
58
|
+
? (0, exports.joinPath)((0, exports.joinPath)(context.enumerationsRoot, folderSegments.join("/")), enumRecipe.name)
|
|
56
59
|
: (0, exports.joinPath)(context.enumerationsRoot, enumRecipe.name);
|
|
57
60
|
};
|
|
58
61
|
exports.resolveEnumFolderPath = resolveEnumFolderPath;
|
|
@@ -799,19 +802,16 @@ function encodeStandardValueDefault(raw, type) {
|
|
|
799
802
|
function resolveFieldSource(field, type, site, recipeHandle, context) {
|
|
800
803
|
const sc = field.sitecore;
|
|
801
804
|
if (sc) {
|
|
802
|
-
const fields =
|
|
803
|
-
sourceTypes: sc.sourceTypes,
|
|
804
|
-
sourceQuery: sc.sourceQuery,
|
|
805
|
-
sourceScope: sc.sourceScope,
|
|
806
|
-
sourceRaw: sc.sourceRaw,
|
|
807
|
-
};
|
|
805
|
+
const fields = (0, source_fields_1.augmentSourceToFields)(sc.source);
|
|
808
806
|
if ((0, source_fields_1.sourceFieldsNeedHandleResolution)(fields)) {
|
|
807
|
+
// `types` is non-empty here because `sourceFieldsNeedHandleResolution`
|
|
808
|
+
// returned true; the cast is to satisfy the IR's `.min(1)` constraint.
|
|
809
809
|
return {
|
|
810
810
|
kind: "ref-source-fields",
|
|
811
811
|
site,
|
|
812
|
-
sourceTypes:
|
|
813
|
-
sourceQuery:
|
|
814
|
-
sourceScope:
|
|
812
|
+
sourceTypes: fields.sourceTypes,
|
|
813
|
+
sourceQuery: fields.sourceQuery,
|
|
814
|
+
sourceScope: fields.sourceScope,
|
|
815
815
|
};
|
|
816
816
|
}
|
|
817
817
|
const rendered = (0, source_fields_1.renderSourceFields)(fields, () => {
|
package/dist/recipe/compile.js
CHANGED
|
@@ -720,10 +720,10 @@ const buildPlaceholderSettingsAggregate = (recipes, context, site) => {
|
|
|
720
720
|
// parent ref + path the leaf placeholder item lands under.
|
|
721
721
|
const emittedFolders = new Set();
|
|
722
722
|
const resolveFolderParent = (folder) => {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
723
|
+
// `folder` is normalised to `string[]` at the schema boundary
|
|
724
|
+
// (`FolderPath` in schema/recipe.ts) — both array and legacy
|
|
725
|
+
// slash-string inputs land here as a clean segment list.
|
|
726
|
+
const segments = folder ?? [];
|
|
727
727
|
let parentPath = root;
|
|
728
728
|
let parentRef = { kind: "ref-path", value: root };
|
|
729
729
|
let cumulative = "";
|