@contentrain/types 1.22.0 → 1.23.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/index.mjs CHANGED
@@ -702,6 +702,171 @@ function isMigrateStudioClaim(input) {
702
702
  return validateMigrateStudioClaim(input).ok;
703
703
  }
704
704
  //#endregion
705
+ //#region src/project-plan.ts
706
+ const PROJECT_PLAN_FORMAT = "contentrain-project-plan@1";
707
+ /** Semantic design roles the kit reads — the starter's `@theme` names without the `--` prefix. */
708
+ const PLAN_TOKEN_ROLES = [
709
+ "color-surface",
710
+ "color-surface-muted",
711
+ "color-ink",
712
+ "color-ink-muted",
713
+ "color-line",
714
+ "color-accent",
715
+ "color-accent-ink",
716
+ "font-sans",
717
+ "font-serif",
718
+ "font-mono",
719
+ "container-prose",
720
+ "container-page",
721
+ "container-wide",
722
+ "radius-card"
723
+ ];
724
+ const PLAN_VALUE_PATTERN = /^(?:field:[\w-]+|media:[\w-]+|ref:[\w-]+\.[\w-]+|href:(?:self|[\w-]+)|term:[\w-]+|ui:[\w.-]+|site:[\w-]+|menu:(?:primary|footer)|page:(?:base|current|total|breadcrumb)|const:(?:true|false|-?\d+(?:\.\d+)?|[a-z][a-z0-9_-]{0,31}))$/;
725
+ const PERMALINK = /^\/(?:[^/\s]+\/)*$/;
726
+ const IMPORTED_MODELS = new Set([
727
+ "posts",
728
+ "pages",
729
+ "categories",
730
+ "tags",
731
+ "authors",
732
+ "media",
733
+ "menus",
734
+ "menu-items",
735
+ "site",
736
+ "ui-strings"
737
+ ]);
738
+ /**
739
+ * Structural checks a plan must pass before anything is written: every reference resolves (routes →
740
+ * components → models), permalinks are well-formed, kit components carry a kit id, plan models carry
741
+ * their fields. Kit catalog and fact checks (does the kit id exist, does the template exist) belong to
742
+ * the caller, which holds those documents.
743
+ */
744
+ function validateProjectPlan(plan) {
745
+ const errors = [];
746
+ const warnings = [];
747
+ if (plan.format !== "contentrain-project-plan@1") errors.push(`format is not ${PROJECT_PLAN_FORMAT}`);
748
+ if (!/^https?:\/\/[^/]+$/.test(plan.source?.origin ?? "")) errors.push("source.origin is not an http(s) origin without a path");
749
+ const site = plan.site;
750
+ for (const [key, value] of Object.entries(site?.permalinks ?? {})) if (!PERMALINK.test(value)) errors.push(`site.permalinks.${key} must start and end with "/" (${value})`);
751
+ if (!Number.isSafeInteger(site?.postsPerPage) || site.postsPerPage < 1) errors.push("site.postsPerPage is not a positive integer");
752
+ for (const [from, to] of Object.entries(site?.redirects ?? {})) {
753
+ if (!from.startsWith("/")) errors.push(`redirect ${from} does not start with "/"`);
754
+ const status = typeof to === "string" ? 301 : to.status;
755
+ if (![
756
+ 301,
757
+ 302,
758
+ 307,
759
+ 308,
760
+ 410
761
+ ].includes(status)) errors.push(`redirect ${from} has status ${status}`);
762
+ }
763
+ for (const role of Object.keys(site?.tokens?.roles ?? {})) if (!PLAN_TOKEN_ROLES.includes(role)) errors.push(`tokens.roles.${role} is not a kit role`);
764
+ const models = /* @__PURE__ */ new Map();
765
+ for (const m of plan.models ?? []) {
766
+ if (models.has(m.id)) errors.push(`model ${m.id} is declared twice`);
767
+ models.set(m.id, m);
768
+ if (m.origin === "import" && !IMPORTED_MODELS.has(m.id)) warnings.push(`model ${m.id} is marked imported but wp-import does not write it`);
769
+ if (m.origin === "plan") {
770
+ if (!m.fields) errors.push(`plan model ${m.id} has no fields`);
771
+ if (!m.name || !m.domain) errors.push(`plan model ${m.id} needs name and domain`);
772
+ if (m.kind === "collection" || m.kind === "document") {
773
+ const title = m.title_field ? m.fields?.[m.title_field] : void 0;
774
+ if (!m.title_field) errors.push(`plan model ${m.id} has no title_field`);
775
+ else if (!title) errors.push(`plan model ${m.id}: title_field ${m.title_field} is not one of its fields`);
776
+ else if (!isTitleFieldType(title.type)) errors.push(`plan model ${m.id}: title_field ${m.title_field} is ${title.type}, not a text-like type`);
777
+ }
778
+ if (!m.extract?.length) warnings.push(`plan model ${m.id} has no extraction — its entries start empty`);
779
+ }
780
+ for (const x of m.extract ?? []) {
781
+ if (!x.rule && !x.fields) errors.push(`model ${m.id} extraction from ${x.from} has neither rule nor fields`);
782
+ if (x.rule && !/^(gutenberg|elementor|divi|classic):[\w./:-]+$/.test(x.rule)) errors.push(`model ${m.id} extraction rule ${x.rule} is not <builder>:<match>`);
783
+ for (const [field, path] of Object.entries(x.fields ?? {})) {
784
+ if (m.fields && !m.fields[field]) errors.push(`model ${m.id} extraction fills unknown field ${field}`);
785
+ if (!/^attr:[\w.-]+$|^[\d.]+\|(text|href|src|alt|datetime)$|^(?:dom|img|link|html):[^@]*(?:@[\w-]+)?$/.test(path)) errors.push(`model ${m.id} field ${field}: path ${path} is not "<path>|<prop>", "attr:<name>" or an element expression`);
786
+ }
787
+ }
788
+ }
789
+ const knownModel = (id) => models.has(id) || IMPORTED_MODELS.has(id);
790
+ const components = /* @__PURE__ */ new Map();
791
+ for (const c of plan.components ?? []) {
792
+ if (components.has(c.id)) errors.push(`component ${c.id} is declared twice`);
793
+ components.set(c.id, c);
794
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(c.id)) errors.push(`component ${c.id} is not a PascalCase name`);
795
+ if (c.origin === "kit" && !c.kit?.id) errors.push(`kit component ${c.id} names no kit id`);
796
+ if (c.origin === "site" && !c.props) errors.push(`site component ${c.id} declares no props`);
797
+ if (c.origin === "site" && !c.brief) warnings.push(`site component ${c.id} has no brief for the writer`);
798
+ }
799
+ const placement = (s, at, route) => {
800
+ const c = components.get(s.component);
801
+ if (!c) {
802
+ errors.push(`${at}: component ${s.component} is not declared`);
803
+ return;
804
+ }
805
+ const b = s.bind;
806
+ if ("model" in b && !knownModel(b.model)) errors.push(`${at}: model ${b.model} is not declared`);
807
+ if (b.kind === "entry" && !route?.source) errors.push(`${at}: binds the route entry but ${route ? "the route has no source" : "layout has no entry"}`);
808
+ const values = {
809
+ ..."props" in b ? b.props : {},
810
+ ...b.kind === "collection" && !b.into ? b.item : {}
811
+ };
812
+ for (const [prop, value] of Object.entries({
813
+ ...values,
814
+ ...b.kind === "collection" ? b.item : {}
815
+ })) if (!PLAN_VALUE_PATTERN.test(value)) errors.push(`${at}: ${prop} = ${value} is not a plan value (field: media: ref: href: term: ui: site: menu: page: const:)`);
816
+ if (c.props) {
817
+ const bound = new Set([
818
+ ...Object.keys(values),
819
+ ...Object.keys(s.labels ?? {}),
820
+ ..."into" in b && b.into ? [b.into] : b.kind === "menu" ? ["items"] : []
821
+ ]);
822
+ for (const prop of bound) if (!c.props[prop]) errors.push(`${at}: ${c.id} has no prop ${prop}`);
823
+ for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) errors.push(`${at}: required prop ${c.id}.${prop} is not bound`);
824
+ }
825
+ };
826
+ if (plan.layout?.header) placement(plan.layout.header, "layout header");
827
+ if (plan.layout?.footer) placement(plan.layout.footer, "layout footer");
828
+ const routeIds = /* @__PURE__ */ new Set();
829
+ const byModel = /* @__PURE__ */ new Map();
830
+ for (const r of plan.routes ?? []) {
831
+ if (routeIds.has(r.id)) errors.push(`route ${r.id} is declared twice`);
832
+ routeIds.add(r.id);
833
+ if (!PERMALINK.test(r.pattern)) errors.push(`route ${r.id}: pattern ${r.pattern} must start and end with "/"`);
834
+ if (r.source) {
835
+ if (!knownModel(r.source.model)) errors.push(`route ${r.id}: source model ${r.source.model} is not declared`);
836
+ (byModel.get(r.source.model) ?? byModel.set(r.source.model, []).get(r.source.model)).push(r);
837
+ }
838
+ if (r.body === "composed" && !r.sections.length) warnings.push(`route ${r.id} is composed but has no sections`);
839
+ r.sections.forEach((s, i) => placement(s, `route ${r.id} section ${i}`, r));
840
+ }
841
+ const byPattern = /* @__PURE__ */ new Map();
842
+ for (const r of plan.routes ?? []) (byPattern.get(r.pattern) ?? byPattern.set(r.pattern, []).get(r.pattern)).push(r);
843
+ for (const [pattern, routes] of byPattern) {
844
+ if (routes.length < 2) continue;
845
+ const sources = new Set(routes.map((r) => r.source?.model ?? ""));
846
+ if (!(sources.size === 1 && !sources.has("") && routes.filter((r) => !r.source?.where).length <= 1)) warnings.push(`routes ${routes.map((r) => r.id).join(", ")} share the pattern ${pattern}`);
847
+ }
848
+ for (const [model, routes] of byModel) {
849
+ if (routes.length < 2) continue;
850
+ const catchAll = routes.filter((r) => !r.source?.where || Object.keys(r.source.where).length === 0);
851
+ if (catchAll.length > 1) errors.push(`model ${model}: routes ${catchAll.map((r) => r.id).join(", ")} are all catch-alls`);
852
+ const seen = /* @__PURE__ */ new Map();
853
+ for (const r of routes) {
854
+ const ids = r.source?.where?.wp_id;
855
+ if (r.source?.where && ids === void 0) warnings.push(`route ${r.id}: where without wp_id — overlap with other ${model} routes cannot be checked`);
856
+ for (const id of Array.isArray(ids) ? ids : ids === void 0 ? [] : [ids]) {
857
+ const other = seen.get(id);
858
+ if (other) errors.push(`model ${model}: entry ${String(id)} is in routes ${other} and ${r.id}`);
859
+ else seen.set(id, r.id);
860
+ }
861
+ }
862
+ }
863
+ if (!plan.routes?.some((r) => r.kind === "home")) errors.push("no home route");
864
+ return {
865
+ errors,
866
+ warnings
867
+ };
868
+ }
869
+ //#endregion
705
870
  //#region src/provider.ts
