IMPORTANT: These are detailed instructions that you MUST read carefully and follow exactly. Every rule, constraint, format requirement, and example below exists for a reason. Do NOT skip sections, do NOT invent your own conventions, and do NOT deviate from the specified output format. If a rule says "MUST" or "NEVER", treat it as absolute.


You are helping create an AIMEAT service. AIMEAT is an AI agent infrastructure protocol.

Available building blocks:
- CSM (Community Service Manifest): YAML defining data schemas, fields, consent rules, validation.
- MSM (Micro Service Manifest): YAML defining external API integrations, auth, endpoints.
- Extension: V8-sandboxed JavaScript logic with YAML manifest. Actions get ctx object with memory, wallet, consent, trust, fetch APIs.
- App: HTML/JS user interface published to the apps catalog.
- Memory: Key-value storage with namespace isolation.
- Translation: Per-locale i18n strings.
- Cortex: Client-side JS domain library (IIFE on AIMEAT namespace). Wraps extension APIs and memory reads into clean domain methods for apps.

Extensions run in an ISOLATED V8 sandbox with ONLY this API (no Node.js, no global fetch, no setTimeout, no require, no import):
  ctx.memory.get(key) → value or null (ALWAYS null-check before using: 

The user wants to create this service:
---
Joka hakee yrityksen tiedot PRH:lta avoimesta APIsta ja voin laittaa seurantaan sen. Käytä testaukseen "Overscale Solutions" yritystä API hauissa.
---

## Refined Specification (from requirements interview)
```json
{
  "version": "1.0",
  "locale": "fi",
  "projectName": "PRH Yritystietopalvelu",
  "description": "AIMEAT-palvelu joka hakee yritysten perustiedot PRH:n avoimesta YTJ v3 APIsta. Käyttäjä voi hakea yrityksiä nimellä tai Y-tunnuksella, tarkastella yrityskorttia kaikilla rekisteritiedoilla, lisätä yrityksiä seurantaan ja saada push-ilmoituksia muutoksista, vertailla yrityksiä rinnakkain sekä selata seurattujen yritysten muutoshistoriaa aikajanalla. Tilinpäätösdata (XBRL API) varattu Phase 2:een.",
  "technicalLevel": "advanced",
  "useCases": [
    {
      "id": "uc-1",
      "title": "Yrityksen haku",
      "description": "Hae PRH:n avoimesta APIsta yrityksen perustiedot nimellä tai Y-tunnuksella",
      "priority": "must-have"
    },
    {
      "id": "uc-2",
      "title": "Yrityksen seuranta + push-ilmoitukset",
      "description": "Lisää yritys seurantalistalle ja saa push-ilmoitus kun yrityksen tiedot muuttuvat PRH:ssa",
      "priority": "must-have"
    },
    {
      "id": "uc-3",
      "title": "Yrityskortti / yhteenveto",
      "description": "Näe yrityksen keskeiset tiedot yhdellä näkymällä: nimi, Y-tunnus, EU-tunniste, rekisteröintipäivä, toimiala, yhtiömuoto, osoite, rekisterimerkinnät, status, verkkosivut",
      "priority": "must-have"
    },
    {
      "id": "uc-4",
      "title": "Vapaamuotoinen vertailu",
      "description": "Valitse N yritystä ja vertaa niiden tietoja rinnakkain",
      "priority": "must-have"
    },
    {
      "id": "uc-5",
      "title": "Muutoshistoria",
      "description": "Näe seuratun yrityksen muutokset aikajärjestyksessä — mikä muuttui, milloin, vanha vs. uusi arvo",
      "priority": "must-have"
    }
  ],
  "dataSources": [
    {
      "id": "ds-1",
      "name": "PRH YTJ v3 API",
      "type": "api",
      "url": "https://avoindata.prh.fi/opendata-ytj-api/v3/companies",
      "format": "json",
      "encoding": "utf-8",
      "sampleEntry": {
        "businessId": {
          "value": "3323553-5",
          "registrationDate": "2022-11-07",
          "source": "3"
        },
        "euId": {
          "value": "FIFPRO.3323553-5",
          "source": "1"
        },
        "names": [
          {
            "name": "Overscale Solutions Oy",
            "type": "1",
            "registrationDate": "2022-11-09",
            "version": 1,
            "source": "1"
          }
        ],
        "mainBusinessLine": {
          "type": "62100",
          "descriptions": [
            {
              "languageCode": "3",
              "description": "Computer programming activities"
            },
            {
              "languageCode": "2",
              "description": "Dataprogrammering"
            },
            {
              "languageCode": "1",
              "description": "Ohjelmistojen suunnittelu ja valmistus"
            }
          ],
          "typeCodeSet": "TOIMI4",
          "registrationDate": "2026-01-01",
          "source": "2"
        },
        "website": {
          "url": "www.overscalesolutions.com",
          "registrationDate": "2022-11-07",
          "source": "0"
        },
        "companyForms": [
          {
            "type": "16",
            "descriptions": [
              {
                "languageCode": "2",
                "description": "Aktiebolag"
              },
              {
                "languageCode": "3",
                "description": "Limited company"
              },
              {
                "languageCode": "1",
                "description": "Osakeyhtiö"
              }
            ],
            "registrationDate": "2022-11-09",
            "version": 1,
            "source": "1"
          }
        ],
        "companySituations": [],
        "registeredEntries": [
          {
            "type": "1",
            "descriptions": [
              {
                "languageCode": "3",
                "description": "Registered"
              },
              {
                "languageCode": "2",
                "description": "Registrerad"
              },
              {
                "languageCode": "1",
                "description": "Rekisterissä"
              }
            ],
            "registrationDate": "2022-11-09",
            "register": "1",
            "authority": "2"
          }
        ],
        "addresses": [
          {
            "type": 2,
            "street": "Pohjantie",
            "postCode": "02100",
            "postOffices": [
              {
                "city": "ESBO",
                "languageCode": "2",
                "municipalityCode": "049"
              },
              {
                "city": "ESPOO",
                "languageCode": "1",
                "municipalityCode": "049"
              }
            ],
            "postOfficeBox": "",
            "buildingNumber": "8-10",
            "entrance": "A",
            "apartmentNumber": "17",
            "apartmentIdSuffix": "",
            "co": "",
            "registrationDate": "2022-11-07",
            "source": "0"
          }
        ],
        "tradeRegisterStatus": "1",
        "status": "2",
        "registrationDate": "2022-11-09",
        "lastModified": "2026-02-03T13:17:42"
      },
      "responseEnvelope": {
        "totalResults": "number",
        "companies": "array of company objects"
      },
      "updateFrequency": "daily",
      "sampleFields": [
        "businessId",
        "euId",
        "names",
        "mainBusinessLine",
        "website",
        "companyForms",
        "companySituations",
        "registeredEntries",
        "addresses",
        "tradeRegisterStatus",
        "status",
        "registrationDate",
        "lastModified"
      ],
      "notes": "Avoin API, ei vaadi API-avainta. PRH päivittää datan kerran päivässä. Haku nimellä (name), Y-tunnuksella (businessId), paikkakunnalla (location) tai yhtiömuodolla (companyForm). Descriptions-kentissä languageCode: 1=suomi, 2=ruotsi, 3=englanti. Rate limit: 300 kyselyä/minuutti kaikille käyttäjille yhteensä.",
      "verified": true,
      "verifiedBy": "ai"
    },
    {
      "id": "ds-2",
      "name": "PRH XBRL Tilinpäätös-API",
      "type": "api",
      "url": "https://avoindata.prh.fi/opendata-xbrl-api/v3/financials",
      "format": "xml",
      "encoding": "utf-8",
      "sampleEntry": null,
      "responseEnvelope": {
        "totalResults": "number",
        "financials": "array of {businessId, financialDate, registrationDate}"
      },
      "updateFrequency": "daily",
      "sampleFields": [
        "businessId",
        "financialDate",
        "registrationDate"
      ],
      "notes": "Phase 2. Listausendpoint palauttaa JSONia, mutta varsinainen tilinpäätösdata (/financial) palautuu XBRL/XML-muodossa suomalaisella taksonomialla (fi_met-namespace). Vaatii XBRL-parsintaa. Ei kaikilla yrityksillä ole digitaalista tilinpäätöstä.",
      "verified": true,
      "verifiedBy": "ai",
      "fallback": "defer"
    }
  ],
  "dataModel": {
    "entities": [
      {
        "name": "company",
        "description": "PRH:sta haettu yrityksen perustiedot",
        "fields": [
          {
            "name": "businessId",
            "type": "string",
            "required": true,
            "description": "Y-tunnus (esim. 3323553-5)"
          },
          {
            "name": "euId",
            "type": "string",
            "required": false,
            "description": "EU-tunniste (esim. FIFPRO.3323553-5)"
          },
          {
            "name": "names",
            "type": "array",
            "required": true,
            "description": "Yrityksen nimet (nykyiset, entiset, rinnakkais-, aputoiminimet)"
          },
          {
            "name": "mainBusinessLine",
            "type": "object",
            "required": false,
            "description": "Päätoimiala — koodi ja kuvaukset fi/sv/en"
          },
          {
            "name": "companyForm",
            "type": "object",
            "required": true,
            "description": "Yhtiömuoto (OY, OYJ, KY jne.)"
          },
          {
            "name": "addresses",
            "type": "array",
            "required": false,
            "description": "Katuosoite ja/tai postiosoite"
          },
          {
            "name": "website",
            "type": "string",
            "required": false,
            "description": "Yrityksen verkkosivut"
          },
          {
            "name": "registeredEntries",
            "type": "array",
            "required": false,
            "description": "Rekisterimerkinnät (rekisteröinnit, rekisteristä poistot)"
          },
          {
            "name": "companySituations",
            "type": "array",
            "required": false,
            "description": "Yrityksen erityistilanteet (saneeraus, selvitystila, konkurssi)"
          },
          {
            "name": "tradeRegisterStatus",
            "type": "string",
            "required": false,
            "description": "Kaupparekisterin status"
          },
          {
            "name": "status",
            "type": "string",
            "required": false,
            "description": "Yrityksen yleinen status"
          },
          {
            "name": "registrationDate",
            "type": "date",
            "required": true,
            "description": "Rekisteröintipäivä"
          },
          {
            "name": "lastModified",
            "type": "date",
            "required": false,
            "description": "Viimeisin muutos PRH:ssa"
          }
        ],
        "relationships": [
          "tracked by watchlist-item"
        ]
      },
      {
        "name": "watchlist-item",
        "description": "Käyttäjän seurantaan lisäämä yritys",
        "fields": [
          {
            "name": "businessId",
            "type": "string",
            "required": true,
            "description": "Seurattavan yrityksen Y-tunnus"
          },
          {
            "name": "companyName",
            "type": "string",
            "required": true,
            "description": "Yrityksen nimi lisäyshetkellä"
          },
          {
            "name": "addedAt",
            "type": "date",
            "required": true,
            "description": "Seurantaan lisäämisen ajankohta"
          },
          {
            "name": "lastChecked",
            "type": "date",
            "required": false,
            "description": "Viimeisin tarkistusajankohta"
          },
          {
            "name": "lastSnapshot",
            "type": "object",
            "required": false,
            "description": "Viimeisin tallennettu tilannekuva yrityksen tiedoista vertailua varten"
          }
        ],
        "relationships": [
          "references company via businessId",
          "has many changes"
        ]
      },
      {
        "name": "change",
        "description": "Havaittu muutos seuratun yrityksen tiedoissa",
        "fields": [
          {
            "name": "businessId",
            "type": "string",
            "required": true,
            "description": "Yrityksen Y-tunnus"
          },
          {
            "name": "field",
            "type": "string",
            "required": true,
            "description": "Muuttunut kenttä (esim. 'addresses', 'mainBusinessLine')"
          },
          {
            "name": "oldValue",
            "type": "object",
            "required": false,
            "description": "Edellinen arvo"
          },
          {
            "name": "newValue",
            "type": "object",
            "required": false,
            "description": "Uusi arvo"
          },
          {
            "name": "detectedAt",
            "type": "date",
            "required": true,
            "description": "Muutoksen havaitsemisajankohta"
          },
          {
            "name": "notified",
            "type": "boolean",
            "required": true,
            "description": "Onko push-ilmoitus lähetetty"
          }
        ],
        "relationships": [
          "belongs to watchlist-item via businessId"
        ]
      },
      {
        "name": "comparison",
        "description": "Tallennettu vertailu N yrityksen välillä",
        "fields": [
          {
            "name": "title",
            "type": "string",
            "required": false,
            "description": "Vapaaehtoinen nimi vertailulle"
          },
          {
            "name": "businessIds",
            "type": "array",
            "required": true,
            "description": "Vertailtavien yritysten Y-tunnukset"
          },
          {
            "name": "createdAt",
            "type": "date",
            "required": true,
            "description": "Vertailun luomisajankohta"
          }
        ],
        "relationships": [
          "references multiple companies via businessIds"
        ]
      }
    ]
  },
  "views": [
    {
      "id": "view-1",
      "type": "list",
      "title": "Yrityshaku",
      "description": "Hakukenttä (nimi tai Y-tunnus) ja tuloslistaus kompakteina kortteina",
      "dataEntities": [
        "company"
      ],
      "interactions": [
        "search",
        "filter"
      ],
      "visualizations": []
    },
    {
      "id": "view-2",
      "type": "detail",
      "title": "Yrityskortti",
      "description": "Yrityksen kaikki tiedot yhdellä sivulla: perustiedot, toimiala, osoitteet, rekisterimerkinnät, status, tilanteet",
      "dataEntities": [
        "company"
      ],
      "interactions": [
        "add-to-watchlist",
        "add-to-comparison"
      ],
      "visualizations": []
    },
    {
      "id": "view-3",
      "type": "list",
      "title": "Seurantalista",
      "description": "Käyttäjän seuraamat yritykset badgeineen jotka näyttävät uusien muutosten lukumäärän",
      "dataEntities": [
        "watchlist-item",
        "change"
      ],
      "interactions": [
        "remove-from-watchlist",
        "navigate-to-detail",
        "navigate-to-changes"
      ],
      "visualizations": []
    },
    {
      "id": "view-4",
      "type": "timeline",
      "title": "Muutoshistoria",
      "description": "Aikajana seuratun yrityksen havaituista muutoksista — kenttä, vanha arvo, uusi arvo, ajankohta",
      "dataEntities": [
        "change"
      ],
      "interactions": [
        "filter"
      ],
      "visualizations": []
    },
    {
      "id": "view-5",
      "type": "cards",
      "title": "Yritysvertailu",
      "description": "Rinnakkaisnäkymä valittujen yritysten tiedoista vertailutaulukossa",
      "dataEntities": [
        "company",
        "comparison"
      ],
      "interactions": [
        "add-company",
        "remove-company",
        "save-comparison"
      ],
      "visualizations": []
    }
  ],
  "style": {
    "mood": "data-dense",
    "colorPalette": "Siniharmaa viranomaisdatapaletti — tumma sininen (#1a365d) pääväri, teräksen harmaa (#4a5568) teksti, vaalea tausta (#f7fafc), korostus (#3182ce). Muutosilmoituksissa oranssi (#dd6b20) huomiovärinä.",
    "typography": "compact",
    "layout": "tabbed",
    "animations": "subtle",
    "displayContext": "desktop",
    "references": null
  },
  "externalServices": [],
  "sharedService": false,
  "adminAppRecommended": false,
  "adminAppReason": null,
  "userSettings": [
    {
      "key": "default_language",
      "type": "select",
      "label": "Hakutulosten kieli",
      "default": "fi"
    },
    {
      "key": "notifications_enabled",
      "type": "boolean",
      "label": "Push-ilmoitukset muutoksista",
      "default": true
    }
  ],
  "constraints": {
    "updateMode": "scheduled",
    "scheduleInterval": "daily",
    "locales": [
      "fi",
      "en"
    ],
    "domainRules": "PRH API rate limit 300 req/min kaikille käyttäjille yhteensä — seurantatarkistukset pitää ajastaa ja hajauttaa. PRH data päivittyy kerran päivässä. Descriptions-kenttien languageCode: 1=suomi, 2=ruotsi, 3=englanti. Kaikilla yrityksillä ei ole kaikkia kenttiä (website, companySituations voi olla tyhjä). Toiminimiyrittäjät eivät ole APIssa.",
    "notes": "Phase 2: XBRL-tilinpäätösdata vaatii XML-parsintaa ja suomalaisen XBRL-taksonomian tuntemusta. Overscale Solutions Oy (3323553-5) käytetty testiyrityksenä."
  },
  "interviewNotes": "Käyttäjä on kokenut arkkitehti ja AIMEAT-alustan kehittäjä. Kaikki viisi käyttötapausta must-have. Vertailu on vapaamuotoinen (valitse N yritystä). XBRL-tilinpäätösdata lykätty Phase 2:een koska se palauttaa raakaa XBRL/XML:ää joka vaatii monimutkaista parsintaa. PRH YTJ v3 API testattu onnistuneesti Overscale Solutions Oy:lla — ei vaadi API-avainta, palauttaa rikasta JSON-dataa."
}
```

Use the specification above to determine the exact components needed. The data sources, entities, views, and constraints have been validated with the user.

## LANGUAGE

The user's language is "fi". Write all human-readable labels and descriptions in that language.
JSON keys and technical identifiers stay in English.

## Available Cortex Libraries (reuse these — do NOT recreate)

The following cortex libraries are already installed on this node. If the project needs their capabilities, reference them in the cortex component's "uses" field instead of reimplementing.

### aimeat-canvas — A full-featured drawing canvas for your apps. Draw with pen, shapes, and text. Auto-saves to AIMEAT storage. Touch, mouse, and mobile support with undo/redo and PNG/SVG export.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-canvas/libs/aimeat-canvas.js"></script>`

### aimeat-charts — Beautiful interactive charts for your apps. Bar, line, pie, doughnut, radar, scatter, and bubble charts with touch support, hover tooltips, and responsive layout.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-charts/libs/aimeat-charts.js"></script>`

### aimeat-ui-dialogs — Modal dialogs, toasts, alerts, context menus, and dropdowns for AIMEAT apps. Replaces native browser alert/confirm.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-ui-dialogs/libs/aimeat-ui-dialogs.js"></script>`

### aimeat-ui-forms — Form components — inputs, selects, checkboxes, radios, toggles, textareas, and declarative form groups with built-in validation.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-ui-forms/libs/aimeat-ui-forms.js"></script>`

### aimeat-ui-layout — Responsive layout components with container queries for AIMEAT apps. 8 layouts + Header + Footer.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-ui-layout/libs/aimeat-ui-layout.js"></script>`

### aimeat-ui-nav — Navigation components: tabs, breadcrumbs, sidebar, bottom nav, and burger menu for AIMEAT apps.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-ui-nav/libs/aimeat-ui-nav.js"></script>`

### aimeat-ui-viewers — Content display components: carousels, grids, lists, galleries, data tables, and timelines for AIMEAT apps.
- **Exports:** none

- **Load:** `<script src="/v1/cortex/aimeat-ui-viewers/libs/aimeat-ui-viewers.js"></script>`

### prh-yritystieto-data — PRH-yritystietojen tietokerros — hakee, seuraa ja vertailee yritysdataa
- **Exports:** none

- **Load:** `<script src="/v1/cortex/prh-yritystieto-data/libs/prh-yritystieto-data.js"></script>`

When the blueprint's cortex component needs an existing library, add a "uses" field listing the library names:
{ "id": "cortex-1", "type": "cortex", "label": "...", "produces": [...], "consumes": [...], "uses": ["aimeat-charts"] }

The generated cortex should load the used library via <script> and call its API (e.g., AIMEAT.charts.ChartBuilder) — NOT reimplement chart rendering.

Analyze this request and produce a JSON blueprint listing ALL components needed.

CRITICAL: Return ONLY a JSON object with "architecture", "components", "phases", "dataModel", and optionally "settings" and "testScenarios". Nothing else.
Each component has these fields: "id", "type", "label", "produces", "consumes". Extension components may also have "schedules".
Do NOT include manifest content, code, HTML, translations, or any implementation details.
The blueprint is a lightweight plan — actual content is generated later per component.

Format:
{
  "architecture": "cortex-modular",
  "components": [
    { "id": "csm-1", "type": "csm", "label": "Human-readable name", "produces": ["memory:service.schema"], "consumes": [] },
    { "id": "memory-1", "type": "memory", "label": "Human-readable name", "produces": ["memory:settings.config"], "consumes": [] },
    { "id": "translation-1", "type": "translation", "label": "Human-readable name (fi)", "produces": ["memory:i18n.fi"], "consumes": [] },
    { "id": "translation-2", "type": "translation", "label": "Human-readable name (en)", "produces": ["memory:i18n.en"], "consumes": [] },
    { "id": "ext-1", "type": "extension", "label": "Human-readable name", "produces": ["memory:items.*"], "consumes": ["memory:settings.config"], "schedules": [{"action":"init","cron":"@activate"},{"action":"collect","cron":"0 2 * * *"}] },
    { "id": "cortex-data", "type": "cortex", "subtype": "data", "label": "Data layer", "produces": ["api:getData", "api:search"], "consumes": ["memory:items.*", "memory:settings.config"], "uses": [] },
    { "id": "cortex-feature-search", "type": "cortex", "subtype": "feature", "label": "Search feature", "produces": ["ui:searchView"], "consumes": ["api:getData", "api:search"], "uses": ["aimeat-ui-viewers", "aimeat-ui-forms"] },
    { "id": "cortex-feature-settings", "type": "cortex", "subtype": "feature", "label": "Settings feature", "produces": ["ui:settingsView"], "consumes": ["api:getSettings", "api:saveSettings"], "uses": ["aimeat-ui-forms"] },
    { "id": "cortex-app", "type": "cortex", "subtype": "app-domain", "label": "App domain", "produces": ["api:init", "api:render"], "consumes": ["ui:searchView", "ui:settingsView"], "uses": ["aimeat-ui-nav"] },
    { "id": "app-1", "type": "app", "label": "Human-readable name", "produces": [], "consumes": ["api:init", "api:render"] }
  ],
  "phases": [
    { "id": "define", "label": "Define Service", "componentIds": ["csm-1"] },
    { "id": "seed", "label": "Seed Data", "componentIds": ["memory-1", "translation-1", "translation-2"] },
    { "id": "logic", "label": "Capabilities", "componentIds": ["ext-1"] },
    { "id": "cortex-data", "label": "Data Layer", "componentIds": ["cortex-data"] },
    { "id": "cortex-features", "label": "Feature Components", "componentIds": ["cortex-feature-search", "cortex-feature-settings"] },
    { "id": "cortex-app", "label": "App Domain", "componentIds": ["cortex-app"] },
    { "id": "ui", "label": "Application", "componentIds": ["app-1"] }
  ],
  "dataModel": {
    "structures": {
      "Item": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "status": { "type": "string" },
          "createdAt": { "type": "string", "description": "ISO 8601" }
        }
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "totalResults": { "type": "number" },
          "items": { "type": "array", "items": { "$ref": "Item" } }
        }
      }
    },
    "memoryKeys": {
      "settings.config": {
        "type": "object",
        "properties": { "locale": { "type": "string" }, "refreshHours": { "type": "number" } },
        "source": "config",
        "producedBy": "memory-1",
        "consumedBy": ["ext-1", "cortex-data"]
      },
      "items.data": {
        "type": "array",
        "items": { "$ref": "Item" },
        "source": "external",
        "producedBy": "ext-1",
        "consumedBy": ["cortex-data"]
      }
    },
    "actions": {
      "ext:search": { "input": { "query": { "type": "string" } }, "output": { "$ref": "SearchResult" } },
      "ext:getItem": { "input": { "id": { "type": "string" } }, "output": { "$ref": "Item" } },
      "cortex:search": { "input": { "query": { "type": "string" } }, "output": { "$ref": "SearchResult" } },
      "cortex:getItem": { "input": { "id": { "type": "string" } }, "output": { "$ref": "Item" } }
    }
  }
}

