@bicon/mcp-composer 0.1.7 → 0.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.
Files changed (3) hide show
  1. package/README.md +79 -24
  2. package/dist/index.mjs +473 -69
  3. package/package.json +8 -2
package/README.md CHANGED
@@ -24,10 +24,54 @@ via API keys is a separate, security-reviewed phase.
24
24
  | Name | What it does |
25
25
  |---|---|
26
26
  | resource `initiative://schema` / tool `get_initiative_schema` | The field catalog: required fields, enum values, the four dimensions with their named sub-dimensions (EN/DE labels + help text, plus the wording the app uses in digital / innovation portfolios), the exact shape of every optional section, the EU-AI-Act vocabularies, and the handoff steps. **Read this first.** |
27
- | `assemble_initiative` | Compact spec (metadata + named sub-ratings 0–100 + confidence per dimension + optional sections) → fully-formed initiative validated against the API contract (`CreateInitiativeBody` from `@workspace/api-zod`). Mints id/timestamps, **derives** the four dimension scores (the app's own scoring function), returns `{ok, initiative, derivedScores, warnings, errors}`. |
27
+ | `assemble_initiative` | Compact spec (metadata + named sub-ratings 0–100 + confidence per dimension + optional sections, optionally the organisation's `context`) → fully-formed initiative validated against the API contract (`CreateInitiativeBody` from `@workspace/api-zod`). Mints the initiative id and every nested row id (decision-log entries, status reports, incidents — a client never invents ids), **derives** the four dimension scores (the app's own scoring function), accepts dates as dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd, returns `{ok, initiative, derivedScores, warnings, errors, contextId}`. Pass `id` (an existing initiative's) only to **update** that initiative. |
28
28
  | `export_initiative_file` | Takes the **same compact spec**, assembles it and wraps the result in the app's single-initiative JSON envelope; returns `{ok, filename, json, importInstructions, derivedScores, warnings}`. There is one pipeline — a hand-edited initiative object is not an input, so client-supplied scores or unknown keys can never bypass assembly. |
29
+ | `read_portfolio_context` | Takes the organisation's **portfolio-context file** (see below) and returns the interview plan for that organisation — its own labels, help texts and rating notes per sub-dimension, which sub-dimensions are switched off, its business units, categories, tags, skills (id + name) and non-archived initiatives (id + name), currency, discount rate, whether the EU-AI-Act module is on — plus a short **contextId** that stands in for the file in the two other tools. |
29
30
  | `composer_version` | Returns `{composerVersion, capabilities}`. Every *successful* tool result also carries `composerVersion`. |
30
31
 
32
+ ### The portfolio context — how the Composer learns an organisation's settings
33
+
34
+ The Composer holds no credentials and calls no API, so on its own it cannot
35
+ know how an organisation configured its portfolio. The app therefore exports
36
+ a small **portfolio-context** JSON (Import/Export → the AI-assistant card →
37
+ *Download portfolio context*): the active portfolio mode, which evaluation
38
+ sliders are switched off, the organisation's own slider wording and rating
39
+ notes, business units, categories, tags, the skill roster and the live
40
+ initiatives (id + name), currency, discount rate and the AI-Act module switch.
41
+ The user hands that file to the assistant at the start; the assistant calls
42
+ `read_portfolio_context` once and then passes the returned **contextId**
43
+ (`ctx_<16 hex>`, a content hash — or the whole file again) as `context` to
44
+ `assemble_initiative` / `export_initiative_file`, so a 5–40 KB document is
45
+ not re-emitted three times per initiative. The id resolves against a
46
+ per-process content cache; an unknown id (server restarted) is a clear error
47
+ naming the fix, never a wrong answer. Effects:
48
+
49
+ - the interview uses the organisation's wording (org label › mode overlay ›
50
+ base) and skips switched-off sub-dimensions;
51
+ - switched-off sub-dimensions are optional in the spec, stored as 0 (the app's
52
+ blank default — a value sent anyway is zeroed with a warning) and
53
+ **excluded from the dimension score** — the app's own derivation, so the
54
+ score does not change on the first save in Evaluate; a dimension with every
55
+ sub-dimension off needs only `{ confidence }`;
56
+ - `businessUnit` and `aiCategory` **must be on the organisation's lists** (no
57
+ inventing — matched case-insensitively and normalised to the organisation's
58
+ spelling; new ones are created in the app's Settings); `dependsOn`,
59
+ `requiredSkills[].skillId` and an update `id` must be ids that exist (any
60
+ id format; Retired initiatives count, archived ones do not);
61
+ - ratings must be on the organisation's **slider step** (Settings › Slider
62
+ granularity: 5, 10, 20 or 25) — the plan states it, a value off the grid is
63
+ rejected with the step in the message (files exported before 2026-08-21
64
+ carry no step and are not checked);
65
+ - every EU-AI-Act field the app strips is warned about while the
66
+ organisation's AI-Act module is off; the export file defaults to the
67
+ context's mode and language; a file newer than the Composer knows is flagged.
68
+
69
+ Without the file everything falls back to the built-in catalog: business unit
70
+ and category are free text, dependency / update ids must at least be UUID-shaped
71
+ (zod's rule, the same the API uses), and scores are derived over all 30
72
+ sub-dimensions. Export the file again after changing settings — the plan
73
+ reports its export date.
74
+
31
75
  ### How validation works (and why it lives at the wire)
32
76
 
33
77
  The MCP SDK validates every call with the registered schema **and hands the
@@ -35,9 +79,9 @@ handler the parsed value** — whatever the schema strips, the server never sees
35
79
  So the spec schema is registered as a full **strict** object, with every
36
80
  optional section a **deep-strict copy of the API contract**: an unknown key
37
81
  anywhere is rejected by the SDK with its exact path
38
- (`Unrecognized key(s) in object: 'literacy' at aiAct`), enum values, date
39
- formats (`YYYY-MM-DD`, real calendar dates), blank names and non-UUID ids are
40
- caught with field paths, and the advertised JSON Schema
82
+ (`Unrecognized key(s) in object: 'literacy' at aiAct`), enum values, dates
83
+ (dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd — real calendar dates, normalised to
84
+ yyyy-mm-dd) and blank names are caught with field paths, and the advertised JSON Schema
41
85
  (`additionalProperties: false`, no `$ref` pointers) means what it says. A
42
86
  section may also be sent as a **JSON string** (clients working from a stale
43
87
  tool descriptor did that); it is parsed and validated identically. What a JSON
@@ -45,7 +89,11 @@ Schema cannot express — the 30 named ratings, the AI-Act controlled
45
89
  vocabularies (`aiActVocabularies` in the descriptor; the app drops unknown
46
90
  obligation keys and never shows unknown artifact keys), the score derivation —
47
91
  is checked in the assembler, which reports **all** problems of a call at once
48
- with the allowed values in the message.
92
+ with the allowed values in the message (id shapes included). Row ids of
93
+ decision-log entries, status reports and incidents are minted by the
94
+ Composer — a client-supplied UUID is kept, anything else replaced — because
95
+ the app keys those rows per initiative and a re-used id from another
96
+ initiative is exactly what collided on import once.
49
97
 
50
98
  Errors the SDK raises before a handler runs (`Input validation error: …`) carry
51
99
  no `composerVersion` — that is normal. A *successful* result without it means an
@@ -73,9 +121,9 @@ The Composer imports the app's own domain leaf, `@workspace/initiative-model`
73
121
  (types and value lists incl. the AI-Act vocabularies, the score derivation, the
74
122
  portfolio-mode wording overlays, the backup-format version) — the same package
75
123
  the web app renders and validates with — and derives its enum lists from the
76
- generated API contract. The only hand-listed part is the 30 sub-dimension keys
77
- with the app's base wording; `catalog-drift.test.ts` pins them to the
78
- contract in both directions. `server.test.ts` drives the real server through
124
+ generated API contract. The 30 sub-dimension keys are the shared leaf's too;
125
+ the only hand-listed part is their base wording, and `catalog-drift.test.ts`
126
+ pins the keys to the contract in both directions. `server.test.ts` drives the real server through
79
127
  the real SDK client over an in-memory transport, so what is asserted is what a
80
128
  client sees.
81
129
 
@@ -144,7 +192,7 @@ For Claude Desktop, build and install the extension instead.
144
192
  from `package.json` — the checked-in `mcpb/manifest.json` carries
145
193
  placeholders), adds the single self-contained bundle, and — before zipping —
146
194
  boots the staged server and drives it through the protocol: `initialize`
147
- (version), `tools/list` (four tools, no `$ref`), `composer_version`,
195
+ (version), `tools/list` (five tools, no `$ref`), `composer_version`,
148
196
  `resources/list`, and an `assemble_initiative` call with an unknown AI-Act key
149
197
  that must be rejected by name. It then copies the `.mcpb` to
150
198
  `artifacts/bicon-portfolio/public/downloads/` and writes
@@ -153,22 +201,29 @@ card imports. Requires `python3` for the zip container.
153
201
 
154
202
  ## Using it (the interview → import flow)
155
203
 
156
- 1. Ask Claude: *"Help me set up a new initiative in BICon."* It reads
157
- `initiative://schema` and interviews you — metadata first, then the 30
158
- sub-dimensions with the app's own help text (ask which portfolio mode you
159
- work in; digital and innovation portfolios use different wording for some
160
- of them), then a confidence per dimension.
161
- 2. Claude calls `assemble_initiative` and shows you the **derived scores** and
204
+ 1. (Recommended) In the app, Import/Export → *Download portfolio context*,
205
+ and give that file to Claude first — it then interviews with your
206
+ organisation's own wording, skips switched-off sliders and only accepts
207
+ your business units, categories, skills and initiatives.
208
+ 2. Ask Claude: *"Help me set up a new initiative in BICon."* It reads
209
+ `initiative://schema` (and your context) and interviews you — metadata
210
+ first, then the sub-dimensions with the app's help text, then a confidence
211
+ per dimension. Dates are dd/mm/yyyy in conversation.
212
+ 3. Claude calls `assemble_initiative` and shows you the **derived scores** and
162
213
  any warnings as a sanity check (fix and re-run if anything looks off).
