@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.
- package/README.md +79 -24
- package/dist/index.mjs +473 -69
- 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
|
|
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,
|
|
39
|
-
|
|
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
|
|
77
|
-
|
|
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` (
|
|
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.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
JSON under the suggested filename (`initiative-<name>-<date>.json`).
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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.
|
|
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: "
|
|
8519
|
-
{ key: "timelineEnd", type: "date", hint: { en: "
|
|
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: "
|
|
8531
|
-
de: "
|
|
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
|
|
8549
|
-
de: "[{skillId, fte}] \u2014 skillId ist die
|
|
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.
|
|
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
|
-
|
|
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))
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
8725
|
-
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
8963
|
-
|
|
8964
|
-
|
|
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.
|
|
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": [
|
|
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"
|