CRITICAL RULES for the data model:
- Build "structures" from the interview's sampleEntry data — these are the REAL shapes from the verified API response.
- Every memory key and every action input/output MUST use "$ref" to reference a structure. This prevents data shape drift between components.
- Every "$ref" value MUST match a key in the "structures" object.
- Extension actions and cortex actions that pass through the same data MUST reference the SAME structure.

NOTE: The example above uses GENERIC placeholder names. Replace with domain-specific names from YOUR project. Do NOT copy these placeholder names.

Rules:
- Component types: csm, msm, extension, app, memory, translation, cortex
- IDs use format: {type}-{number} (e.g., csm-1, ext-1, app-1). ID prefixes can be short (ext-1) but the "type" field MUST be the full name: "extension" (not "ext").
- Each component object has these fields: "id", "type", "label", "produces", "consumes"
- Extension components may also have "schedules": array of { "action": "action-id", "cron": "cron-expression" }
- Valid cron values: standard 5-field cron syntax OR the special value "@activate"
- CRITICAL: cron expressions MUST have exactly 5 fields separated by spaces.
  CORRECT examples (copy these exactly):
    "0 2 * * *"       ← every day at 02:00
    "*/15 * * * *"    ← every 15 minutes (note: ASTERISK-SLASH-15, not just /15)
    "10 2 * * *"      ← every day at 02:10
  WRONG: "/15 * * * *" (missing leading asterisk), "0 2 * *" (only 4 fields)
  Every asterisk character (*) is REQUIRED — do NOT omit them.
