@ssobig/writer-cli 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.
Files changed (73) hide show
  1. package/README.md +35 -0
  2. package/asset-repository.js +278 -0
  3. package/config.js +14 -0
  4. package/package.json +28 -0
  5. package/project-runtime.js +102 -0
  6. package/storage-path.js +110 -0
  7. package/templates/mystery-v1/authoring-view-preference.js +34 -0
  8. package/templates/mystery-v1/character-perspective-preview.js +61 -0
  9. package/templates/mystery-v1/component-asset-operations.js +103 -0
  10. package/templates/mystery-v1/component-autosave.js +121 -0
  11. package/templates/mystery-v1/component-catalog-contract.js +340 -0
  12. package/templates/mystery-v1/component-checkpoint-history.js +145 -0
  13. package/templates/mystery-v1/component-contract.js +90 -0
  14. package/templates/mystery-v1/component-draft-operations.js +313 -0
  15. package/templates/mystery-v1/component-field-contracts.js +595 -0
  16. package/templates/mystery-v1/component-id-policy.js +64 -0
  17. package/templates/mystery-v1/component-manager.js +396 -0
  18. package/templates/mystery-v1/component-navigation-counts.js +64 -0
  19. package/templates/mystery-v1/component-registry.js +205 -0
  20. package/templates/mystery-v1/component-renderers.js +139 -0
  21. package/templates/mystery-v1/component-storage-contract.js +237 -0
  22. package/templates/mystery-v1/external-update-coordinator.js +91 -0
  23. package/templates/mystery-v1/output-clue-card-layout.js +46 -0
  24. package/templates/mystery-v1/page-header.js +26 -0
  25. package/templates/mystery-v1/render-ui-state.js +76 -0
  26. package/templates/mystery-v1/runtime-snapshot-reconciler.js +40 -0
  27. package/templates/mystery-v1/tab-bar.js +87 -0
  28. package/templates/mystery-v1/view-component-contract.js +152 -0
  29. package/templates/mystery-v1/view-component-registry.js +44 -0
  30. package/templates/mystery-v1/view-component-runtime.js +95 -0
  31. package/tools/writer-cli/bin/ssobig-writer-daemon.cjs +34 -0
  32. package/tools/writer-cli/bin/ssobig-writer.cjs +12 -0
  33. package/tools/writer-cli/package-lock.json +121 -0
  34. package/tools/writer-cli/package.json +22 -0
  35. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +38 -0
  36. package/tools/writer-cli/skills/ssobig-writer-cli/agents/openai.yaml +4 -0
  37. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -0
  38. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +10 -0
  39. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +7 -0
  40. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -0
  41. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +5 -0
  42. package/tools/writer-cli/src/agent-paths.cjs +114 -0
  43. package/tools/writer-cli/src/agent-service.cjs +496 -0
  44. package/tools/writer-cli/src/asset-policy.cjs +113 -0
  45. package/tools/writer-cli/src/auth.cjs +655 -0
  46. package/tools/writer-cli/src/checkpoint-diff.cjs +128 -0
  47. package/tools/writer-cli/src/command-registry.cjs +152 -0
  48. package/tools/writer-cli/src/commands.cjs +841 -0
  49. package/tools/writer-cli/src/corpus.cjs +83 -0
  50. package/tools/writer-cli/src/daemon-app.cjs +106 -0
  51. package/tools/writer-cli/src/daemon-client.cjs +187 -0
  52. package/tools/writer-cli/src/daemon-protocol.cjs +184 -0
  53. package/tools/writer-cli/src/daemon-runner.cjs +97 -0
  54. package/tools/writer-cli/src/daemon-server.cjs +378 -0
  55. package/tools/writer-cli/src/diagnostics.cjs +235 -0
  56. package/tools/writer-cli/src/domain.cjs +731 -0
  57. package/tools/writer-cli/src/errors.cjs +47 -0
  58. package/tools/writer-cli/src/gateway.cjs +357 -0
  59. package/tools/writer-cli/src/investigation-board-layout.cjs +328 -0
  60. package/tools/writer-cli/src/json-patch.cjs +98 -0
  61. package/tools/writer-cli/src/json.cjs +26 -0
  62. package/tools/writer-cli/src/local-index-cache.cjs +139 -0
  63. package/tools/writer-cli/src/local-index-lookup.cjs +98 -0
  64. package/tools/writer-cli/src/local-index-query.cjs +304 -0
  65. package/tools/writer-cli/src/local-index-snapshot.cjs +235 -0
  66. package/tools/writer-cli/src/local-index-storage.cjs +284 -0
  67. package/tools/writer-cli/src/local-index.cjs +199 -0
  68. package/tools/writer-cli/src/mutations.cjs +722 -0
  69. package/tools/writer-cli/src/platform-runner.cjs +55 -0
  70. package/tools/writer-cli/src/project-import.cjs +485 -0
  71. package/tools/writer-cli/src/skill-manager.cjs +255 -0
  72. package/tools/writer-cli/src/source-fingerprint.cjs +90 -0
  73. package/tools/writer-cli/src/update-gate.cjs +102 -0
