@roottale/cms-renderer-next 0.40.0 → 0.41.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.
@@ -0,0 +1,90 @@
1
+ import { Severity, MEDICAL_LINT_RULESET } from '@roottale/audit-medical';
2
+
3
+ interface SectionLock {
4
+ move?: boolean;
5
+ remove?: boolean;
6
+ structure?: boolean;
7
+ }
8
+ type SectionLockViolationCode = "locked_section_inserted" | "locked_section_removed" | "locked_section_moved" | "locked_section_lock_changed" | "locked_section_structure_changed";
9
+ interface SectionLockViolation {
10
+ code: SectionLockViolationCode;
11
+ sectionId: string;
12
+ sectionType: string;
13
+ field: string;
14
+ }
15
+ declare function isSectionContentPropPath(sectionType: string, path: readonly (string | number)[]): boolean;
16
+ declare function findSectionLockViolations(beforeDoc: unknown, afterDoc: unknown): SectionLockViolation[];
17
+ declare function formatSectionLockViolations(violations: readonly SectionLockViolation[]): string;
18
+
19
+ declare const PUBLISH_GATE_RULESET = "publish-gate-2026-07-02";
20
+ type PublishGateError = {
21
+ readonly code: string;
22
+ readonly message: string;
23
+ readonly ruleId?: string;
24
+ readonly severity?: Severity;
25
+ readonly snippet?: string;
26
+ };
27
+ type PublishGateRuleset = {
28
+ readonly publish: typeof PUBLISH_GATE_RULESET;
29
+ readonly medical?: typeof MEDICAL_LINT_RULESET;
30
+ };
31
+ type PublishGateResult = {
32
+ readonly errors: readonly PublishGateError[];
33
+ readonly ruleset: PublishGateRuleset;
34
+ };
35
+ type PublishGateDoc = {
36
+ readonly title?: string | null;
37
+ readonly excerpt?: string | null;
38
+ readonly bodyJson: Record<string, unknown>;
39
+ readonly metaJson?: Record<string, unknown> | null;
40
+ };
41
+ /**
42
+ * h1 모델 (W6-1) — 문서 타입별로 h1 소유자가 다르다:
43
+ * - `"page"`: 본문(hero 섹션 또는 heading level=1)이 유일 h1. 렌더러가 `doc.title`
44
+ * 을 h1 으로 그리지 않으므로 body h1Labels.length !== 1 → 차단.
45
+ * - `"post"`: 렌더러(`RootTaleBlogPost`)가 `post.title` 을 h1 으로 그린다 —
46
+ * 본문에 h1 이 있으면 실렌더 h1=2 가 되므로 body h1Labels.length !== 0 → 차단.
47
+ * `documentType` 은 fail-closed 로 필수(optional 금지) — 신규 호출부가 실수로
48
+ * type 분기를 빼먹으면 컴파일 에러가 나야 한다.
49
+ */
50
+ type PublishGateOptions = {
51
+ readonly vertical: string;
52
+ readonly complianceFlags?: readonly string[];
53
+ readonly documentType: "page" | "post";
54
+ };
55
+ type PublishOverrideInput = {
56
+ readonly reason: string;
57
+ readonly userId: string;
58
+ readonly at: string;
59
+ };
60
+ declare function assertPublishable(doc: PublishGateDoc, options: PublishGateOptions): PublishGateResult;
61
+ declare function writePublishGateMeta(metaJson: Record<string, unknown> | null | undefined, result: PublishGateResult): Record<string, unknown>;
62
+ declare function writePublishOverrideMeta(metaJson: Record<string, unknown> | null | undefined, override: PublishOverrideInput): Record<string, unknown>;
63
+ declare function isComplianceBlocked(metaJson: Record<string, unknown> | null | undefined): boolean;
64
+ declare function hasPublishOverride(metaJson: Record<string, unknown> | null | undefined): boolean;
65
+
66
+ /** 검증 실패 1건. path = 문서 내 섹션 노드 위치(0-base), field = zod issue 경로. */
67
+ interface SectionValidationIssue {
68
+ /** 문서 최상위 content 배열에서 섹션 노드의 index. */
69
+ index: number;
70
+ sectionType: string;
71
+ /** zod issue 경로 (예: "props.title"). */
72
+ field: string;
73
+ message: string;
74
+ }
75
+ interface SectionValidationResult {
76
+ valid: boolean;
77
+ issues: SectionValidationIssue[];
78
+ }
79
+ /**
80
+ * 문서 내 모든 섹션 노드를 SectionSchema 로 검증. 섹션 노드가 없으면 valid=true
81
+ * (no-op). 하나라도 스키마 위반이면 valid=false + issue 목록.
82
+ *
83
+ * posts.ts(create/update) 가 이 결과를 보고 저장을 거부한다. rawHtml 생성 금지 —
84
+ * 실패는 저장 거부일 뿐 노드를 변형하지 않는다.
85
+ */
86
+ declare function validateSectionNodes(doc: unknown): SectionValidationResult;
87
+ /** issue 목록을 사람이 읽는 한 줄 메시지로 (저장 거부 오류 문구용). */
88
+ declare function formatSectionIssues(issues: SectionValidationIssue[]): string;
89
+
90
+ export { PUBLISH_GATE_RULESET, type PublishGateDoc, type PublishGateError, type PublishGateOptions, type PublishGateResult, type PublishGateRuleset, type PublishOverrideInput, type SectionLock, type SectionLockViolation, type SectionLockViolationCode, type SectionValidationIssue, type SectionValidationResult, assertPublishable, findSectionLockViolations, formatSectionIssues, formatSectionLockViolations, hasPublishOverride, isComplianceBlocked, isSectionContentPropPath, validateSectionNodes, writePublishGateMeta, writePublishOverrideMeta };
@@ -0,0 +1,415 @@
1
+ import {
2
+ SectionSchema
3
+ } from "./chunk-IOYBEH6E.js";
4
+
5
+ // src/templates/section-locks.ts
6
+ var CONTENT_PROP_PATHS = {
7
+ hero: [
8
+ ["eyebrow"],
9
+ ["title"],
10
+ ["description"],
11
+ ["imageSrc"],
12
+ ["imageAlt"],
13
+ ["cta", "label"],
14
+ ["cta", "href"]
15
+ ],
16
+ "rich-text": [["heading"], ["body"]],
17
+ services: [
18
+ ["heading"],
19
+ ["description"],
20
+ ["items", Number.NaN, "title"],
21
+ ["items", Number.NaN, "description"],
22
+ ["items", Number.NaN, "href"]
23
+ ],
24
+ faq: [
25
+ ["heading"],
26
+ ["items", Number.NaN, "question"],
27
+ ["items", Number.NaN, "answer"]
28
+ ],
29
+ "cta-banner": [["title"], ["description"], ["cta", "label"], ["cta", "href"]],
30
+ "blog-list": [["heading"], ["emptyMessage"]],
31
+ "lead-form": [["heading"], ["description"], ["submitLabel"]],
32
+ trust: [
33
+ ["heading"],
34
+ ["metrics", Number.NaN, "value"],
35
+ ["metrics", Number.NaN, "unit"],
36
+ ["metrics", Number.NaN, "label"],
37
+ ["metrics", Number.NaN, "source"],
38
+ ["credentials", Number.NaN, "name"],
39
+ ["credentials", Number.NaN, "issuer"],
40
+ ["credentials", Number.NaN, "kind"],
41
+ ["credentials", Number.NaN, "image"],
42
+ ["logos", Number.NaN, "src"],
43
+ ["logos", Number.NaN, "alt"],
44
+ ["logos", Number.NaN, "href"],
45
+ ["disclaimer"]
46
+ ],
47
+ contact: [
48
+ ["heading"],
49
+ ["form", "enabled"],
50
+ ["phone"],
51
+ ["address"],
52
+ ["hours", Number.NaN, "day"],
53
+ ["hours", Number.NaN, "open"],
54
+ ["hours", Number.NaN, "close"],
55
+ ["hours", Number.NaN, "note"],
56
+ ["mapEmbedUrl"]
57
+ ]
58
+ };
59
+ function readSectionLock(value) {
60
+ if (!value || typeof value !== "object") return {};
61
+ const input = value;
62
+ return {
63
+ ...typeof input.move === "boolean" ? { move: input.move } : {},
64
+ ...typeof input.remove === "boolean" ? { remove: input.remove } : {},
65
+ ...typeof input.structure === "boolean" ? { structure: input.structure } : {}
66
+ };
67
+ }
68
+ function isSectionLocked(lock) {
69
+ return lock.move === true || lock.remove === true || lock.structure === true;
70
+ }
71
+ function valueEquals(left, right) {
72
+ return JSON.stringify(left) === JSON.stringify(right);
73
+ }
74
+ function isRecord(value) {
75
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
76
+ }
77
+ function collectChangedPaths(before, after, prefix = []) {
78
+ if (valueEquals(before, after)) return [];
79
+ if (Array.isArray(before) && Array.isArray(after)) {
80
+ if (before.length !== after.length) return [prefix];
81
+ return before.flatMap(
82
+ (item, index) => collectChangedPaths(item, after[index], [...prefix, index])
83
+ );
84
+ }
85
+ if (isRecord(before) && isRecord(after)) {
86
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
87
+ return [...keys].flatMap(
88
+ (key) => collectChangedPaths(before[key], after[key], [...prefix, key])
89
+ );
90
+ }
91
+ return [prefix];
92
+ }
93
+ function contentPathMatches(pattern, path) {
94
+ if (pattern.length !== path.length) return false;
95
+ return pattern.every((segment, index) => {
96
+ const actual = path[index];
97
+ return Number.isNaN(segment) ? typeof actual === "number" : segment === actual;
98
+ });
99
+ }
100
+ function isSectionContentPropPath(sectionType, path) {
101
+ const patterns = CONTENT_PROP_PATHS[sectionType] ?? [];
102
+ return patterns.some((pattern) => contentPathMatches(pattern, path));
103
+ }
104
+ function collectSectionSnapshots(doc) {
105
+ const content = doc && typeof doc === "object" ? doc.content : void 0;
106
+ if (!Array.isArray(content)) return [];
107
+ return content.flatMap((node, index) => {
108
+ if (!node || typeof node !== "object") return [];
109
+ const tiptapNode = node;
110
+ if (tiptapNode.type !== "section") return [];
111
+ const attrs = tiptapNode.attrs ?? {};
112
+ const id = typeof attrs._id === "string" ? attrs._id : "";
113
+ if (!id) return [];
114
+ return [
115
+ {
116
+ id,
117
+ index,
118
+ sectionType: typeof attrs.sectionType === "string" ? attrs.sectionType : "",
119
+ attrs,
120
+ props: attrs.props ?? {},
121
+ lock: readSectionLock(attrs.lock)
122
+ }
123
+ ];
124
+ });
125
+ }
126
+ function findSectionLockViolations(beforeDoc, afterDoc) {
127
+ const before = collectSectionSnapshots(beforeDoc);
128
+ const after = collectSectionSnapshots(afterDoc);
129
+ const afterById = new Map(after.map((section) => [section.id, section]));
130
+ const beforeById = new Map(before.map((section) => [section.id, section]));
131
+ const violations = [];
132
+ for (const section of before) {
133
+ const next = afterById.get(section.id);
134
+ if (!next) {
135
+ if (section.lock.remove === true) {
136
+ violations.push({
137
+ code: "locked_section_removed",
138
+ sectionId: section.id,
139
+ sectionType: section.sectionType,
140
+ field: "(node)"
141
+ });
142
+ }
143
+ continue;
144
+ }
145
+ if (section.lock.move === true && section.index !== next.index) {
146
+ violations.push({
147
+ code: "locked_section_moved",
148
+ sectionId: section.id,
149
+ sectionType: section.sectionType,
150
+ field: "(index)"
151
+ });
152
+ }
153
+ if (!valueEquals(section.lock, next.lock) && (isSectionLocked(section.lock) || isSectionLocked(next.lock))) {
154
+ violations.push({
155
+ code: "locked_section_lock_changed",
156
+ sectionId: section.id,
157
+ sectionType: section.sectionType,
158
+ field: "lock"
159
+ });
160
+ }
161
+ if (section.lock.structure !== true) continue;
162
+ for (const field of ["_id", "sectionType", "variant", "theme"]) {
163
+ if (!valueEquals(section.attrs[field], next.attrs[field])) {
164
+ violations.push({
165
+ code: "locked_section_structure_changed",
166
+ sectionId: section.id,
167
+ sectionType: section.sectionType,
168
+ field
169
+ });
170
+ }
171
+ }
172
+ for (const path of collectChangedPaths(section.props, next.props)) {
173
+ if (!isSectionContentPropPath(section.sectionType, path)) {
174
+ violations.push({
175
+ code: "locked_section_structure_changed",
176
+ sectionId: section.id,
177
+ sectionType: section.sectionType,
178
+ field: `props.${path.join(".") || "(root)"}`
179
+ });
180
+ }
181
+ }
182
+ }
183
+ for (const section of after) {
184
+ if (beforeById.has(section.id) || !isSectionLocked(section.lock)) continue;
185
+ violations.push({
186
+ code: "locked_section_inserted",
187
+ sectionId: section.id,
188
+ sectionType: section.sectionType,
189
+ field: "(node)"
190
+ });
191
+ }
192
+ return violations;
193
+ }
194
+ function formatSectionLockViolations(violations) {
195
+ return violations.map((violation) => {
196
+ const label = `${violation.sectionType || "section"} \uC139\uC158(${violation.sectionId})`;
197
+ if (violation.code === "locked_section_inserted") return `${label} \uCD94\uAC00`;
198
+ if (violation.code === "locked_section_removed") return `${label} \uC0AD\uC81C`;
199
+ if (violation.code === "locked_section_moved") return `${label} \uC774\uB3D9`;
200
+ if (violation.code === "locked_section_lock_changed") {
201
+ return `${label} \uC7A0\uAE08 \uBCC0\uACBD`;
202
+ }
203
+ return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
204
+ }).join(" / ");
205
+ }
206
+
207
+ // src/templates/publish-gate.ts
208
+ import {
209
+ MEDICAL_LINT_RULESET,
210
+ lintMedicalPage
211
+ } from "@roottale/audit-medical";
212
+ var PUBLISH_GATE_RULESET = "publish-gate-2026-07-02";
213
+ function isRecord2(value) {
214
+ return value !== null && typeof value === "object" && !Array.isArray(value);
215
+ }
216
+ function recordOrEmpty(value) {
217
+ return isRecord2(value) ? value : {};
218
+ }
219
+ function stringOrEmpty(value) {
220
+ return typeof value === "string" ? value.trim() : "";
221
+ }
222
+ function collectText(value, out) {
223
+ if (typeof value === "string") {
224
+ out.push(value);
225
+ return;
226
+ }
227
+ if (Array.isArray(value)) {
228
+ for (const item of value) collectText(item, out);
229
+ return;
230
+ }
231
+ if (!isRecord2(value)) return;
232
+ if (typeof value.text === "string") out.push(value.text);
233
+ for (const child of Object.values(value)) collectText(child, out);
234
+ }
235
+ function textFrom(value) {
236
+ const out = [];
237
+ collectText(value, out);
238
+ return out.join("\n");
239
+ }
240
+ function seoDescription(doc) {
241
+ const meta = recordOrEmpty(doc.metaJson);
242
+ const seo = recordOrEmpty(meta.seo);
243
+ return stringOrEmpty(seo.description) || stringOrEmpty(doc.excerpt);
244
+ }
245
+ function collectH1Labels(doc) {
246
+ const labels = [];
247
+ const walk = (node) => {
248
+ if (!isRecord2(node)) return;
249
+ const type = stringOrEmpty(node.type);
250
+ if (type === "heading") {
251
+ const attrs = recordOrEmpty(node.attrs);
252
+ if (Number(attrs.level ?? 2) === 1) labels.push(textFrom(node));
253
+ }
254
+ if (type === "section") {
255
+ const attrs = recordOrEmpty(node.attrs);
256
+ if (attrs.sectionType === "hero") {
257
+ const props = recordOrEmpty(attrs.props);
258
+ const title = stringOrEmpty(props.title);
259
+ if (title) labels.push(title);
260
+ }
261
+ }
262
+ const content = node.content;
263
+ if (Array.isArray(content)) {
264
+ for (const child of content) walk(child);
265
+ }
266
+ };
267
+ walk(doc);
268
+ return labels;
269
+ }
270
+ function assertPublishable(doc, options) {
271
+ const errors = [];
272
+ const ruleset = { publish: PUBLISH_GATE_RULESET };
273
+ const h1Labels = collectH1Labels(doc.bodyJson);
274
+ if (options.documentType === "post") {
275
+ if (h1Labels.length !== 0) {
276
+ errors.push({
277
+ code: "SEO_H1_COUNT",
278
+ message: `post \uB294 \uC81C\uBAA9\uC774 h1 \uC774\uBA70 \uBCF8\uBB38\uC5D0 h1 \uAE08\uC9C0, \uD604\uC7AC ${h1Labels.length}\uAC1C\uC785\uB2C8\uB2E4.`
279
+ });
280
+ }
281
+ } else {
282
+ if (h1Labels.length !== 1) {
283
+ errors.push({
284
+ code: "SEO_H1_COUNT",
285
+ message: `h1\uC740 \uC815\uD655\uD788 1\uAC1C\uC5EC\uC57C \uD569\uB2C8\uB2E4. \uD604\uC7AC ${h1Labels.length}\uAC1C\uC785\uB2C8\uB2E4.`
286
+ });
287
+ }
288
+ }
289
+ if (!stringOrEmpty(doc.title)) {
290
+ errors.push({
291
+ code: "SEO_TITLE_REQUIRED",
292
+ message: "\uBC1C\uD589\uD558\uB824\uBA74 \uAE00 \uC81C\uBAA9\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."
293
+ });
294
+ }
295
+ if (!seoDescription(doc)) {
296
+ errors.push({
297
+ code: "SEO_META_DESCRIPTION_REQUIRED",
298
+ message: "\uBC1C\uD589\uD558\uB824\uBA74 SEO meta description \uB610\uB294 excerpt\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4."
299
+ });
300
+ }
301
+ if (options.vertical === "medical" || options.complianceFlags?.includes("medical-review")) {
302
+ ruleset.medical = MEDICAL_LINT_RULESET;
303
+ const lint = lintMedicalPage({
304
+ title: doc.title ?? "",
305
+ excerpt: doc.excerpt,
306
+ contentJson: {
307
+ type: "doc",
308
+ content: [{ type: "text", text: textFrom(doc.bodyJson) }]
309
+ }
310
+ });
311
+ for (const finding of lint.findings) {
312
+ if (finding.severity !== "critical") continue;
313
+ errors.push({
314
+ code: "MEDICAL_COMPLIANCE",
315
+ ruleId: finding.ruleId,
316
+ severity: finding.severity,
317
+ message: finding.message,
318
+ snippet: finding.snippet
319
+ });
320
+ }
321
+ }
322
+ return { errors, ruleset };
323
+ }
324
+ function writePublishGateMeta(metaJson, result) {
325
+ const meta = { ...recordOrEmpty(metaJson) };
326
+ const current = recordOrEmpty(meta.compliance);
327
+ meta.compliance = {
328
+ ...current,
329
+ blocked: result.errors.length > 0,
330
+ ruleset: result.ruleset,
331
+ errors: result.errors,
332
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
333
+ ...result.errors.length === 0 ? { override: null } : {}
334
+ };
335
+ return meta;
336
+ }
337
+ function writePublishOverrideMeta(metaJson, override) {
338
+ const meta = { ...recordOrEmpty(metaJson) };
339
+ const compliance = recordOrEmpty(meta.compliance);
340
+ meta.compliance = {
341
+ ...compliance,
342
+ blocked: false,
343
+ override
344
+ };
345
+ return meta;
346
+ }
347
+ function isComplianceBlocked(metaJson) {
348
+ const compliance = recordOrEmpty(recordOrEmpty(metaJson).compliance);
349
+ return compliance.blocked === true && !hasPublishOverride(metaJson);
350
+ }
351
+ function hasPublishOverride(metaJson) {
352
+ const compliance = recordOrEmpty(recordOrEmpty(metaJson).compliance);
353
+ const override = recordOrEmpty(compliance.override);
354
+ return stringOrEmpty(override.reason).length > 0 && stringOrEmpty(override.userId).length > 0;
355
+ }
356
+
357
+ // src/templates/validate-sections.ts
358
+ function collectSectionNodes(doc) {
359
+ const content = doc && typeof doc === "object" ? doc.content : void 0;
360
+ if (!Array.isArray(content)) return [];
361
+ const out = [];
362
+ content.forEach((node, index) => {
363
+ if (node && typeof node === "object" && node.type === "section") {
364
+ out.push({ index, node });
365
+ }
366
+ });
367
+ return out;
368
+ }
369
+ function sectionInputFromAttrs(attrs) {
370
+ const a = attrs ?? {};
371
+ return {
372
+ type: typeof a.sectionType === "string" ? a.sectionType : "",
373
+ variant: a.variant ?? void 0,
374
+ theme: a.theme ?? void 0,
375
+ props: a.props ?? {}
376
+ };
377
+ }
378
+ function validateSectionNodes(doc) {
379
+ const sections = collectSectionNodes(doc);
380
+ const issues = [];
381
+ for (const { index, node } of sections) {
382
+ const input = sectionInputFromAttrs(node.attrs);
383
+ const result = SectionSchema.safeParse(input);
384
+ if (!result.success) {
385
+ for (const issue of result.error.issues) {
386
+ issues.push({
387
+ index,
388
+ sectionType: input.type || "(\uBBF8\uC9C0\uC815)",
389
+ field: issue.path.join(".") || "(root)",
390
+ message: issue.message
391
+ });
392
+ }
393
+ }
394
+ }
395
+ return { valid: issues.length === 0, issues };
396
+ }
397
+ function formatSectionIssues(issues) {
398
+ return issues.map(
399
+ (i) => `${i.sectionType} \uC139\uC158(${i.index + 1}\uBC88\uC9F8)\uC758 ${i.field}: ${i.message}`
400
+ ).join(" / ");
401
+ }
402
+ export {
403
+ PUBLISH_GATE_RULESET,
404
+ assertPublishable,
405
+ findSectionLockViolations,
406
+ formatSectionIssues,
407
+ formatSectionLockViolations,
408
+ hasPublishOverride,
409
+ isComplianceBlocked,
410
+ isSectionContentPropPath,
411
+ validateSectionNodes,
412
+ writePublishGateMeta,
413
+ writePublishOverrideMeta
414
+ };
415
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/templates/section-locks.ts","../src/templates/publish-gate.ts","../src/templates/validate-sections.ts"],"sourcesContent":["export interface SectionLock {\n move?: boolean;\n remove?: boolean;\n structure?: boolean;\n}\n\ninterface TiptapNodeLike {\n type?: string;\n attrs?: Record<string, unknown>;\n}\n\ninterface SectionNodeSnapshot {\n id: string;\n index: number;\n sectionType: string;\n attrs: Record<string, unknown>;\n props: unknown;\n lock: SectionLock;\n}\n\nexport type SectionLockViolationCode =\n | \"locked_section_inserted\"\n | \"locked_section_removed\"\n | \"locked_section_moved\"\n | \"locked_section_lock_changed\"\n | \"locked_section_structure_changed\";\n\nexport interface SectionLockViolation {\n code: SectionLockViolationCode;\n sectionId: string;\n sectionType: string;\n field: string;\n}\n\nconst CONTENT_PROP_PATHS: Readonly<\n Record<string, readonly (readonly (string | number)[])[]>\n> = {\n hero: [\n [\"eyebrow\"],\n [\"title\"],\n [\"description\"],\n [\"imageSrc\"],\n [\"imageAlt\"],\n [\"cta\", \"label\"],\n [\"cta\", \"href\"],\n ],\n \"rich-text\": [[\"heading\"], [\"body\"]],\n services: [\n [\"heading\"],\n [\"description\"],\n [\"items\", Number.NaN, \"title\"],\n [\"items\", Number.NaN, \"description\"],\n [\"items\", Number.NaN, \"href\"],\n ],\n faq: [\n [\"heading\"],\n [\"items\", Number.NaN, \"question\"],\n [\"items\", Number.NaN, \"answer\"],\n ],\n \"cta-banner\": [[\"title\"], [\"description\"], [\"cta\", \"label\"], [\"cta\", \"href\"]],\n \"blog-list\": [[\"heading\"], [\"emptyMessage\"]],\n \"lead-form\": [[\"heading\"], [\"description\"], [\"submitLabel\"]],\n trust: [\n [\"heading\"],\n [\"metrics\", Number.NaN, \"value\"],\n [\"metrics\", Number.NaN, \"unit\"],\n [\"metrics\", Number.NaN, \"label\"],\n [\"metrics\", Number.NaN, \"source\"],\n [\"credentials\", Number.NaN, \"name\"],\n [\"credentials\", Number.NaN, \"issuer\"],\n [\"credentials\", Number.NaN, \"kind\"],\n [\"credentials\", Number.NaN, \"image\"],\n [\"logos\", Number.NaN, \"src\"],\n [\"logos\", Number.NaN, \"alt\"],\n [\"logos\", Number.NaN, \"href\"],\n [\"disclaimer\"],\n ],\n contact: [\n [\"heading\"],\n [\"form\", \"enabled\"],\n [\"phone\"],\n [\"address\"],\n [\"hours\", Number.NaN, \"day\"],\n [\"hours\", Number.NaN, \"open\"],\n [\"hours\", Number.NaN, \"close\"],\n [\"hours\", Number.NaN, \"note\"],\n [\"mapEmbedUrl\"],\n ],\n};\n\nfunction readSectionLock(value: unknown): SectionLock {\n if (!value || typeof value !== \"object\") return {};\n const input = value as Record<string, unknown>;\n return {\n ...(typeof input.move === \"boolean\" ? { move: input.move } : {}),\n ...(typeof input.remove === \"boolean\" ? { remove: input.remove } : {}),\n ...(typeof input.structure === \"boolean\"\n ? { structure: input.structure }\n : {}),\n };\n}\n\nfunction isSectionLocked(lock: SectionLock): boolean {\n return lock.move === true || lock.remove === true || lock.structure === true;\n}\n\nfunction valueEquals(left: unknown, right: unknown): boolean {\n return JSON.stringify(left) === JSON.stringify(right);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction collectChangedPaths(\n before: unknown,\n after: unknown,\n prefix: readonly (string | number)[] = [],\n): Array<readonly (string | number)[]> {\n if (valueEquals(before, after)) return [];\n if (Array.isArray(before) && Array.isArray(after)) {\n if (before.length !== after.length) return [prefix];\n return before.flatMap((item, index) =>\n collectChangedPaths(item, after[index], [...prefix, index]),\n );\n }\n if (isRecord(before) && isRecord(after)) {\n const keys = new Set([...Object.keys(before), ...Object.keys(after)]);\n return [...keys].flatMap((key) =>\n collectChangedPaths(before[key], after[key], [...prefix, key]),\n );\n }\n return [prefix];\n}\n\nfunction contentPathMatches(\n pattern: readonly (string | number)[],\n path: readonly (string | number)[],\n): boolean {\n if (pattern.length !== path.length) return false;\n return pattern.every((segment, index) => {\n const actual = path[index];\n return Number.isNaN(segment)\n ? typeof actual === \"number\"\n : segment === actual;\n });\n}\n\nexport function isSectionContentPropPath(\n sectionType: string,\n path: readonly (string | number)[],\n): boolean {\n const patterns = CONTENT_PROP_PATHS[sectionType] ?? [];\n return patterns.some((pattern) => contentPathMatches(pattern, path));\n}\n\nfunction collectSectionSnapshots(doc: unknown): SectionNodeSnapshot[] {\n const content =\n doc && typeof doc === \"object\"\n ? (doc as { content?: unknown }).content\n : undefined;\n if (!Array.isArray(content)) return [];\n return content.flatMap((node, index) => {\n if (!node || typeof node !== \"object\") return [];\n const tiptapNode = node as TiptapNodeLike;\n if (tiptapNode.type !== \"section\") return [];\n const attrs = tiptapNode.attrs ?? {};\n const id = typeof attrs._id === \"string\" ? attrs._id : \"\";\n if (!id) return [];\n return [\n {\n id,\n index,\n sectionType:\n typeof attrs.sectionType === \"string\" ? attrs.sectionType : \"\",\n attrs,\n props: attrs.props ?? {},\n lock: readSectionLock(attrs.lock),\n },\n ];\n });\n}\n\nexport function findSectionLockViolations(\n beforeDoc: unknown,\n afterDoc: unknown,\n): SectionLockViolation[] {\n const before = collectSectionSnapshots(beforeDoc);\n const after = collectSectionSnapshots(afterDoc);\n const afterById = new Map(after.map((section) => [section.id, section]));\n const beforeById = new Map(before.map((section) => [section.id, section]));\n const violations: SectionLockViolation[] = [];\n\n for (const section of before) {\n const next = afterById.get(section.id);\n if (!next) {\n if (section.lock.remove === true) {\n violations.push({\n code: \"locked_section_removed\",\n sectionId: section.id,\n sectionType: section.sectionType,\n field: \"(node)\",\n });\n }\n continue;\n }\n\n if (section.lock.move === true && section.index !== next.index) {\n violations.push({\n code: \"locked_section_moved\",\n sectionId: section.id,\n sectionType: section.sectionType,\n field: \"(index)\",\n });\n }\n\n if (\n !valueEquals(section.lock, next.lock) &&\n (isSectionLocked(section.lock) || isSectionLocked(next.lock))\n ) {\n violations.push({\n code: \"locked_section_lock_changed\",\n sectionId: section.id,\n sectionType: section.sectionType,\n field: \"lock\",\n });\n }\n\n if (section.lock.structure !== true) continue;\n\n for (const field of [\"_id\", \"sectionType\", \"variant\", \"theme\"] as const) {\n if (!valueEquals(section.attrs[field], next.attrs[field])) {\n violations.push({\n code: \"locked_section_structure_changed\",\n sectionId: section.id,\n sectionType: section.sectionType,\n field,\n });\n }\n }\n\n for (const path of collectChangedPaths(section.props, next.props)) {\n if (!isSectionContentPropPath(section.sectionType, path)) {\n violations.push({\n code: \"locked_section_structure_changed\",\n sectionId: section.id,\n sectionType: section.sectionType,\n field: `props.${path.join(\".\") || \"(root)\"}`,\n });\n }\n }\n }\n\n for (const section of after) {\n if (beforeById.has(section.id) || !isSectionLocked(section.lock)) continue;\n violations.push({\n code: \"locked_section_inserted\",\n sectionId: section.id,\n sectionType: section.sectionType,\n field: \"(node)\",\n });\n }\n\n return violations;\n}\n\nexport function formatSectionLockViolations(\n violations: readonly SectionLockViolation[],\n): string {\n return violations\n .map((violation) => {\n const label = `${violation.sectionType || \"section\"} 섹션(${violation.sectionId})`;\n if (violation.code === \"locked_section_inserted\") return `${label} 추가`;\n if (violation.code === \"locked_section_removed\") return `${label} 삭제`;\n if (violation.code === \"locked_section_moved\") return `${label} 이동`;\n if (violation.code === \"locked_section_lock_changed\") {\n return `${label} 잠금 변경`;\n }\n return `${label} 구조 변경(${violation.field})`;\n })\n .join(\" / \");\n}\n","import {\n MEDICAL_LINT_RULESET,\n lintMedicalPage,\n type Severity,\n} from \"@roottale/audit-medical\";\n\nexport const PUBLISH_GATE_RULESET = \"publish-gate-2026-07-02\";\n\nexport type PublishGateError = {\n readonly code: string;\n readonly message: string;\n readonly ruleId?: string;\n readonly severity?: Severity;\n readonly snippet?: string;\n};\n\nexport type PublishGateRuleset = {\n readonly publish: typeof PUBLISH_GATE_RULESET;\n readonly medical?: typeof MEDICAL_LINT_RULESET;\n};\n\nexport type PublishGateResult = {\n readonly errors: readonly PublishGateError[];\n readonly ruleset: PublishGateRuleset;\n};\n\nexport type PublishGateDoc = {\n readonly title?: string | null;\n readonly excerpt?: string | null;\n readonly bodyJson: Record<string, unknown>;\n readonly metaJson?: Record<string, unknown> | null;\n};\n\n/**\n * h1 모델 (W6-1) — 문서 타입별로 h1 소유자가 다르다:\n * - `\"page\"`: 본문(hero 섹션 또는 heading level=1)이 유일 h1. 렌더러가 `doc.title`\n * 을 h1 으로 그리지 않으므로 body h1Labels.length !== 1 → 차단.\n * - `\"post\"`: 렌더러(`RootTaleBlogPost`)가 `post.title` 을 h1 으로 그린다 —\n * 본문에 h1 이 있으면 실렌더 h1=2 가 되므로 body h1Labels.length !== 0 → 차단.\n * `documentType` 은 fail-closed 로 필수(optional 금지) — 신규 호출부가 실수로\n * type 분기를 빼먹으면 컴파일 에러가 나야 한다.\n */\nexport type PublishGateOptions = {\n readonly vertical: string;\n readonly complianceFlags?: readonly string[];\n readonly documentType: \"page\" | \"post\";\n};\n\nexport type PublishOverrideInput = {\n readonly reason: string;\n readonly userId: string;\n readonly at: string;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction recordOrEmpty(value: unknown): Record<string, unknown> {\n return isRecord(value) ? value : {};\n}\n\nfunction stringOrEmpty(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\nfunction collectText(value: unknown, out: string[]): void {\n if (typeof value === \"string\") {\n out.push(value);\n return;\n }\n if (Array.isArray(value)) {\n for (const item of value) collectText(item, out);\n return;\n }\n if (!isRecord(value)) return;\n if (typeof value.text === \"string\") out.push(value.text);\n for (const child of Object.values(value)) collectText(child, out);\n}\n\nfunction textFrom(value: unknown): string {\n const out: string[] = [];\n collectText(value, out);\n return out.join(\"\\n\");\n}\n\nfunction seoDescription(doc: PublishGateDoc): string {\n const meta = recordOrEmpty(doc.metaJson);\n const seo = recordOrEmpty(meta.seo);\n return stringOrEmpty(seo.description) || stringOrEmpty(doc.excerpt);\n}\n\nfunction collectH1Labels(doc: Record<string, unknown>): string[] {\n const labels: string[] = [];\n const walk = (node: unknown): void => {\n if (!isRecord(node)) return;\n const type = stringOrEmpty(node.type);\n if (type === \"heading\") {\n const attrs = recordOrEmpty(node.attrs);\n if (Number(attrs.level ?? 2) === 1) labels.push(textFrom(node));\n }\n if (type === \"section\") {\n const attrs = recordOrEmpty(node.attrs);\n if (attrs.sectionType === \"hero\") {\n const props = recordOrEmpty(attrs.props);\n const title = stringOrEmpty(props.title);\n if (title) labels.push(title);\n }\n }\n const content = node.content;\n if (Array.isArray(content)) {\n for (const child of content) walk(child);\n }\n };\n walk(doc);\n return labels;\n}\n\nexport function assertPublishable(\n doc: PublishGateDoc,\n options: PublishGateOptions,\n): PublishGateResult {\n const errors: PublishGateError[] = [];\n const ruleset: {\n publish: typeof PUBLISH_GATE_RULESET;\n medical?: typeof MEDICAL_LINT_RULESET;\n } = { publish: PUBLISH_GATE_RULESET };\n const h1Labels = collectH1Labels(doc.bodyJson);\n\n if (options.documentType === \"post\") {\n // post = 렌더러가 title 을 h1 으로 그린다 — 본문에 h1 이 있으면 실렌더 h1=2.\n if (h1Labels.length !== 0) {\n errors.push({\n code: \"SEO_H1_COUNT\",\n message: `post 는 제목이 h1 이며 본문에 h1 금지, 현재 ${h1Labels.length}개입니다.`,\n });\n }\n } else {\n // page = 본문(hero 또는 heading1)이 유일 h1 — 렌더러는 title 을 h1 으로 그리지 않는다.\n if (h1Labels.length !== 1) {\n errors.push({\n code: \"SEO_H1_COUNT\",\n message: `h1은 정확히 1개여야 합니다. 현재 ${h1Labels.length}개입니다.`,\n });\n }\n }\n if (!stringOrEmpty(doc.title)) {\n errors.push({\n code: \"SEO_TITLE_REQUIRED\",\n message: \"발행하려면 글 제목이 필요합니다.\",\n });\n }\n if (!seoDescription(doc)) {\n errors.push({\n code: \"SEO_META_DESCRIPTION_REQUIRED\",\n message: \"발행하려면 SEO meta description 또는 excerpt가 필요합니다.\",\n });\n }\n\n if (\n options.vertical === \"medical\" ||\n options.complianceFlags?.includes(\"medical-review\")\n ) {\n ruleset.medical = MEDICAL_LINT_RULESET;\n const lint = lintMedicalPage({\n title: doc.title ?? \"\",\n excerpt: doc.excerpt,\n contentJson: {\n type: \"doc\",\n content: [{ type: \"text\", text: textFrom(doc.bodyJson) }],\n },\n });\n for (const finding of lint.findings) {\n if (finding.severity !== \"critical\") continue;\n errors.push({\n code: \"MEDICAL_COMPLIANCE\",\n ruleId: finding.ruleId,\n severity: finding.severity,\n message: finding.message,\n snippet: finding.snippet,\n });\n }\n }\n\n return { errors, ruleset };\n}\n\nexport function writePublishGateMeta(\n metaJson: Record<string, unknown> | null | undefined,\n result: PublishGateResult,\n): Record<string, unknown> {\n const meta = { ...recordOrEmpty(metaJson) };\n const current = recordOrEmpty(meta.compliance);\n meta.compliance = {\n ...current,\n blocked: result.errors.length > 0,\n ruleset: result.ruleset,\n errors: result.errors,\n checkedAt: new Date().toISOString(),\n ...(result.errors.length === 0 ? { override: null } : {}),\n };\n return meta;\n}\n\nexport function writePublishOverrideMeta(\n metaJson: Record<string, unknown> | null | undefined,\n override: PublishOverrideInput,\n): Record<string, unknown> {\n const meta = { ...recordOrEmpty(metaJson) };\n const compliance = recordOrEmpty(meta.compliance);\n meta.compliance = {\n ...compliance,\n blocked: false,\n override,\n };\n return meta;\n}\n\nexport function isComplianceBlocked(\n metaJson: Record<string, unknown> | null | undefined,\n): boolean {\n const compliance = recordOrEmpty(recordOrEmpty(metaJson).compliance);\n return compliance.blocked === true && !hasPublishOverride(metaJson);\n}\n\nexport function hasPublishOverride(\n metaJson: Record<string, unknown> | null | undefined,\n): boolean {\n const compliance = recordOrEmpty(recordOrEmpty(metaJson).compliance);\n const override = recordOrEmpty(compliance.override);\n return (\n stringOrEmpty(override.reason).length > 0 &&\n stringOrEmpty(override.userId).length > 0\n );\n}\n","/**\n * `@roottale/cms-renderer-next/validate` — 섹션 노드 저장 검증 (ADR-0066 §5 / §2-A).\n *\n * 위치 근거(codex v4-R1 + W1-1 위임 결정):\n * - 검증은 **SectionSchema(정의 SSoT)** 로 하는데, 그 스키마는\n * `cms-renderer-next/templates/site-config.ts` 에 있다.\n * - 청사진(blueprint R1-4)은 \"cms-core 위치\"를 제안했으나, `cms-core → cms-renderer-next`\n * 는 **역방향 import(금지 — cms-core 가 base 패키지)**. 따라서 SectionSchema 를\n * 소비하는 검증기는 cms-renderer-next 안에 있어야 한다.\n * - 순수 zod(React/cms-client 무의존)이라 별도 진입점 `./validate` 로 노출해\n * admin `posts.ts`(\"use server\") 가 렌더러 번들 전체를 끌어오지 않고 import 한다.\n * - 조립 게이트(R3-a-3)도 같은 함수를 `@roottale/cms-renderer-next/validate` 로\n * 공유한다(이중 구현 금지).\n *\n * 섹션 = generic Tiptap 노드 `{ type:\"section\", attrs:{ sectionType, variant, theme, props } }`\n * (ADR-0055 §3-2). 검증은 `{ type: attrs.sectionType, variant, theme, props }` 를\n * SectionSchema 로 parse — 빌트인 type 은 스키마로 강제(union fall-through 차단),\n * 미지 type 은 CustomSection 으로 통과(렌더러 customSections 로 해석).\n */\nimport { SectionSchema } from \"./site-config.js\";\n\nexport {\n findSectionLockViolations,\n formatSectionLockViolations,\n isSectionContentPropPath,\n type SectionLock,\n type SectionLockViolation,\n type SectionLockViolationCode,\n} from \"./section-locks.js\";\n\nexport {\n PUBLISH_GATE_RULESET,\n assertPublishable,\n hasPublishOverride,\n isComplianceBlocked,\n writePublishGateMeta,\n writePublishOverrideMeta,\n type PublishGateDoc,\n type PublishGateError,\n type PublishGateOptions,\n type PublishGateResult,\n type PublishGateRuleset,\n type PublishOverrideInput,\n} from \"./publish-gate.js\";\n\n/** 검증 실패 1건. path = 문서 내 섹션 노드 위치(0-base), field = zod issue 경로. */\nexport interface SectionValidationIssue {\n /** 문서 최상위 content 배열에서 섹션 노드의 index. */\n index: number;\n sectionType: string;\n /** zod issue 경로 (예: \"props.title\"). */\n field: string;\n message: string;\n}\n\nexport interface SectionValidationResult {\n valid: boolean;\n issues: SectionValidationIssue[];\n}\n\ninterface TiptapNodeLike {\n type?: string;\n attrs?: Record<string, unknown>;\n content?: unknown;\n text?: unknown;\n}\n\n/** 문서 최상위 content 에서 섹션 노드를 index 와 함께 수집. */\nfunction collectSectionNodes(\n doc: unknown,\n): Array<{ index: number; node: TiptapNodeLike }> {\n const content =\n doc && typeof doc === \"object\"\n ? (doc as { content?: unknown }).content\n : undefined;\n if (!Array.isArray(content)) return [];\n const out: Array<{ index: number; node: TiptapNodeLike }> = [];\n content.forEach((node, index) => {\n if (node && typeof node === \"object\" && (node as TiptapNodeLike).type === \"section\") {\n out.push({ index, node: node as TiptapNodeLike });\n }\n });\n return out;\n}\n\n/**\n * 섹션 노드의 attrs → SectionSchema 입력 `{type, variant, theme, props}`.\n * sectionType 이 SectionSchema 의 `type` 이 된다.\n */\nfunction sectionInputFromAttrs(attrs: Record<string, unknown> | undefined): {\n type: string;\n variant?: unknown;\n theme?: unknown;\n props?: unknown;\n} {\n const a = attrs ?? {};\n return {\n type: typeof a.sectionType === \"string\" ? a.sectionType : \"\",\n variant: a.variant ?? undefined,\n theme: a.theme ?? undefined,\n props: a.props ?? {},\n };\n}\n\n/**\n * 문서 내 모든 섹션 노드를 SectionSchema 로 검증. 섹션 노드가 없으면 valid=true\n * (no-op). 하나라도 스키마 위반이면 valid=false + issue 목록.\n *\n * posts.ts(create/update) 가 이 결과를 보고 저장을 거부한다. rawHtml 생성 금지 —\n * 실패는 저장 거부일 뿐 노드를 변형하지 않는다.\n */\nexport function validateSectionNodes(doc: unknown): SectionValidationResult {\n const sections = collectSectionNodes(doc);\n const issues: SectionValidationIssue[] = [];\n\n for (const { index, node } of sections) {\n const input = sectionInputFromAttrs(node.attrs);\n const result = SectionSchema.safeParse(input);\n if (!result.success) {\n for (const issue of result.error.issues) {\n issues.push({\n index,\n sectionType: input.type || \"(미지정)\",\n field: issue.path.join(\".\") || \"(root)\",\n message: issue.message,\n });\n }\n }\n }\n\n return { valid: issues.length === 0, issues };\n}\n\n/** issue 목록을 사람이 읽는 한 줄 메시지로 (저장 거부 오류 문구용). */\nexport function formatSectionIssues(issues: SectionValidationIssue[]): string {\n return issues\n .map(\n (i) =>\n `${i.sectionType} 섹션(${i.index + 1}번째)의 ${i.field}: ${i.message}`,\n )\n .join(\" / \");\n}\n"],"mappings":"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","names":["isRecord"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@roottale/cms-renderer-next",
3
- "version": "0.40.0",
3
+ "version": "0.41.1",
4
4
  "type": "module",
5
- "description": "RootTale CMS public-render React/Next.js Server Components. SSR-only RSC components (RootTaleBlogList / RootTaleBlogPost / RootTaleLeadForm) for external customer sites. Companion of @roottale/cms-renderer-astro (ADR-0034 §1.5 amended).",
5
+ "description": "RootTale CMS public-render React/Next.js Server Components. SSR-only RSC components (RootTaleBlogList / RootTaleBlogPost / RootTaleLeadForm) for external customer sites.",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
@@ -31,6 +31,11 @@
31
31
  "import": "./dist/templates.js",
32
32
  "default": "./dist/templates.js"
33
33
  },
34
+ "./validate": {
35
+ "types": "./dist/validate.d.ts",
36
+ "import": "./dist/validate.js",
37
+ "default": "./dist/validate.js"
38
+ },
34
39
  "./styles": "./dist/cms-public.css",
35
40
  "./templates/styles": "./dist/templates.css",
36
41
  "./package.json": "./package.json"
@@ -42,8 +47,9 @@
42
47
  ],
43
48
  "dependencies": {
44
49
  "zod": "^3.23.8",
45
- "@roottale/cms-client": "^0.40.0",
46
- "@roottale/cms-core": "^0.39.0"
50
+ "@roottale/audit-medical": "^0.0.1",
51
+ "@roottale/cms-client": "^0.41.1",
52
+ "@roottale/cms-core": "^0.41.0"
47
53
  },
48
54
  "peerDependencies": {
49
55
  "react": "^19.0.0"