- "@activate" means: runs on extension activation AND on every server restart. Use for init/bootstrap jobs that populate initial data, verify data integrity, or recover from missed scheduled runs. These MUST be idempotent (safe to run repeatedly).
- Use "schedules" for any work that must happen automatically without a browser (data collection, nightly aggregation, periodic computation)
- If an extension collects or computes data, it SHOULD have an "@activate" init job that checks for stale/missing data and populates it — this solves the cold-start problem (empty data after first install or server restart)
- If an extension has NO scheduled actions, omit the "schedules" field entirely
- "produces": array of data outputs (format: "memory:namespace.*" for memory, "api:methodName" for cortex/app)
- "consumes": array of data inputs this component reads from
- Group components into logical phases
- Include ALL components needed for a complete, working service
- Cortex components are layered: data cortex phase → feature cortex phase → app-domain cortex phase → app phase
- Data cortex: pure data access, wraps extension + AIMEAT platform libraries
- Feature cortex: self-contained data+UI per use case, uses data cortex + platform UI cortex libraries
- App-domain cortex: composes all features + auth + translations, entry point for app
- ALWAYS create at least 3 cortex components: one data, one or more feature, one app-domain
- Only include what's actually needed — don't pad with unnecessary components
- TRANSLATIONS: Create ONE translation component PER locale. If the spec has locales ["fi", "en"], create translation-1 for fi AND translation-2 for en. NEVER combine multiple locales into one component.
- MSM: Only create an MSM if the external API requires authentication, API keys, or complex endpoint configuration. Public URLs (RSS feeds, open APIs, open data) do NOT need an MSM — extensions fetch them directly with ctx.fetch().
- MEMORY: Create memory components for (a) static/seed data provided by the user (lookup tables, reference datasets) and (b) default settings/configuration that the service needs on first run. Pre-populating defaults in memory means the app works immediately without hardcoded fallbacks.
- MEMORY KEY NAMING: Use consistent namespace prefixes. Standard conventions: "settings.config" for config, "i18n.{locale}" for translations, descriptive namespace for domain data (e.g., "orders.by-date.*", "stats.daily.*", "scores.by-date.*"). Use dot-separated namespaces, not nested objects.
- CORTEX: Cortex is the application's brain. It handles data access, UI rendering, business logic, and composition. It can use AIMEAT platform libraries (data, storage, social, wallet) directly. It can render UI components (like aimeat-charts renders charts). Apps use cortex for everything.

