levrops-contracts 1.3.1 → 1.3.2

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,71 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.levrops.com/creative/creative_signal_profile.schema.json",
4
+ "title": "CreativeSignalProfile",
5
+ "description": "Extracted creative signal profile from a single asset. Captures emotional, thematic, and narrative dimensions for Creative Whisperer synthesis.",
6
+ "type": "object",
7
+ "required": ["asset_id", "asset_type", "energy"],
8
+ "properties": {
9
+ "asset_id": {
10
+ "type": "string",
11
+ "description": "Unique identifier of the source asset",
12
+ "minLength": 1,
13
+ "maxLength": 255
14
+ },
15
+ "asset_type": {
16
+ "type": "string",
17
+ "description": "Type of creative asset",
18
+ "enum": ["note", "audio", "url", "lyric", "image", "other"]
19
+ },
20
+ "energy": {
21
+ "type": "string",
22
+ "description": "Perceived energy level of the creative content",
23
+ "enum": ["low", "mid", "high"]
24
+ },
25
+ "emotions": {
26
+ "type": "array",
27
+ "description": "Detected emotional tones",
28
+ "items": { "type": "string", "maxLength": 100 },
29
+ "maxItems": 20,
30
+ "default": []
31
+ },
32
+ "themes": {
33
+ "type": "array",
34
+ "description": "Recurring themes or topics",
35
+ "items": { "type": "string", "maxLength": 100 },
36
+ "maxItems": 20,
37
+ "default": []
38
+ },
39
+ "narrative_posture": {
40
+ "type": "string",
41
+ "description": "Stance or posture of the narrative voice",
42
+ "maxLength": 200
43
+ },
44
+ "intensity": {
45
+ "type": "number",
46
+ "description": "Intensity score (0–1)",
47
+ "minimum": 0,
48
+ "maximum": 1
49
+ },
50
+ "vulnerability": {
51
+ "type": "number",
52
+ "description": "Vulnerability/openness score (0–1)",
53
+ "minimum": 0,
54
+ "maximum": 1
55
+ },
56
+ "certainty": {
57
+ "type": "number",
58
+ "description": "Certainty/conviction score (0–1)",
59
+ "minimum": 0,
60
+ "maximum": 1
61
+ },
62
+ "imagery": {
63
+ "type": "array",
64
+ "description": "Evocative imagery or visual motifs",
65
+ "items": { "type": "string", "maxLength": 200 },
66
+ "maxItems": 30,
67
+ "default": []
68
+ }
69
+ },
70
+ "additionalProperties": false
71
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.levrops.com/creative/creative_whisper.schema.json",
4
+ "title": "CreativeWhisper",
5
+ "description": "Synthesis output from Creative Whisperer. Summarizes clusters, patterns, tensions, and suggests follow-up exploration with citations.",
6
+ "type": "object",
7
+ "required": ["cluster_summary", "confidence"],
8
+ "properties": {
9
+ "cluster_summary": {
10
+ "type": "string",
11
+ "description": "Summary of the creative cluster(s) analyzed",
12
+ "minLength": 1,
13
+ "maxLength": 2000
14
+ },
15
+ "detected_pattern": {
16
+ "type": "string",
17
+ "description": "Detected creative pattern or recurring motif",
18
+ "maxLength": 500
19
+ },
20
+ "creative_tension": {
21
+ "type": "string",
22
+ "description": "Identified creative tension or dialectic",
23
+ "maxLength": 500
24
+ },
25
+ "suggested_exploration": {
26
+ "type": "string",
27
+ "description": "Suggested direction for further creative exploration",
28
+ "maxLength": 1000
29
+ },
30
+ "confidence": {
31
+ "type": "number",
32
+ "description": "Confidence in the synthesis (0–1)",
33
+ "minimum": 0,
34
+ "maximum": 1
35
+ },
36
+ "followup_questions": {
37
+ "type": "array",
38
+ "description": "Suggested follow-up questions to deepen exploration",
39
+ "items": { "type": "string", "maxLength": 300 },
40
+ "maxItems": 10,
41
+ "default": []
42
+ },
43
+ "citations": {
44
+ "type": "array",
45
+ "description": "Citations referencing asset IDs with short quotes",
46
+ "items": {
47
+ "type": "object",
48
+ "required": ["asset_id", "quote"],
49
+ "properties": {
50
+ "asset_id": {
51
+ "type": "string",
52
+ "description": "ID of the cited asset",
53
+ "minLength": 1,
54
+ "maxLength": 255
55
+ },
56
+ "quote": {
57
+ "type": "string",
58
+ "description": "Short excerpt (≤140 chars)",
59
+ "minLength": 1,
60
+ "maxLength": 140
61
+ }
62
+ },
63
+ "additionalProperties": false
64
+ },
65
+ "maxItems": 20,
66
+ "default": []
67
+ }
68
+ },
69
+ "additionalProperties": false
70
+ }
@@ -0,0 +1,86 @@
1
+ """
2
+ Pydantic models for Creative Whisperer synthesis contracts.
3
+
4
+ Mirrors the JSON schemas in creative/*.schema.json.
5
+ Install: pip install pydantic
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class AssetType(str, Enum):
15
+ """Creative asset type."""
16
+
17
+ NOTE = "note"
18
+ AUDIO = "audio"
19
+ URL = "url"
20
+ LYRIC = "lyric"
21
+ IMAGE = "image"
22
+ OTHER = "other"
23
+
24
+
25
+ class Energy(str, Enum):
26
+ """Perceived energy level."""
27
+
28
+ LOW = "low"
29
+ MID = "mid"
30
+ HIGH = "high"
31
+
32
+
33
+ class CreativeSignalProfile(BaseModel):
34
+ """Extracted creative signal profile from a single asset."""
35
+
36
+ asset_id: str = Field(..., min_length=1, max_length=255)
37
+ asset_type: AssetType
38
+ energy: Energy
39
+ emotions: list[str] = Field(default_factory=list, max_length=20)
40
+ themes: list[str] = Field(default_factory=list, max_length=20)
41
+ narrative_posture: str | None = Field(None, max_length=200)
42
+ intensity: float | None = Field(None, ge=0.0, le=1.0)
43
+ vulnerability: float | None = Field(None, ge=0.0, le=1.0)
44
+ certainty: float | None = Field(None, ge=0.0, le=1.0)
45
+ imagery: list[str] = Field(default_factory=list, max_length=30)
46
+
47
+
48
+ class CreativeEdgeAlignment(BaseModel):
49
+ """Alignment metrics between two creative signal profiles."""
50
+
51
+ source_asset_id: str = Field(..., min_length=1, max_length=255)
52
+ target_asset_id: str = Field(..., min_length=1, max_length=255)
53
+ semantic_score: float = Field(..., ge=0.0, le=1.0)
54
+ emotional_score: float = Field(..., ge=0.0, le=1.0)
55
+ theme_overlap_count: int = Field(..., ge=0)
56
+ motif_overlap_count: int = Field(..., ge=0)
57
+ resonance_score: float = Field(..., ge=0.0, le=1.0)
58
+
59
+
60
+ class CreativeCluster(BaseModel):
61
+ """Cluster of related creative signal profiles."""
62
+
63
+ cluster_id: str = Field(..., min_length=1, max_length=255)
64
+ asset_ids: list[str] = Field(..., min_length=1, max_length=100)
65
+ dominant_emotions: list[str] = Field(default_factory=list, max_length=10)
66
+ dominant_themes: list[str] = Field(default_factory=list, max_length=10)
67
+ cohesion_score: float | None = Field(None, ge=0.0, le=1.0)
68
+
69
+
70
+ class CreativeWhisperCitation(BaseModel):
71
+ """Citation referencing an asset with a short quote."""
72
+
73
+ asset_id: str = Field(..., min_length=1, max_length=255)
74
+ quote: str = Field(..., min_length=1, max_length=140)
75
+
76
+
77
+ class CreativeWhisper(BaseModel):
78
+ """Synthesis output from Creative Whisperer."""
79
+
80
+ cluster_summary: str = Field(..., min_length=1, max_length=2000)
81
+ detected_pattern: str | None = Field(None, max_length=500)
82
+ creative_tension: str | None = Field(None, max_length=500)
83
+ suggested_exploration: str | None = Field(None, max_length=1000)
84
+ confidence: float = Field(..., ge=0.0, le=1.0)
85
+ followup_questions: list[str] = Field(default_factory=list, max_length=10)
86
+ citations: list[CreativeWhisperCitation] = Field(default_factory=list, max_length=20)
@@ -0,0 +1,2 @@
1
+ # Optional: for Pydantic models (creative/models.py)
2
+ pydantic>=2.0.0
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "levrops-contracts",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "LevrOps API contracts, schemas, code generators, and Sanity content contracts",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/jackmckracken/levrops-contracts.git"
8
+ "url": "git+https://github.com/jackmckracken/levrops-contracts.git"
9
9
  },
10
10
  "homepage": "https://github.com/jackmckracken/levrops-contracts#readme",
11
11
  "type": "module",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "files": [
25
25
  "contracts/",
26
+ "creative/",
26
27
  "sanity/",
27
28
  "index.ts",
28
29
  "README.md",
@@ -31,13 +32,15 @@
31
32
  ],
32
33
  "scripts": {
33
34
  "sanity:codegen": "tsx tools/levrops-sanity-codegen.ts",
34
- "sanity:check": "tsx tools/sanity-check.ts"
35
+ "sanity:check": "tsx tools/sanity-check.ts",
36
+ "test:creative": "node --test tests/creative/validation.test.mjs"
35
37
  },
36
38
  "dependencies": {
37
39
  "sanity": "^3.56.0"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/node": "^20.11.24",
43
+ "ajv": "^8.12.0",
41
44
  "tsx": "^4.7.0",
42
45
  "typescript": "^5.4.0"
43
46
  },
@@ -48,4 +51,3 @@
48
51
  "access": "public"
49
52
  }
50
53
  }
51
-
@@ -0,0 +1,68 @@
1
+ import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
2
+
3
+ /**
4
+ * CTA
5
+ *
6
+ * Generated from Structure DSL block: "cta"
7
+ * Sanity type: "block.cta"
8
+ * Call to action block
9
+ */
10
+ export const blockCTA = defineType({
11
+ name: "block.cta",
12
+ title: "CTA",
13
+ type: "object",
14
+ description: "Call to action block",
15
+ fields: [
16
+ defineField({
17
+ name: "_key",
18
+ type: "string",
19
+ title: "Key",
20
+ description: "Unique key for array item (auto-generated)",
21
+ readOnly: true,
22
+ }),
23
+ defineField({
24
+ name: "headline",
25
+ type: string(),
26
+ title: "Headline",
27
+ description: "Constraints: Max 100 characters",
28
+ validation: (Rule) => Rule.required().max(100),
29
+ }),
30
+ defineField({
31
+ name: "description",
32
+ type: string(),
33
+ title: "Description",
34
+ description: "Constraints: Max 300 characters",
35
+ validation: (Rule) => Rule.max(300),
36
+ }),
37
+ defineField({
38
+ name: "buttonText",
39
+ type: string(),
40
+ title: "Button Text",
41
+ description: "Constraints: Max 50 characters",
42
+ validation: (Rule) => Rule.required().max(50),
43
+ }),
44
+ defineField({
45
+ name: "buttonLink",
46
+ type: url(),
47
+ title: "Button Link",
48
+ validation: (Rule) => Rule.required(),
49
+ }),
50
+ defineField({
51
+ name: "variant",
52
+ type: string(),
53
+ title: "Variant",
54
+ initialValue: "primary",
55
+ }),
56
+ ],
57
+ preview: {
58
+ select: {
59
+ headline: "headline",
60
+ },
61
+ prepare({ headline }) {
62
+ return {
63
+ title: headline || "CTA",
64
+ subtitle: "Call to action block",
65
+ };
66
+ },
67
+ },
68
+ });
@@ -0,0 +1,58 @@
1
+ import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
2
+
3
+ /**
4
+ * Editorial Grid
5
+ *
6
+ * Generated from Structure DSL block: "editorial_grid"
7
+ * Sanity type: "block.editorial_grid"
8
+ * Grid of editorial content
9
+ */
10
+ export const blockEditorialGrid = defineType({
11
+ name: "block.editorial_grid",
12
+ title: "Editorial Grid",
13
+ type: "object",
14
+ description: "Grid of editorial content",
15
+ fields: [
16
+ defineField({
17
+ name: "_key",
18
+ type: "string",
19
+ title: "Key",
20
+ description: "Unique key for array item (auto-generated)",
21
+ readOnly: true,
22
+ }),
23
+ defineField({
24
+ name: "title",
25
+ type: string(),
26
+ title: "Title",
27
+ description: "Constraints: Max 200 characters",
28
+ validation: (Rule) => Rule.required().max(200),
29
+ }),
30
+ defineField({
31
+ name: "items",
32
+ type: array({
33
+ of: [{ type: "reference", to: [{ type: "blog-post" }] }],
34
+ }),
35
+ title: "Items",
36
+ validation: (Rule) => Rule.required().min(1),
37
+ }),
38
+ defineField({
39
+ name: "columns",
40
+ type: number(),
41
+ title: "Columns",
42
+ description: "Constraints: Min value: 1, Max value: 4",
43
+ validation: (Rule) => Rule.min(1).max(4),
44
+ initialValue: 3,
45
+ }),
46
+ ],
47
+ preview: {
48
+ select: {
49
+ title: "title",
50
+ },
51
+ prepare({ title }) {
52
+ return {
53
+ title: title || "Editorial Grid",
54
+ subtitle: "Grid of editorial content",
55
+ };
56
+ },
57
+ },
58
+ });
@@ -0,0 +1,72 @@
1
+ import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
2
+
3
+ /**
4
+ * Hero
5
+ *
6
+ * Generated from Structure DSL block: "hero"
7
+ * Sanity type: "block.hero"
8
+ * Large headline with supporting text and CTA
9
+ */
10
+ export const blockHero = defineType({
11
+ name: "block.hero",
12
+ title: "Hero",
13
+ type: "object",
14
+ description: "Large headline with supporting text and CTA",
15
+ fields: [
16
+ defineField({
17
+ name: "_key",
18
+ type: "string",
19
+ title: "Key",
20
+ description: "Unique key for array item (auto-generated)",
21
+ readOnly: true,
22
+ }),
23
+ defineField({
24
+ name: "headline",
25
+ type: string(),
26
+ title: "Headline",
27
+ description: "Constraints: Max 100 characters",
28
+ validation: (Rule) => Rule.required().max(100),
29
+ }),
30
+ defineField({
31
+ name: "subheadline",
32
+ type: string(),
33
+ title: "Subheadline",
34
+ description: "Constraints: Max 200 characters",
35
+ validation: (Rule) => Rule.max(200),
36
+ }),
37
+ defineField({
38
+ name: "backgroundImage",
39
+ type: image({
40
+ options: {
41
+ hotspot: true,
42
+ },
43
+ }),
44
+ title: "Background Image",
45
+ description: "Constraints: Allowed formats: jpg, png, webp, Max size: 5.0MB",
46
+ validation: (Rule) => Rule.required(),
47
+ }),
48
+ defineField({
49
+ name: "ctaText",
50
+ type: string(),
51
+ title: "CTA Button Text",
52
+ description: "Constraints: Max 50 characters",
53
+ validation: (Rule) => Rule.max(50),
54
+ }),
55
+ defineField({
56
+ name: "ctaLink",
57
+ type: url(),
58
+ title: "CTA Link",
59
+ }),
60
+ ],
61
+ preview: {
62
+ select: {
63
+ headline: "headline",
64
+ },
65
+ prepare({ headline }) {
66
+ return {
67
+ title: headline || "Hero",
68
+ subtitle: "Large headline with supporting text and CTA",
69
+ };
70
+ },
71
+ },
72
+ });
@@ -0,0 +1,66 @@
1
+ import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
2
+
3
+ /**
4
+ * Product Block
5
+ *
6
+ * Generated from Structure DSL block: "product"
7
+ * Sanity type: "block.product"
8
+ * Product showcase block
9
+ */
10
+ export const blockProductBlock = defineType({
11
+ name: "block.product",
12
+ title: "Product Block",
13
+ type: "object",
14
+ description: "Product showcase block",
15
+ fields: [
16
+ defineField({
17
+ name: "_key",
18
+ type: "string",
19
+ title: "Key",
20
+ description: "Unique key for array item (auto-generated)",
21
+ readOnly: true,
22
+ }),
23
+ defineField({
24
+ name: "title",
25
+ type: string(),
26
+ title: "Title",
27
+ description: "Constraints: Max 200 characters",
28
+ validation: (Rule) => Rule.required().max(200),
29
+ }),
30
+ defineField({
31
+ name: "description",
32
+ type: string(),
33
+ title: "Description",
34
+ description: "Constraints: Max 500 characters",
35
+ validation: (Rule) => Rule.max(500),
36
+ }),
37
+ defineField({
38
+ name: "products",
39
+ type: array({
40
+ of: [{ type: "reference", to: [{ type: "product" }] }],
41
+ }),
42
+ title: "Products",
43
+ validation: (Rule) => Rule.required().min(1).max(10),
44
+ }),
45
+ defineField({
46
+ name: "image",
47
+ type: image({
48
+ options: {
49
+ hotspot: true,
50
+ },
51
+ }),
52
+ title: "Image",
53
+ }),
54
+ ],
55
+ preview: {
56
+ select: {
57
+ title: "title",
58
+ },
59
+ prepare({ title }) {
60
+ return {
61
+ title: title || "Product Block",
62
+ subtitle: "Product showcase block",
63
+ };
64
+ },
65
+ },
66
+ });
@@ -0,0 +1,59 @@
1
+ import { defineType, defineField, string, text, url, image, boolean, number, array, reference } from "sanity";
2
+
3
+ /**
4
+ * Story Section
5
+ *
6
+ * Generated from Structure DSL block: "story"
7
+ * Sanity type: "block.story"
8
+ * Narrative content block
9
+ */
10
+ export const blockStorySection = defineType({
11
+ name: "block.story",
12
+ title: "Story Section",
13
+ type: "object",
14
+ description: "Narrative content block",
15
+ fields: [
16
+ defineField({
17
+ name: "_key",
18
+ type: "string",
19
+ title: "Key",
20
+ description: "Unique key for array item (auto-generated)",
21
+ readOnly: true,
22
+ }),
23
+ defineField({
24
+ name: "title",
25
+ type: string(),
26
+ title: "Title",
27
+ description: "Constraints: Max 200 characters",
28
+ validation: (Rule) => Rule.required().max(200),
29
+ }),
30
+ defineField({
31
+ name: "content",
32
+ type: array({
33
+ of: [{ type: "block" }],
34
+ }),
35
+ title: "Content",
36
+ validation: (Rule) => Rule.required(),
37
+ }),
38
+ defineField({
39
+ name: "image",
40
+ type: image({
41
+ options: {
42
+ hotspot: true,
43
+ },
44
+ }),
45
+ title: "Image",
46
+ }),
47
+ ],
48
+ preview: {
49
+ select: {
50
+ title: "title",
51
+ },
52
+ prepare({ title }) {
53
+ return {
54
+ title: title || "Story Section",
55
+ subtitle: "Narrative content block",
56
+ };
57
+ },
58
+ },
59
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generated Sanity Schema Index
3
+ *
4
+ * This file is auto-generated from Structure DSL.
5
+ * DO NOT EDIT THIS FILE DIRECTLY.
6
+ *
7
+ * To regenerate:
8
+ * node tools/codegen/structure-to-sanity/index.js --input <path> --out <dir>
9
+ */
10
+
11
+ export { page } from './page';
12
+ export { blockHero } from './blocks/hero';
13
+ export { blockStorySection } from './blocks/story';
14
+ export { blockProductBlock } from './blocks/product';
15
+ export { blockEditorialGrid } from './blocks/editorial_grid';
16
+ export { blockCTA } from './blocks/cta';
@@ -0,0 +1,74 @@
1
+ import { defineType, defineField, string, slug, array } from "sanity";
2
+
3
+ /**
4
+ * Page Document
5
+ *
6
+ * Generated from Structure DSL page templates.
7
+ * Single document type with pageType discriminant.
8
+ */
9
+ export const page = defineType({
10
+ name: "page",
11
+ title: "Page",
12
+ type: "document",
13
+ description: "A page composed of ordered blocks, discriminated by pageType",
14
+ fields: [
15
+ defineField({
16
+ name: "title",
17
+ type: string(),
18
+ title: "Title",
19
+ validation: (Rule) => Rule.required().max(200),
20
+ description: "Page title",
21
+ }),
22
+ defineField({
23
+ name: "slug",
24
+ type: slug({
25
+ source: "title",
26
+ }),
27
+ title: "Slug",
28
+ validation: (Rule) => Rule.required(),
29
+ description: "URL-friendly identifier",
30
+ }),
31
+ defineField({
32
+ name: "pageType",
33
+ type: string(),
34
+ title: "Page Type",
35
+ validation: (Rule) => Rule.required(),
36
+ description: "Template type (maps to Structure DSL page template ID)",
37
+ options: {
38
+ list: [
39
+ { title: "Homepage", value: "homepage" },
40
+ { title: "Product Page", value: "product_page" },
41
+ { title: "Editorial Page", value: "editorial_page" },
42
+ ],
43
+ },
44
+ }),
45
+ defineField({
46
+ name: "content",
47
+ type: array({
48
+ of: [
49
+ { type: "block.hero" },
50
+ { type: "block.story" },
51
+ { type: "block.product" },
52
+ { type: "block.editorial_grid" },
53
+ { type: "block.cta" },
54
+ ],
55
+ }),
56
+ title: "Content Blocks",
57
+ description: "Ordered composition of content blocks",
58
+ validation: (Rule) => Rule.required().min(1),
59
+ }),
60
+ ],
61
+ preview: {
62
+ select: {
63
+ title: "title",
64
+ pageType: "pageType",
65
+ slug: "slug.current",
66
+ },
67
+ prepare({ title, pageType, slug }) {
68
+ return {
69
+ title: title || "Untitled Page",
70
+ subtitle: `${pageType || "unknown"} • /${slug || ""}`,
71
+ };
72
+ },
73
+ },
74
+ });