163
- 3. Claude calls `export_initiative_file` with the same spec → save the returned
164
- JSON under the suggested filename (`initiative-<name>-<date>.json`).
165
- 4. In the app (signed in to the target organisation): if the initiative carries
166
- EU-AI-Act data, make sure the portfolio is in **AI mode** (the app strips
167
- AI-Act fields on import when it is not — the file's `sourceMode` is
168
- informational and cannot override that). Then **Import/Export → Single
169
- initiative** card → drop the file → the preview should show **0 warnings**
170
- → **Add as new** → **Confirm**.
171
- 5. The initiative appears in the Inventory with exactly the scores Claude
214
+ 4. Claude calls `export_initiative_file` with the same spec → save the returned
215
+ JSON under the suggested filename (`initiative-<name>-<date>.json`). To
216
+ **update** an existing initiative, name it — Claude passes its id from the
217
+ context and the app offers "Update"; a re-export without the id is
218
+ recognised by name and offered as an update too.
219
+ 5. In the app (signed in to the target organisation): if the initiative carries
220
+ EU-AI-Act data, make sure the organisation's **EU-AI-Act module is switched
221
+ on** (Settings › AI Act — the app strips AI-Act fields on import while it is
222
+ off; the file's `sourceMode` is informational and cannot override that).
223
+ Then **Import/Export → Single initiative** card → drop the file → the
224
+ preview should show **0 warnings** → **Add as new** (or **Update**, which
225
+ keeps the existing initiative's id, creation date and links) → **Confirm**.
226
+ 6. The initiative appears in the Inventory with exactly the scores Claude
172
227
  reported.
173
228
 
174
229
  ## Publishing (maintainers)
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
 
14
14
  // src/version.ts
15
- var COMPOSER_VERSION = true ? "0.1.7" : readPackageVersion();
15
+ var COMPOSER_VERSION = true ? "0.3.2" : readPackageVersion();
16
16
 
17
17
  // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
18
18
  var external_exports = {};
@@ -4205,6 +4205,8 @@ var ListSnapshotsResponseItem = objectType({
4205
4205
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
4206
4206
  "aiCategory": stringType(),
4207
4207
  "tags": arrayType(stringType()),
4208
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
4209
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
4208
4210
  "archived": booleanType(),
4209
4211
  "evaluation": objectType({
4210
4212
  "valueScale": objectType({
@@ -4455,6 +4457,8 @@ var CreateSnapshotBody = objectType({
4455
4457
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
4456
4458
  "aiCategory": stringType(),
4457
4459
  "tags": arrayType(stringType()),
4460
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
4461
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
4458
4462
  "archived": booleanType(),
4459
4463
  "evaluation": objectType({
4460
4464
  "valueScale": objectType({
@@ -4710,6 +4714,8 @@ var RenameSnapshotResponse = objectType({
4710
4714
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
4711
4715
  "aiCategory": stringType(),
4712
4716
  "tags": arrayType(stringType()),
4717
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
4718
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
4713
4719
  "archived": booleanType(),
4714
4720
  "evaluation": objectType({
4715
4721
  "valueScale": objectType({
@@ -4956,6 +4962,8 @@ var ListInitiativesResponseItem = objectType({
4956
4962
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
4957
4963
  "aiCategory": stringType(),
4958
4964
  "tags": arrayType(stringType()),
4965
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
4966
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
4959
4967
  "archived": booleanType(),
4960
4968
  "evaluation": objectType({
4961
4969
  "valueScale": objectType({
@@ -5198,6 +5206,8 @@ var CreateInitiativeBody = objectType({
5198
5206
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
5199
5207
  "aiCategory": stringType(),
5200
5208
  "tags": arrayType(stringType()),
5209
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
5210
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
5201
5211
  "archived": booleanType(),
5202
5212
  "evaluation": objectType({
5203
5213
  "valueScale": objectType({
@@ -5448,6 +5458,8 @@ var GetInitiativeResponse = objectType({
5448
5458
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
5449
5459
  "aiCategory": stringType(),
5450
5460
  "tags": arrayType(stringType()),
5461
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
5462
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
5451
5463
  "archived": booleanType(),
5452
5464
  "evaluation": objectType({
5453
5465
  "valueScale": objectType({
@@ -5692,6 +5704,8 @@ var UpdateInitiativeBody = objectType({
5692
5704
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
5693
5705
  "aiCategory": stringType(),
5694
5706
  "tags": arrayType(stringType()),
5707
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
5708
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
5695
5709
  "archived": booleanType(),
5696
5710
  "evaluation": objectType({
5697
5711
  "valueScale": objectType({
@@ -5939,6 +5953,8 @@ var UpdateInitiativeResponse = objectType({
5939
5953
  "solutionType": enumType(["Internal", "External", "Hybrid"]),
5940
5954
  "aiCategory": stringType(),
5941
5955
  "tags": arrayType(stringType()),
5956
+ "docsUrl": stringType().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
5957
+ "pmUrl": stringType().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
5942
5958
  "archived": booleanType(),
5943
5959
  "evaluation": objectType({
5944
5960
  "valueScale": objectType({
@@ -8178,6 +8194,15 @@ var MODES = {
8178
8194
  defaultCompanyName: "Halcyon Biosciences AG"
8179
8195
  }
8180
8196
  };
8197
+ function getModeOverride(mode, language, key) {
8198
+ const md = MODES[mode];
8199
+ if (!md) return void 0;
8200
+ const dict = md.overrides[language];
8201
+ if (dict && key in dict) return dict[key];
8202
+ const enDict = md.overrides.en;
8203
+ if (enDict && key in enDict) return enDict[key];
8204
+ return void 0;
8205
+ }
8181
8206
  var AI_GATE_ITEMS = [
8182
8207
  { id: "businessCase", label: { en: "Business case validated", de: "Business Case validiert" } },
8183
8208
  { id: "dpia", label: { en: "DPIA / risk assessment signed", de: "DSFA / Risikobewertung unterzeichnet" } },
@@ -8211,6 +8236,59 @@ var DEFAULT_GATE_CHECKLISTS_BY_MODE = {
8211
8236
  innovation: buildDefaultsForMode(INNOVATION_GATE_ITEMS)
8212
8237
  };
8213
8238
 
8239
+ // ../../initiative-model/src/dimensions.ts
8240
+ var DIMENSION_KEYS = ["valueScale", "feasibility", "governance", "strategic"];
8241
+ var VALUE_SUBS = [
8242
+ "frequencyOfProcess",
8243
+ "labourIntensity",
8244
+ "operationalLeverage",
8245
+ "scalability",
8246
+ "volumeSensitivity",
8247
+ "costReduction",
8248
+ "revenueImpact"
8249
+ ];
8250
+ var FEASIBILITY_SUBS = [
8251
+ "dataAvailability",
8252
+ "dataQuality",
8253
+ "modelRobustness",
8254
+ "reproducibility",
8255
+ "explainability",
8256
+ "technologyMaturity",
8257
+ "integrationFeasibility"
8258
+ ];
8259
+ var GOVERNANCE_SUBS = [
8260
+ "regulatoryExposure",
8261
+ "humanOversight",
8262
+ "decisionCriticality",
8263
+ "reputationalRisk",
8264
+ "operationalDependency",
8265
+ "auditability",
8266
+ "monitoringComplexity",
8267
+ "cybersecurityExposure"
8268
+ ];
8269
+ var STRATEGIC_SUBS = [
8270
+ "strategicDifferentiation",
8271
+ "qualityImprovement",
8272
+ "operatingModelAlignment",
8273
+ "transformationPotential",
8274
+ "competitiveAdvantage",
8275
+ "clientImpact",
8276
+ "organisationalLeverage",
8277
+ "innovationCapability"
8278
+ ];
8279
+ var DIMENSION_SUBS = {
8280
+ valueScale: VALUE_SUBS,
8281
+ feasibility: FEASIBILITY_SUBS,
8282
+ governance: GOVERNANCE_SUBS,
8283
+ strategic: STRATEGIC_SUBS
8284
+ };
8285
+ var ALL_SLIDER_IDS = [...VALUE_SUBS, ...FEASIBILITY_SUBS, ...GOVERNANCE_SUBS, ...STRATEGIC_SUBS];
8286
+ var SLIDER_QUADRANT = (() => {
8287
+ const out = {};
8288
+ for (const dim of DIMENSION_KEYS) for (const k of DIMENSION_SUBS[dim]) out[k] = dim;
8289
+ return out;
8290
+ })();
8291
+
8214
8292
  // src/catalog.ts
8215
8293
  var enumValues = (e) => Object.values(e);
8216
8294
  var STATUS_VALUES = enumValues(InitiativeStatus);
@@ -8233,7 +8311,6 @@ var AI_ACT_VOCABULARIES = {
8233
8311
  artifactKeys: ACT_ARTIFACT_KEYS,
8234
8312
  obligationKeys: OPERATIONAL_OBLIGATION_KEYS
8235
8313
  };
8236
- var DIMENSION_KEYS = ["valueScale", "feasibility", "governance", "strategic"];
8237
8314
  var sd = (key, en, de, helpEn, helpDe) => ({
8238
8315
  key,
8239
8316
  label: { en, de },
@@ -8475,10 +8552,10 @@ var DIMENSIONS = [
8475
8552
  ]
8476
8553
  }
8477
8554
  ];
8478
- var DIMENSION_SUBS = Object.fromEntries(
8479
- DIMENSIONS.map((d) => [d.key, d.subs.map((s) => s.key)])
8480
- );
8481
8555
  function modeOverlays() {
8556
+ return MODE_OVERLAYS;
8557
+ }
8558
+ function computeModeOverlays() {
8482
8559
  const out = {};
8483
8560
  for (const mode of PORTFOLIO_MODES2) {
8484
8561
  if (mode === "ai") continue;
@@ -8509,14 +8586,15 @@ function modeOverlays() {
8509
8586
  }
8510
8587
  return out;
8511
8588
  }
8589
+ var MODE_OVERLAYS = computeModeOverlays();
8512
8590
  var REQUIRED_FIELDS = [
8513
8591
  { key: "name", type: "string", hint: { en: "Short initiative name", de: "Kurzer Name der Initiative" } },
8514
8592
  { key: "businessUnit", type: "string", hint: { en: "Owning business unit (use the org's own BU names)", de: "Zust\xE4ndiger Gesch\xE4ftsbereich (Namen der Organisation verwenden)" } },
8515
8593
  { key: "owner", type: "string", hint: { en: "Accountable person", de: "Verantwortliche Person" } },
8516
8594
  { key: "description", type: "string", hint: { en: "One or two sentences on what it does and for whom", de: "Ein bis zwei S\xE4tze: was sie tut und f\xFCr wen" } },
8517
8595
  { key: "status", type: "enum", values: STATUS_VALUES, hint: { en: "Lifecycle stage", de: "Lebenszyklus-Phase" } },
8518
- { key: "timelineStart", type: "date", hint: { en: "YYYY-MM-DD", de: "JJJJ-MM-TT" } },
8519
- { key: "timelineEnd", type: "date", hint: { en: "YYYY-MM-DD", de: "JJJJ-MM-TT" } },
8596
+ { key: "timelineStart", type: "date", hint: { en: "dd/mm/yyyy (stored as yyyy-mm-dd)", de: "TT/MM/JJJJ (gespeichert als JJJJ-MM-TT)" } },
8597
+ { key: "timelineEnd", type: "date", hint: { en: "dd/mm/yyyy (stored as yyyy-mm-dd)", de: "TT/MM/JJJJ (gespeichert als JJJJ-MM-TT)" } },
8520
8598
  { key: "budget", type: "number", hint: { en: "Total budget in the org's currency", de: "Gesamtbudget in der Organisationsw\xE4hrung" } },
8521
8599
  { key: "vendorDependency", type: "enum", values: VENDOR_DEPS, hint: { en: "How dependent on external vendors", de: "Abh\xE4ngigkeit von externen Anbietern" } },
8522
8600
  { key: "solutionType", type: "enum", values: SOLUTION_TYPES, hint: { en: "Built internally, bought, or hybrid", de: "Eigenbau, Zukauf oder hybrid" } },
@@ -8524,11 +8602,18 @@ var REQUIRED_FIELDS = [
8524
8602
  ];
8525
8603
  var OPTIONAL_SECTIONS = [
8526
8604
  { key: "tags", hint: { en: "Free-text tags", de: "Freie Schlagworte" } },
8605
+ {
8606
+ key: "docsUrl / pmUrl",
8607
+ hint: {
8608
+ en: "Links to the project documentation (Confluence, SharePoint, \u2026) and the project-management tool (Jira, MS Project, \u2026)",
8609
+ de: "Links zur Projektdokumentation (Confluence, SharePoint, \u2026) und zum Projektmanagement-Werkzeug (Jira, MS Project, \u2026)"
8610
+ }
8611
+ },
8527
8612
  {
8528
8613
  key: "dependsOn",
8529
8614
  hint: {
8530
- en: "UUIDs of initiatives this one depends on. The Composer cannot look ids up \u2014 leave empty and link dependencies in the app unless the user gives you the ids.",
8531
- de: "UUIDs vorgelagerter Initiativen. Der Composer kann keine IDs nachschlagen \u2014 leer lassen und in der App verkn\xFCpfen, sofern der Nutzer keine IDs nennt."
8615
+ en: "Ids of initiatives this one depends on. With the portfolio-context file, pick them from read_portfolio_context \u2192 lists.initiatives (id + name); without it, leave empty and link dependencies in the app.",
8616
+ de: "IDs vorgelagerter Initiativen. Mit der Portfolio-Kontext-Datei aus read_portfolio_context \u2192 lists.initiatives (ID + Name) w\xE4hlen; ohne sie leer lassen und in der App verkn\xFCpfen."
8532
8617
  }
8533
8618
  },
8534
8619
  { key: "aiRiskTier / hitlPosture / dpiaStatus / modelCardStatus", hint: { en: "EU AI Act governance flags", de: "EU-AI-Act-Governance-Flags" } },
@@ -8545,8 +8630,8 @@ var OPTIONAL_SECTIONS = [
8545
8630
  {
8546
8631
  key: "requiredSkills",
8547
8632
  hint: {
8548
- en: "[{skillId, fte}] \u2014 skillId is the UUID of a skill that exists in the org; the Composer cannot look ids up \u2014 leave empty and link skills in the app unless the user gives you the ids.",
8549
- de: "[{skillId, fte}] \u2014 skillId ist die UUID eines vorhandenen Skills; leer lassen und in der App verkn\xFCpfen, sofern der Nutzer keine IDs nennt."
8633
+ en: "[{skillId, fte}] \u2014 skillId is the id of a skill that exists in the organisation. With the portfolio-context file, pick from read_portfolio_context \u2192 lists.skills (id + name); without it, leave empty and link skills in the app.",
8634
+ de: "[{skillId, fte}] \u2014 skillId ist die ID eines vorhandenen Skills. Mit der Portfolio-Kontext-Datei aus read_portfolio_context \u2192 lists.skills (ID + Name) w\xE4hlen; ohne sie leer lassen und in der App verkn\xFCpfen."
8550
8635
  }
8551
8636
  }
8552
8637
  ];
@@ -8594,7 +8679,7 @@ function describeZodShape(schema, depth = 0) {
8594
8679
  }
8595
8680
  function buildSchemaDescriptor(sectionShapes) {
8596
8681
  return {
8597
- about: "BICon initiative composer. Collect the required fields, then rate every sub-dimension 0\u2013100 (higher = more of the named quality) and a confidence per dimension. Dimension scores are DERIVED (mean of subs) \u2014 never ask the user for them. Values are used verbatim; enum values must match exactly; unknown keys are rejected.",
8682
+ about: "BICon initiative composer. Collect the required fields, then rate every sub-dimension 0\u2013100 (higher = more of the named quality) and a confidence per dimension. Dimension scores are DERIVED (mean of subs) \u2014 never ask the user for them. Values are used verbatim; enum values must match exactly; unknown keys are rejected. Dates: talk dd/mm/yyyy with the user (dd/mm/yyyy, dd.mm.yyyy and yyyy-mm-dd are all accepted; stored as yyyy-mm-dd). Row ids of decision-log entries, status reports and incidents are minted by the Composer \u2014 never invent ids. To UPDATE an existing initiative pass its id as `id`; omit it for a new one.",
8598
8683
  requiredFields: REQUIRED_FIELDS,
8599
8684
  enums: {
8600
8685
  status: STATUS_VALUES,
@@ -8621,7 +8706,7 @@ function buildSchemaDescriptor(sectionShapes) {
8621
8706
  // keys, same 0–100 scale) — ask the user which mode their portfolio is in
8622
8707
  // and interview with that mode's wording. Missing entries = unchanged.
8623
8708
  portfolioModes: {
8624
- note: "Ask which portfolio mode the user works in (ai | digital | innovation). Keys and scale never change; only the wording below does. The app keeps EU-AI-Act fields only while it is in AI mode.",
8709
+ note: "Ask which portfolio mode the user works in (ai | digital | innovation) \u2014 or read it from the portfolio-context file. Keys and scale never change; only the wording below does. Whether EU-AI-Act fields are kept depends on the organisation's AI-Act module switch (on by default in AI portfolios, configurable in every mode), not on the mode itself.",
8625
8710
  overlays: modeOverlays()
8626
8711
  },
8627
8712
  optionalSections: OPTIONAL_SECTIONS,
@@ -8636,7 +8721,8 @@ function buildSchemaDescriptor(sectionShapes) {
8636
8721
  ...AI_ACT_VOCABULARIES,
8637
8722
  note: "prohibitedPractices/art50Flags: arrays of these ids. artifacts/obligations: maps keyed by these ids. annexIIICategory: one of these, or omit when no Annex III area applies (record the reasoning in classificationRationale; annexExceptionRationale is only for an Art. 6(3) exception claim). Empty is honest; invented values are rejected."
8638
8723
  },
8639
- handoff: "Call assemble_initiative with the compact spec, review derivedScores (and any warnings) with the user, then call export_initiative_file with the SAME spec \u2192 save the JSON \u2192 in the app: Import/Export \u2192 Single initiative \u2192 drop the file \u2192 'Add as new' \u2192 Confirm. If the initiative carries EU-AI-Act data, the app must be in AI mode when importing."
8724
+ portfolioContext: "Ask the user for the organisation's portfolio-context file first (in the app: Import/Export \u2192 'Download portfolio context'). Call read_portfolio_context on it: it returns the interview plan in the organisation's own wording, the sub-dimensions that are switched off, the slider step the ratings must follow, and the organisation's business units, categories, tags, skills and initiatives \u2014 with the file, business unit and category MUST be from those lists (no inventing) and skill / dependency ids are validated. Pass the same context to assemble_initiative and export_initiative_file. Without the file, interview with the wording below and treat business unit / category as free text.",
8725
+ handoff: "Call assemble_initiative with the compact spec (+ context), review derivedScores (and any warnings) with the user, then call export_initiative_file with the SAME spec (+ context) \u2192 save the JSON \u2192 in the app: Import/Export \u2192 Single initiative \u2192 drop the file \u2192 'Add as new' \u2192 Confirm. If the initiative carries EU-AI-Act data, the organisation's EU-AI-Act module must be switched on (Settings \u203A AI Act) when importing."
8640
8726
  };
8641
8727
  }
8642
8728
 
@@ -8653,24 +8739,48 @@ function contractSection(key) {
8653
8739
  return s instanceof external_exports.ZodOptional ? s.unwrap() : s;
8654
8740
  }
8655
8741
  var keepDescription = (from, to) => from.description ? to.describe(from.description) : to;
8742
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
8743
+ var DMY = /^(\d{1,2})[./](\d{1,2})[./](\d{4})$/;
8744
+ function normaliseDateInput(v) {
8745
+ if (typeof v !== "string") return v;
8746
+ const m = DMY.exec(v.trim());
8747
+ if (!m) return v;
8748
+ const [, d, mo, y] = m;
8749
+ return `${y}-${mo.padStart(2, "0")}-${d.padStart(2, "0")}`;
8750
+ }
8751
+ function isCalendarDate(s) {
8752
+ if (!ISO_DATE.test(s)) return false;
8753
+ const [y, m, d] = s.split("-").map(Number);
8754
+ const dt = new Date(Date.UTC(y, m - 1, d));
8755
+ return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
8756
+ }
8757
+ var dateField = (what) => external_exports.preprocess(normaliseDateInput, external_exports.string().refine(isCalendarDate, { message: `${what} must be a real calendar date \u2014 dd/mm/yyyy or yyyy-mm-dd` })).describe("dd/mm/yyyy (or yyyy-mm-dd)");
8758
+ var DATE_LIKE_KEY = /^date$|Date$|At$/;
8759
+ var MINTED_KEYS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
8760
+ var holdsString = (s) => s instanceof external_exports.ZodString || s instanceof external_exports.ZodOptional && s.unwrap() instanceof external_exports.ZodString || s instanceof external_exports.ZodNullable && s.unwrap() instanceof external_exports.ZodString;
8656
8761
  function deepStrict(schema) {
8657
- const walk = (s) => {
8762
+ const walk = (s, inArrayItem = false) => {
8658
8763
  if (s instanceof external_exports.ZodObject) {
8659
8764
  const next = {};
8660
- for (const [k, v] of Object.entries(s.shape)) next[k] = walk(v);
8765
+ for (const [k, v] of Object.entries(s.shape)) {
8766
+ let field = walk(v, false);
8767
+ if (inArrayItem && MINTED_KEYS.has(k) && !(field instanceof external_exports.ZodOptional)) field = field.optional();
8768
+ if (DATE_LIKE_KEY.test(k) && holdsString(field)) field = keepDescription(field, external_exports.preprocess(normaliseDateInput, field));
8769
+ next[k] = field;
8770
+ }
8661
8771
  return keepDescription(s, external_exports.object(next).strict());
8662
8772
  }
8663
- if (s instanceof external_exports.ZodOptional) return keepDescription(s, walk(s.unwrap()).optional());
8664
- if (s instanceof external_exports.ZodNullable) return keepDescription(s, walk(s.unwrap()).nullable());
8665
- if (s instanceof external_exports.ZodDefault) return keepDescription(s, walk(s.removeDefault()).default(s._def.defaultValue()));
8666
- if (s instanceof external_exports.ZodEffects) return new external_exports.ZodEffects({ ...s._def, schema: walk(s._def.schema) });
8667
- if (s instanceof external_exports.ZodArray) return new external_exports.ZodArray({ ...s._def, type: walk(s.element) });
8668
- if (s instanceof external_exports.ZodRecord) return new external_exports.ZodRecord({ ...s._def, valueType: walk(s.valueSchema) });
8669
- if (s instanceof external_exports.ZodUnion) return new external_exports.ZodUnion({ ...s._def, options: s.options.map(walk) });
8773
+ if (s instanceof external_exports.ZodOptional) return keepDescription(s, walk(s.unwrap(), inArrayItem).optional());
8774
+ if (s instanceof external_exports.ZodNullable) return keepDescription(s, walk(s.unwrap(), inArrayItem).nullable());
8775
+ if (s instanceof external_exports.ZodDefault) return keepDescription(s, walk(s.removeDefault(), inArrayItem).default(s._def.defaultValue()));
8776
+ if (s instanceof external_exports.ZodEffects) return new external_exports.ZodEffects({ ...s._def, schema: walk(s._def.schema, inArrayItem) });
8777
+ if (s instanceof external_exports.ZodArray) return new external_exports.ZodArray({ ...s._def, type: walk(s.element, true) });
8778
+ if (s instanceof external_exports.ZodRecord) return new external_exports.ZodRecord({ ...s._def, valueType: walk(s.valueSchema, inArrayItem) });
8779
+ if (s instanceof external_exports.ZodUnion) return new external_exports.ZodUnion({ ...s._def, options: s.options.map((o) => walk(o, inArrayItem)) });
8670
8780
  if (s instanceof external_exports.ZodIntersection) {
8671
8781
  const { left, right } = s._def;
8672
- if (left instanceof external_exports.ZodObject && right instanceof external_exports.ZodObject) return keepDescription(s, walk(left.merge(right)));
8673
- return new external_exports.ZodIntersection({ ...s._def, left: walk(left), right: walk(right) });
8782
+ if (left instanceof external_exports.ZodObject && right instanceof external_exports.ZodObject) return keepDescription(s, walk(left.merge(right), inArrayItem));
8783
+ return new external_exports.ZodIntersection({ ...s._def, left: walk(left, inArrayItem), right: walk(right, inArrayItem) });
8674
8784
  }
8675
8785
  return s;
8676
8786
  };
@@ -8694,41 +8804,41 @@ var SECTION_SCHEMAS = Object.fromEntries(
8694
8804
  SECTION_KEYS.map((k) => [k, contractSection(k)])
8695
8805
  );
8696
8806
  var SECTION_DESCRIPTIONS = {
8697
- aiAct: "EU AI Act block \u2014 see get_initiative_schema \u2192 sectionShapes.aiAct for the exact keys (role, gpai, prohibitedPractices, art50Flags, annexIIICategory, artifacts, obligations, registration, aiLiteracy, incidents\u2026) and aiActVocabularies for the allowed ids. Unknown keys are rejected.",
8807
+ aiAct: "EU AI Act block \u2014 see get_initiative_schema \u2192 sectionShapes.aiAct for the exact keys (role, gpai, prohibitedPractices, art50Flags, annexIIICategory, artifacts, obligations, registration, aiLiteracy, incidents\u2026) and aiActVocabularies for the allowed ids. Unknown keys are rejected. Incident ids/stamps are minted by the Composer \u2014 leave them out.",
8698
8808
  economics: "Economics block (P10/P50/P90 benefit & cost, ramp, confidence, discount rate) \u2014 see sectionShapes.economics.",
8699
8809
  actuals: "Actuals block (In Production only) \u2014 see sectionShapes.actuals.",
8700
- decisionLog: "Decision log entries \u2014 see sectionShapes.decisionLog.",
8701
- statusReports: "Status reports \u2014 see sectionShapes.statusReports.",
8702
- requiredSkills: "Required skills [{skillId, fte}] \u2014 skillId must be an existing skill's UUID in the target organisation (the Composer cannot look ids up; leave the list empty and link skills in the app when unsure)."
8810
+ decisionLog: "Decision log entries \u2014 see sectionShapes.decisionLog. Entry ids are minted by the Composer \u2014 leave them out. Dates dd/mm/yyyy or yyyy-mm-dd.",
8811
+ statusReports: "Status reports \u2014 see sectionShapes.statusReports. Report ids are minted by the Composer \u2014 leave them out. Dates dd/mm/yyyy or yyyy-mm-dd.",
8812
+ requiredSkills: "Required skills [{skillId, fte}] \u2014 skillId must be the id of a skill that exists in the organisation. With the portfolio context the Composer validates it (read_portfolio_context \u2192 lists.skills gives id + name); without it, leave the list empty and link skills in the app."
8703
8813
  };
8704
- var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
8705
- function isCalendarDate(s) {
8706
- if (!ISO_DATE.test(s)) return false;
8707
- const [y, m, d] = s.split("-").map(Number);
8708
- const dt = new Date(Date.UTC(y, m - 1, d));
8709
- return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
8710
- }
8711
- var isoDate = (what) => external_exports.string().refine(isCalendarDate, { message: `${what} must be a real calendar date in YYYY-MM-DD` }).describe("YYYY-MM-DD");
8814
+ var contextWire = () => external_exports.preprocess(parseIfString, external_exports.union([external_exports.string().regex(/^ctx_[0-9a-f]{16}$/, "a contextId looks like ctx_<16 hex>"), external_exports.object({}).passthrough()]));
8712
8815
  var requiredText = () => external_exports.string().trim().min(1, "must not be empty");
8713
- var UUID_HINT = "must be the UUID of an initiative that exists in the target organisation \u2014 the Composer cannot look ids up; leave the list empty and link dependencies in the app when unsure";
8816
+ var DEPENDS_ON_HINT = "the id of an initiative that exists in the organisation. With the portfolio context the Composer validates it (read_portfolio_context \u2192 lists.initiatives gives id + name); without it, only UUID-shaped ids are accepted \u2014 leave the list empty and link dependencies in the app when unsure";
8714
8817
  var dimensionInput = () => external_exports.object({
8715
- subs: external_exports.record(external_exports.string(), external_exports.number()).describe("Named sub-dimension ratings 0\u2013100 \u2014 every key of the dimension is required, no others"),
8818
+ subs: external_exports.record(external_exports.string(), external_exports.number()).optional().describe(
8819
+ "Named sub-dimension ratings 0\u2013100 \u2014 every ENABLED key of the dimension is required and no other key is accepted; switched-off ones (portfolio context) are left out"
8820
+ ),
8716
8821
  confidence: external_exports.enum(CONFIDENCES)
8717
8822
  }).strict();
8718
8823
  var compactSpecSchema = external_exports.object({
8824
+ id: external_exports.string().trim().min(1).optional().describe(
8825
+ "ONLY to UPDATE an existing initiative: its id (from read_portfolio_context \u2192 lists.initiatives, or the app). The exported file then updates that initiative in place. Omit for a new initiative \u2014 never invent an id."
8826
+ ),
8719
8827
  name: requiredText(),
8720
- businessUnit: requiredText(),
8828
+ businessUnit: requiredText().describe("With a portfolio context: one of the organisation's business units (read_portfolio_context \u2192 lists.businessUnits)"),
8721
8829
  owner: requiredText(),
8722
8830
  description: requiredText(),
8723
8831
  status: external_exports.enum(STATUS_VALUES),
8724
- timelineStart: isoDate("timelineStart"),
8725
- timelineEnd: isoDate("timelineEnd"),
8832
+ timelineStart: dateField("timelineStart"),
8833
+ timelineEnd: dateField("timelineEnd"),
8726
8834
  budget: external_exports.number().finite(),
8727
8835
  vendorDependency: external_exports.enum(VENDOR_DEPS),
8728
8836
  solutionType: external_exports.enum(SOLUTION_TYPES),
8729
- aiCategory: requiredText(),
8837
+ aiCategory: requiredText().describe("With a portfolio context: one of the organisation's categories (read_portfolio_context \u2192 lists.categories)"),
8730
8838
  tags: external_exports.array(external_exports.string().trim().min(1)).optional(),
8731
- dependsOn: external_exports.array(external_exports.string().uuid(`each entry ${UUID_HINT}`)).optional().describe(`Ids of initiatives this one depends on \u2014 each ${UUID_HINT}`),
8839
+ docsUrl: external_exports.string().optional().describe("Link to the project documentation (Confluence, SharePoint, \u2026)"),
8840
+ pmUrl: external_exports.string().optional().describe("Link to the project-management tool (Jira, MS Project, \u2026)"),
8841
+ dependsOn: external_exports.array(external_exports.string().trim().min(1)).optional().describe(`Ids of initiatives this one depends on \u2014 each entry ${DEPENDS_ON_HINT}`),
8732
8842
  evaluation: external_exports.object({
8733
8843
  valueScale: dimensionInput(),
8734
8844
  feasibility: dimensionInput(),
@@ -8748,15 +8858,22 @@ var compactSpecSchema = external_exports.object({
8748
8858
  modelCardStatus: external_exports.enum(MODEL_CARD_STATUSES).optional(),
8749
8859
  modelCardUrl: external_exports.string().optional(),
8750
8860
  prohibitedOverride: external_exports.string().optional(),
8751
- aiAct: wireSection(SECTION_SCHEMAS.aiAct, SECTION_DESCRIPTIONS.aiAct).optional()
8861
+ aiAct: wireSection(SECTION_SCHEMAS.aiAct, SECTION_DESCRIPTIONS.aiAct).optional(),
8862
+ context: contextWire().optional().describe(
8863
+ `OPTIONAL but strongly recommended: the organisation's portfolio context \u2014 the contextId returned by read_portfolio_context ("ctx_\u2026") or the file's JSON itself. With it, switched-off sub-dimensions are skipped in the score, business unit / category must be the organisation's own, and skill / dependency / update ids are validated.`
8864
+ )
8752
8865
  }).strict();
8753
8866
  var exportInputSchema = external_exports.object({
8754
8867
  spec: external_exports.preprocess(parseIfString, external_exports.object({}).passthrough()).describe("The compact spec exactly as accepted by assemble_initiative (same object \u2014 see that tool's schema)"),
8755
8868
  sourceMode: external_exports.enum(PORTFOLIO_MODES2).optional().describe(
8756
- "Portfolio mode the user works in (default ai) \u2014 recorded in the file. The app keeps AI-Act fields only while it is itself in AI mode; this field does not change that."
8869
+ "Portfolio mode recorded in the file (default: the context's mode, else ai). Informational \u2014 whether the app keeps EU-AI-Act fields depends on the organisation's AI-Act module switch, not on this."
8757
8870
  ),
8758
- language: external_exports.enum(LANGUAGES2).optional().describe("Language recorded in the file (default en)"),
8759
- filename: external_exports.string().trim().min(1).optional().describe("Override the suggested filename")
8871
+ language: external_exports.enum(LANGUAGES2).optional().describe("Language recorded in the file (default: the context's language, else en)"),
8872
+ filename: external_exports.string().trim().min(1).optional().describe("Override the suggested filename"),
8873
+ context: contextWire().optional().describe("The organisation's portfolio context (contextId or file), same as for assemble_initiative \u2014 also sets the file's default sourceMode and language.")
8874
+ }).strict();
8875
+ var readContextInputSchema = external_exports.object({
8876
+ context: contextWire().describe("The portfolio-context file's JSON (object or string), exactly as downloaded from the app \u2014 or a contextId from an earlier call")
8760
8877
  }).strict();
8761
8878
  function formatIssues(err) {
8762
8879
  return err.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
@@ -8783,21 +8900,240 @@ function dimensionScore(subcomponents, disabled) {
8783
8900
  return normaliseScore(sum / values.length);
8784
8901
  }
8785
8902
 
8903
+ // src/context.ts
8904
+ import { createHash } from "node:crypto";
8905
+
8906
+ // ../../initiative-model/src/portfolio-context.ts
8907
+ var PORTFOLIO_CONTEXT_KIND = "portfolio-context";
8908
+ var PORTFOLIO_CONTEXT_VERSION = 1;
8909
+
8910
+ // ../../initiative-model/src/ai-act-fields.ts
8911
+ var AI_ACT_FIELDS = [
8912
+ "aiAct",
8913
+ "aiRiskTier",
8914
+ "hitlPosture",
8915
+ "dpiaStatus",
8916
+ "dpiaUrl",
8917
+ "modelCardStatus",
8918
+ "modelCardUrl",
8919
+ "prohibitedOverride"
8920
+ ];
8921
+
8922
+ // src/context.ts
8923
+ var sliderSchema = external_exports.object({
8924
+ key: external_exports.string().min(1),
8925
+ dimension: external_exports.enum(DIMENSION_KEYS),
8926
+ enabled: external_exports.boolean(),
8927
+ label: external_exports.string().optional(),
8928
+ help: external_exports.string().optional(),
8929
+ note: external_exports.string().optional()
8930
+ }).passthrough();
8931
+ var portfolioContextSchema = external_exports.object({
8932
+ kind: external_exports.literal(PORTFOLIO_CONTEXT_KIND),
8933
+ version: external_exports.number().int().min(1),
8934
+ exportedAt: external_exports.string(),
8935
+ companyName: external_exports.string().optional(),
8936
+ portfolioMode: external_exports.enum(PORTFOLIO_MODES2),
8937
+ language: external_exports.enum(LANGUAGES2),
8938
+ currency: external_exports.string(),
8939
+ aiActEnabled: external_exports.boolean(),
8940
+ discountRatePct: external_exports.number(),
8941
+ /** Slider step (5/10/20/25). Files from before 2026-08-21 lack it → no grid check. */
8942
+ sliderGranularity: external_exports.number().int().positive().max(100).optional(),
8943
+ businessUnits: external_exports.array(external_exports.string()),
8944
+ categories: external_exports.array(external_exports.string()),
8945
+ suggestedTags: external_exports.array(external_exports.string()),
8946
+ skills: external_exports.array(external_exports.object({ id: external_exports.string().min(1), name: external_exports.string() }).passthrough()),
8947
+ initiatives: external_exports.array(external_exports.object({ id: external_exports.string().min(1), name: external_exports.string(), status: external_exports.enum(STATUS_VALUES) }).passthrough()),
8948
+ sliders: external_exports.array(sliderSchema)
8949
+ }).passthrough();
8950
+ var CONTEXT_SUPPORTED_VERSION = PORTFOLIO_CONTEXT_VERSION;
8951
+ function versionNotice(ctx) {
8952
+ return ctx.version > CONTEXT_SUPPORTED_VERSION ? `The portfolio-context file is version ${ctx.version}; this Composer understands version ${CONTEXT_SUPPORTED_VERSION}. Newer fields are ignored \u2014 update the Composer (Import/Export \u2192 download the extension again) to be safe.` : void 0;
8953
+ }
8954
+ var CONTEXT_ID_PREFIX = "ctx_";
8955
+ var CACHE_MAX = 32;
8956
+ var cache = /* @__PURE__ */ new Map();
8957
+ function contextIdOf(ctx) {
8958
+ return CONTEXT_ID_PREFIX + createHash("sha256").update(JSON.stringify(ctx)).digest("hex").slice(0, 16);
8959
+ }
8960
+ function remember(ctx) {
8961
+ const id = contextIdOf(ctx);
8962
+ cache.delete(id);
8963
+ cache.set(id, ctx);
8964
+ while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
8965
+ return id;
8966
+ }
8967
+ function resolveContext(raw) {
8968
+ if (typeof raw === "string") {
8969
+ if (raw.startsWith(CONTEXT_ID_PREFIX)) {
8970
+ const hit = cache.get(raw);
8971
+ if (hit) {
8972
+ remember(hit);
8973
+ return { ok: true, context: hit, contextId: raw };
8974
+ }
8975
+ return {
8976
+ ok: false,
8977
+ errors: [
8978
+ `context: unknown contextId "${raw}" \u2014 the Composer has restarted since it was issued, or it belongs to a different file. Call read_portfolio_context again with the file's JSON and use the new contextId (or pass the file itself).`
8979
+ ]
8980
+ };
8981
+ }
8982
+ return { ok: false, errors: ["context: expected the portfolio-context file's JSON (object or JSON string) or a contextId from read_portfolio_context."] };
8983
+ }
8984
+ const parsed = portfolioContextSchema.safeParse(raw);
8985
+ if (!parsed.success) return { ok: false, errors: formatIssues(parsed.error).map((e) => `context.${e}`) };
8986
+ const context = parsed.data;
8987
+ return { ok: true, context, contextId: remember(context) };
8988
+ }
8989
+ function disabledSliders(ctx) {
8990
+ return new Set(ctx.sliders.filter((s) => !s.enabled).map((s) => s.key));
8991
+ }
8992
+ function allowedList(values, max = 12) {
8993
+ if (values.length <= max) return values.join(" | ");
8994
+ return `${values.slice(0, max).join(" | ")} \u2026 (${values.length - max} more \u2014 see read_portfolio_context)`;
8995
+ }
8996
+ var norm = (s) => s.trim().toLowerCase();
8997
+ function canonical(value, list) {
8998
+ const key = norm(value);
8999
+ return list.find((v) => norm(v) === key);
9000
+ }
9001
+ function checkAgainstContext(spec, ctx) {
9002
+ const errors = [];
9003
+ const warnings = [];
9004
+ const v = versionNotice(ctx);
9005
+ if (v) warnings.push(v);
9006
+ const bus = ctx.businessUnits;
9007
+ let businessUnit = spec.businessUnit;
9008
+ const buMatch = canonical(spec.businessUnit, bus);
9009
+ if (!buMatch) {
9010
+ errors.push(
9011
+ bus.length ? `businessUnit: "${spec.businessUnit}" is not a business unit of this organisation \u2014 use one of: ${allowedList(bus)}. New business units are created in the app's Settings, not here.` : `businessUnit: this organisation has no business units configured \u2014 add them in the app's Settings, download the portfolio context again, then continue.`
9012
+ );
9013
+ } else {
9014
+ if (buMatch !== spec.businessUnit) warnings.push(`businessUnit: "${spec.businessUnit}" normalised to the organisation's spelling "${buMatch}".`);
9015
+ businessUnit = buMatch;
9016
+ }
9017
+ const cats = ctx.categories;
9018
+ let aiCategory = spec.aiCategory;
9019
+ const catMatch = canonical(spec.aiCategory, cats);
9020
+ if (!catMatch) {
9021
+ errors.push(
9022
+ cats.length ? `aiCategory: "${spec.aiCategory}" is not a category of this organisation \u2014 use one of: ${allowedList(cats)}. New categories are created in the app's Settings, not here.` : `aiCategory: this organisation has no categories configured \u2014 add them in the app's Settings, download the portfolio context again, then continue.`
9023
+ );
9024
+ } else {
9025
+ if (catMatch !== spec.aiCategory) warnings.push(`aiCategory: "${spec.aiCategory}" normalised to the organisation's spelling "${catMatch}".`);
9026
+ aiCategory = catMatch;
9027
+ }
9028
+ const initiativeIds = new Set(ctx.initiatives.map((i) => i.id));
9029
+ if (spec.id !== void 0 && !initiativeIds.has(spec.id)) {
9030
+ errors.push(`id: "${spec.id}" is not an initiative of this organisation \u2014 an id is only for UPDATING an existing initiative (read_portfolio_context \u2192 lists.initiatives); omit it to create a new one.`);
9031
+ }
9032
+ for (const [i, id] of (spec.dependsOn ?? []).entries()) {
9033
+ if (!initiativeIds.has(id)) errors.push(`dependsOn.${i}: "${id}" is not the id of an initiative in this organisation \u2014 pick from read_portfolio_context \u2192 lists.initiatives (id + name), or leave dependsOn empty.`);
9034
+ }
9035
+ if (spec.id !== void 0 && (spec.dependsOn ?? []).includes(spec.id)) errors.push("dependsOn: an initiative cannot depend on itself.");
9036
+ const skillIds = new Set(ctx.skills.map((s) => s.id));
9037
+ for (const [i, s] of (spec.requiredSkills ?? []).entries()) {
9038
+ if (!skillIds.has(s.skillId)) errors.push(`requiredSkills.${i}.skillId: "${s.skillId}" is not a skill of this organisation \u2014 pick from read_portfolio_context \u2192 lists.skills (id + name), or leave requiredSkills empty.`);
9039
+ }
9040
+ const aiFieldsSet = AI_ACT_FIELDS.filter((f) => spec[f] !== void 0);
9041
+ if (aiFieldsSet.length && !ctx.aiActEnabled) {
9042
+ warnings.push(
9043
+ `EU-AI-Act fields are set (${aiFieldsSet.join(", ")}), but this organisation's AI-Act module is switched OFF \u2014 the app removes ${AI_ACT_FIELDS.join(" / ")} on import unless the module is switched on first (Settings \u203A AI Act).`
9044
+ );
9045
+ }
9046
+ return { errors, warnings, businessUnit, aiCategory };
9047
+ }
9048
+ function resolveSub(base, ctxSlider, mode, lang) {
9049
+ const out = {
9050
+ key: base.key,
9051
+ label: ctxSlider?.label?.trim() || getModeOverride(mode, lang, `sub.${base.key}`) || base.label[lang],
9052
+ help: ctxSlider?.help?.trim() || getModeOverride(mode, lang, `subHelp.${base.key}`) || base.help[lang]
9053
+ };
9054
+ const note = ctxSlider?.note?.trim();
9055
+ if (note) out.note = note;
9056
+ return out;
9057
+ }
9058
+ function gridExample(step) {
9059
+ const pts = [0, step, 2 * step].filter((v) => v < 100);
9060
+ return `${pts.join(", ")} \u2026 100`;
9061
+ }
9062
+ function buildInterviewPlan(ctx, contextId) {
9063
+ const mode = ctx.portfolioMode;
9064
+ const lang = ctx.language;
9065
+ const ctxByKey = new Map(ctx.sliders.map((s) => [s.key, s]));
9066
+ const disabled = disabledSliders(ctx);
9067
+ const dimensions = DIMENSIONS.map((d) => {
9068
+ const subs = d.subs.filter((s) => !disabled.has(s.key)).map((s) => resolveSub(s, ctxByKey.get(s.key), mode, lang));
9069
+ return { key: d.key, title: getModeOverride(mode, lang, `dim.${d.key}.title`) || d.title[lang], subs, allSwitchedOff: subs.length === 0 };
9070
+ });
9071
+ const skipped = DIMENSION_KEYS.flatMap((dim) => DIMENSION_SUBS[dim].filter((k) => disabled.has(k)).map((key) => ({ key, dimension: dim })));
9072
+ const allOff = dimensions.filter((d) => d.allSwitchedOff).map((d) => d.key);
9073
+ const warnings = [];
9074
+ const v = versionNotice(ctx);
9075
+ if (v) warnings.push(v);
9076
+ const rules = [
9077
+ "Interview with these labels, help texts and notes \u2014 they are the organisation's own wording for its portfolio mode.",
9078
+ skipped.length ? `Do NOT ask about the ${skipped.length} switched-off sub-dimension(s) listed under interview.skipped and do not send them; the app excludes them from the dimension score, and so does the Composer when the same context is passed to assemble_initiative / export_initiative_file. Every OTHER sub-dimension of a dimension is still required.` : "No sub-dimension is switched off in this mode: all 30 are asked and scored.",
9079
+ ...allOff.length ? [`Dimension(s) with EVERY sub-dimension switched off (${allOff.join(", ")}) still need an entry in evaluation: { subs: {}, confidence } \u2014 the score is 0 there; ask only for the confidence.`] : [],
9080
+ ctx.businessUnits.length && ctx.categories.length ? "businessUnit MUST be one of lists.businessUnits and aiCategory one of lists.categories (matched case-insensitively, normalised to the organisation's spelling) \u2014 the Composer rejects anything else; new ones are created in the app's Settings, not in the interview." : "This organisation has no business units and/or categories configured yet \u2014 they must be added in the app's Settings and the context downloaded again before an initiative can be assembled.",
9081
+ "dependsOn entries must be ids from lists.initiatives; requiredSkills[].skillId must be ids from lists.skills. Never invent an id; leave the list empty when unsure.",
9082
+ "To UPDATE an existing initiative instead of creating a new one, pass its id (from lists.initiatives) as `id` in the spec \u2014 the exported file then updates it in the app (Import/Export \u2192 Single initiative \u2192 'Update'). Omit id for a new initiative.",
9083
+ ...ctx.sliderGranularity ? [`Ratings are in steps of ${ctx.sliderGranularity} (the organisation's slider step \u2014 Settings \u203A Slider granularity): ask for and send only multiples of ${ctx.sliderGranularity} between 0 and 100 (${gridExample(ctx.sliderGranularity)}); any other value is rejected.`] : [],
9084
+ "Dates: talk to the user in dd/mm/yyyy; you may send dates as dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd \u2014 the Composer stores yyyy-mm-dd.",
9085
+ ctx.aiActEnabled ? "The organisation's EU-AI-Act module is ON: collect the AI-Act block and governance flags." : "The organisation's EU-AI-Act module is OFF: skip the AI-Act block and governance flags (the app would remove them on import).",
9086
+ `Pass \`context: "${contextId}"\` (this contextId \u2014 or the whole file again) to assemble_initiative and export_initiative_file so scores are derived over the enabled sub-dimensions and ids are validated. Amounts are in ${ctx.currency}; the default discount rate is ${ctx.discountRatePct}%.`,
9087
+ `The file is from ${ctx.exportedAt}; if the user changed settings since, ask them to export it again (Import/Export \u2192 Download portfolio context).`
9088
+ ];
9089
+ return {
9090
+ contextId,
9091
+ organisation: {
9092
+ ...ctx.companyName ? { companyName: ctx.companyName } : {},
9093
+ portfolioMode: mode,
9094
+ language: lang,
9095
+ currency: ctx.currency,
9096
+ aiActEnabled: ctx.aiActEnabled,
9097
+ discountRatePct: ctx.discountRatePct,
9098
+ ...ctx.sliderGranularity ? { sliderGranularity: ctx.sliderGranularity } : {},
9099
+ contextExportedAt: ctx.exportedAt
9100
+ },
9101
+ lists: {
9102
+ businessUnits: ctx.businessUnits,
9103
+ categories: ctx.categories,
9104
+ suggestedTags: ctx.suggestedTags,
9105
+ skills: ctx.skills.map((s) => ({ id: s.id, name: s.name })),
9106
+ initiatives: ctx.initiatives.map((i) => ({ id: i.id, name: i.name, status: i.status }))
9107
+ },
9108
+ interview: {
9109
+ dimensions,
9110
+ skipped,
9111
+ scoreDerivation: "mean of the ENABLED sub-dimension values (switched-off ones excluded), rounded, clamped 0\u2013100 \u2014 the app's own function"
9112
+ },
9113
+ rules,
9114
+ warnings
9115
+ };
9116
+ }
9117
+
8786
9118
  // src/assembler.ts
8787
- function checkSubs(dim, subs, errors) {
9119
+ var uuidSchema = external_exports.string().uuid();
9120
+ var isUuid = (v) => typeof v === "string" && uuidSchema.safeParse(v).success;
9121
+ function checkSubs(dim, subs, errors, disabled, step) {
8788
9122
  const expected = DIMENSION_SUBS[dim];
9123
+ const enabled = expected.filter((k) => !disabled.has(k));
8789
9124
  if (!subs || typeof subs !== "object") {
8790
- errors.push(`evaluation.${dim}.subs is missing (expected keys: ${expected.join(", ")})`);
9125
+ if (enabled.length) errors.push(`evaluation.${dim}.subs is missing (expected keys: ${enabled.join(", ")})`);
8791
9126
  return;
8792
9127
  }
8793
- for (const key of expected) {
9128
+ for (const key of enabled) {
8794
9129
  const v = subs[key];
8795
9130
  if (v === void 0) errors.push(`evaluation.${dim}.subs.${key} is missing`);
8796
9131
  else if (typeof v !== "number" || !Number.isFinite(v)) errors.push(`evaluation.${dim}.subs.${key} must be a number`);
8797
9132
  else if (v < 0 || v > 100) errors.push(`evaluation.${dim}.subs.${key} must be within 0\u2013100 (got ${v})`);
9133
+ else if (step && v % step !== 0) errors.push(`evaluation.${dim}.subs.${key} must be a multiple of ${step} \u2014 the organisation's slider step (got ${v})`);
8798
9134
  }
8799
9135
  for (const key of Object.keys(subs)) {
8800
- if (!expected.includes(key)) errors.push(`evaluation.${dim}.subs.${key} is not a known sub-dimension (expected: ${expected.join(", ")})`);
9136
+ if (!expected.includes(key)) errors.push(`evaluation.${dim}.subs.${key} is not a known sub-dimension (expected: ${enabled.join(", ")})`);
8801
9137
  }
8802
9138
  }
8803
9139
  function checkAiActVocabularies(block, errors) {
@@ -8826,44 +9162,89 @@ function checkAiActVocabularies(block, errors) {
8826
9162
  errors.push(`aiAct.annexIIICategory: "${cat}" is not an Annex III category \u2014 allowed: ${ANNEX_III_CATEGORIES.join(" | ")}; omit it when no Annex III area applies.`);
8827
9163
  }
8828
9164
  }
8829
- function assembleInitiative(spec, now = /* @__PURE__ */ new Date()) {
9165
+ function mintNestedIds(rows, stamp, withStamps = false) {
9166
+ if (!rows) return void 0;
9167
+ return rows.map((r) => {
9168
+ const next = { ...r, id: isUuid(r.id) ? r.id : randomUUID() };
9169
+ if (withStamps) {
9170
+ if (!next.createdAt) next.createdAt = stamp;
9171
+ if (!next.updatedAt) next.updatedAt = stamp;
9172
+ }
9173
+ return next;
9174
+ });
9175
+ }
9176
+ function assembleInitiative(input, now = /* @__PURE__ */ new Date()) {
8830
9177
  const errors = [];
8831
9178
  const warnings = [];
9179
+ const { context: rawContext, ...spec } = input;
9180
+ let context;
9181
+ let contextId;
9182
+ if (rawContext !== void 0) {
9183
+ const resolved = resolveContext(rawContext);
9184
+ if (!resolved.ok) return { ok: false, errors: resolved.errors, warnings };
9185
+ context = resolved.context;
9186
+ contextId = resolved.contextId;
9187
+ }
9188
+ const disabled = context ? disabledSliders(context) : /* @__PURE__ */ new Set();
8832
9189
  for (const dim of DIMENSION_KEYS) {
8833
9190
  const d = spec.evaluation?.[dim];
8834
9191
  if (!d) {
8835
9192
  errors.push(`evaluation.${dim} is missing`);
8836
9193
  continue;
8837
9194
  }
8838
- checkSubs(dim, d.subs, errors);
9195
+ checkSubs(dim, d.subs, errors, disabled, context?.sliderGranularity);
8839
9196
  if (!d.confidence) errors.push(`evaluation.${dim}.confidence is missing (Low | Medium | High)`);
9197
+ for (const key of Object.keys(d.subs ?? {})) {
9198
+ if (disabled.has(key)) warnings.push(`evaluation.${dim}.subs.${key}: this sub-dimension is switched off in the organisation \u2014 value ignored (stored as 0, not scored).`);
9199
+ }
8840
9200
  }
8841
9201
  if (spec.aiAct && typeof spec.aiAct === "object") checkAiActVocabularies(spec.aiAct, errors);
9202
+ let businessUnit = spec.businessUnit;
9203
+ let aiCategory = spec.aiCategory;
9204
+ if (context) {
9205
+ const c = checkAgainstContext(spec, context);
9206
+ errors.push(...c.errors);
9207
+ warnings.push(...c.warnings);
9208
+ businessUnit = c.businessUnit;
9209
+ aiCategory = c.aiCategory;
9210
+ } else {
9211
+ if (spec.id !== void 0 && !isUuid(spec.id)) errors.push(`id: "${spec.id}" is not a UUID \u2014 an id is only for UPDATING an existing initiative; omit it to create a new one, or provide the organisation's portfolio context so the id can be verified.`);
9212
+ for (const [i, id] of (spec.dependsOn ?? []).entries()) {
9213
+ if (!isUuid(id)) errors.push(`dependsOn.${i}: "${id}" is not a UUID \u2014 without the organisation's portfolio context the Composer cannot verify ids; leave dependsOn empty and link dependencies in the app, or provide the context.`);
9214
+ }
9215
+ }
8842
9216
  if (spec.timelineStart && spec.timelineEnd && spec.timelineEnd < spec.timelineStart) {
8843
9217
  warnings.push("timelineEnd is before timelineStart");
8844
9218
  }
8845
- if (errors.length) return { ok: false, errors, warnings };
9219
+ if (errors.length) return { ok: false, errors, warnings, ...contextId ? { contextId } : {} };
8846
9220
  const evaluation = Object.fromEntries(
8847
9221
  DIMENSION_KEYS.map((dim) => {
8848
9222
  const d = spec.evaluation[dim];
8849
- return [dim, { score: dimensionScore(d.subs), confidence: d.confidence, subs: d.subs }];
9223
+ const subs = Object.fromEntries(DIMENSION_SUBS[dim].map((k) => [k, disabled.has(k) ? 0 : d.subs?.[k] ?? 0]));
9224
+ return [dim, { score: dimensionScore(subs, disabled), confidence: d.confidence, subs }];
8850
9225
  })
8851
9226
  );
8852
9227
  const stamp = now.toISOString();
9228
+ const aiAct = spec.aiAct;
8853
9229
  const candidate = {
8854
9230
  ...spec,
8855
- id: randomUUID(),
9231
+ id: spec.id ?? randomUUID(),
9232
+ businessUnit,
9233
+ aiCategory,
8856
9234
  tags: spec.tags ?? [],
8857
9235
  archived: false,
8858
9236
  dependsOn: spec.dependsOn ?? [],
8859
9237
  evaluation,
9238
+ decisionLog: mintNestedIds(spec.decisionLog, stamp),
9239
+ statusReports: mintNestedIds(spec.statusReports, stamp),
9240
+ aiAct: aiAct ? { ...aiAct, ...aiAct.incidents ? { incidents: mintNestedIds(aiAct.incidents, stamp, true) } : {} } : void 0,
8860
9241
  createdAt: stamp,
8861
9242
  updatedAt: stamp
8862
9243
  };
8863
9244
  const parsed = CreateInitiativeBody.safeParse(candidate);
8864
- if (!parsed.success) return { ok: false, errors: formatIssues(parsed.error), warnings };
9245
+ if (!parsed.success) return { ok: false, errors: formatIssues(parsed.error), warnings, ...contextId ? { contextId } : {} };
8865
9246
  const derivedScores = Object.fromEntries(DIMENSION_KEYS.map((dim) => [dim, evaluation[dim].score]));
8866
- return { ok: true, initiative: parsed.data, derivedScores, warnings, errors };
9247
+ return { ok: true, initiative: parsed.data, derivedScores, warnings, errors, ...contextId ? { contextId } : {} };
8867
9248
  }
8868
9249
 
8869
9250
  // ../../initiative-model/src/backup-version.ts
@@ -8891,9 +9272,9 @@ function suggestFilename(name, now = /* @__PURE__ */ new Date()) {
8891
9272
  var IMPORT_INSTRUCTIONS = [
8892
9273
  "1. Save the JSON below as the suggested filename.",
8893
9274
  "2. In the BICon app (signed in to the target organisation), open Import/Export.",
8894
- "3. If the initiative carries EU-AI-Act data, make sure the portfolio is in AI mode first \u2014 the app strips AI-Act fields when it is not.",
9275
+ "3. If the initiative carries EU-AI-Act data, make sure the organisation's EU-AI-Act module is switched on (Settings \u203A AI Act) \u2014 the app strips AI-Act fields while it is off.",
8895
9276
  "4. In the 'Single initiative' card, drop the file (or Browse).",
8896
- "5. Review the preview \u2014 it should show 0 warnings \u2014 then choose 'Add as new' and Confirm.",
9277
+ "5. Review the preview \u2014 it should show 0 warnings \u2014 then choose 'Add as new' (or 'Update' when the file carries an existing initiative's id or the same name \u2014 the app then keeps that initiative's id and history) and Confirm.",
8897
9278
  "6. The initiative appears in the Inventory with the derived dimension scores."
8898
9279
  ].join("\n");
8899
9280
 
@@ -8925,10 +9306,12 @@ function createServer() {
8925
9306
  async () => text({
8926
9307
  capabilities: [
8927
9308
  "strict typed schemas: unknown keys anywhere in the spec are rejected with their exact path (never silently dropped)",
8928
- "sections (aiAct/economics/actuals/decisionLog/statusReports/requiredSkills) accept a JSON object/array or the same JSON as a string",
9309
+ "sections (aiAct/economics/actuals/decisionLog/statusReports/requiredSkills) accept a JSON object/array or the same JSON as a string; row ids are minted by the Composer",
9310
+ "dates accepted as dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd (stored as yyyy-mm-dd)",
8929
9311
  "get_initiative_schema: sectionShapes, aiActVocabularies, portfolio-mode wording overlays",
8930
9312
  "AI-Act vocabulary and id-format enforcement with the allowed values in the error",
8931
- "one pipeline: export_initiative_file takes the same spec as assemble_initiative \u2014 scores are always derived"
9313
+ "one pipeline: export_initiative_file takes the same spec as assemble_initiative \u2014 scores are always derived; pass `id` to update an existing initiative",
9314
+ "portfolio context: read_portfolio_context turns the app's exported context file into an organisation-specific interview plan and a short contextId; with it, switched-off sub-dimensions are skipped in the score, business unit / category must be the organisation's own, and skill / dependency / update ids are validated"
8932
9315
  ]
8933
9316
  })
8934
9317
  );
@@ -8940,11 +9323,24 @@ function createServer() {
8940
9323
  },
8941
9324
  async () => ({ content: [{ type: "text", text: DESCRIPTOR_TEXT }] })
8942
9325
  );
9326
+ server.registerTool(
9327
+ "read_portfolio_context",
9328
+ {
9329
+ title: "Read portfolio context",
9330
+ description: "Turns the organisation's portfolio-context file (downloaded in the BICon app: Import/Export \u2192 'Download portfolio context') into the interview plan for THIS organisation: its own labels, help texts and rating notes for the sub-dimensions, which sub-dimensions are switched off (do not ask, not scored), its business units, categories, tags, skills (id + name) and initiatives (id + name), currency, discount rate and whether the EU-AI-Act module is on \u2014 plus a short contextId. Call it right after get_initiative_schema when the user provides the file, then pass `context: \"<contextId>\"` to assemble_initiative and export_initiative_file.",
9331
+ inputSchema: readContextInputSchema
9332
+ },
9333
+ async ({ context }) => {
9334
+ const resolved = resolveContext(context);
9335
+ if (!resolved.ok) return text({ ok: false, errors: resolved.errors });
9336
+ return text({ ok: true, ...buildInterviewPlan(resolved.context, resolved.contextId) });
9337
+ }
9338
+ );
8943
9339
  server.registerTool(
8944
9340
  "assemble_initiative",
8945
9341
  {
8946
9342
  title: "Assemble initiative",
8947
- description: "Turns a compact spec (metadata + named sub-dimension ratings + confidence per dimension + optional sections) into a fully-formed, contract-valid initiative. Mints the id and timestamps, DERIVES the four dimension scores (mean of subs) and validates against the API contract. Returns {ok, initiative, derivedScores, warnings, errors}. On errors, fix the listed fields and call again. Show derivedScores (and warnings) to the user before exporting.",
9343
+ description: "Turns a compact spec (metadata + named sub-dimension ratings + confidence per dimension + optional sections, plus the organisation's `context` \u2014 contextId or file \u2014 when the user provided it) into a fully-formed, contract-valid initiative. Mints the id and timestamps (pass `id` only to UPDATE an existing initiative), DERIVES the four dimension scores (mean of the enabled subs) and validates against the API contract and \u2014 with context \u2014 the organisation's own lists and ids. Returns {ok, initiative, derivedScores, warnings, errors, contextId}. On errors, fix the listed fields and call again. Show derivedScores (and warnings) to the user before exporting.",
8948
9344
  inputSchema: compactSpecSchema
8949
9345
  },
8950
9346
  async (spec) => text(assembleInitiative(spec))
@@ -8953,22 +9349,30 @@ function createServer() {
8953
9349
  "export_initiative_file",
8954
9350
  {
8955
9351
  title: "Export initiative file",
8956
- description: "Assembles the SAME compact spec you passed to assemble_initiative and wraps the result in the app's single-initiative JSON envelope. Returns {ok, filename, json, importInstructions, derivedScores, warnings}. Save `json` under `filename`; the user imports it via Import/Export \u2192 Single initiative \u2192 'Add as new' \u2192 Confirm. sourceMode/language are recorded in the file; the app keeps AI-Act fields only while it is itself in AI mode.",
9352
+ description: "Assembles the SAME compact spec (and context) you passed to assemble_initiative and wraps the result in the app's single-initiative JSON envelope. Returns {ok, filename, json, importInstructions, derivedScores, warnings}. Save `json` under `filename`; the user imports it via Import/Export \u2192 Single initiative \u2192 'Add as new' (or 'Update' when the spec carried an existing id or the name matches) \u2192 Confirm. sourceMode/language are recorded in the file (defaulting to the context's); whether the app keeps EU-AI-Act fields depends on the organisation's AI-Act module switch.",
8957
9353
  inputSchema: exportInputSchema
8958
9354
  },
8959
9355
  async (input) => {
8960
9356
  const parsed = compactSpecSchema.safeParse(input.spec);
8961
9357
  if (!parsed.success) return text({ ok: false, errors: formatIssues(parsed.error), warnings: [] });
8962
- const result = assembleInitiative(parsed.data);
8963
- if (!result.ok || !result.initiative) return text({ ok: false, errors: result.errors, warnings: result.warnings });
8964
- const envelope = buildEnvelope(result.initiative, { sourceMode: input.sourceMode, language: input.language });
9358
+ const rawContext = input.context ?? parsed.data.context;
9359
+ const result = assembleInitiative({ ...parsed.data, context: rawContext });
9360
+ if (!result.ok || !result.initiative) return text({ ok: false, errors: result.errors, warnings: result.warnings, ...result.contextId ? { contextId: result.contextId } : {} });
9361
+ const ctx = rawContext !== void 0 ? resolveContext(rawContext) : void 0;
9362
+ const ctxData = ctx && ctx.ok ? ctx.context : void 0;
9363
+ const envelope = buildEnvelope(result.initiative, {
9364
+ sourceMode: input.sourceMode ?? ctxData?.portfolioMode,
9365
+ language: input.language ?? ctxData?.language
9366
+ });
8965
9367
  return text({
8966
9368
  ok: true,
8967
9369
  filename: input.filename ?? suggestFilename(result.initiative.name),
8968
9370
  json: JSON.stringify(envelope, null, 2),
8969
9371
  importInstructions: IMPORT_INSTRUCTIONS,
8970
9372
  derivedScores: result.derivedScores,
8971
- warnings: result.warnings
9373
+ warnings: result.warnings,
9374
+ ...result.contextId ? { contextId: result.contextId } : {},
9375
+ update: parsed.data.id !== void 0
8972
9376
  });
8973
9377
  }
8974
9378
  );
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@bicon/mcp-composer",
3
- "version": "0.1.7",
3
+ "version": "0.3.2",
4
4
  "description": "MCP server that helps an AI client compose a valid BICon portfolio initiative and hand it back as an import file (draft-first, no auth, no API calls).",
5
5
  "license": "UNLICENSED",
6
6
  "author": "BICon Establishment",
7
7
  "homepage": "https://www.bicon.digital",
8
- "keywords": ["mcp", "model-context-protocol", "bicon", "portfolio", "eu-ai-act"],
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "bicon",
12
+ "portfolio",
13
+ "eu-ai-act"
14
+ ],
9
15
  "type": "module",
10
16
  "bin": {
11
17
  "bicon-mcp-composer": "dist/index.mjs"