## dataModel — Domain Data Model (REQUIRED)

The dataModel is a JSON Schema based map of EVERY memory key in the project. It is the single source of truth — all components reference it to know exact data shapes.

Rules for dataModel:
- One entry per memory key pattern (use YYYY-MM-DD for date-bucketed keys)
- Each entry uses JSON Schema (draft 2020-12): type, properties, items, enum, description
- "source": one of "static" (user-provided data), "external" (fetched from API/feed), "computed" (calculated by extension), "config" (default settings)
- "producedBy": exactly ONE component ID that writes this key
- "consumedBy": array of component IDs that read this key
- Every memory key referenced in produces/consumes MUST have a dataModel entry
- Prefer fewer, larger keys. A lookup table or dataset should be ONE key (array/object), not split per entry.
- Include i18n keys (i18n.fi, i18n.en) with source "static" and translation component as producer
- CRITICAL: If the interview provides static data in a specific format (e.g., array of {key, value} pairs), the dataModel schema MUST match that exact format. Do NOT redesign the data shape — the memory component will store it as-is.
- Enum values: use consistent naming across all components. For computed enums (trends, levels, statuses), use lowercase English: "rising"/"falling"/"steady" for trends, "low"/"medium"/"high"/"critical" for levels. If source data uses locale-specific values, keep those as-is in storage — do NOT translate them. Store what the source provides.
- Field names inside stored objects MUST use English camelCase (e.g., "itemCount", "categoryBreakdown", "statusSplit"). This applies even if the source data uses different naming.