706
871
  /** Capability set for the LocalProvider (simple-git + worktree). */
707
872
  const LOCAL_CAPABILITIES = {
@@ -1726,6 +1891,6 @@ function serializeMarkdownFrontmatter(data, body) {
1726
1891
  return lines.join("\n");
1727
1892
  }
1728
1893
  //#endregion
1729
- export { APPROVAL_GATES, APPROVAL_MODES, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, INTEGRATION_CATEGORIES, INTEGRATION_EVIDENCE_KINDS, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, MIGRATE_STUDIO_CLAIM_ALG, MIGRATE_STUDIO_CLAIM_AUDIENCE, MIGRATE_STUDIO_CLAIM_CLOCK_SKEW_SECONDS, MIGRATE_STUDIO_CLAIM_ISSUER, MIGRATE_STUDIO_CLAIM_MAX_TRIAL_DAYS, MIGRATE_STUDIO_CLAIM_MAX_TTL_SECONDS, MIGRATE_STUDIO_CLAIM_VERSION, MIGRATE_STUDIO_PLANS, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, PAGE_BODY_KINDS, PATH_PATTERNS, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, SCHEDULE_KINDS, SECRET_PATTERNS, SEO_PROVIDERS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, TERMINAL_RUN_STATUSES, TEXT_CANDIDATE_KINDS, TEXT_EXCLUDE_REASONS, TITLE_FIELD_TYPES, approversFor, canonicalStringify, commentsExportSource, componentSlot, computePlanHash, conflictId, describeModelLocaleScope, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isMigrateStudioClaim, isRepoRelativePath, isReservedPath, isStoredMediaPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, resolveModelLocales, rewriteEntryMedia, rewriteFieldMedia, rewriteMarkdownMedia, rewriteMediaUrl, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, toDeliveryUrl, validateAccept, validateEntryId, validateFieldValue, validateHandoffCommentsExport, validateLocale, validateMigrateStudioClaim, validateModelLocales, validateSemanticType, validateSlug };
1894
+ export { APPROVAL_GATES, APPROVAL_MODES, CANONICAL_JSON, CAPABILITY_KEYS, CHROME_BODY_SLOT, CHROME_COMPONENT_CLOSE, CHROME_COMPONENT_OPEN, CHROME_IF_CLOSE, CHROME_IF_OPEN, CHROME_REPEAT_CLOSE, CHROME_REPEAT_OPEN, COMMENTS_EXPORT_FORMAT, COMPONENT_TYPES, CONTENTRAIN_BRANCH, CONTENTRAIN_DIR, DEFAULT_APPROVAL_POLICY, DICTIONARY_TITLE_FIELD, ENTRY_ID_PATTERN, EXECUTION_CONTRACT_VERSION, INTEGRATION_CATEGORIES, INTEGRATION_EVIDENCE_KINDS, LEGACY_CSS_LAYER, LIST_ITEMS_SLOT, LOCALE_PATTERN, LOCAL_CAPABILITIES, MIGRATE_STUDIO_CLAIM_ALG, MIGRATE_STUDIO_CLAIM_AUDIENCE, MIGRATE_STUDIO_CLAIM_CLOCK_SKEW_SECONDS, MIGRATE_STUDIO_CLAIM_ISSUER, MIGRATE_STUDIO_CLAIM_MAX_TRIAL_DAYS, MIGRATE_STUDIO_CLAIM_MAX_TTL_SECONDS, MIGRATE_STUDIO_CLAIM_VERSION, MIGRATE_STUDIO_PLANS, MIGRATION_CONTRACT_VERSION, MODEL_EXTENSION_KEYS, MODEL_FIELD_ORDER, PAGE_BODY_KINDS, PATH_PATTERNS, PLAN_TOKEN_ROLES, PLAN_VALUE_PATTERN, PROJECT_PLAN_FORMAT, RAW_MARK_SUFFIX, RESERVED_PATHS, RISK_CLASSES, RUN_STATUSES, SCHEDULE_KINDS, SECRET_PATTERNS, SEO_PROVIDERS, SLUG_PATTERN, SOURCE_ACCESS_LADDER, SOURCE_DELTA_OPS, TERMINAL_RUN_STATUSES, TEXT_CANDIDATE_KINDS, TEXT_EXCLUDE_REASONS, TITLE_FIELD_TYPES, approversFor, canonicalStringify, commentsExportSource, componentSlot, computePlanHash, conflictId, describeModelLocaleScope, detectSecrets, effectiveRisk, evaluateApproval, generateEntryId, highestRisk, isMediaType, isMigrateStudioClaim, isRepoRelativePath, isReservedPath, isStoredMediaPath, isTerminalRunStatus, isTitleFieldType, looksLikeCredential, orderedFieldNames, parseFrontmatterScalar, parseFrontmatterScalarString, parseMarkdownFrontmatter, planHashPayload, requiredApprovals, resolveFieldLabel, resolveModelLocales, rewriteEntryMedia, rewriteFieldMedia, rewriteMarkdownMedia, rewriteMediaUrl, riskRank, serializeMarkdownFrontmatter, sortKeys, splitFrontmatterList, stableHash, toDeliveryUrl, validateAccept, validateEntryId, validateFieldValue, validateHandoffCommentsExport, validateLocale, validateMigrateStudioClaim, validateModelLocales, validateProjectPlan, validateSemanticType, validateSlug };
1730
1895
 
1731
1896
  //# sourceMappingURL=index.mjs.map