@@ -0,0 +1,731 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const componentContract = require("../../../templates/mystery-v1/component-storage-contract.js");
5
+ const viewContract = require("../../../templates/mystery-v1/view-component-contract.js");
6
+ const projectRuntime = require("../../../project-runtime.js");
7
+ const { applyPatch } = require("./json-patch.cjs");
8
+ const { clone, equalJson, sha256 } = require("./json.cjs");
9
+ const { cliError } = require("./errors.cjs");
10
+ const { layoutInvestigationBoard } = require("./investigation-board-layout.cjs");
11
+ const {
12
+ IMPORT_PROFILE,
13
+ normalizeImportComponents,
14
+ importCompositionRows,
15
+ sourceProvenance
16
+ } = require("./project-import.cjs");
17
+ const { version: TOOL_VERSION } = require("../package.json");
18
+
19
+ const PLAN_SCHEMA_VERSION = "ssobig-writer-change-plan-v1";
20
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
21
+ const PROJECT_COLUMNS = "id,slug,title,summary,engine_key,storage_namespace,status,author_name,is_catalog_demo,updated_at";
22
+ const VERSION_COLUMNS = "project_id,version_number,title,display_name,created_from_version,updated_at";
23
+ const SET_COLUMNS = "project_id,version_number,revision,canonical_status,component_checksum,migration_metadata";
24
+ const INSTANCE_COLUMNS = "project_id,version_number,instance_id,template_id,tab_label,sort_order,is_enabled,is_archived,is_required,is_removable,editor_view_id,preview_view_id,capabilities,config,data,revision";
25
+ const VIEW_COLUMNS = "view_instance_id,view_template_id,label,description,renderer_id,supported_modes,sort_order,is_enabled,config,revision";
26
+ const BINDING_COLUMNS = "view_instance_id,binding_key,data_instance_id,access_mode,is_required,sort_order";
27
+ const CHECKPOINT_COLUMNS = "checkpoint_id,project_id,version_number,checkpoint_number,parent_checkpoint_id,restored_from_checkpoint_id,operation_id,message,source,component_set_revision,component_checksum,manifest_hash,changed_instance_ids,created_by_email,created_at";
28
+ const CHECKPOINT_ENTRY_COLUMNS = "checkpoint_id,project_id,version_number,instance_id,template_id,tab_label,sort_order,is_enabled,is_archived,is_required,is_removable,editor_view_id,preview_view_id,capabilities,config,source_revision,data_hash,changed_from_parent";
29
+ const CHECKPOINT_MESSAGE_MAX_LENGTH = 500;
30
+
31
+ function isObject(value) {
32
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
33
+ }
34
+
35
+ function validationError(message, cause = null) {
36
+ const text = String(message || "Component data가 schema를 통과하지 못했습니다.");
37
+ if (/파일 pointer는 ssobig\.assets/.test(text)) {
38
+ return cliError("E_ASSET_COMMAND_REQUIRED", `${text} asset 명령을 사용해 주세요.`, null, cause);
39
+ }
40
+ if (/지원되지 않는 (?:Component|필드)|필드는 (?:이 Component schema에서 )?지원되지/.test(text)) {
41
+ return cliError("E_CODE_CHANGE_REQUIRED", `${text} 기존 schema로 표현할 수 없는 변경은 코드 작업으로 진행해야 합니다.`, null, cause);
42
+ }
43
+ return cliError("E_VALIDATION", text, null, cause);
44
+ }
45
+
46
+ function validateLoadedVersion(loaded) {
47
+ if (!isObject(loaded?.project)) throw cliError("E_PROJECT_NOT_FOUND", "작품을 찾을 수 없습니다.");
48
+ if (String(loaded.project.status || "") !== "active") throw cliError("E_MIGRATION_REQUIRED", "활성 작품만 CLI에서 다룰 수 있습니다.");
49
+ let runtime;
50
+ try { runtime = projectRuntime.resolveProject(loaded.project); }
51
+ catch (error) { throw cliError("E_MIGRATION_REQUIRED", error.message, { field: error.field || "" }, error); }
52
+
53
+ const versionNumber = Number(loaded?.version?.version_number);
54
+ if (!Number.isSafeInteger(versionNumber) || versionNumber < 1
55
+ || String(loaded.version.project_id || "") !== String(loaded.project.id || "")) {
56
+ throw cliError("E_VERSION_NOT_FOUND", "선택한 작품 버전을 찾을 수 없습니다.");
57
+ }
58
+ if (String(loaded.componentSet?.project_id || "") !== String(loaded.project.id)
59
+ || Number(loaded.componentSet?.version_number) !== versionNumber) {
60
+ throw cliError("E_MIGRATION_REQUIRED", "작품 버전과 Component Set 식별자가 일치하지 않습니다.");
61
+ }
62
+
63
+ const composition = componentContract.validateActiveComposition(loaded.componentSet, loaded.componentRows || []);
64
+ if (!composition.ok) throw cliError("E_MIGRATION_REQUIRED", composition.error.message, null, composition.error);
65
+ const views = viewContract.validateProjectViews(loaded.viewRows || [], loaded.bindingRows || [], composition.instances);
66
+ if (!views.ok) throw cliError("E_MIGRATION_REQUIRED", views.error.message, null, views.error);
67
+ return Object.freeze({ ...loaded, runtime, versionNumber, instances: composition.instances, views: views.views });
68
+ }
69
+
70
+ function findInstance(validated, selector) {
71
+ const value = String(selector || "").trim();
72
+ const byId = validated.instances.find(instance => instance.instanceId === value);
73
+ if (byId) return byId;
74
+ const byTemplate = validated.instances.filter(instance => instance.templateId === value);
75
+ if (byTemplate.length === 1) return byTemplate[0];
76
+ if (byTemplate.length > 1) throw cliError("E_AMBIGUOUS_INSTANCE", `${value} Template의 Instance가 여러 개입니다. instance ID를 지정해 주세요.`);
77
+ throw cliError("E_COMPONENT_NOT_FOUND", `활성 Component를 찾을 수 없습니다: ${value || "(비어 있음)"}`);
78
+ }
79
+
80
+ function assertGenericPatchAllowed(instance) {
81
+ if (instance.templateId === "ssobig.assets") {
82
+ throw cliError("E_ASSET_COMMAND_REQUIRED", "ssobig.assets Component는 generic patch로 수정할 수 없습니다. asset 명령을 사용해 주세요.");
83
+ }
84
+ if (!componentContract.componentSpec(instance.templateId)) throw cliError("E_CODE_CHANGE_REQUIRED", `지원되지 않는 Component입니다: ${instance.templateId}`);
85
+ }
86
+
87
+ function validateHypotheticalData(validated, instance, afterData) {
88
+ const spec = componentContract.componentSpec(instance.templateId);
89
+ if (!spec) throw cliError("E_CODE_CHANGE_REQUIRED", `지원되지 않는 Component입니다: ${instance.templateId}`);
90
+ const invalidData = spec.validate(afterData);
91
+ if (invalidData) throw validationError(invalidData);
92
+ const nextRows = validated.componentRows.map(row => row.instance_id === instance.instanceId ? { ...row, data: clone(afterData) } : row);
93
+ const composition = componentContract.validateActiveComposition(validated.componentSet, nextRows);
94
+ if (!composition.ok) throw validationError(composition.error.message, composition.error);
95
+ const views = viewContract.validateProjectViews(validated.viewRows, validated.bindingRows, composition.instances);
96
+ if (!views.ok) throw cliError("E_MIGRATION_REQUIRED", views.error.message, null, views.error);
97
+ return composition.instances.find(candidate => candidate.instanceId === instance.instanceId);
98
+ }
99
+
100
+ function planDigest(plan) {
101
+ const source = clone(plan);
102
+ delete source.digest;
103
+ return sha256(source);
104
+ }
105
+
106
+ function finalizePlan(plan) {
107
+ const value = clone(plan);
108
+ value.digest = planDigest(value);
109
+ verifyPlan(value);
110
+ return Object.freeze(value);
111
+ }
112
+
113
+ function basePlan(validated, actorEmail, operation, options = {}) {
114
+ return {
115
+ schemaVersion: PLAN_SCHEMA_VERSION,
116
+ toolVersion: TOOL_VERSION,
117
+ planId: String(options.planId || crypto.randomUUID()),
118
+ createdAt: String(options.createdAt || new Date().toISOString()),
119
+ actorEmail: String(actorEmail || "").toLowerCase(),
120
+ operation,
121
+ project: {
122
+ id: String(validated.project.id),
123
+ slug: String(validated.project.slug),
124
+ storageNamespace: String(validated.project.storage_namespace),
125
+ authorName: String(validated.project.author_name || "")
126
+ },
127
+ versionNumber: validated.versionNumber,
128
+ readiness: {
129
+ componentSetRevision: Number(validated.componentSet.revision),
130
+ componentChecksum: String(validated.componentSet.component_checksum)
131
+ }
132
+ };
133
+ }
134
+
135
+ function checkpointMessage(value, fallback) {
136
+ const message = String(value || fallback || "").trim();
137
+ if (!message || message.length > CHECKPOINT_MESSAGE_MAX_LENGTH) {
138
+ throw cliError("E_VALIDATION", `체크포인트 메시지는 비어 있지 않은 ${CHECKPOINT_MESSAGE_MAX_LENGTH}자 이하 문자열이어야 합니다.`);
139
+ }
140
+ return message;
141
+ }
142
+
143
+ function createComponentPlan(validated, selector, patch, actorEmail, options = {}) {
144
+ const instance = findInstance(validated, selector);
145
+ assertGenericPatchAllowed(instance);
146
+ const afterData = applyPatch(instance.data, patch);
147
+ if (equalJson(instance.data, afterData)) throw cliError("E_NO_CHANGES", "Patch 적용 결과가 현재 Component data와 같습니다.");
148
+ const nextInstance = validateHypotheticalData(validated, instance, afterData);
149
+ componentContract.instancePayload(nextInstance);
150
+ return finalizePlan({
151
+ ...basePlan(validated, actorEmail, "component.patch", options),
152
+ checkpointMessage: checkpointMessage(
153
+ options.checkpointMessage,
154
+ `CLI: ${instance.tab || instance.instanceId} 수정 (${patch.filter(item => item.op !== "test").length}개 변경)`
155
+ ),
156
+ target: {
157
+ instanceId: instance.instanceId,
158
+ templateId: instance.templateId,
159
+ expectedRevision: instance.revision
160
+ },
161
+ patch: clone(patch),
162
+ beforeHash: sha256(instance.data),
163
+ afterHash: sha256(afterData),
164
+ beforeData: clone(instance.data),
165
+ afterData: clone(afterData)
166
+ });
167
+ }
168
+
169
+ function investigationBoardReferences(validated) {
170
+ const references = [];
171
+ const seen = new Set();
172
+ const add = (type, value) => {
173
+ const id = String(value || "").trim();
174
+ const key = `${type}:${id}`;
175
+ if (!id || seen.has(key)) return;
176
+ seen.add(key);
177
+ references.push({ type, id });
178
+ };
179
+
180
+ for (const instance of validated.instances) {
181
+ if (instance.templateId === "ssobig.character") {
182
+ const data = instance.data || {};
183
+ const characters = isObject(data.characters) ? data.characters : {};
184
+ const deleted = new Set(Array.isArray(data.deletedColumns) ? data.deletedColumns : []);
185
+ for (const id of Array.isArray(data.order) ? data.order : []) {
186
+ if (typeof id === "string" && Object.hasOwn(characters, id) && !deleted.has(id)) add("character", id);
187
+ }
188
+ }
189
+ if (instance.templateId === "ssobig.clues") {
190
+ for (const clue of Array.isArray(instance.data?.clues) ? instance.data.clues : []) {
191
+ add("clue", clue?.id);
192
+ }
193
+ }
194
+ }
195
+ return references;
196
+ }
197
+
198
+ function createInvestigationBoardLayoutPlan(validated, selector, spec, actorEmail, options = {}) {
199
+ const instance = findInstance(validated, selector || "investigation-board");
200
+ if (instance.templateId !== "ssobig.investigation-board") {
201
+ throw cliError("E_VALIDATION", "결정론적 추리 보드 배치는 ssobig.investigation-board Component에만 사용할 수 있습니다.");
202
+ }
203
+ const layout = layoutInvestigationBoard(spec, investigationBoardReferences(validated));
204
+ const patch = [
205
+ { op: "test", path: "/viewport", value: clone(instance.data.viewport) },
206
+ { op: "test", path: "/nodes", value: clone(instance.data.nodes) },
207
+ { op: "test", path: "/edges", value: clone(instance.data.edges) },
208
+ { op: "replace", path: "/viewport", value: clone(layout.data.viewport) },
209
+ { op: "replace", path: "/nodes", value: clone(layout.data.nodes) },
210
+ { op: "replace", path: "/edges", value: clone(layout.data.edges) }
211
+ ];
212
+ const componentPlan = createComponentPlan(validated, instance.instanceId, patch, actorEmail, {
213
+ ...options,
214
+ checkpointMessage: options.checkpointMessage || "CLI: 추리 보드 결정론적 초안 배치"
215
+ });
216
+ const plan = clone(componentPlan);
217
+ delete plan.digest;
218
+ plan.layout = {
219
+ schemaVersion: layout.spec.schemaVersion,
220
+ algorithmVersion: layout.review.algorithmVersion,
221
+ specHash: layout.specHash,
222
+ spec: clone(layout.spec),
223
+ review: clone(layout.review)
224
+ };
225
+ return finalizePlan(plan);
226
+ }
227
+
228
+ function createAssetPlan(validated, operation, assetId, actorEmail, options = {}) {
229
+ const assetInstance = componentContract.assetInstance(validated.instances);
230
+ const manifest = componentContract.assetManifest(validated.instances);
231
+ const normalizedOperation = String(operation || "");
232
+ if (!["asset.upload", "asset.delete"].includes(normalizedOperation)) throw cliError("E_USAGE", `지원하지 않는 에셋 plan입니다: ${normalizedOperation}`);
233
+ const previousEntry = manifest.assets?.[assetId] || null;
234
+ if (normalizedOperation === "asset.delete" && (!previousEntry || previousEntry.deleted === true)) {
235
+ throw cliError("E_ASSET_NOT_FOUND", `연결된 에셋을 찾을 수 없습니다: ${assetId}`);
236
+ }
237
+ const plan = {
238
+ ...basePlan(validated, actorEmail, normalizedOperation, options),
239
+ checkpointMessage: checkpointMessage(
240
+ options.checkpointMessage,
241
+ normalizedOperation === "asset.upload"
242
+ ? `CLI: 에셋 ${assetId} 업로드`
243
+ : `CLI: 에셋 ${assetId} 연결 해제`
244
+ ),
245
+ target: {
246
+ instanceId: assetInstance.instanceId,
247
+ templateId: assetInstance.templateId,
248
+ expectedRevision: assetInstance.revision,
249
+ assetId: String(assetId)
250
+ },
251
+ operationId: String(options.operationId || crypto.randomUUID()),
252
+ beforeManifestHash: sha256(manifest),
253
+ previousEntry: clone(previousEntry)
254
+ };
255
+ if (normalizedOperation === "asset.upload") plan.file = clone(options.file);
256
+ return finalizePlan(plan);
257
+ }
258
+
259
+ function normalizeCheckpoint(row, expected = {}) {
260
+ if (!isObject(row)
261
+ || !UUID_PATTERN.test(String(row.checkpoint_id || ""))
262
+ || !UUID_PATTERN.test(String(row.project_id || ""))
263
+ || !Number.isSafeInteger(Number(row.version_number))
264
+ || Number(row.version_number) < 1
265
+ || !Number.isSafeInteger(Number(row.checkpoint_number))
266
+ || Number(row.checkpoint_number) < 1
267
+ || !/^sha256:[0-9a-f]{64}$/.test(String(row.manifest_hash || ""))) {
268
+ throw cliError("E_CHECKPOINT_NOT_FOUND", "체크포인트 identity를 확인할 수 없습니다.");
269
+ }
270
+ if ((expected.projectId && String(row.project_id) !== String(expected.projectId))
271
+ || (expected.versionNumber && Number(row.version_number) !== Number(expected.versionNumber))) {
272
+ throw cliError("E_CHECKPOINT_NOT_FOUND", "다른 작품 workspace의 체크포인트는 사용할 수 없습니다.");
273
+ }
274
+ return Object.freeze({
275
+ id: String(row.checkpoint_id),
276
+ projectId: String(row.project_id),
277
+ versionNumber: Number(row.version_number),
278
+ number: Number(row.checkpoint_number),
279
+ parentId: row.parent_checkpoint_id ? String(row.parent_checkpoint_id) : null,
280
+ restoredFromId: row.restored_from_checkpoint_id ? String(row.restored_from_checkpoint_id) : null,
281
+ operationId: row.operation_id ? String(row.operation_id) : null,
282
+ message: String(row.message || ""),
283
+ source: String(row.source || ""),
284
+ componentSetRevision: Number(row.component_set_revision),
285
+ componentChecksum: String(row.component_checksum || ""),
286
+ manifestHash: String(row.manifest_hash),
287
+ changedInstanceIds: Array.isArray(row.changed_instance_ids) ? row.changed_instance_ids.map(String) : [],
288
+ createdByEmail: String(row.created_by_email || ""),
289
+ createdAt: String(row.created_at || ""),
290
+ ...(typeof row.is_head === "boolean" ? { isHead: row.is_head } : {}),
291
+ ...(typeof row.latest_changed === "boolean" ? { latestChanged: row.latest_changed } : {})
292
+ });
293
+ }
294
+
295
+ function createCheckpointPlan(validated, operation, message, actorEmail, options = {}) {
296
+ if (!["checkpoint.create", "checkpoint.restore"].includes(operation)) {
297
+ throw cliError("E_USAGE", `지원하지 않는 체크포인트 plan입니다: ${String(operation || "")}`);
298
+ }
299
+ const plan = {
300
+ ...basePlan(validated, actorEmail, operation, options),
301
+ operationId: String(options.operationId || crypto.randomUUID()),
302
+ checkpointMessage: checkpointMessage(message, "")
303
+ };
304
+ if (operation === "checkpoint.restore") {
305
+ const target = normalizeCheckpoint(options.target, {
306
+ projectId: validated.project.id,
307
+ versionNumber: validated.versionNumber
308
+ });
309
+ plan.target = {
310
+ checkpointId: target.id,
311
+ checkpointNumber: target.number,
312
+ manifestHash: target.manifestHash,
313
+ message: target.message,
314
+ source: target.source,
315
+ createdAt: target.createdAt
316
+ };
317
+ }
318
+ return finalizePlan(plan);
319
+ }
320
+
321
+ function projectStatusPair(operation) {
322
+ if (operation === "project.archive") return { expectedStatus: "active", targetStatus: "archived" };
323
+ if (operation === "project.restore") return { expectedStatus: "archived", targetStatus: "active" };
324
+ throw cliError("E_USAGE", `지원하지 않는 작품 상태 plan입니다: ${String(operation || "")}`);
325
+ }
326
+
327
+ function createProjectStatusPlan(project, operation, actorEmail, options = {}) {
328
+ if (!isObject(project)) throw cliError("E_PROJECT_NOT_FOUND", "작품을 찾을 수 없습니다.");
329
+ try { projectRuntime.resolveProject(project); }
330
+ catch (error) { throw cliError("E_MIGRATION_REQUIRED", error.message, { field: error.field || "" }, error); }
331
+ const pair = projectStatusPair(operation);
332
+ if (String(project.status || "") !== pair.expectedStatus) {
333
+ throw cliError("E_CONFLICT", `${pair.expectedStatus} 상태의 작품만 이 작업을 계획할 수 있습니다.`);
334
+ }
335
+ const expectedUpdatedAt = String(project.updated_at || "");
336
+ if (!expectedUpdatedAt || Number.isNaN(new Date(expectedUpdatedAt).getTime())) {
337
+ throw cliError("E_MIGRATION_REQUIRED", "작품 updated_at을 확인할 수 없습니다.");
338
+ }
339
+ return finalizePlan({
340
+ schemaVersion: PLAN_SCHEMA_VERSION,
341
+ toolVersion: TOOL_VERSION,
342
+ planId: String(options.planId || crypto.randomUUID()),
343
+ createdAt: String(options.createdAt || new Date().toISOString()),
344
+ actorEmail: String(actorEmail || "").toLowerCase(),
345
+ operation,
346
+ project: {
347
+ id: String(project.id),
348
+ slug: String(project.slug),
349
+ storageNamespace: String(project.storage_namespace),
350
+ authorName: String(project.author_name || "")
351
+ },
352
+ target: { ...pair, expectedUpdatedAt }
353
+ });
354
+ }
355
+
356
+ function createCatalogDemoPlan(project, enabled, actorEmail, options = {}) {
357
+ if (!isObject(project)) throw cliError("E_PROJECT_NOT_FOUND", "작품을 찾을 수 없습니다.");
358
+ try { projectRuntime.resolveProject(project); }
359
+ catch (error) { throw cliError("E_MIGRATION_REQUIRED", error.message, { field: error.field || "" }, error); }
360
+ const expectedUpdatedAt = String(project.updated_at || "");
361
+ if (!expectedUpdatedAt || Number.isNaN(new Date(expectedUpdatedAt).getTime())) {
362
+ throw cliError("E_MIGRATION_REQUIRED", "작품 updated_at을 확인할 수 없습니다.");
363
+ }
364
+ if (typeof enabled !== "boolean" || typeof project.is_catalog_demo !== "boolean") {
365
+ throw cliError("E_MIGRATION_REQUIRED", "공용 데모 상태를 확인할 수 없습니다.");
366
+ }
367
+ if (project.is_catalog_demo === enabled) throw cliError("E_NO_CHANGES", `공용 데모 상태가 이미 ${enabled ? "사용" : "사용 안 함"}입니다.`);
368
+ return finalizePlan({
369
+ schemaVersion: PLAN_SCHEMA_VERSION,
370
+ toolVersion: TOOL_VERSION,
371
+ planId: String(options.planId || crypto.randomUUID()),
372
+ createdAt: String(options.createdAt || new Date().toISOString()),
373
+ actorEmail: String(actorEmail || "").toLowerCase(),
374
+ operation: "project.catalog-demo",
375
+ project: {
376
+ id: String(project.id),
377
+ slug: String(project.slug),
378
+ storageNamespace: String(project.storage_namespace),
379
+ authorName: String(project.author_name || "")
380
+ },
381
+ target: {
382
+ expectedIsCatalogDemo: project.is_catalog_demo,
383
+ enabled,
384
+ expectedStatus: String(project.status || ""),
385
+ expectedUpdatedAt
386
+ }
387
+ });
388
+ }
389
+
390
+ function projectCreationTarget(input = {}) {
391
+ const slug = String(input.slug || "").trim();
392
+ const title = String(input.title || "").trim();
393
+ const authorName = String(input.authorName || "").trim();
394
+ if (!projectRuntime.SLUG_PATTERN.test(slug) || slug.length > 80) {
395
+ throw cliError("E_VALIDATION", "작품 slug는 80자 이하의 영문 소문자 kebab-case여야 합니다.");
396
+ }
397
+ if (!title || title.length > 120) {
398
+ throw cliError("E_VALIDATION", "작품명은 비어 있지 않은 120자 이하 문자열이어야 합니다.");
399
+ }
400
+ if (!authorName || authorName.length > 80) {
401
+ throw cliError("E_VALIDATION", "작가명은 비어 있지 않은 80자 이하 문자열이어야 합니다.");
402
+ }
403
+ return Object.freeze({
404
+ slug,
405
+ title,
406
+ authorName,
407
+ expectedStatus: "absent",
408
+ scaffold: "writer-scaffold-v1"
409
+ });
410
+ }
411
+
412
+ function createProjectCreationPlan(input, actorEmail, options = {}) {
413
+ return finalizePlan({
414
+ schemaVersion: PLAN_SCHEMA_VERSION,
415
+ toolVersion: TOOL_VERSION,
416
+ planId: String(options.planId || crypto.randomUUID()),
417
+ createdAt: String(options.createdAt || new Date().toISOString()),
418
+ actorEmail: String(actorEmail || "").toLowerCase(),
419
+ operation: "project.create",
420
+ target: projectCreationTarget(input)
421
+ });
422
+ }
423
+
424
+ function createProjectImportPlan(input, actorEmail, options = {}) {
425
+ const target = projectCreationTarget(input);
426
+ const components = normalizeImportComponents(input.components);
427
+ importCompositionRows(components);
428
+ if (!isObject(input.source) || typeof input.source.sourceHash !== "string" || !/^[0-9a-f]{64}$/.test(input.source.sourceHash)) {
429
+ throw cliError("E_VALIDATION", "Import source hash가 올바르지 않습니다.");
430
+ }
431
+ if (!isObject(input.coverage) || Number(input.coverage.unexplained) !== 0) {
432
+ throw cliError("E_VALIDATION", "Import coverage에 설명되지 않은 항목이 남아 있습니다.");
433
+ }
434
+ if (!Array.isArray(input.assets)) throw cliError("E_VALIDATION", "Import asset 목록이 필요합니다.");
435
+ return finalizePlan({
436
+ schemaVersion: PLAN_SCHEMA_VERSION,
437
+ toolVersion: TOOL_VERSION,
438
+ planId: String(options.planId || crypto.randomUUID()),
439
+ createdAt: String(options.createdAt || new Date().toISOString()),
440
+ actorEmail: String(actorEmail || "").toLowerCase(),
441
+ operation: "project.import",
442
+ target: {
443
+ slug: target.slug,
444
+ title: target.title,
445
+ authorName: target.authorName,
446
+ expectedStatus: target.expectedStatus,
447
+ importProfile: IMPORT_PROFILE
448
+ },
449
+ source: clone(input.source),
450
+ coverage: clone(input.coverage),
451
+ components: clone(components),
452
+ componentDataHash: sha256(components),
453
+ assets: clone(input.assets)
454
+ });
455
+ }
456
+
457
+ function verifyPlan(plan) {
458
+ if (!isObject(plan) || plan.schemaVersion !== PLAN_SCHEMA_VERSION || plan.toolVersion !== TOOL_VERSION) {
459
+ throw cliError("E_INVALID_PLAN", "지원하지 않는 Writer CLI plan 형식입니다.");
460
+ }
461
+ if (!UUID_PATTERN.test(String(plan.planId || "")) || typeof plan.digest !== "string" || plan.digest !== planDigest(plan)) {
462
+ throw cliError("E_INVALID_PLAN", "plan 식별자 또는 digest가 올바르지 않습니다.");
463
+ }
464
+ if (!["component.patch", "asset.upload", "asset.delete", "project.create", "project.import", "project.archive", "project.restore", "project.catalog-demo", "checkpoint.create", "checkpoint.restore"].includes(plan.operation)) throw cliError("E_INVALID_PLAN", "지원하지 않는 plan operation입니다.");
465
+ if (["component.patch", "asset.upload", "asset.delete", "checkpoint.create", "checkpoint.restore"].includes(plan.operation)) {
466
+ let normalizedCheckpointMessage = "";
467
+ try { normalizedCheckpointMessage = checkpointMessage(plan.checkpointMessage, ""); }
468
+ catch (error) { throw cliError("E_INVALID_PLAN", "체크포인트 메시지가 올바르지 않습니다.", null, error); }
469
+ if (normalizedCheckpointMessage !== plan.checkpointMessage) {
470
+ throw cliError("E_INVALID_PLAN", "체크포인트 메시지가 올바르지 않습니다.");
471
+ }
472
+ }
473
+ if (plan.operation.startsWith("asset.") && !UUID_PATTERN.test(String(plan.operationId || ""))) {
474
+ throw cliError("E_INVALID_PLAN", "에셋 plan의 operationId가 올바른 UUID가 아닙니다.");
475
+ }
476
+ if (plan.operation.startsWith("checkpoint.")) {
477
+ if (!UUID_PATTERN.test(String(plan.operationId || ""))
478
+ || !UUID_PATTERN.test(String(plan.project?.id || ""))
479
+ || !Number.isSafeInteger(Number(plan.versionNumber))
480
+ || Number(plan.versionNumber) < 1
481
+ || !Number.isSafeInteger(Number(plan.readiness?.componentSetRevision))
482
+ || Number(plan.readiness.componentSetRevision) < 1
483
+ || !String(plan.readiness?.componentChecksum || "")) {
484
+ throw cliError("E_INVALID_PLAN", "체크포인트 plan의 workspace identity가 올바르지 않습니다.");
485
+ }
486
+ if (plan.operation === "checkpoint.create" && Object.hasOwn(plan, "target")) {
487
+ throw cliError("E_INVALID_PLAN", "체크포인트 생성 plan에 복원 대상이 포함될 수 없습니다.");
488
+ }
489
+ if (plan.operation === "checkpoint.restore" && (!UUID_PATTERN.test(String(plan.target?.checkpointId || ""))
490
+ || !Number.isSafeInteger(Number(plan.target?.checkpointNumber))
491
+ || Number(plan.target.checkpointNumber) < 1
492
+ || !/^sha256:[0-9a-f]{64}$/.test(String(plan.target?.manifestHash || "")))) {
493
+ throw cliError("E_INVALID_PLAN", "체크포인트 복원 대상이 올바르지 않습니다.");
494
+ }
495
+ }
496
+ if (plan.operation === "project.create") {
497
+ const target = projectCreationTarget(plan.target);
498
+ if (String(plan.target?.slug || "") !== target.slug
499
+ || String(plan.target?.title || "") !== target.title
500
+ || String(plan.target?.authorName || "") !== target.authorName
501
+ || String(plan.target?.expectedStatus || "") !== target.expectedStatus
502
+ || String(plan.target?.scaffold || "") !== target.scaffold) {
503
+ throw cliError("E_INVALID_PLAN", "새 작품 plan의 입력값이 올바르지 않습니다.");
504
+ }
505
+ } else if (plan.operation === "project.import") {
506
+ const target = projectCreationTarget(plan.target);
507
+ const components = normalizeImportComponents(plan.components);
508
+ importCompositionRows(components);
509
+ if (String(plan.target?.slug || "") !== target.slug
510
+ || String(plan.target?.title || "") !== target.title
511
+ || String(plan.target?.authorName || "") !== target.authorName
512
+ || String(plan.target?.expectedStatus || "") !== target.expectedStatus
513
+ || String(plan.target?.importProfile || "") !== IMPORT_PROFILE
514
+ || Object.hasOwn(plan.target || {}, "scaffold")
515
+ || !equalJson(components, plan.components)
516
+ || String(plan.componentDataHash || "") !== sha256(components)
517
+ || !isObject(plan.source)
518
+ || !/^[0-9a-f]{64}$/.test(String(plan.source.sourceHash || ""))
519
+ || !isObject(plan.coverage)
520
+ || Number(plan.coverage.unexplained) !== 0
521
+ || !Array.isArray(plan.assets)) {
522
+ throw cliError("E_INVALID_PLAN", "작품 import plan의 입력값이 올바르지 않습니다.");
523
+ }
524
+ } else if (plan.operation === "project.catalog-demo") {
525
+ if (!UUID_PATTERN.test(String(plan.project?.id || ""))
526
+ || typeof plan.target?.expectedIsCatalogDemo !== "boolean"
527
+ || typeof plan.target?.enabled !== "boolean"
528
+ || plan.target.expectedIsCatalogDemo === plan.target.enabled
529
+ || String(plan.target?.expectedStatus || "") !== "active"
530
+ || Number.isNaN(new Date(String(plan.target?.expectedUpdatedAt || "")).getTime())) {
531
+ throw cliError("E_INVALID_PLAN", "공용 데모 plan의 대상 identity가 올바르지 않습니다.");
532
+ }
533
+ } else if (plan.operation.startsWith("project.")) {
534
+ const pair = projectStatusPair(plan.operation);
535
+ if (!UUID_PATTERN.test(String(plan.project?.id || ""))
536
+ || String(plan.target?.expectedStatus || "") !== pair.expectedStatus
537
+ || String(plan.target?.targetStatus || "") !== pair.targetStatus
538
+ || Number.isNaN(new Date(String(plan.target?.expectedUpdatedAt || "")).getTime())) {
539
+ throw cliError("E_INVALID_PLAN", "작품 상태 plan의 대상 identity가 올바르지 않습니다.");
540
+ }
541
+ }
542
+ return plan;
543
+ }
544
+
545
+ function validateProjectImportPlanForApply(plan, existingProject, actorEmail) {
546
+ verifyPlan(plan);
547
+ if (plan.operation !== "project.import") throw cliError("E_INVALID_PLAN", "작품 import plan이 아닙니다.");
548
+ if (String(plan.actorEmail || "").toLowerCase() !== String(actorEmail || "").toLowerCase()) {
549
+ throw cliError("E_PLAN_ACTOR_MISMATCH", "plan을 만든 계정과 적용 계정이 다릅니다. 현재 계정으로 plan을 다시 만들어 주세요.");
550
+ }
551
+ if (existingProject) throw cliError("E_CONFLICT", `이미 사용 중인 작품 slug입니다: ${plan.target.slug}`);
552
+ const target = projectCreationTarget(plan.target);
553
+ const components = normalizeImportComponents(plan.components);
554
+ importCompositionRows(components);
555
+ return Object.freeze({ target, components, provenance: sourceProvenance(plan.source, plan.coverage) });
556
+ }
557
+
558
+ function validateProjectCreationPlanForApply(plan, existingProject, actorEmail) {
559
+ verifyPlan(plan);
560
+ if (plan.operation !== "project.create") throw cliError("E_INVALID_PLAN", "새 작품 생성 plan이 아닙니다.");
561
+ if (String(plan.actorEmail || "").toLowerCase() !== String(actorEmail || "").toLowerCase()) {
562
+ throw cliError("E_PLAN_ACTOR_MISMATCH", "plan을 만든 계정과 적용 계정이 다릅니다. 현재 계정으로 plan을 다시 만들어 주세요.");
563
+ }
564
+ if (existingProject) throw cliError("E_CONFLICT", `이미 사용 중인 작품 slug입니다: ${plan.target.slug}`);
565
+ return projectCreationTarget(plan.target);
566
+ }
567
+
568
+ function validateProjectStatusPlanForApply(plan, project, actorEmail) {
569
+ verifyPlan(plan);
570
+ if (!plan.operation.startsWith("project.")) throw cliError("E_INVALID_PLAN", "작품 상태 plan이 아닙니다.");
571
+ if (String(plan.actorEmail || "").toLowerCase() !== String(actorEmail || "").toLowerCase()) {
572
+ throw cliError("E_PLAN_ACTOR_MISMATCH", "plan을 만든 계정과 적용 계정이 다릅니다. 현재 계정으로 plan을 다시 만들어 주세요.");
573
+ }
574
+ if (!isObject(project)
575
+ || String(plan.project?.id || "") !== String(project.id || "")
576
+ || String(plan.project?.slug || "") !== String(project.slug || "")
577
+ || String(plan.project?.storageNamespace || "") !== String(project.storage_namespace || "")
578
+ || String(plan.project?.authorName || "") !== String(project.author_name || "")) {
579
+ throw cliError("E_INVALID_PLAN", "plan의 작품 identity가 현재 DB 원본과 일치하지 않습니다.");
580
+ }
581
+ if (String(project.status || "") !== String(plan.target.expectedStatus)
582
+ || String(project.updated_at || "") !== String(plan.target.expectedUpdatedAt)) {
583
+ throw cliError("E_CONFLICT", "plan 생성 후 작품 상태가 변경되었습니다. 새 plan을 만들어 주세요.");
584
+ }
585
+ return project;
586
+ }
587
+
588
+ function validateCatalogDemoPlanForApply(plan, project, actorEmail) {
589
+ verifyPlan(plan);
590
+ if (plan.operation !== "project.catalog-demo") throw cliError("E_INVALID_PLAN", "공용 데모 plan이 아닙니다.");
591
+ if (String(plan.actorEmail || "").toLowerCase() !== String(actorEmail || "").toLowerCase()) {
592
+ throw cliError("E_PLAN_ACTOR_MISMATCH", "plan을 만든 계정과 적용 계정이 다릅니다. 현재 계정으로 plan을 다시 만들어 주세요.");
593
+ }
594
+ if (!isObject(project)
595
+ || String(plan.project?.id || "") !== String(project.id || "")
596
+ || String(plan.project?.slug || "") !== String(project.slug || "")
597
+ || String(plan.project?.storageNamespace || "") !== String(project.storage_namespace || "")
598
+ || String(plan.project?.authorName || "") !== String(project.author_name || "")) {
599
+ throw cliError("E_INVALID_PLAN", "plan의 작품 identity가 현재 DB 원본과 일치하지 않습니다.");
600
+ }
601
+ if (String(project.status || "") !== String(plan.target.expectedStatus)
602
+ || Boolean(project.is_catalog_demo) !== plan.target.expectedIsCatalogDemo
603
+ || String(project.updated_at || "") !== String(plan.target.expectedUpdatedAt)) {
604
+ throw cliError("E_CONFLICT", "plan 생성 후 작품 또는 공용 데모 상태가 변경되었습니다. 새 plan을 만들어 주세요.");
605
+ }
606
+ return project;
607
+ }
608
+
609
+ function assertPlanContext(plan, validated, actorEmail) {
610
+ verifyPlan(plan);
611
+ if (String(plan.actorEmail || "").toLowerCase() !== String(actorEmail || "").toLowerCase()) {
612
+ throw cliError("E_PLAN_ACTOR_MISMATCH", "plan을 만든 계정과 적용 계정이 다릅니다. 현재 계정으로 plan을 다시 만들어 주세요.");
613
+ }
614
+ if (String(plan.project?.id || "") !== String(validated.project.id)
615
+ || String(plan.project?.slug || "") !== String(validated.project.slug)
616
+ || String(plan.project?.storageNamespace || "") !== String(validated.project.storage_namespace)
617
+ || String(plan.project?.authorName || "") !== String(validated.project.author_name || "")
618
+ || Number(plan.versionNumber) !== validated.versionNumber) {
619
+ throw cliError("E_INVALID_PLAN", "plan의 작품 또는 버전 식별자가 현재 DB 원본과 일치하지 않습니다.");
620
+ }
621
+ if (Number(plan.readiness?.componentSetRevision) !== Number(validated.componentSet.revision)
622
+ || String(plan.readiness?.componentChecksum || "") !== String(validated.componentSet.component_checksum || "")) {
623
+ throw cliError("E_CONFLICT", "plan 생성 후 작품 Component 상태가 변경되었습니다. 새 plan을 만들어 주세요.");
624
+ }
625
+ }
626
+
627
+ function validateComponentPlanForApply(plan, validated, actorEmail) {
628
+ assertPlanContext(plan, validated, actorEmail);
629
+ if (plan.operation !== "component.patch") throw cliError("E_INVALID_PLAN", "Component patch plan이 아닙니다.");
630
+ const instance = findInstance(validated, plan.target?.instanceId);
631
+ assertGenericPatchAllowed(instance);
632
+ if (instance.templateId !== plan.target.templateId
633
+ || instance.revision !== Number(plan.target.expectedRevision)) {
634
+ throw cliError("E_CONFLICT", "plan 대상 Component의 identity 또는 revision이 변경되었습니다.");
635
+ }
636
+ if (sha256(instance.data) !== plan.beforeHash || !equalJson(instance.data, plan.beforeData)) throw cliError("E_CONFLICT", "plan 생성 후 Component data가 변경되었습니다.");
637
+ const recomputed = applyPatch(instance.data, plan.patch);
638
+ if (sha256(recomputed) !== plan.afterHash || !equalJson(recomputed, plan.afterData)) throw cliError("E_INVALID_PLAN", "plan의 patch 결과와 afterData가 일치하지 않습니다.");
639
+ if (Object.hasOwn(plan, "layout")) {
640
+ let layout;
641
+ try {
642
+ layout = layoutInvestigationBoard(plan.layout?.spec, investigationBoardReferences(validated));
643
+ } catch (error) {
644
+ if (error.code === "E_VALIDATION") {
645
+ throw cliError("E_CONFLICT", "plan 생성 후 추리 보드의 캐릭터·단서 참조 조건이 변경되었습니다. 새 plan을 만들어 주세요.", null, error);
646
+ }
647
+ throw error;
648
+ }
649
+ if (plan.layout.schemaVersion !== layout.spec.schemaVersion
650
+ || plan.layout.algorithmVersion !== layout.review.algorithmVersion
651
+ || plan.layout.specHash !== layout.specHash
652
+ || !equalJson(plan.layout.spec, layout.spec)
653
+ || !equalJson(plan.layout.review, layout.review)
654
+ || !equalJson(plan.afterData, layout.data)) {
655
+ throw cliError("E_INVALID_PLAN", "추리 보드 layout plan의 spec, review 또는 생성 결과가 일치하지 않습니다.");
656
+ }
657
+ }
658
+ const nextInstance = validateHypotheticalData(validated, instance, recomputed);
659
+ return { instance, nextInstance, payload: componentContract.instancePayload(nextInstance) };
660
+ }
661
+
662
+ function validateAssetPlanForApply(plan, validated, actorEmail) {
663
+ assertPlanContext(plan, validated, actorEmail);
664
+ if (!["asset.upload", "asset.delete"].includes(plan.operation)) throw cliError("E_INVALID_PLAN", "에셋 plan이 아닙니다.");
665
+ const instance = componentContract.assetInstance(validated.instances);
666
+ const manifest = componentContract.assetManifest(validated.instances);
667
+ if (instance.instanceId !== plan.target?.instanceId
668
+ || instance.templateId !== plan.target?.templateId
669
+ || instance.revision !== Number(plan.target?.expectedRevision)
670
+ || sha256(manifest) !== plan.beforeManifestHash
671
+ || !equalJson(manifest.assets?.[String(plan.target?.assetId)] || null, plan.previousEntry ?? null)) {
672
+ throw cliError("E_CONFLICT", "plan 생성 후 에셋 manifest가 변경되었습니다. 새 plan을 만들어 주세요.");
673
+ }
674
+ return { instance, manifest };
675
+ }
676
+
677
+ function validateCheckpointPlanForApply(plan, validated, actorEmail, targetRow = null) {
678
+ assertPlanContext(plan, validated, actorEmail);
679
+ if (!["checkpoint.create", "checkpoint.restore"].includes(plan.operation)) {
680
+ throw cliError("E_INVALID_PLAN", "체크포인트 plan이 아닙니다.");
681
+ }
682
+ if (plan.operation === "checkpoint.restore") {
683
+ const target = normalizeCheckpoint(targetRow, {
684
+ projectId: validated.project.id,
685
+ versionNumber: validated.versionNumber
686
+ });
687
+ if (target.id !== String(plan.target.checkpointId)
688
+ || target.number !== Number(plan.target.checkpointNumber)
689
+ || target.manifestHash !== String(plan.target.manifestHash)) {
690
+ throw cliError("E_CONFLICT", "복원 대상 체크포인트가 plan 생성 후 변경되었습니다.");
691
+ }
692
+ return target;
693
+ }
694
+ return null;
695
+ }
696
+
697
+ module.exports = Object.freeze({
698
+ PLAN_SCHEMA_VERSION,
699
+ TOOL_VERSION,
700
+ UUID_PATTERN,
701
+ PROJECT_COLUMNS,
702
+ VERSION_COLUMNS,
703
+ SET_COLUMNS,
704
+ INSTANCE_COLUMNS,
705
+ VIEW_COLUMNS,
706
+ BINDING_COLUMNS,
707
+ CHECKPOINT_COLUMNS,
708
+ CHECKPOINT_ENTRY_COLUMNS,
709
+ componentContract,
710
+ validateLoadedVersion,
711
+ findInstance,
712
+ createComponentPlan,
713
+ investigationBoardReferences,
714
+ createInvestigationBoardLayoutPlan,
715
+ createAssetPlan,
716
+ createProjectCreationPlan,
717
+ createProjectImportPlan,
718
+ createProjectStatusPlan,
719
+ createCatalogDemoPlan,
720
+ normalizeCheckpoint,
721
+ createCheckpointPlan,
722
+ verifyPlan,
723
+ validateProjectCreationPlanForApply,
724
+ validateProjectImportPlanForApply,
725
+ validateProjectStatusPlanForApply,
726
+ validateCatalogDemoPlanForApply,
727
+ validateComponentPlanForApply,
728
+ validateAssetPlanForApply,
729
+ validateCheckpointPlanForApply,
730
+ validationError
731
+ });