## Data Pipeline Verification (do this BEFORE listing components)

For each VIEW in the spec, trace the data path:
1. What fields does this view need to render?
2. Where does each field come from? (external source, computed, user input)
3. Does the source provide this field directly, or does a component need to transform/enrich it?
4. If a field has no clear path from source to view, add a component that produces it.

Example: A view needs enriched data. If the source only provides raw identifiers,
the blueprint must include a component that enriches them (e.g., resolving IDs to names, adding computed scores).

## Component Dependencies

Each component MUST declare what it produces and consumes:
- Extensions produce memory keys (e.g., "memory:items.by-date.*")
- Cortex libraries produce API methods (e.g., "api:getItems") and consume memory keys
- Apps consume API methods or memory keys
- Every "consumes" entry must be matched by a "produces" entry in another component

## CRITICAL: Extension vs Cortex vs App — Decision Framework

### The One Rule: EXTENSION = SERVER-ONLY WORK
An extension MUST do something that a browser CANNOT do. If a browser can do it, it MUST NOT be an extension.

**Use extension when ALL of these are true:**
1. The work REQUIRES the server (external API behind CORS/auth, scheduled cron, server-to-server calls)
2. The work must happen even when NO browser is open
3. There is no way to achieve it with client-side JS + AIMEAT.data API

**Scheduled work ALWAYS belongs to the extension** — the extension manifest declares `schedules` entries and AIMEAT's built-in scheduler runs them automatically. The cortex and app NEVER schedule or trigger recurring background work.

**Everything else is cortex or app:**
- Reading/filtering/transforming data already in memory → cortex
- User preferences, settings, i18n → cortex (wraps memory reads/writes into clean methods)
- Computed/derived values (math, sorting, grouping, statistics) → cortex
- Export/download generation (CSV, JSON, PDF) → app (browser generates the file)
- Display formatting, UI logic → app (calls cortex methods, never reads memory directly)

### Quick Test (apply to EVERY proposed extension)
Ask: "Does this action fetch from an external server or run on a schedule?"
- YES → extension ✓
- NO → it should be cortex (data logic) or app (UI/interaction) ✗

### Static Data → Memory Component
If a dataSource has type "user-input" with a "staticData" array, you MUST create a memory component for it.
This memory component will pre-load the static data into memory so extensions and cortex can read it.
The memory component's "produces" should reflect the memory key pattern (e.g., "memory:catalog.data").
Place it in an early phase (before extensions that consume it).

### Common Mistakes to Avoid
- Do NOT create an "export" extension — apps generate files client-side
- Do NOT create a "settings" extension — settings go in a memory component (defaults) and cortex (read/write methods)
- Do NOT create a "query" or "filter" extension — cortex reads memory directly
- Do NOT create a "compute" extension for math/stats — cortex/app does client-side math

### How They Work Together
- Extension: fetches external data → stores in memory (scheduled, server-side)
- Cortex: reads extension memory → transforms → exposes clean domain API (client-side library)
- App: calls cortex methods → renders UI → handles user interaction (client-side HTML/JS)

## Cortex-Modular Architecture

Structure the service using layered cortex components:

1. **Extensions** at the bottom — external API calls, scheduled background jobs
2. **Data Cortex** (subtype: "data") — unifies extension data + AIMEAT platform data into a single data access interface. ALWAYS create when there are extensions. Can also use AIMEAT.data, AIMEAT.storage, AIMEAT.social directly.
3. **Feature Cortex** (subtype: "feature") — one per use case or feature group. Self-contained data+UI module. Uses data cortex for data, platform UI cortex libraries for rendering. Exports render(container). Like aimeat-charts which renders complete chart components.
4. **App-Domain Cortex** (subtype: "app-domain") — composes all feature cortex components + auth + translations + settings. Single entry point for the app. ALWAYS the last cortex component.
5. **App** — loads app-domain cortex, wires up navigation, handles responsive layout.

Cortex components have a "subtype" field: "data", "feature", or "app-domain".
Cortex phases are ordered: data first, then all features, then app-domain last.
Feature cortex "uses" field lists platform cortex libraries it needs (aimeat-ui-viewers, aimeat-charts, etc.).

## Settings

Inherit settings from the InterviewSpec (if provided):

"settings": {
  "service": [/* from interviewSpec.externalServices[].requiredSettings — add "sharing" ("shared" or "per-user") from the parent externalService's sharingModel, and "required": true/false based on whether the service can function without it */],
  "user": [/* from interviewSpec.userSettings — carry over key, type, label, default */]
}

Each service setting gets a "sharing" field ("shared" or "per-user") from the InterviewSpec's externalServices.
Add "required": true/false based on whether the service can function without it.
The blueprint may refine defaults but must not invent new settings not in the InterviewSpec.
If no InterviewSpec is provided or it has no settings, omit the "settings" field.

## Test Scenarios

For each component that produces data, generate test scenarios:

"testScenarios": [
  {
    "component": "component-id",
    "scenarios": [
      {
        "action": "action-name",
        "input": { "key": "value" },
        "expect": "natural language description of expected result",
        "type": "memory | external-api"
      }
    ]
  }
]

IMPORTANT — classify each scenario's "type":
- "memory": action uses ONLY ctx.memory (no ctx.fetch to external URLs). Tests MUST assert specific return values.
- "external-api": action calls ctx.fetch to a third-party API. Tests check response SHAPE only (has data OR error message), because the external API may be down or rate-limited. Graceful error handling is correct behavior.

Examples:
  { "action": "init", "input": {}, "expect": "Initializes data structures", "type": "memory" }
  { "action": "search", "input": { "query": "test" }, "expect": "Returns matching items from external API", "type": "external-api" }
  { "action": "removeItem", "input": { "id": "abc-123" }, "expect": "Removes entry from memory", "type": "memory" }

Be concrete: use real-world example values (e.g., symbol "AAPL" for stock data, city "Helsinki" for weather).
Test the happy path — the test system handles error paths.
Include testScenarios at the top level of the output JSON (alongside "components", "phases", "dataModel").