@bicon/mcp-composer 0.13.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -7
  2. package/dist/index.mjs +849 -47
  3. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -11,9 +11,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
11
11
  // src/server.ts
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
 
14
- // src/version.ts
15
- var COMPOSER_VERSION = true ? "0.13.1" : readPackageVersion();
16
-
17
14
  // ../../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/external.js
18
15
  var external_exports = {};
19
16
  __export(external_exports, {
@@ -19731,6 +19728,9 @@ function date4(params) {
19731
19728
  return _coercedDate(ZodDate, params);
19732
19729
  }
19733
19730
 
19731
+ // src/version.ts
19732
+ var COMPOSER_VERSION = true ? "0.17.0" : readPackageVersion();
19733
+
19734
19734
  // ../../api-zod/src/generated/api.ts
19735
19735
  var HealthCheckResponse = object({
19736
19736
  "status": string2(),
@@ -19846,7 +19846,7 @@ var ListOrganizationMembersResponseItem = object({
19846
19846
  "email": string2(),
19847
19847
  "displayName": string2().nullable(),
19848
19848
  "role": _enum2(["admin", "member", "viewer"]),
19849
- "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups.")
19849
+ "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups, manage_quality.")
19850
19850
  });
19851
19851
  var ListOrganizationMembersResponse = array(ListOrganizationMembersResponseItem);
19852
19852
  var SyncOrganizationMembersResponse = object({
@@ -19856,7 +19856,7 @@ var SyncOrganizationMembersResponse = object({
19856
19856
  var GetMyOrganizationAccessResponse = object({
19857
19857
  "userId": string2().describe(`The caller's own local user id (matches AuthorRef.userId on initiatives), so the client can tell its own changes apart from a teammate's \u2014 e.g. to suppress the "updated by X" collaboration banner when an edit echoing back is the caller's own save.`),
19858
19858
  "role": _enum2(["admin", "member", "viewer"]),
19859
- "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups."),
19859
+ "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups, manage_quality."),
19860
19860
  "initiativeLimit": number2().describe("The most initiatives this organisation may hold, archived ones included. Creating one more answers 507 with code initiative_limit_reached, so the client blocks New, Duplicate and Import up front.")
19861
19861
  });
19862
19862
  var ListOrganizationDirectoryResponseItem = object({
@@ -19868,11 +19868,11 @@ var UpdateOrganizationMemberPermissionsParams = object({
19868
19868
  "userId": guid2()
19869
19869
  });
19870
19870
  var UpdateOrganizationMemberPermissionsBody = object({
19871
- "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups.")
19871
+ "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups, manage_quality.")
19872
19872
  });
19873
19873
  var UpdateOrganizationMemberPermissionsResponse = object({
19874
19874
  "userId": guid2(),
19875
- "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups.")
19875
+ "permissions": array(string2()).describe("App-managed permission overrides on top of the fixed role (free-form strings, not a closed enum). Known keys: manage_skills, manage_ai_act, manage_metadata, manage_stage_gate, manage_economics, manage_decision_log, manage_dependencies, manage_required_skills, manage_scoring, manage_status_report, manage_prioritisation, manage_capabilities, manage_capability_impact, manage_milestones, manage_risks, manage_follow_ups, manage_quality.")
19876
19876
  });
19877
19877
  var ListPermissionProfilesResponseItem = object({
19878
19878
  "id": guid2(),
@@ -19986,6 +19986,10 @@ var listSnapshotsResponseInitiativesItemFollowUpsItemDueDateRegExp = new RegExp(
19986
19986
  var listSnapshotsResponseInitiativesItemFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
19987
19987
  var listSnapshotsResponseInitiativesItemFollowUpsItemNoteMax = 1e4;
19988
19988
  var listSnapshotsResponseInitiativesItemFollowUpsMax = 50;
19989
+ var listSnapshotsResponseInitiativesItemQualityFactorsItemStateMax = 5;
19990
+ var listSnapshotsResponseInitiativesItemQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
19991
+ var listSnapshotsResponseInitiativesItemQualityFactorsItemNoteMax = 2e3;
19992
+ var listSnapshotsResponseInitiativesItemQualityFactorsMax = 8;
19989
19993
  var listSnapshotsResponseInitiativesItemDependsOnMax = 1e3;
19990
19994
  var listSnapshotsResponseInitiativesItemRequiredSkillsItemRunFteMin = 0;
19991
19995
  var listSnapshotsResponseInitiativesItemRequiredSkillsMax = 1e3;
@@ -20224,6 +20228,13 @@ var ListSnapshotsResponseItem = object({
20224
20228
  "riskId": string2().optional(),
20225
20229
  "note": string2().max(listSnapshotsResponseInitiativesItemFollowUpsItemNoteMax).optional()
20226
20230
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(listSnapshotsResponseInitiativesItemFollowUpsMax).optional(),
20231
+ "qualityFactors": array(object({
20232
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
20233
+ "required": _enum2(["low", "medium", "high"]).optional(),
20234
+ "state": number2().min(1).max(listSnapshotsResponseInitiativesItemQualityFactorsItemStateMax).optional(),
20235
+ "targetDate": string2().regex(listSnapshotsResponseInitiativesItemQualityFactorsItemTargetDateRegExp).optional(),
20236
+ "note": string2().max(listSnapshotsResponseInitiativesItemQualityFactorsItemNoteMax).optional()
20237
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(listSnapshotsResponseInitiativesItemQualityFactorsMax).optional(),
20227
20238
  "dependsOn": array(string2()).max(listSnapshotsResponseInitiativesItemDependsOnMax),
20228
20239
  "requiredSkills": array(object({
20229
20240
  "skillId": string2(),
@@ -20386,6 +20397,10 @@ var createSnapshotBodyInitiativesItemFollowUpsItemDueDateRegExp = new RegExp("^\
20386
20397
  var createSnapshotBodyInitiativesItemFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
20387
20398
  var createSnapshotBodyInitiativesItemFollowUpsItemNoteMax = 1e4;
20388
20399
  var createSnapshotBodyInitiativesItemFollowUpsMax = 50;
20400
+ var createSnapshotBodyInitiativesItemQualityFactorsItemStateMax = 5;
20401
+ var createSnapshotBodyInitiativesItemQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
20402
+ var createSnapshotBodyInitiativesItemQualityFactorsItemNoteMax = 2e3;
20403
+ var createSnapshotBodyInitiativesItemQualityFactorsMax = 8;
20389
20404
  var createSnapshotBodyInitiativesItemDependsOnMax = 1e3;
20390
20405
  var createSnapshotBodyInitiativesItemRequiredSkillsItemRunFteMin = 0;
20391
20406
  var createSnapshotBodyInitiativesItemRequiredSkillsMax = 1e3;
@@ -20624,6 +20639,13 @@ var CreateSnapshotBody = object({
20624
20639
  "riskId": string2().optional(),
20625
20640
  "note": string2().max(createSnapshotBodyInitiativesItemFollowUpsItemNoteMax).optional()
20626
20641
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(createSnapshotBodyInitiativesItemFollowUpsMax).optional(),
20642
+ "qualityFactors": array(object({
20643
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
20644
+ "required": _enum2(["low", "medium", "high"]).optional(),
20645
+ "state": number2().min(1).max(createSnapshotBodyInitiativesItemQualityFactorsItemStateMax).optional(),
20646
+ "targetDate": string2().regex(createSnapshotBodyInitiativesItemQualityFactorsItemTargetDateRegExp).optional(),
20647
+ "note": string2().max(createSnapshotBodyInitiativesItemQualityFactorsItemNoteMax).optional()
20648
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(createSnapshotBodyInitiativesItemQualityFactorsMax).optional(),
20627
20649
  "dependsOn": array(string2()).max(createSnapshotBodyInitiativesItemDependsOnMax),
20628
20650
  "requiredSkills": array(object({
20629
20651
  "skillId": string2(),
@@ -20792,6 +20814,10 @@ var renameSnapshotResponseInitiativesItemFollowUpsItemDueDateRegExp = new RegExp
20792
20814
  var renameSnapshotResponseInitiativesItemFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
20793
20815
  var renameSnapshotResponseInitiativesItemFollowUpsItemNoteMax = 1e4;
20794
20816
  var renameSnapshotResponseInitiativesItemFollowUpsMax = 50;
20817
+ var renameSnapshotResponseInitiativesItemQualityFactorsItemStateMax = 5;
20818
+ var renameSnapshotResponseInitiativesItemQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
20819
+ var renameSnapshotResponseInitiativesItemQualityFactorsItemNoteMax = 2e3;
20820
+ var renameSnapshotResponseInitiativesItemQualityFactorsMax = 8;
20795
20821
  var renameSnapshotResponseInitiativesItemDependsOnMax = 1e3;
20796
20822
  var renameSnapshotResponseInitiativesItemRequiredSkillsItemRunFteMin = 0;
20797
20823
  var renameSnapshotResponseInitiativesItemRequiredSkillsMax = 1e3;
@@ -21030,6 +21056,13 @@ var RenameSnapshotResponse = object({
21030
21056
  "riskId": string2().optional(),
21031
21057
  "note": string2().max(renameSnapshotResponseInitiativesItemFollowUpsItemNoteMax).optional()
21032
21058
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(renameSnapshotResponseInitiativesItemFollowUpsMax).optional(),
21059
+ "qualityFactors": array(object({
21060
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
21061
+ "required": _enum2(["low", "medium", "high"]).optional(),
21062
+ "state": number2().min(1).max(renameSnapshotResponseInitiativesItemQualityFactorsItemStateMax).optional(),
21063
+ "targetDate": string2().regex(renameSnapshotResponseInitiativesItemQualityFactorsItemTargetDateRegExp).optional(),
21064
+ "note": string2().max(renameSnapshotResponseInitiativesItemQualityFactorsItemNoteMax).optional()
21065
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(renameSnapshotResponseInitiativesItemQualityFactorsMax).optional(),
21033
21066
  "dependsOn": array(string2()).max(renameSnapshotResponseInitiativesItemDependsOnMax),
21034
21067
  "requiredSkills": array(object({
21035
21068
  "skillId": string2(),
@@ -21193,6 +21226,10 @@ var listInitiativesResponseFollowUpsItemDueDateRegExp = new RegExp("^\\d{4}-\\d{
21193
21226
  var listInitiativesResponseFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
21194
21227
  var listInitiativesResponseFollowUpsItemNoteMax = 1e4;
21195
21228
  var listInitiativesResponseFollowUpsMax = 50;
21229
+ var listInitiativesResponseQualityFactorsItemStateMax = 5;
21230
+ var listInitiativesResponseQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
21231
+ var listInitiativesResponseQualityFactorsItemNoteMax = 2e3;
21232
+ var listInitiativesResponseQualityFactorsMax = 8;
21196
21233
  var listInitiativesResponseDependsOnMax = 1e3;
21197
21234
  var listInitiativesResponseRequiredSkillsItemRunFteMin = 0;
21198
21235
  var listInitiativesResponseRequiredSkillsMax = 1e3;
@@ -21425,6 +21462,13 @@ var ListInitiativesResponseItem = object({
21425
21462
  "riskId": string2().optional(),
21426
21463
  "note": string2().max(listInitiativesResponseFollowUpsItemNoteMax).optional()
21427
21464
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(listInitiativesResponseFollowUpsMax).optional(),
21465
+ "qualityFactors": array(object({
21466
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
21467
+ "required": _enum2(["low", "medium", "high"]).optional(),
21468
+ "state": number2().min(1).max(listInitiativesResponseQualityFactorsItemStateMax).optional(),
21469
+ "targetDate": string2().regex(listInitiativesResponseQualityFactorsItemTargetDateRegExp).optional(),
21470
+ "note": string2().max(listInitiativesResponseQualityFactorsItemNoteMax).optional()
21471
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(listInitiativesResponseQualityFactorsMax).optional(),
21428
21472
  "dependsOn": array(string2()).max(listInitiativesResponseDependsOnMax),
21429
21473
  "requiredSkills": array(object({
21430
21474
  "skillId": string2(),
@@ -21584,6 +21628,10 @@ var createInitiativeBodyOneFollowUpsItemDueDateRegExp = new RegExp("^\\d{4}-\\d{
21584
21628
  var createInitiativeBodyOneFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
21585
21629
  var createInitiativeBodyOneFollowUpsItemNoteMax = 1e4;
21586
21630
  var createInitiativeBodyOneFollowUpsMax = 50;
21631
+ var createInitiativeBodyOneQualityFactorsItemStateMax = 5;
21632
+ var createInitiativeBodyOneQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
21633
+ var createInitiativeBodyOneQualityFactorsItemNoteMax = 2e3;
21634
+ var createInitiativeBodyOneQualityFactorsMax = 8;
21587
21635
  var createInitiativeBodyOneDependsOnMax = 1e3;
21588
21636
  var createInitiativeBodyOneRequiredSkillsItemRunFteMin = 0;
21589
21637
  var createInitiativeBodyOneRequiredSkillsMax = 1e3;
@@ -21816,6 +21864,13 @@ var CreateInitiativeBody = object({
21816
21864
  "riskId": string2().optional(),
21817
21865
  "note": string2().max(createInitiativeBodyOneFollowUpsItemNoteMax).optional()
21818
21866
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(createInitiativeBodyOneFollowUpsMax).optional(),
21867
+ "qualityFactors": array(object({
21868
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
21869
+ "required": _enum2(["low", "medium", "high"]).optional(),
21870
+ "state": number2().min(1).max(createInitiativeBodyOneQualityFactorsItemStateMax).optional(),
21871
+ "targetDate": string2().regex(createInitiativeBodyOneQualityFactorsItemTargetDateRegExp).optional(),
21872
+ "note": string2().max(createInitiativeBodyOneQualityFactorsItemNoteMax).optional()
21873
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(createInitiativeBodyOneQualityFactorsMax).optional(),
21819
21874
  "dependsOn": array(string2()).max(createInitiativeBodyOneDependsOnMax),
21820
21875
  "requiredSkills": array(object({
21821
21876
  "skillId": string2(),
@@ -21985,6 +22040,10 @@ var getInitiativeResponseFollowUpsItemDueDateRegExp = new RegExp("^\\d{4}-\\d{2}
21985
22040
  var getInitiativeResponseFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
21986
22041
  var getInitiativeResponseFollowUpsItemNoteMax = 1e4;
21987
22042
  var getInitiativeResponseFollowUpsMax = 50;
22043
+ var getInitiativeResponseQualityFactorsItemStateMax = 5;
22044
+ var getInitiativeResponseQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
22045
+ var getInitiativeResponseQualityFactorsItemNoteMax = 2e3;
22046
+ var getInitiativeResponseQualityFactorsMax = 8;
21988
22047
  var getInitiativeResponseDependsOnMax = 1e3;
21989
22048
  var getInitiativeResponseRequiredSkillsItemRunFteMin = 0;
21990
22049
  var getInitiativeResponseRequiredSkillsMax = 1e3;
@@ -22217,6 +22276,13 @@ var GetInitiativeResponse = object({
22217
22276
  "riskId": string2().optional(),
22218
22277
  "note": string2().max(getInitiativeResponseFollowUpsItemNoteMax).optional()
22219
22278
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(getInitiativeResponseFollowUpsMax).optional(),
22279
+ "qualityFactors": array(object({
22280
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
22281
+ "required": _enum2(["low", "medium", "high"]).optional(),
22282
+ "state": number2().min(1).max(getInitiativeResponseQualityFactorsItemStateMax).optional(),
22283
+ "targetDate": string2().regex(getInitiativeResponseQualityFactorsItemTargetDateRegExp).optional(),
22284
+ "note": string2().max(getInitiativeResponseQualityFactorsItemNoteMax).optional()
22285
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(getInitiativeResponseQualityFactorsMax).optional(),
22220
22286
  "dependsOn": array(string2()).max(getInitiativeResponseDependsOnMax),
22221
22287
  "requiredSkills": array(object({
22222
22288
  "skillId": string2(),
@@ -22378,6 +22444,10 @@ var updateInitiativeBodyOneFollowUpsItemDueDateRegExp = new RegExp("^\\d{4}-\\d{
22378
22444
  var updateInitiativeBodyOneFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
22379
22445
  var updateInitiativeBodyOneFollowUpsItemNoteMax = 1e4;
22380
22446
  var updateInitiativeBodyOneFollowUpsMax = 50;
22447
+ var updateInitiativeBodyOneQualityFactorsItemStateMax = 5;
22448
+ var updateInitiativeBodyOneQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
22449
+ var updateInitiativeBodyOneQualityFactorsItemNoteMax = 2e3;
22450
+ var updateInitiativeBodyOneQualityFactorsMax = 8;
22381
22451
  var updateInitiativeBodyOneDependsOnMax = 1e3;
22382
22452
  var updateInitiativeBodyOneRequiredSkillsItemRunFteMin = 0;
22383
22453
  var updateInitiativeBodyOneRequiredSkillsMax = 1e3;
@@ -22610,6 +22680,13 @@ var UpdateInitiativeBody = object({
22610
22680
  "riskId": string2().optional(),
22611
22681
  "note": string2().max(updateInitiativeBodyOneFollowUpsItemNoteMax).optional()
22612
22682
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(updateInitiativeBodyOneFollowUpsMax).optional(),
22683
+ "qualityFactors": array(object({
22684
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
22685
+ "required": _enum2(["low", "medium", "high"]).optional(),
22686
+ "state": number2().min(1).max(updateInitiativeBodyOneQualityFactorsItemStateMax).optional(),
22687
+ "targetDate": string2().regex(updateInitiativeBodyOneQualityFactorsItemTargetDateRegExp).optional(),
22688
+ "note": string2().max(updateInitiativeBodyOneQualityFactorsItemNoteMax).optional()
22689
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(updateInitiativeBodyOneQualityFactorsMax).optional(),
22613
22690
  "dependsOn": array(string2()).max(updateInitiativeBodyOneDependsOnMax),
22614
22691
  "requiredSkills": array(object({
22615
22692
  "skillId": string2(),
@@ -22776,6 +22853,10 @@ var updateInitiativeResponseFollowUpsItemDueDateRegExp = new RegExp("^\\d{4}-\\d
22776
22853
  var updateInitiativeResponseFollowUpsItemDoneDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
22777
22854
  var updateInitiativeResponseFollowUpsItemNoteMax = 1e4;
22778
22855
  var updateInitiativeResponseFollowUpsMax = 50;
22856
+ var updateInitiativeResponseQualityFactorsItemStateMax = 5;
22857
+ var updateInitiativeResponseQualityFactorsItemTargetDateRegExp = new RegExp("^\\d{4}-\\d{2}-\\d{2}$");
22858
+ var updateInitiativeResponseQualityFactorsItemNoteMax = 2e3;
22859
+ var updateInitiativeResponseQualityFactorsMax = 8;
22779
22860
  var updateInitiativeResponseDependsOnMax = 1e3;
22780
22861
  var updateInitiativeResponseRequiredSkillsItemRunFteMin = 0;
22781
22862
  var updateInitiativeResponseRequiredSkillsMax = 1e3;
@@ -23008,6 +23089,13 @@ var UpdateInitiativeResponse = object({
23008
23089
  "riskId": string2().optional(),
23009
23090
  "note": string2().max(updateInitiativeResponseFollowUpsItemNoteMax).optional()
23010
23091
  }).describe("An agreed next step: who does what by when (docs/risks-issues-follow-ups-concept-2026-10-19.md). assigneeUserId is a member of the org (an id that is not is dropped on save, the name kept); milestoneId and riskId name the same initiative's milestones and risks (a link that does not is cleared on save). Dates are yyyy-mm-dd.")).max(updateInitiativeResponseFollowUpsMax).optional(),
23092
+ "qualityFactors": array(object({
23093
+ "factor": _enum2(["reliability", "security", "safety", "performance", "usability", "maintainability", "compatibility", "dataQuality"]),
23094
+ "required": _enum2(["low", "medium", "high"]).optional(),
23095
+ "state": number2().min(1).max(updateInitiativeResponseQualityFactorsItemStateMax).optional(),
23096
+ "targetDate": string2().regex(updateInitiativeResponseQualityFactorsItemTargetDateRegExp).optional(),
23097
+ "note": string2().max(updateInitiativeResponseQualityFactorsItemNoteMax).optional()
23098
+ }).describe("The quality of the delivered solution on one factor (docs/quality-factors-concept-2026-10-21.md, after ISO/IEC 25010). One entry per factor; required is how much it matters, state (1-5) how well the solution meets it today. Never part of a score.")).max(updateInitiativeResponseQualityFactorsMax).optional(),
23011
23099
  "dependsOn": array(string2()).max(updateInitiativeResponseDependsOnMax),
23012
23100
  "requiredSkills": array(object({
23013
23101
  "skillId": string2(),
@@ -23286,6 +23374,17 @@ var RISK_CATEGORIES = [
23286
23374
  var RISK_STATUSES = ["open", "mitigating", "closed", "occurred"];
23287
23375
  var MAX_RISKS_PER_INITIATIVE = 50;
23288
23376
  var MAX_FOLLOW_UPS_PER_INITIATIVE = 50;
23377
+ var QUALITY_FACTORS = [
23378
+ "reliability",
23379
+ "security",
23380
+ "safety",
23381
+ "performance",
23382
+ "usability",
23383
+ "maintainability",
23384
+ "compatibility",
23385
+ "dataQuality"
23386
+ ];
23387
+ var QUALITY_REQUIRED_LEVELS = ["low", "medium", "high"];
23289
23388
  var GATE_TRANSITIONS = [
23290
23389
  { key: "idea-pilot", from: "Idea", to: "Pilot" },
23291
23390
  { key: "pilot-scaling", from: "Pilot", to: "Scaling" },
@@ -23617,7 +23716,16 @@ var EN_DIGITAL = {
23617
23716
  "method.audit.scope.governance": "<strong>Status</strong> and the four <strong>evaluation dimension scores</strong> (Value, Delivery Feasibility, Compliance &amp; Operational Risk, Strategic Impact) with their confidence.",
23618
23717
  "method.audit.scope.metadata": "<strong>All Core Metadata fields</strong> \u2014 name, business unit, owner, description, timeline, budget, vendor strategy, solution type, digital category and tags.",
23619
23718
  "method.gates.enforced.intro": "Two governance fields are enforced as hard checks when the lifecycle status changes \u2014 active because the EU AI Act module is enabled for this workspace:",
23620
- "method.gates.enforced.scope": "Both checks feed the EU AI Act readiness score in addition to the hard block."
23719
+ "method.gates.enforced.scope": "Both checks feed the EU AI Act readiness score in addition to the hard block.",
23720
+ // Quality factor help (docs/quality-factors-concept-2026-10-21.md).
23721
+ "quality.factor.reliability.help": "Does the service stay up and behave correctly? Availability against the agreed SLA, incident rate, recovery time after an outage, batch and job success.",
23722
+ "quality.factor.security.help": "Is the platform protected? Access control and roles, protection of customer data, secure interfaces, pen test or security review done, findings closed.",
23723
+ "quality.factor.safety.help": "Can it cause harm when it fails? Safety-critical processes it touches (payments, physical operations, client advice), fail-safe behaviour and a manual fallback, hazard or impact analysis done, safety incidents.",
23724
+ "quality.factor.performance.help": "Is it fast enough at peak load? Page and API response times, throughput at month-end or campaign peaks, headroom to grow.",
23725
+ "quality.factor.usability.help": "Can customers and staff use it without help? Task completion, adoption, accessibility, number of support calls.",
23726
+ "quality.factor.maintainability.help": "Can the team change and run it safely? Code quality, automated tests and deployment, documentation, technical debt, dependency on single experts.",
23727
+ "quality.factor.compatibility.help": "Does it fit the landscape? Interfaces to core and neighbouring systems, standards (APIs, formats), vendor lock-in.",
23728
+ "quality.factor.dataQuality.help": "Is the data the service uses fit for purpose? Accuracy, completeness, timeliness and lineage of master and transaction data (ISO/IEC 25012). The Data & Process Quality slider rates feasibility before delivery; this rates the delivered state."
23621
23729
  };
23622
23730
  var DE_DIGITAL = {
23623
23731
  "brand.app": "BICon Portfolio Steering",
@@ -23827,7 +23935,16 @@ var DE_DIGITAL = {
23827
23935
  "method.audit.scope.governance": "<strong>Status</strong> und die vier <strong>Bewertungsdimensions-Scores</strong> (Wert, Liefer-Machbarkeit, Compliance &amp; Betriebsrisiko, Strategischer Impact) mit ihrer Konfidenz.",
23828
23936
  "method.audit.scope.metadata": "<strong>Alle Kern-Metadatenfelder</strong> \u2014 Name, Gesch\xE4ftseinheit, Verantwortliche, Beschreibung, Zeitplan, Budget, Anbieterstrategie, L\xF6sungstyp, Digital-Kategorie und Tags.",
23829
23937
  "method.gates.enforced.intro": "Zwei Governance-Felder werden beim Wechsel des Lebenszyklus-Status als harte Pr\xFCfungen erzwungen \u2014 aktiv, weil das EU-AI-Act-Modul f\xFCr diesen Arbeitsbereich aktiviert ist:",
23830
- "method.gates.enforced.scope": "Beide Pr\xFCfungen fliessen zus\xE4tzlich zum harten Block in den EU-AI-Act-Reifegrad ein."
23938
+ "method.gates.enforced.scope": "Beide Pr\xFCfungen fliessen zus\xE4tzlich zum harten Block in den EU-AI-Act-Reifegrad ein.",
23939
+ // Quality factor help (docs/quality-factors-concept-2026-10-21.md).
23940
+ "quality.factor.reliability.help": "Bleibt der Dienst verf\xFCgbar und arbeitet korrekt? Verf\xFCgbarkeit gegen\xFCber dem vereinbarten SLA, St\xF6rungsrate, Wiederherstellungszeit nach Ausf\xE4llen, Erfolg von Batches und Jobs.",
23941
+ "quality.factor.security.help": "Ist die Plattform gesch\xFCtzt? Zugriffsschutz und Rollen, Schutz der Kundendaten, sichere Schnittstellen, Pentest oder Sicherheitspr\xFCfung erfolgt, Befunde behoben.",
23942
+ "quality.factor.safety.help": "Kann es bei einem Fehler Schaden anrichten? Sicherheitskritische Abl\xE4ufe, die es ber\xFChrt (Zahlungen, physische Abl\xE4ufe, Kundenberatung), ausfallsicheres Verhalten und ein manueller R\xFCckfall, Gef\xE4hrdungs- oder Auswirkungsanalyse erfolgt, Vorf\xE4lle mit Personen- oder Sachschaden.",
23943
+ "quality.factor.performance.help": "Ist es bei Spitzenlast schnell genug? Antwortzeiten von Seiten und APIs, Durchsatz am Monatsende oder bei Kampagnen, Reserve f\xFCr Wachstum.",
23944
+ "quality.factor.usability.help": "K\xF6nnen Kundinnen, Kunden und Mitarbeitende es ohne Hilfe nutzen? Aufgabenerfolg, Akzeptanz, Barrierefreiheit, Zahl der Supportanfragen.",
23945
+ "quality.factor.maintainability.help": "Kann das Team es sicher \xE4ndern und betreiben? Codequalit\xE4t, automatisierte Tests und Deployments, Dokumentation, technische Schulden, Abh\xE4ngigkeit von einzelnen Fachleuten.",
23946
+ "quality.factor.compatibility.help": "Passt es in die Landschaft? Schnittstellen zu Kern- und Nachbarsystemen, Standards (APIs, Formate), Anbieterabh\xE4ngigkeit.",
23947
+ "quality.factor.dataQuality.help": "Sind die Daten, die der Dienst nutzt, f\xFCr den Zweck geeignet? Richtigkeit, Vollst\xE4ndigkeit, Aktualit\xE4t und Herkunft von Stamm- und Bewegungsdaten (ISO/IEC 25012). Der Schieberegler Daten- und Prozessqualit\xE4t bewertet die Machbarkeit vor der Lieferung; hier wird der gelieferte Stand bewertet."
23831
23948
  };
23832
23949
  var EN_INNOVATION = {
23833
23950
  "brand.app": "BICon Portfolio Steering",
@@ -24091,7 +24208,29 @@ var EN_INNOVATION = {
24091
24208
  "method.audit.scope.governance": "<strong>Status</strong> and the four <strong>evaluation dimension scores</strong> (Value Potential, Validation &amp; Evidence, Governance &amp; Uncertainty, Strategic Impact) with their confidence.",
24092
24209
  "method.audit.scope.metadata": "<strong>All Core Metadata fields</strong> \u2014 name, business unit, owner, description, timeline, budget, partner strategy, solution type, innovation category and tags.",
24093
24210
  "method.gates.enforced.intro": "Two governance fields are enforced as hard checks when the lifecycle status changes \u2014 active because the EU AI Act module is enabled for this workspace:",
24094
- "method.gates.enforced.scope": "Both checks feed the EU AI Act readiness score in addition to the hard block."
24211
+ "method.gates.enforced.scope": "Both checks feed the EU AI Act readiness score in addition to the hard block.",
24212
+ // Quality factor help (docs/quality-factors-concept-2026-10-21.md).
24213
+ "quality.factor.reliability.help": "Does the prototype or pilot work reliably enough for the next stage? Repeatable results, uptime during the trial, known failure modes.",
24214
+ "quality.factor.security.help": "Is what the pilot handles protected? Access to lab and trial data, protection of IP, a security review before real users or data come in.",
24215
+ "quality.factor.safety.help": "Is it safe for the people and the environment it reaches? Hazard and risk analysis (e.g. ISO 14971 for medical devices), safety testing in the trial, adverse events, what must be shown before the next stage.",
24216
+ "quality.factor.performance.help": "Will it hold at the next stage's scale? Throughput and cost per unit at pilot volume, a credible path to production volume. Often still Low required before Pilot.",
24217
+ "quality.factor.usability.help": "Can the intended users work with it? Feedback from trial users, task success, what must change before roll-out.",
24218
+ "quality.factor.maintainability.help": "Can the work be handed over? Someone other than the inventors can run and change it: documentation, a reproducible build or protocol, technical debt taken on for speed, no single expert. Often still Low required before Pilot.",
24219
+ "quality.factor.compatibility.help": "Does it fit the operation that takes it over? Interfaces to lab, clinical, production or partner systems and their processes, standards, lock-in to a partner.",
24220
+ "quality.factor.dataQuality.help": "Is the data behind the results fit for purpose? Accuracy, completeness and lineage of experimental and trial data (ISO/IEC 25012). The Quality of Evidence slider rates feasibility before delivery; this rates the delivered state.",
24221
+ // R&D labels for the same factor keys (docs/quality-factors-innovation-concept-2026-10-25.md):
24222
+ // the stored keys stay, so ratings survive a mode switch.
24223
+ "quality.factor.reliability": "Robustness and reproducibility",
24224
+ "quality.factor.security": "Security and IP protection",
24225
+ "quality.factor.safety": "Product safety",
24226
+ "quality.factor.performance": "Scale-up and unit cost",
24227
+ "quality.factor.usability": "User acceptance",
24228
+ "quality.factor.maintainability": "Transferability",
24229
+ "quality.factor.compatibility": "Fit with operations",
24230
+ "quality.factor.dataQuality": "Data and result quality",
24231
+ "evaluate.quality.subtitle": "Is the result solid enough for the next stage? Rate each factor that matters: how much it is required here, and how well the result meets it today. Factors after ISO/IEC 25010, adapted for R&D. The ratings flag gaps; they do not change any score.",
24232
+ "evaluate.quality.notePlaceholder": "e.g. results repeated at 2 sites; transfer protocol v1",
24233
+ "method.qf.what": "Each initiative can rate eight <strong>quality factors</strong> of the result it delivers, after the product-quality model of ISO/IEC 25010:2023, adapted for R&amp;D: robustness and reproducibility, security and IP protection, product safety, scale-up and unit cost, user acceptance, transferability, fit with operations, and data and result quality (ISO/IEC 25012). The factors are the same in every mode; labels and help texts follow the mode, so ratings survive a mode switch."
24095
24234
  };
24096
24235
  var DE_INNOVATION = {
24097
24236
  "brand.app": "BICon Portfolio Steering",
@@ -24355,7 +24494,28 @@ var DE_INNOVATION = {
24355
24494
  "method.audit.scope.governance": "<strong>Status</strong> und die vier <strong>Bewertungsdimensions-Scores</strong> (Wertpotenzial, Validierung &amp; Evidenz, Governance &amp; Unsicherheit, Strategischer Impact) mit ihrer Konfidenz.",
24356
24495
  "method.audit.scope.metadata": "<strong>Alle Kern-Metadatenfelder</strong> \u2014 Name, Gesch\xE4ftseinheit, Verantwortliche, Beschreibung, Zeitplan, Budget, Partnerstrategie, L\xF6sungstyp, Innovations-Kategorie und Tags.",
24357
24496
  "method.gates.enforced.intro": "Zwei Governance-Felder werden beim Wechsel des Lebenszyklus-Status als harte Pr\xFCfungen erzwungen \u2014 aktiv, weil das EU-AI-Act-Modul f\xFCr diesen Arbeitsbereich aktiviert ist:",
24358
- "method.gates.enforced.scope": "Beide Pr\xFCfungen fliessen zus\xE4tzlich zum harten Block in den EU-AI-Act-Reifegrad ein."
24497
+ "method.gates.enforced.scope": "Beide Pr\xFCfungen fliessen zus\xE4tzlich zum harten Block in den EU-AI-Act-Reifegrad ein.",
24498
+ // Quality factor help (docs/quality-factors-concept-2026-10-21.md).
24499
+ "quality.factor.reliability.help": "Funktioniert der Prototyp oder Pilot zuverl\xE4ssig genug f\xFCr die n\xE4chste Stufe? Wiederholbare Ergebnisse, Verf\xFCgbarkeit w\xE4hrend des Versuchs, bekannte Fehlerbilder.",
24500
+ "quality.factor.security.help": "Ist gesch\xFCtzt, was der Pilot verarbeitet? Zugriff auf Labor- und Versuchsdaten, Schutz des geistigen Eigentums, Sicherheitspr\xFCfung, bevor echte Nutzer oder Daten dazukommen.",
24501
+ "quality.factor.safety.help": "Ist es sicher f\xFCr die Menschen und die Umwelt, die es erreicht? Gef\xE4hrdungs- und Risikoanalyse (z. B. ISO 14971 f\xFCr Medizinprodukte), Sicherheitstests im Versuch, unerw\xFCnschte Ereignisse, was vor der n\xE4chsten Stufe nachgewiesen sein muss.",
24502
+ "quality.factor.performance.help": "H\xE4lt es im Massstab der n\xE4chsten Stufe? Durchsatz und St\xFCckkosten im Pilotvolumen, ein glaubw\xFCrdiger Weg zum Produktionsvolumen. Vor dem Pilot oft noch niedrig gefordert.",
24503
+ "quality.factor.usability.help": "K\xF6nnen die vorgesehenen Nutzerinnen und Nutzer damit arbeiten? R\xFCckmeldungen aus dem Versuch, Aufgabenerfolg, was vor dem Rollout \xE4ndern muss.",
24504
+ "quality.factor.maintainability.help": "L\xE4sst sich die Arbeit \xFCbergeben? Jemand ausser den Erfinderinnen und Erfindern kann das Ergebnis betreiben und \xE4ndern: Dokumentation, reproduzierbarer Aufbau oder reproduzierbares Protokoll, technische Schulden zugunsten der Geschwindigkeit, keine Abh\xE4ngigkeit von einzelnen Fachleuten. Vor dem Pilot oft noch niedrig gefordert.",
24505
+ "quality.factor.compatibility.help": "Passt es in den Betrieb, der es \xFCbernimmt? Schnittstellen zu Labor-, Klinik-, Produktions- oder Partnersystemen und deren Abl\xE4ufen, Standards, Abh\xE4ngigkeit von einem Partner.",
24506
+ "quality.factor.dataQuality.help": "Sind die Daten hinter den Ergebnissen f\xFCr den Zweck geeignet? Richtigkeit, Vollst\xE4ndigkeit und Herkunft der Versuchs- und Testdaten (ISO/IEC 25012). Der Schieberegler Qualit\xE4t der Evidenz bewertet die Machbarkeit vor der Lieferung; hier wird der gelieferte Stand bewertet.",
24507
+ // F&E-Bezeichnungen für dieselben Faktorschlüssel (docs/quality-factors-innovation-concept-2026-10-25.md).
24508
+ "quality.factor.reliability": "Robustheit und Reproduzierbarkeit",
24509
+ "quality.factor.security": "Informationssicherheit und Schutz geistigen Eigentums",
24510
+ "quality.factor.safety": "Produktsicherheit",
24511
+ "quality.factor.performance": "Hochskalierung und St\xFCckkosten",
24512
+ "quality.factor.usability": "Nutzerakzeptanz",
24513
+ "quality.factor.maintainability": "\xDCbertragbarkeit",
24514
+ "quality.factor.compatibility": "Integration in den Betrieb",
24515
+ "quality.factor.dataQuality": "Daten- und Ergebnisqualit\xE4t",
24516
+ "evaluate.quality.subtitle": "Ist das Ergebnis solide genug f\xFCr die n\xE4chste Stufe? Bewerten Sie jeden Faktor, der z\xE4hlt: wie stark er hier gefordert ist und wie gut das Ergebnis ihn heute erf\xFCllt. Faktoren nach ISO/IEC 25010, ausgelegt f\xFCr F&E. Die Bewertung zeigt L\xFCcken; sie ver\xE4ndert keinen Score.",
24517
+ "evaluate.quality.notePlaceholder": "z. B. Ergebnisse an 2 Standorten wiederholt; Transferprotokoll v1",
24518
+ "method.qf.what": "Jede Initiative kann acht <strong>Qualit\xE4tsfaktoren</strong> des gelieferten Ergebnisses bewerten, nach dem Produktqualit\xE4tsmodell der ISO/IEC 25010:2023, f\xFCr F&amp;E ausgelegt: Robustheit und Reproduzierbarkeit, Informationssicherheit und Schutz geistigen Eigentums, Produktsicherheit, Hochskalierung und St\xFCckkosten, Nutzerakzeptanz, \xDCbertragbarkeit, Integration in den Betrieb sowie Daten- und Ergebnisqualit\xE4t (ISO/IEC 25012). Die Faktoren sind in jedem Modus gleich; Bezeichnungen und Hilfetexte folgen dem Modus, die Bewertungen bleiben bei einem Moduswechsel erhalten."
24359
24519
  };
24360
24520
  var DEMO_DIGITAL = {
24361
24521
  "Advisor Copilot": {
@@ -25729,6 +25889,13 @@ var OPTIONAL_SECTIONS = [
25729
25889
  de: "[{title, dueDate, assigneeName?, assigneeUserId?, doneDate?, milestoneId?, riskId?, note?}] \u2014 vereinbarte n\xE4chste Schritte: wer macht was bis wann (h\xF6chstens 50, Titel bis 300 Zeichen). assigneeName ist ein Freitext-Name; assigneeUserId nur, wenn der Benutzer eine Mitglieds-ID aus der App nennt (die App l\xF6scht eine ID, die kein Mitglied ist, der Name bleibt). milestoneId / riskId zeigen auf einen Meilenstein oder ein Risiko derselben Spezifikation \xFCber die dort vergebene id (z. B. \u201Em1\u201C, \u201Er1\u201C); der Composer pr\xE4gt die gespeicherten IDs und beh\xE4lt die Verkn\xFCpfung. Nach den offenen Schritten fragen, besonders je einem pro schwerem Risiko. Bei einem Update alle Pendenzen senden, die bleiben sollen: die Datei ersetzt die Liste."
25730
25890
  }
25731
25891
  },
25892
+ {
25893
+ key: "qualityFactors",
25894
+ hint: {
25895
+ en: "[{factor, required?, state?, targetDate?, note?}] \u2014 is the delivered solution good enough to run? One row per factor that was rated (after ISO/IEC 25010): reliability (availability, stable output), security, safety (harm to people or the environment, fail-safe behaviour), performance (speed, running cost at load), usability, maintainability, compatibility (interfaces, lock-in), dataQuality (the delivered data, not the feasibility slider). In an innovation portfolio the app names them for R&D: reliability = robustness and reproducibility, security = security and IP protection, safety = product safety, performance = scale-up and unit cost, usability = user acceptance, maintainability = transferability, compatibility = fit with operations, dataQuality = data and result quality. required low | medium | high = how much the factor matters here; state 1\u20135 as a whole number (1 unknown or poor, 2 weak, 3 adequate, 4 good, 5 proven in production); targetDate when a gap should be closed; note = evidence or an SLA figure. A gap = required high with state 1\u20132, or required medium with state 1; nothing here changes a score. Ask for the factors that matter once the initiative is at Pilot or later. On an update, send every rating the initiative keeps: the file replaces the list.",
25896
+ de: "[{factor, required?, state?, targetDate?, note?}] \u2014 ist die gelieferte L\xF6sung gut genug f\xFCr den Betrieb? Eine Zeile je bewertetem Faktor (nach ISO/IEC 25010): reliability (Verf\xFCgbarkeit, stabile Ergebnisse), security, safety (Schaden f\xFCr Menschen oder Umwelt, ausfallsicheres Verhalten), performance (Tempo, Betriebskosten unter Last), usability, maintainability, compatibility (Schnittstellen, Anbieterabh\xE4ngigkeit), dataQuality (die gelieferten Daten, nicht der Machbarkeits-Regler). In einem Innovationsportfolio nennt die App sie f\xFCr F&E: reliability = Robustheit und Reproduzierbarkeit, security = Informationssicherheit und Schutz geistigen Eigentums, safety = Produktsicherheit, performance = Hochskalierung und St\xFCckkosten, usability = Nutzerakzeptanz, maintainability = \xDCbertragbarkeit, compatibility = Integration in den Betrieb, dataQuality = Daten- und Ergebnisqualit\xE4t. required low | medium | high = wie stark der Faktor hier z\xE4hlt; state 1\u20135 als ganze Zahl (1 unbekannt oder ungen\xFCgend, 2 schwach, 3 ausreichend, 4 gut, 5 im Betrieb bew\xE4hrt); targetDate, bis wann eine L\xFCcke geschlossen sein soll; note = Nachweis oder SLA-Wert. Eine L\xFCcke = gefordert hoch mit Stand 1\u20132 oder gefordert mittel mit Stand 1; nichts davon \xE4ndert einen Score. Ab Pilot nach den Faktoren fragen, die z\xE4hlen. Bei einem Update alle Bewertungen senden, die bleiben sollen: die Datei ersetzt die Liste."
25897
+ }
25898
+ },
25732
25899
  {
25733
25900
  key: "capabilityImpacts",
25734
25901
  hint: {
@@ -25822,7 +25989,7 @@ function buildSchemaDescriptor(sectionShapes) {
25822
25989
  optionalSections: OPTIONAL_SECTIONS,
25823
25990
  // Exact shapes of the object/array sections (aiAct, economics, actuals,
25824
25991
  // decisionLog, statusReports, requiredSkills, capabilityImpacts,
25825
- // milestones, risks, followUps). Keys ending in "?" are
25992
+ // milestones, risks, followUps, qualityFactors). Keys ending in "?" are
25826
25993
  // optional; "enum[a|b]" lists the allowed values; "<key>" marks a map.
25827
25994
  // Pass these sections as JSON objects/arrays. Any key not listed is
25828
25995
  // rejected with its path.
@@ -25996,7 +26163,7 @@ var parseIfString = (v) => typeof v === "string" ? tryParseJson(v) : v;
25996
26163
  function wireSection(inner, description) {
25997
26164
  return external_exports.preprocess(parseIfString, deepStrict(inner)).describe(description);
25998
26165
  }
25999
- var SECTION_KEYS = ["aiAct", "economics", "actuals", "decisionLog", "statusReports", "requiredSkills", "capabilityImpacts", "milestones", "risks", "followUps"];
26166
+ var SECTION_KEYS = ["aiAct", "economics", "actuals", "decisionLog", "statusReports", "requiredSkills", "capabilityImpacts", "milestones", "risks", "followUps", "qualityFactors"];
26000
26167
  var SECTION_SCHEMAS = Object.fromEntries(
26001
26168
  SECTION_KEYS.map((k) => [k, contractSection(k)])
26002
26169
  );
@@ -26010,6 +26177,7 @@ var SECTION_DESCRIPTIONS = {
26010
26177
  milestones: `Milestones (max 30, title \u2264300 chars): baselineDate = the plan, forecastDate = today's expectation (omit = baseline), doneDate = reached. Dates dd/mm/yyyy or yyyy-mm-dd; ids minted \u2014 an id you give (e.g. "m1") is a label a follow-up's milestoneId can point at. An update replaces the list: send all.`,
26011
26178
  risks: `Risks (max 50, title \u2264300 chars): category delivery|vendor|data|people|regulatory|technical|financial; probability and impact 1\u20135 as whole numbers (score = product); status open|mitigating|closed|occurred (occurred = an issue, with occurredDate); residualProbability + residualImpact = the expectation after mitigation (both or neither); owner (free text), mitigation (the plan), raisedDate, reviewDate, note. Dates dd/mm/yyyy or yyyy-mm-dd; ids minted \u2014 an id you give (e.g. "r1") is a label a follow-up's riskId can point at. An update replaces the list: send all.`,
26012
26179
  followUps: "Follow-ups (max 50, title \u2264300 chars): who does what by when \u2014 title, dueDate, assigneeName (free text; assigneeUserId only when the user gives a member id from the app), doneDate once done, note; milestoneId / riskId = the id label of a milestone or risk in THIS spec (the Composer keeps the link when it mints the stored ids). Dates dd/mm/yyyy or yyyy-mm-dd; ids minted. An update replaces the list: send all.",
26180
+ qualityFactors: "Quality factors of the delivered solution (one row per factor, at most 8): factor reliability|security|safety|performance|usability|maintainability|compatibility|dataQuality; required low|medium|high; state 1\u20135 as a whole number (1 unknown or poor \u2026 5 proven in production); targetDate; note (evidence or SLA). Leave out a factor nobody rated. An update replaces the list: send all.",
26013
26181
  requiredSkills: "Required skills [{skillId, fte, runFte?}] \u2014 fte is the build-phase demand, the optional runFte the run-team demand after the planned end (absent = build fte continues, 0 = no run demand). 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."
26014
26182
  };
26015
26183
  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()]));
@@ -26059,6 +26227,7 @@ var compactSpecSchema = external_exports.object({
26059
26227
  milestones: wireSection(SECTION_SCHEMAS.milestones, SECTION_DESCRIPTIONS.milestones).optional(),
26060
26228
  risks: wireSection(SECTION_SCHEMAS.risks, SECTION_DESCRIPTIONS.risks).optional(),
26061
26229
  followUps: wireSection(SECTION_SCHEMAS.followUps, SECTION_DESCRIPTIONS.followUps).optional(),
26230
+ qualityFactors: wireSection(SECTION_SCHEMAS.qualityFactors, SECTION_DESCRIPTIONS.qualityFactors).optional(),
26062
26231
  aiRiskTier: external_exports.enum(AI_RISK_TIERS2).optional(),
26063
26232
  hitlPosture: external_exports.enum(HITL_POSTURES2).optional(),
26064
26233
  dpiaStatus: external_exports.enum(DPIA_STATUSES2).optional(),
@@ -26123,6 +26292,181 @@ function initiativeQuadrant(i) {
26123
26292
  );
26124
26293
  }
26125
26294
 
26295
+ // ../../initiative-model/src/portfolio-scope.ts
26296
+ function isLiveInitiative(i) {
26297
+ return !i.archived && i.status !== "Retired";
26298
+ }
26299
+ function isInFlightInitiative(i) {
26300
+ return isLiveInitiative(i) && i.status !== "On Hold";
26301
+ }
26302
+
26303
+ // ../../initiative-model/src/quality.ts
26304
+ var QUALITY_NOTE_MAX = 2e3;
26305
+ var QUALITY_STATE_MIN = 1;
26306
+ var QUALITY_STATE_MAX = 5;
26307
+ function isQualityGap(r) {
26308
+ if (r.state === void 0) return false;
26309
+ if (r.required === "high") return r.state <= 2;
26310
+ if (r.required === "medium") return r.state <= 1;
26311
+ return false;
26312
+ }
26313
+ var REQUIRED_RANK = { high: 0, medium: 1, low: 2 };
26314
+ var FACTOR_RANK = new Map(QUALITY_FACTORS.map((f, i) => [f, i]));
26315
+ function weaker(a, b) {
26316
+ const ra = a.required ? REQUIRED_RANK[a.required] : 3;
26317
+ const rb = b.required ? REQUIRED_RANK[b.required] : 3;
26318
+ return ra - rb || (a.state ?? 99) - (b.state ?? 99) || (FACTOR_RANK.get(a.factor) ?? 99) - (FACTOR_RANK.get(b.factor) ?? 99);
26319
+ }
26320
+ function qualityGaps(list) {
26321
+ return (list ?? []).filter(isQualityGap).sort(weaker);
26322
+ }
26323
+ function summariseQuality(list) {
26324
+ const rows = list ?? [];
26325
+ const gaps = qualityGaps(rows);
26326
+ const withState = rows.filter((r) => r.state !== void 0).sort((a, b) => a.state - b.state || weaker(a, b));
26327
+ return {
26328
+ rated: rows.filter((r) => r.required !== void 0 || r.state !== void 0).length,
26329
+ gaps: gaps.length,
26330
+ weakest: gaps[0] ?? withState[0]
26331
+ };
26332
+ }
26333
+ var QUALITY_SCALING_STATUSES = ["Scaling", "In Production"];
26334
+ function scalingQualityGaps(i) {
26335
+ if (!QUALITY_SCALING_STATUSES.includes(i.status)) return [];
26336
+ return qualityGaps(i.qualityFactors).filter((r) => r.required === "high");
26337
+ }
26338
+ function hasQualityGaps(i) {
26339
+ return (i.qualityFactors ?? []).some(isQualityGap);
26340
+ }
26341
+ var BOARD_QUALITY_ROWS = 10;
26342
+ var STAGE_RANK = { "In Production": 0, Scaling: 1, Pilot: 2, Idea: 3 };
26343
+ function boardQualitySlice(initiatives) {
26344
+ const scope = initiatives.filter((i) => isInFlightInitiative(i) && summariseQuality(i.qualityFactors).rated > 0);
26345
+ if (scope.length === 0) return void 0;
26346
+ const rows = [];
26347
+ const perFactor = /* @__PURE__ */ new Map();
26348
+ let withGaps = 0;
26349
+ let scaling = 0;
26350
+ for (const i of scope) {
26351
+ const gaps = qualityGaps(i.qualityFactors);
26352
+ if (gaps.length === 0) continue;
26353
+ withGaps += 1;
26354
+ if (scalingQualityGaps(i).length > 0) scaling += 1;
26355
+ for (const g of gaps) {
26356
+ perFactor.set(g.factor, (perFactor.get(g.factor) ?? 0) + 1);
26357
+ rows.push({
26358
+ initiativeId: i.id,
26359
+ initiativeName: i.name,
26360
+ status: i.status,
26361
+ factor: g.factor,
26362
+ required: g.required,
26363
+ state: g.state,
26364
+ ...g.targetDate ? { targetDate: g.targetDate } : {}
26365
+ });
26366
+ }
26367
+ }
26368
+ rows.sort((a, b) => REQUIRED_RANK[a.required] - REQUIRED_RANK[b.required] || (STAGE_RANK[a.status] ?? 9) - (STAGE_RANK[b.status] ?? 9) || a.state - b.state || a.initiativeName.localeCompare(b.initiativeName) || (FACTOR_RANK.get(a.factor) ?? 99) - (FACTOR_RANK.get(b.factor) ?? 99));
26369
+ const byFactor = QUALITY_FACTORS.filter((f) => perFactor.has(f)).map((f) => ({ factor: f, initiatives: perFactor.get(f) })).sort((a, b) => b.initiatives - a.initiatives);
26370
+ return {
26371
+ gaps: rows.slice(0, BOARD_QUALITY_ROWS),
26372
+ gapCount: rows.length,
26373
+ ratedInitiativeCount: scope.length,
26374
+ initiativesWithGaps: withGaps,
26375
+ scalingWithGaps: scaling,
26376
+ byFactor
26377
+ };
26378
+ }
26379
+ function hasContent(r) {
26380
+ return r.required !== void 0 || r.state !== void 0 || !!r.targetDate || !!r.note;
26381
+ }
26382
+ function canonicalQualityFactors(list) {
26383
+ const byFactor = /* @__PURE__ */ new Map();
26384
+ for (const r of list) if (!byFactor.has(r.factor)) byFactor.set(r.factor, r);
26385
+ return QUALITY_FACTORS.flatMap((factor) => {
26386
+ const r = byFactor.get(factor);
26387
+ if (!r || !hasContent(r)) return [];
26388
+ return [{
26389
+ factor,
26390
+ ...r.required ? { required: r.required } : {},
26391
+ ...r.state !== void 0 ? { state: r.state } : {},
26392
+ ...r.targetDate ? { targetDate: r.targetDate } : {},
26393
+ ...r.note ? { note: r.note } : {}
26394
+ }];
26395
+ });
26396
+ }
26397
+ var squash = (v) => v.toLowerCase().replace(/[^a-z]/g, "");
26398
+ var FACTOR_BY_NAME = new Map(QUALITY_FACTORS.map((f) => [squash(f), f]));
26399
+ var REQUIRED_BY_NAME = new Map([
26400
+ ...QUALITY_REQUIRED_LEVELS.map((l) => [l, l]),
26401
+ ["l", "low"],
26402
+ ["m", "medium"],
26403
+ ["h", "high"],
26404
+ ["niedrig", "low"],
26405
+ ["mittel", "medium"],
26406
+ ["hoch", "high"]
26407
+ ]);
26408
+ function stateOf(v) {
26409
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v.trim()) : NaN;
26410
+ if (!Number.isFinite(n)) return void 0;
26411
+ const r = Math.round(n);
26412
+ return r >= QUALITY_STATE_MIN && r <= QUALITY_STATE_MAX ? r : void 0;
26413
+ }
26414
+ function sanitiseQualityFactors(raw, idx, warnings) {
26415
+ if (raw === void 0 || raw === null) return [];
26416
+ if (!Array.isArray(raw)) {
26417
+ warnings.push(`Initiative #${idx + 1} qualityFactors is not an array; skipped.`);
26418
+ return [];
26419
+ }
26420
+ const out = [];
26421
+ const seen = /* @__PURE__ */ new Set();
26422
+ raw.forEach((entry, eIdx) => {
26423
+ if (!entry || typeof entry !== "object") return;
26424
+ const e = entry;
26425
+ const where = `Initiative #${idx + 1} quality factor #${eIdx + 1}`;
26426
+ const factor = typeof e.factor === "string" ? FACTOR_BY_NAME.get(squash(e.factor)) : void 0;
26427
+ if (!factor) {
26428
+ warnings.push(`${where}: factor "${String(e.factor ?? "")}" is unknown (expected ${QUALITY_FACTORS.join(", ")}); skipped.`);
26429
+ return;
26430
+ }
26431
+ const given = (v) => v !== void 0 && v !== null && v !== "";
26432
+ const required2 = typeof e.required === "string" ? REQUIRED_BY_NAME.get(e.required.trim().toLowerCase()) : void 0;
26433
+ if (given(e.required) && !required2) {
26434
+ warnings.push(`${where}: required level "${String(e.required)}" is unknown (expected low, medium or high); cleared.`);
26435
+ }
26436
+ const state = stateOf(e.state);
26437
+ if (given(e.state) && state === void 0) {
26438
+ warnings.push(`${where}: state "${String(e.state)}" is not a whole number from 1 to 5; cleared.`);
26439
+ }
26440
+ let targetDate;
26441
+ if (given(e.targetDate) && typeof e.targetDate !== "string") {
26442
+ warnings.push(`${where}: targetDate is not a date text (use dd/mm/yyyy or yyyy-mm-dd); cleared.`);
26443
+ } else if (typeof e.targetDate === "string" && e.targetDate.trim()) {
26444
+ const d = normaliseDateCell(e.targetDate);
26445
+ if (d && isCalendarDate(d)) targetDate = d;
26446
+ else warnings.push(`${where}: targetDate "${e.targetDate}" is not a recognised date (use dd/mm/yyyy or yyyy-mm-dd); cleared.`);
26447
+ }
26448
+ const note = typeof e.note === "string" && e.note.trim() ? e.note.slice(0, QUALITY_NOTE_MAX) : void 0;
26449
+ if (note && e.note.length > QUALITY_NOTE_MAX) {
26450
+ warnings.push(`${where}: the note is longer than ${QUALITY_NOTE_MAX} characters; cut.`);
26451
+ }
26452
+ const rating = {
26453
+ factor,
26454
+ ...required2 ? { required: required2 } : {},
26455
+ ...state !== void 0 ? { state } : {},
26456
+ ...targetDate ? { targetDate } : {},
26457
+ ...note ? { note } : {}
26458
+ };
26459
+ if (!hasContent(rating)) return;
26460
+ if (seen.has(factor)) {
26461
+ warnings.push(`${where}: ${factor} is rated twice; the later row was dropped.`);
26462
+ return;
26463
+ }
26464
+ seen.add(factor);
26465
+ out.push(rating);
26466
+ });
26467
+ return canonicalQualityFactors(out);
26468
+ }
26469
+
26126
26470
  // src/context.ts
26127
26471
  import { createHash } from "node:crypto";
26128
26472
 
@@ -26565,6 +26909,21 @@ function assembleInitiative(input2, now = /* @__PURE__ */ new Date()) {
26565
26909
  warnings.push(`followUps.${i}.assigneeUserId: the Composer cannot verify workspace members, and the app clears an id that is not a member of the workspace \u2014 send assigneeName too, so the follow-up keeps its assignee either way.`);
26566
26910
  }
26567
26911
  }
26912
+ const quality = spec.qualityFactors ?? [];
26913
+ const seenFactors = /* @__PURE__ */ new Set();
26914
+ for (const [i, q] of quality.entries()) {
26915
+ const blankNote = q.note !== void 0 && !q.note.trim();
26916
+ const empty = q.required === void 0 && q.state === void 0 && q.targetDate === void 0 && (q.note === void 0 || blankNote);
26917
+ if (empty) {
26918
+ errors.push(`qualityFactors.${i}: ${q.factor} has nothing set \u2014 the app drops an empty row; leave the factor out.`);
26919
+ continue;
26920
+ }
26921
+ if (seenFactors.has(q.factor)) errors.push(`qualityFactors.${i}.factor: ${q.factor} is rated twice \u2014 send one row per factor.`);
26922
+ seenFactors.add(q.factor);
26923
+ if (q.state !== void 0 && !Number.isInteger(q.state)) errors.push(`qualityFactors.${i}.state: must be a whole number 1\u20135 (got ${q.state}).`);
26924
+ if (q.targetDate !== void 0 && !isCalendarDate(q.targetDate)) errors.push(`qualityFactors.${i}.targetDate: "${q.targetDate}" is not a real calendar date \u2014 dd/mm/yyyy or yyyy-mm-dd`);
26925
+ if (blankNote) errors.push(`qualityFactors.${i}.note: must not be blank \u2014 leave it out instead.`);
26926
+ }
26568
26927
  const norm3 = (v) => (v ?? "").trim().toLowerCase();
26569
26928
  const others = context?.initiatives ?? [];
26570
26929
  const ext = norm3(spec.externalId);
@@ -26579,12 +26938,14 @@ function assembleInitiative(input2, now = /* @__PURE__ */ new Date()) {
26579
26938
  for (const [key, what] of [
26580
26939
  ["milestones", "milestones"],
26581
26940
  ["risks", "risks"],
26582
- ["followUps", "follow-ups"]
26941
+ ["followUps", "follow-ups"],
26942
+ ["qualityFactors", "quality ratings"]
26583
26943
  ]) {
26584
26944
  if (spec[key] === void 0) {
26585
26945
  warnings.push(`${key}: none sent, and the app will offer this file as an update \u2014 it replaces the initiative's ${what}, so any the app holds are removed. Send the full list to keep them.`);
26586
26946
  }
26587
26947
  }
26948
+ warnings.push("gateEvidence: the Composer cannot carry stage-gate evidence, and the app will offer this file as an update \u2014 it replaces the record, so the initiative's ticked gate items and a gate bypass are cleared. Re-tick them in the app after the import.");
26588
26949
  }
26589
26950
  let businessUnit = spec.businessUnit;
26590
26951
  let aiCategory = spec.aiCategory;
@@ -26657,6 +27018,9 @@ function assembleInitiative(input2, now = /* @__PURE__ */ new Date()) {
26657
27018
  milestones: mintedMilestones,
26658
27019
  risks: mintedRisks,
26659
27020
  followUps: mintedFollowUps,
27021
+ // The order and key order every writer stores (the leaf's canonical form);
27022
+ // the note as typed, as the app and the API keep it.
27023
+ ...spec.qualityFactors !== void 0 ? { qualityFactors: canonicalQualityFactors(spec.qualityFactors) } : {},
26660
27024
  aiAct: aiAct ? { ...aiAct, ...aiAct.incidents ? { incidents: mintNestedIds(aiAct.incidents, stamp, true) } : {} } : void 0,
26661
27025
  createdAt: stamp,
26662
27026
  updatedAt: stamp
@@ -26694,7 +27058,7 @@ var IMPORT_INSTRUCTIONS = [
26694
27058
  "2. In the BICon app (signed in to the target organisation), open Import/Export.",
26695
27059
  "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.",
26696
27060
  "4. In the 'Single initiative' card, drop the file (or Browse).",
26697
- "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, Initiative ID or name \u2014 the app keeps that initiative's id and creation date, and REPLACES everything else with this file, decision log, status reports, milestones, risks and follow-ups included) and Confirm.",
27061
+ "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, Initiative ID or name \u2014 the app keeps that initiative's id and creation date, and REPLACES everything else with this file, decision log, status reports, milestones, risks, follow-ups and quality ratings included) and Confirm.",
26698
27062
  "6. The initiative appears in the Inventory with the derived dimension scores."
26699
27063
  ].join("\n");
26700
27064
 
@@ -27055,7 +27419,6 @@ var RULE_CATEGORY_NAME = {
27055
27419
  };
27056
27420
  var UNCATEGORISED_CATEGORY = "Uncategorised / Tactical";
27057
27421
  var CATEGORY_NAME_MAX = 60;
27058
- var RULES_ORDER_REV = 2;
27059
27422
  var bound = (min, max) => ({
27060
27423
  ...min !== void 0 ? { min } : {},
27061
27424
  ...max !== void 0 ? { max } : {}
@@ -27076,8 +27439,7 @@ var DEFAULT_PRIORITISATION_RULES = {
27076
27439
  names: {},
27077
27440
  checkOrder: [...DEFAULT_CHECK_ORDER],
27078
27441
  fundingOrder: [...DEFAULT_FUNDING_ORDER],
27079
- weightValueAtStake: false,
27080
- orderRev: RULES_ORDER_REV
27442
+ weightValueAtStake: false
27081
27443
  };
27082
27444
  var LEGACY_DEFAULTS = {
27083
27445
  quickWins: { v: 60, f: 60, g: 40 },
@@ -27150,28 +27512,23 @@ function sanitiseNames(input2) {
27150
27512
  }
27151
27513
  return out;
27152
27514
  }
27153
- var sameOrder = (a, b) => a.length === b.length && a.every((k, i) => k === b[i]);
27154
27515
  function sanitisePrioritisationRules(input2) {
27155
27516
  const d = DEFAULT_PRIORITISATION_RULES;
27156
27517
  const r = input2 && typeof input2 === "object" ? input2 : {};
27157
27518
  const weightValueAtStake = typeof r.weightValueAtStake === "boolean" ? r.weightValueAtStake : d.weightValueAtStake;
27158
27519
  if (isLegacyShape(r)) {
27159
- return { categories: migrateLegacy(r), names: {}, checkOrder: [...LEGACY_CHECK_ORDER], fundingOrder: [...DEFAULT_FUNDING_ORDER], weightValueAtStake, orderRev: RULES_ORDER_REV };
27520
+ return { categories: migrateLegacy(r), names: {}, checkOrder: [...LEGACY_CHECK_ORDER], fundingOrder: [...DEFAULT_FUNDING_ORDER], weightValueAtStake };
27160
27521
  }
27161
27522
  const cats = r.categories && typeof r.categories === "object" ? r.categories : {};
27162
27523
  const categories = Object.fromEntries(
27163
27524
  RULE_CATEGORY_KEYS.map((k) => [k, sanitiseCategory(cats[k], d.categories[k])])
27164
27525
  );
27165
- const names = sanitiseNames(r.names);
27166
- let checkOrder = sanitiseCategoryOrder(r.checkOrder, DEFAULT_CHECK_ORDER);
27167
- if (r.orderRev !== RULES_ORDER_REV && sameOrder(checkOrder, LEGACY_CHECK_ORDER) && Object.keys(names).length === 0 && JSON.stringify(categories) === JSON.stringify(d.categories)) checkOrder = [...DEFAULT_CHECK_ORDER];
27168
27526
  return {
27169
27527
  categories,
27170
- names,
27171
- checkOrder,
27528
+ names: sanitiseNames(r.names),
27529
+ checkOrder: sanitiseCategoryOrder(r.checkOrder, DEFAULT_CHECK_ORDER),
27172
27530
  fundingOrder: sanitiseCategoryOrder(r.fundingOrder, DEFAULT_FUNDING_ORDER),
27173
- weightValueAtStake,
27174
- orderRev: RULES_ORDER_REV
27531
+ weightValueAtStake
27175
27532
  };
27176
27533
  }
27177
27534
  var KEY_BY_NAME = new Map(RULE_CATEGORY_KEYS.map((k) => [RULE_CATEGORY_NAME[k], k]));
@@ -28086,6 +28443,7 @@ function sanitiseInitiative(raw, idx, warnings) {
28086
28443
  const milestones = sanitiseMilestones(o.milestones, idx, warnings);
28087
28444
  const risks = sanitiseRisks(o.risks, idx, warnings);
28088
28445
  const followUps = pruneFollowUpLinks(sanitiseFollowUps(o.followUps, idx, warnings), milestones, risks) ?? [];
28446
+ const qualityFactors = sanitiseQualityFactors(o.qualityFactors, idx, warnings);
28089
28447
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
28090
28448
  return {
28091
28449
  id,
@@ -28142,6 +28500,7 @@ function sanitiseInitiative(raw, idx, warnings) {
28142
28500
  ...milestones.length > 0 ? { milestones } : {},
28143
28501
  ...risks.length > 0 ? { risks } : {},
28144
28502
  ...followUps.length > 0 ? { followUps } : {},
28503
+ ...qualityFactors.length > 0 ? { qualityFactors } : {},
28145
28504
  dependsOn,
28146
28505
  requiredSkills,
28147
28506
  ...capabilityImpacts.length > 0 ? { capabilityImpacts } : {},
@@ -29608,14 +29967,6 @@ function budgetAmount(i, field = "budget") {
29608
29967
  return typeof v === "number" && Number.isFinite(v) ? Math.max(0, v) : 0;
29609
29968
  }
29610
29969
 
29611
- // ../../initiative-model/src/portfolio-scope.ts
29612
- function isLiveInitiative(i) {
29613
- return !i.archived && i.status !== "Retired";
29614
- }
29615
- function isInFlightInitiative(i) {
29616
- return isLiveInitiative(i) && i.status !== "On Hold";
29617
- }
29618
-
29619
29970
  // ../../initiative-model/src/guardrails.ts
29620
29971
  var GUARDRAIL_KEYS = [
29621
29972
  "maxBuBudgetSharePct",
@@ -30169,6 +30520,64 @@ function deriveCapabilityImpact(capabilities, initiatives) {
30169
30520
  linkedInitiativeCount
30170
30521
  };
30171
30522
  }
30523
+ function quarterOf(isoDate) {
30524
+ if (!isoDate) return null;
30525
+ const m = /^(\d{4})-(\d{2})/.exec(isoDate);
30526
+ if (!m) return null;
30527
+ const month = Number(m[2]);
30528
+ if (month < 1 || month > 12) return null;
30529
+ return `${m[1]}-Q${Math.floor((month - 1) / 3) + 1}`;
30530
+ }
30531
+ function maturityTimeline(capabilities, initiatives) {
30532
+ const byId = new Map(capabilities.map((c) => [c.id, c]));
30533
+ const order = /* @__PURE__ */ new Map();
30534
+ const bySort = (a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name);
30535
+ const domains = capabilities.filter((c) => c.parentId === null).sort(bySort);
30536
+ let n = 0;
30537
+ for (const d of domains) {
30538
+ for (const c of capabilities.filter((x) => x.parentId === d.id).sort(bySort)) order.set(c.id, n++);
30539
+ }
30540
+ const steps = [];
30541
+ for (const i of inScope(initiatives)) {
30542
+ if (!countsAsAchieved(i) && !countsAsPlanned(i)) continue;
30543
+ const best = /* @__PURE__ */ new Map();
30544
+ for (const link of i.capabilityImpacts ?? []) {
30545
+ const cap = byId.get(link.capabilityId);
30546
+ if (!cap || cap.parentId === null || !order.has(cap.id)) continue;
30547
+ const level2 = levelOfImpact(link, cap);
30548
+ const prev = best.get(cap.id);
30549
+ if (!prev || level2 !== null && (prev.level === null || level2 > prev.level)) {
30550
+ best.set(cap.id, { cap, kind: link.kind, level: level2 });
30551
+ }
30552
+ }
30553
+ const achieved = countsAsAchieved(i);
30554
+ const endDate = achieved ? null : isoDay(i.timelineEnd);
30555
+ for (const { cap, kind, level: level2 } of best.values()) {
30556
+ steps.push({
30557
+ capability: cap,
30558
+ domainName: byId.get(cap.parentId)?.name ?? "",
30559
+ initiative: i,
30560
+ kind,
30561
+ level: level2,
30562
+ quarter: achieved ? null : quarterOf(endDate),
30563
+ achieved,
30564
+ endDate
30565
+ });
30566
+ }
30567
+ }
30568
+ const groups = /* @__PURE__ */ new Map();
30569
+ for (const s of steps) {
30570
+ const key = s.achieved ? "achieved" : s.quarter ?? "undated";
30571
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(s);
30572
+ }
30573
+ const rank = (key) => key === "achieved" ? 0 : key === "undated" ? 2 : 1;
30574
+ return [...groups.entries()].sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b)).map(([key, list]) => ({
30575
+ key,
30576
+ steps: list.sort(
30577
+ (a, b) => (order.get(a.capability.id) ?? 0) - (order.get(b.capability.id) ?? 0) || a.initiative.name.localeCompare(b.initiative.name)
30578
+ )
30579
+ }));
30580
+ }
30172
30581
  function linkedCapabilityIds(initiative, capabilities) {
30173
30582
  const leafIds = new Set(capabilities.filter((c) => c.parentId !== null).map((c) => c.id));
30174
30583
  const out = [];
@@ -30831,6 +31240,26 @@ function computeBoardActions(input2) {
30831
31240
  });
30832
31241
  }
30833
31242
  }
31243
+ const qualityGaps2 = scalingQualityGaps(i);
31244
+ if (qualityGaps2.length > 0) {
31245
+ const first = qualityGaps2[0];
31246
+ actions.push({
31247
+ id: `qualityGapScaling:${i.id}`,
31248
+ kind: "qualityGapScaling",
31249
+ severity: "medium",
31250
+ href: `/evaluate/${i.id}`,
31251
+ initiativeId: i.id,
31252
+ initiativeName: i.name,
31253
+ params: {
31254
+ count: qualityGaps2.length,
31255
+ factor: first.factor,
31256
+ state: first.state,
31257
+ status: i.status,
31258
+ ...first.targetDate ? { targetDate: first.targetDate } : {}
31259
+ },
31260
+ weight: i.budget || 0
31261
+ });
31262
+ }
30834
31263
  if (i.status === "In Production") {
30835
31264
  if (hasActualsData(i.actuals)) {
30836
31265
  const si = computeSollIst(i.economics, i.actuals);
@@ -31459,6 +31888,7 @@ function applyScope(p, f, now, base) {
31459
31888
  if (f.hasOverdueMilestone !== void 0 && summariseMilestones(i, now).overdue > 0 !== f.hasOverdueMilestone) return false;
31460
31889
  if (f.hasOverdueFollowUp !== void 0 && summariseFollowUps(i.followUps, now).overdue > 0 !== f.hasOverdueFollowUp) return false;
31461
31890
  if (f.hasSevereRisk !== void 0 && summariseRisks(i.risks).severe > 0 !== f.hasSevereRisk) return false;
31891
+ if (f.hasQualityGap !== void 0 && hasQualityGaps(i) !== f.hasQualityGap) return false;
31462
31892
  return true;
31463
31893
  });
31464
31894
  return { items, errors };
@@ -31500,6 +31930,7 @@ var FIELD_NAMES = [
31500
31930
  "nextMilestone",
31501
31931
  "openFollowUps",
31502
31932
  "overdueFollowUps",
31933
+ "qualityGaps",
31503
31934
  "lastStatusReport",
31504
31935
  "capabilities",
31505
31936
  "requiredSkills",
@@ -31592,6 +32023,11 @@ function fieldValue(i, f, c) {
31592
32023
  }
31593
32024
  case "overdueFollowUps":
31594
32025
  return summariseFollowUps(i.followUps, c.now).overdue;
32026
+ case "qualityGaps": {
32027
+ const s = summariseQuality(i.qualityFactors);
32028
+ if (s.rated === 0) return null;
32029
+ return { rated: s.rated, gaps: s.gaps, weakest: s.gaps > 0 && s.weakest?.state !== void 0 ? { factor: s.weakest.factor, state: s.weakest.state } : null };
32030
+ }
31595
32031
  case "lastStatusReport": {
31596
32032
  const r = latestStatusReport(i);
31597
32033
  return r ? { date: r.date, scope: r.scope, timeline: r.timeline, budget: r.budget, skills: r.skills, summary: r.summary ?? null } : null;
@@ -31703,6 +32139,11 @@ function portfolioSummary(p) {
31703
32139
  warnings: p.warnings
31704
32140
  };
31705
32141
  }
32142
+ function qualityGapSeverity(i) {
32143
+ const s = summariseQuality(i.qualityFactors);
32144
+ if (s.rated === 0) return null;
32145
+ return s.gaps === 0 ? 0 : s.gaps * 10 + (10 - (s.weakest?.state ?? 1));
32146
+ }
31706
32147
  function queryInitiatives(p, q, now = Date.now()) {
31707
32148
  const { items, errors } = applyScope(p, q.filter, now);
31708
32149
  if (errors.length) return { ok: false, errors };
@@ -31712,7 +32153,7 @@ function queryInitiatives(p, q, now = Date.now()) {
31712
32153
  if (q.sortBy) {
31713
32154
  const key = q.sortBy;
31714
32155
  const dir = q.sortDir === "asc" ? 1 : -1;
31715
- const val = (i) => fieldValue(i, key, c);
32156
+ const val = (i) => key === "qualityGaps" ? qualityGapSeverity(i) : fieldValue(i, key, c);
31716
32157
  ordered = [...items].sort((a, b) => {
31717
32158
  const va = val(a);
31718
32159
  const vb = val(b);
@@ -32027,6 +32468,7 @@ function initiativeDetail(p, ref, now = Date.now()) {
32027
32468
  economics: effEcon(i) ?? null,
32028
32469
  milestonesList: sortMilestones(i.milestones ?? []).map((m) => ({ title: m.title, baseline: m.baselineDate, forecast: effectiveForecast(m), done: m.doneDate ?? null, state: milestoneState(m, now) })),
32029
32470
  risksList: (i.risks ?? []).map((r) => ({ title: r.title, category: r.category, probability: r.probability, impact: r.impact, residual: r.residualProbability !== void 0 ? [r.residualProbability, r.residualImpact] : null, status: r.status, owner: r.owner ?? null, mitigation: r.mitigation ?? null, unmitigated: isUnmitigated(r, i.followUps) })),
32471
+ qualityFactors: canonicalQualityFactors(i.qualityFactors ?? []).map((q) => ({ factor: q.factor, required: q.required ?? null, state: q.state ?? null, targetDate: q.targetDate ?? null, note: q.note ?? null, gap: isQualityGap(q) })),
32030
32472
  followUpsList: sortFollowUps(i.followUps ?? []).map((f) => ({ title: f.title, due: f.dueDate, done: f.doneDate ?? null, state: followUpState(f, now), assignee: f.assigneeName ?? null })),
32031
32473
  // The six latest by reporting date, newest first, as the Status Report page.
32032
32474
  statusReports: sortStatusReports(i.statusReports ?? []).slice(0, 6),
@@ -32216,7 +32658,7 @@ function compareSnapshots(p, q, now = Date.now()) {
32216
32658
  };
32217
32659
  }
32218
32660
  var BOARD_RISK_ROWS = 10;
32219
- var BOARD_SECTIONS = ["kpis", "capacity", "milestones", "risks", "capabilityImpact"];
32661
+ var BOARD_SECTIONS = ["kpis", "capacity", "milestones", "risks", "quality", "capabilityImpact"];
32220
32662
  function boardSlices(p, q, now = Date.now()) {
32221
32663
  const want = new Set(q.sections?.length ? q.sections : BOARD_SECTIONS);
32222
32664
  const { items, errors } = applyScope(p, q.filter, now, p.initiatives.filter(isLiveInitiative));
@@ -32288,6 +32730,17 @@ function boardSlices(p, q, now = Date.now()) {
32288
32730
  roseCount: s.rose.length
32289
32731
  } : null;
32290
32732
  }
32733
+ if (want.has("quality")) {
32734
+ const s = boardQualitySlice(items);
32735
+ out.quality = s ? {
32736
+ ratedInitiatives: s.ratedInitiativeCount,
32737
+ initiativesWithGaps: s.initiativesWithGaps,
32738
+ scalingWithHighGap: s.scalingWithGaps,
32739
+ gapCount: s.gapCount,
32740
+ byFactor: s.byFactor,
32741
+ gaps: s.gaps.map((g) => ({ initiative: g.initiativeName, status: g.status, factor: g.factor, required: g.required, state: g.state, targetDate: g.targetDate ?? null }))
32742
+ } : null;
32743
+ }
32291
32744
  if (want.has("capabilityImpact")) {
32292
32745
  if (!p.capabilities.some((c) => c.parentId !== null)) out.capabilityImpact = null;
32293
32746
  else {
@@ -32312,6 +32765,11 @@ function boardSlices(p, q, now = Date.now()) {
32312
32765
  };
32313
32766
  }
32314
32767
  var SKILL_MONTHS_DEFAULT = 24;
32768
+ function queueStateOf(cl, names) {
32769
+ const funded = new Set(cl.funded.map((i) => i.id));
32770
+ const ranked = new Set(cl.ranked.map((i) => i.id));
32771
+ return (i) => funded.has(i.id) ? "funded" : !ranked.has(i.id) ? "not in queue" : cl.excludedInitiativeIds.has(i.id) ? "parked: excluded" : cl.parkedSkillReason[i.id] ? `parked: skill cap ${cl.parkedSkillReason[i.id]}` : cl.parkedDependencyReason[i.id] ? `parked: waits on ${names.get(cl.parkedDependencyUpstreamId[i.id] ?? "") ?? cl.parkedDependencyReason[i.id]}` : "parked: budget";
32772
+ }
32315
32773
  function capDeltaErrors(p, deltas) {
32316
32774
  const errors = [];
32317
32775
  for (const [id, d] of Object.entries(deltas ?? {})) {
@@ -32346,7 +32804,7 @@ function skillAllocation(p, q, now = Date.now()) {
32346
32804
  const base = baselineScenario(p.budgetField, p.exclusions);
32347
32805
  const cl = computeScenarioCutLine(candidates, p.rules, p.discountRatePct, p.skills, p.budgetCap, base, liveAll);
32348
32806
  const funded = new Set(cl.funded.map((i) => i.id));
32349
- const ranked = new Set(cl.ranked.map((i) => i.id));
32807
+ const queueState = queueStateOf(cl, names);
32350
32808
  const pipeline = pipelineSkillDemand(items.filter(isInFlightInitiative));
32351
32809
  const run = inProductionSkillFte(liveAll);
32352
32810
  const includeRun = q.includeRunTeams ?? false;
@@ -32368,7 +32826,6 @@ function skillAllocation(p, q, now = Date.now()) {
32368
32826
  const perSkill = skills.map((s) => {
32369
32827
  const users = items.filter((i) => (i.requiredSkills ?? []).some((rs) => rs.skillId === s.id && (rs.fte > 0 || (rs.runFte ?? 0) > 0))).map((i) => {
32370
32828
  const rs = (i.requiredSkills ?? []).find((x) => x.skillId === s.id);
32371
- const parkedBySkill = cl.parkedSkillReason[i.id];
32372
32829
  return {
32373
32830
  id: i.id,
32374
32831
  name: i.name,
@@ -32378,8 +32835,7 @@ function skillAllocation(p, q, now = Date.now()) {
32378
32835
  buildFte: rs.fte,
32379
32836
  ...rs.runFte !== void 0 ? { runFte: rs.runFte } : {},
32380
32837
  timeline: { start: i.timelineStart ?? null, end: i.timelineEnd ?? null },
32381
- // The reasons in rank_initiatives' order.
32382
- queue: funded.has(i.id) ? "funded" : !ranked.has(i.id) ? "not in queue" : cl.excludedInitiativeIds.has(i.id) ? "parked: excluded" : parkedBySkill ? `parked: skill cap ${parkedBySkill}` : cl.parkedDependencyReason[i.id] ? `parked: waits on ${names.get(cl.parkedDependencyUpstreamId[i.id] ?? "") ?? cl.parkedDependencyReason[i.id]}` : "parked: budget"
32838
+ queue: queueState(i)
32383
32839
  };
32384
32840
  }).sort((a, b) => b.fte - a.fte || a.name.localeCompare(b.name));
32385
32841
  const series = rows.map((r) => ({ month: String(r.month), fte: Number(r[s.id] ?? 0) }));
@@ -32446,6 +32902,307 @@ function skillAllocation(p, q, now = Date.now()) {
32446
32902
  while (rowCap > 5 && JSON.stringify(build(rowCap, perSkill)).length > OUTPUT_BUDGET_CHARS) rowCap = Math.max(5, Math.floor(rowCap / 2));
32447
32903
  return fitRows((list) => build(rowCap, list), perSkill).body;
32448
32904
  }
32905
+ var PLANNED = /* @__PURE__ */ new Set(["Idea", "Pilot", "Scaling"]);
32906
+ function capabilityAllocation(p, q, now = Date.now()) {
32907
+ const errors = [];
32908
+ const leaves = p.capabilities.filter((c) => c.parentId !== null);
32909
+ if (leaves.length === 0) return { ok: false, errors: ["The backup has no capability map (Settings \u2192 Taxonomy \u2192 Capabilities in the app)."] };
32910
+ const domainName = (c) => p.capabilities.find((d) => d.id === c.parentId)?.name ?? "";
32911
+ const path2 = (c) => `${domainName(c)} / ${c.name}`;
32912
+ let pool = leaves;
32913
+ if (q.domain) {
32914
+ const ds = p.capabilities.filter((x) => x.parentId === null && lc(x.name) === lc(q.domain));
32915
+ if (ds.length === 0) errors.push(`domain: "${q.domain}" is not a domain of this map (have: ${p.capabilities.filter((x) => x.parentId === null).map((x) => x.name).join(" | ")}).`);
32916
+ else pool = leaves.filter((c) => ds.some((d) => d.id === c.parentId));
32917
+ }
32918
+ const chosen = /* @__PURE__ */ new Map();
32919
+ for (const ref of q.capabilities ?? []) {
32920
+ const hits = pool.filter((c) => c.id === ref || lc(path2(c)) === lc(ref));
32921
+ const byName = hits.length ? hits : pool.filter((c) => lc(c.name) === lc(ref));
32922
+ if (byName.length === 1) chosen.set(byName[0].id, byName[0]);
32923
+ else if (byName.length > 1) errors.push(`capabilities: "${ref}" names ${byName.length} capabilities (${byName.map(path2).join(" | ")}); pass "Domain / Capability" or the id.`);
32924
+ else if (q.domain && leaves.some((c) => c.id === ref || lc(path2(c)) === lc(ref) || lc(c.name) === lc(ref))) errors.push(`capabilities: "${ref}" is not in the domain ${q.domain}.`);
32925
+ else errors.push(`capabilities: "${ref}" is not a capability of this map (a domain is not a capability; pass domain for a whole domain).`);
32926
+ }
32927
+ const known = new Set(p.initiatives.map((i) => i.id));
32928
+ for (const id of q.excludedIds ?? []) if (!known.has(id)) errors.push(`excludedIds: "${id}" is not an initiative id of this backup.`);
32929
+ const liveAll = p.initiatives.filter(isLiveInitiative);
32930
+ const scoped = applyScope(p, q.filter, now, liveAll);
32931
+ errors.push(...scoped.errors);
32932
+ if (errors.length) return { ok: false, errors };
32933
+ const items = scoped.items;
32934
+ const names = new Map(p.initiatives.map((i) => [i.id, i.name]));
32935
+ const wantedIds = new Set((chosen.size ? [...chosen.values()] : pool).map((c) => c.id));
32936
+ const candidates = applyScope(p, q.filter ? { ...q.filter, status: void 0 } : void 0, now, liveAll).items;
32937
+ const base = baselineScenario(p.budgetField, p.exclusions);
32938
+ const cl = computeScenarioCutLine(candidates, p.rules, p.discountRatePct, p.skills, p.budgetCap, base, liveAll);
32939
+ const queueState = queueStateOf(cl, names);
32940
+ const funded = new Set(cl.funded.map((i) => i.id));
32941
+ const ranked = new Set(cl.ranked.map((i) => i.id));
32942
+ const parked = (i) => PLANNED.has(i.status) && ranked.has(i.id) && !funded.has(i.id);
32943
+ const result = deriveCapabilityImpact(p.capabilities, items);
32944
+ const steps = maturityTimeline(p.capabilities, items).flatMap((g) => g.steps.map((st) => ({ ...st, group: g.key })));
32945
+ const leafIds = new Set(leaves.map((c) => c.id));
32946
+ const limit = Math.min(q.limit ?? ROW_LIMIT_DEFAULT, ROW_LIMIT_MAX);
32947
+ const perCap = result.capabilities.filter((cp) => wantedIds.has(cp.capability.id)).map((cp) => {
32948
+ const c = cp.capability;
32949
+ const rows = cp.linked.map((i) => {
32950
+ const link = (i.capabilityImpacts ?? []).find((l) => l.capabilityId === c.id);
32951
+ return {
32952
+ id: i.id,
32953
+ name: i.name,
32954
+ status: i.status,
32955
+ parked: parked(i),
32956
+ kind: link.kind,
32957
+ level: levelOfImpact(link, c),
32958
+ counts: cp.counting.includes(i),
32959
+ plannedQuarter: PLANNED.has(i.status) ? quarterOf(i.timelineEnd) : null,
32960
+ spend: Math.round(attributedSpend(i, leafIds).get(c.id) ?? 0),
32961
+ queue: i.status === "In Production" ? "in production" : queueState(i)
32962
+ };
32963
+ }).sort((a, b) => (b.level ?? -1) - (a.level ?? -1) || a.name.localeCompare(b.name));
32964
+ const securedLevel = Math.max(cp.current, ...rows.filter((r) => r.counts && !r.parked).map((r) => r.level ?? 0));
32965
+ const atRisk = cp.projected > securedLevel;
32966
+ return {
32967
+ id: c.id,
32968
+ capability: c.name,
32969
+ domain: cp.domainName,
32970
+ importance: c.importance,
32971
+ current: cp.current,
32972
+ projected: cp.projected,
32973
+ target: cp.target,
32974
+ achieved: cp.achieved,
32975
+ planned: cp.planned,
32976
+ plannedBy: cp.plannedBy,
32977
+ change: cp.change,
32978
+ gap: cp.gap,
32979
+ coverage: cp.coverage,
32980
+ concentrated: cp.concentrated,
32981
+ retire: cp.retire,
32982
+ spend: Math.round(cp.spend),
32983
+ spendSharePct: Math.round(cp.spendShare * 1e3) / 10,
32984
+ ...atRisk ? { atRisk: { securedLevel, because: `The projected ${cp.projected} relies on pipeline work the baseline cut line parks; funded and running work brings ${securedLevel}.` } } : {},
32985
+ initiatives: rows.map(({ parked: _parked, ...r }) => r),
32986
+ initiativeCount: rows.length,
32987
+ path: steps.filter((st) => st.capability.id === c.id).map((st) => ({ when: st.group, initiative: st.initiative.name, kind: st.kind, level: st.level }))
32988
+ };
32989
+ });
32990
+ let whatIf;
32991
+ if (q.excludedIds || q.budgetCap !== void 0) {
32992
+ const scenario = { ...base, ...q.excludedIds ? { excludedIds: Object.fromEntries(q.excludedIds.map((id) => [id, {}])) } : {} };
32993
+ const cap = q.budgetCap === null ? void 0 : q.budgetCap ?? p.budgetCap;
32994
+ const alt = computeScenarioCutLine(candidates, p.rules, p.discountRatePct, p.skills, cap, scenario, liveAll);
32995
+ const altFunded = new Set(alt.funded.map((i) => i.id));
32996
+ const lost = items.filter((i) => PLANNED.has(i.status) && (funded.has(i.id) && !altFunded.has(i.id) || alt.excludedInitiativeIds.has(i.id) && !cl.excludedInitiativeIds.has(i.id)));
32997
+ const lostIds = new Set(lost.map((i) => i.id));
32998
+ const after = deriveCapabilityImpact(p.capabilities, items.filter((i) => !lostIds.has(i.id)));
32999
+ const afterById = new Map(after.capabilities.map((cp) => [cp.capability.id, cp]));
33000
+ const changed = result.capabilities.filter((cp) => wantedIds.has(cp.capability.id)).map((cp) => ({ cp, a: afterById.get(cp.capability.id) })).filter(({ cp, a }) => a.projected !== cp.projected || a.gap !== cp.gap || a.coverage !== cp.coverage).map(({ cp, a }) => ({
33001
+ capability: cp.capability.name,
33002
+ domain: cp.domainName,
33003
+ importance: cp.capability.importance,
33004
+ projected: { before: cp.projected, after: a.projected },
33005
+ gap: { before: cp.gap, after: a.gap },
33006
+ coverage: { before: cp.coverage, after: a.coverage }
33007
+ }));
33008
+ whatIf = {
33009
+ excluded: [...alt.excludedInitiativeIds].map((id) => names.get(id) ?? id),
33010
+ budgetCap: alt.cap.budgetCap ?? null,
33011
+ // Funded in the baseline and not in the what-if, or newly excluded.
33012
+ takenOut: lost.slice(0, limit).map((i) => ({ name: i.name, was: queueState(i) })),
33013
+ ...lost.length > limit ? { takenOutCount: lost.length } : {},
33014
+ capabilitiesChanged: changed.slice(0, limit),
33015
+ ...changed.length > limit ? { changedCount: changed.length } : {},
33016
+ newGaps: changed.filter((x) => x.gap.before === 0 && x.gap.after > 0).map((x) => x.capability),
33017
+ note: "Estimate with the app's rules: the planned initiatives that lose their funding, or that the what-if newly excludes, are taken out and the map is derived again. The baseline projection already counts parked pipeline work (as the page does), so a higher budget cap cannot show a gain here; see atRisk instead. excludedIds replaces the organisation's exclusions. The app has no such what-if of its own."
33018
+ };
33019
+ }
33020
+ const build = (rowCap2, list) => ({
33021
+ ok: true,
33022
+ asOf: p.exportedAt,
33023
+ scope: `${items.length} live initiatives${q.filter ? " in the filter" : ""}, ${result.linkedInitiativeCount} linked to the map`,
33024
+ hasCap: cl.hasCap,
33025
+ capabilities: list.map((x) => ({ ...x, initiatives: x.initiatives.slice(0, rowCap2), path: x.path.slice(0, rowCap2) })),
33026
+ ...list.length < perCap.length ? { capabilitiesReturned: list.length, capabilityCount: perCap.length, cutForSize: `Cut to the first ${list.length} of ${perCap.length} capabilities to fit; pass capabilities or domain to see the others.` } : {},
33027
+ ...rowCap2 < limit && perCap.some((x) => x.initiatives.length > rowCap2) ? { rowsCutForSize: `Initiative rows and path steps cut to ${rowCap2} per capability to fit (initiativeCount gives the full number).` } : {},
33028
+ ...whatIf ? { whatIf } : {},
33029
+ note: "The Capabilities page's rules over live initiatives (archived and Retired ones are out of scope): projected = the highest level the counting initiatives bring, never below current; In Production = achieved, Idea/Pilot/Scaling = planned (plannedBy = earliest end date reaching it), On Hold and retire links do not count; a link without its own level brings the target; spend = each initiative's budget split evenly over its mapped links; coverage and concentration as on the page. queue = the baseline cut line (rank_initiatives' rule); hasCap = the cut line has a budget cap, a skill cap or exclusions (without any, everything is funded). atRisk = the projection needs parked pipeline work. path = the Timeline tab (achieved, then quarters of the planned end dates, then undated); a retire step has level null. Quote these figures rather than recomputing them."
33030
+ });
33031
+ let rowCap = limit;
33032
+ while (rowCap > 5 && JSON.stringify(build(rowCap, perCap)).length > OUTPUT_BUDGET_CHARS) rowCap = Math.max(5, Math.floor(rowCap / 2));
33033
+ return fitRows((list) => build(rowCap, list), perCap).body;
33034
+ }
33035
+
33036
+ // src/analysis-templates.ts
33037
+ var RULES = [
33038
+ "Rules for every step:",
33039
+ "- If no portfolio is loaded in this conversation, ask for the path of the backup file (BICon app: Import/Export \u2192 Backup \u2192 Export; often in Downloads) and call `load_portfolio` first; pass the portfolioId it returns as `portfolio` to every other tool.",
33040
+ "- Use only figures the tools return. Never compute, re-derive or round them differently; when two figures differ, say which tool gave which.",
33041
+ "- Name the export date the figures stand on (exportedAt in load_portfolio's answer), and pass on every note, cut or size line the tools return.",
33042
+ "- Answer in the user's language, plainly, without jargon."
33043
+ ].join("\n");
33044
+ var ANALYSIS_TEMPLATES = [
33045
+ {
33046
+ key: "board_briefing",
33047
+ title: "Board briefing on the portfolio",
33048
+ description: "A one-page briefing for the board: health, capacity, delivery, risks, capabilities and the actions to decide.",
33049
+ example: "Give me a one-page board briefing on the portfolio.",
33050
+ tools: ["board_slices", "portfolio_readings", "rank_initiatives"],
33051
+ args: [],
33052
+ body: () => [
33053
+ "Write a one-page board briefing on the loaded portfolio.",
33054
+ "1. Call `board_slices` (all `sections`). It gives the Board Memo's own chapter data: KPIs, capacity, milestones, risks, quality gaps and capability impact.",
33055
+ '2. Call `portfolio_readings` with `readings` ["boardActions", "signals"] for the actions the app proposes and the early warnings.',
33056
+ "3. Call `rank_initiatives` with no other input for the baseline cut line: what is funded, what is parked and why.",
33057
+ "Structure: three headline sentences; then Health (KPIs), Capacity (funded vs parked, the must-do block), Delivery (missed and due milestones), Risks (severe and rising), Capabilities (top changes and uncovered gaps); end with the three to five decisions the board should take, each tied to a Board Action."
33058
+ ].join("\n")
33059
+ },
33060
+ {
33061
+ key: "budget_what_if",
33062
+ title: "What if the budget changes",
33063
+ description: "What a budget cut or rise does to the cut line: which initiatives fall below it or come back.",
33064
+ example: "What happens if the budget drops by 10 %?",
33065
+ tools: ["rank_initiatives"],
33066
+ args: [{ name: "changePct", description: "Budget change in percent, a whole number from -90 to 200, e.g. -10 or 20 (default -10)", required: false, number: { min: -90, max: 200 } }],
33067
+ body: (a) => {
33068
+ const pct = a.changePct?.trim() || "-10";
33069
+ return [
33070
+ `Show what a budget change of ${pct} % does to the portfolio.`,
33071
+ "1. Call `rank_initiatives` with `limit` 200: the baseline cut line. If its scenario.budgetCap is null, say that the organisation has no budget cap, so a budget change moves nothing, and stop.",
33072
+ `2. Call \`rank_initiatives\` again with \`tradeoffPct\` ${pct} and \`limit\` 200.`,
33073
+ "3. Compare the two answers row by row: initiatives that lose or gain funding, the budget cap before and after (scenario.budgetCap) and the funded budget before and after (totals). If either answer says truncated, say that only the rows shown were compared.",
33074
+ "Report: the change in one sentence; a table of the initiatives that move (name, status, bucket, budget, before \u2192 after); what happens to the must-do block; which parked initiatives come closest to funding."
33075
+ ].join("\n");
33076
+ }
33077
+ },
33078
+ {
33079
+ key: "skill_bottlenecks",
33080
+ title: "Skill bottlenecks and what frees them",
33081
+ description: "Which skills are over their cap, when, who drives the peak, and how many FTE would free the parked work.",
33082
+ example: "Which skills are over their cap in the next 12 months, and who drives the peak?",
33083
+ tools: ["skill_allocation"],
33084
+ args: [{ name: "months", description: "Months to look ahead, 1 to 48 (default 12)", required: false, number: { min: 1, max: 48 } }],
33085
+ body: (a) => {
33086
+ const months = a.months?.trim() || "12";
33087
+ return [
33088
+ `Find the skill bottlenecks over the next ${months} months.`,
33089
+ `1. Call \`skill_allocation\` with \`months\` ${months}.`,
33090
+ '2. For each skill with months over cap (timeline.monthsOverCap) or initiatives parked by its cap (queue "parked: skill cap \u2026"), call `skill_allocation` again for that skill with `skillFteDeltas` mapping that skill\'s id from step 1 to the extra FTE, which is the larger of the highest overBy in monthsOverCap and the shortfall now (minus now.headroom, when headroom is below zero), to see what the extra FTE frees.',
33091
+ "Report per bottleneck skill: cap vs pipeline demand now; the months over cap and the peak with the initiatives behind it; the initiatives the cap parks; what the extra FTE frees (nowFunded). End with a short ranking of where extra FTE helps most."
33092
+ ].join("\n");
33093
+ }
33094
+ },
33095
+ {
33096
+ key: "capability_gaps",
33097
+ title: "Capability gaps and who closes them",
33098
+ description: "Which capabilities stay below target, which gaps nobody covers, and which depend on parked work.",
33099
+ example: "Which capability gaps stay open, and which ones depend on work we have parked?",
33100
+ tools: ["capability_allocation"],
33101
+ args: [{ name: "domain", description: "Only this domain of the capability map (default all)", required: false }],
33102
+ body: (a) => {
33103
+ const domain2 = a.domain?.trim();
33104
+ return [
33105
+ `Review the capability gaps${domain2 ? ` in the domain ${domain2}` : ""}.`,
33106
+ `1. Call \`capability_allocation\`${domain2 ? ` with \`domain\` ${JSON.stringify(domain2)}` : ""}.`,
33107
+ "2. Group the capabilities: (a) uncovered gaps (coverage uncovered), differentiating first; (b) gaps closed only on paper (atRisk: the projection needs parked work); (c) partly covered gaps; (d) concentration (concentrated).",
33108
+ '3. For the two atRisk capabilities with the largest gap between projected and securedLevel, name the parked initiatives (queue starting "parked:") and their reason.',
33109
+ "Report: a table per group (capability, domain, importance, current \u2192 projected \u2192 target, initiatives); then the maturity path of the differentiating capabilities by quarter (path); end with the decisions that would close the most important gaps."
33110
+ ].join("\n");
33111
+ }
33112
+ },
33113
+ {
33114
+ key: "risk_review",
33115
+ title: "Risk and delivery review",
33116
+ description: "Severe and rising risks, missed milestones, overdue follow-ups and the early warnings not yet recorded.",
33117
+ example: "Which high risks have no owner, and which milestones are slipping?",
33118
+ tools: ["portfolio_readings", "board_slices", "query_initiatives", "initiative_detail"],
33119
+ args: [],
33120
+ body: () => [
33121
+ "Review risks and delivery across the portfolio.",
33122
+ '1. Call `portfolio_readings` with `readings` ["risks", "signals"].',
33123
+ '2. Call `board_slices` with `sections` ["milestones", "risks"] for missed and due milestones and the risks that rose since the baseline.',
33124
+ '3. Call `query_initiatives` with `filter` {"hasSevereRisk": true} and `fields` ["status", "severeRisks", "topRisk", "unmitigatedRisks", "overdueMilestones", "nextMilestone", "overdueFollowUps"] for the initiatives behind them.',
33125
+ '4. Call `query_initiatives` with `filter` {"hasOverdueFollowUp": true} and `fields` ["status", "overdueFollowUps", "openFollowUps"] for overdue follow-ups anywhere in the portfolio.',
33126
+ "5. For each initiative from step 3 with unmitigatedRisks above zero (at most five, most first), call `initiative_detail` with `initiative` set to its name: risksList marks each unmitigated risk (no owner, or no linked follow-up).",
33127
+ "Report: severe risks without an owner or a follow-up (from step 5, and say how many initiatives were not opened); risks that rose; missed milestones and the next 90 days; overdue follow-ups; early warnings not yet in the register; end with the five actions that reduce exposure most."
33128
+ ].join("\n")
33129
+ },
33130
+ {
33131
+ key: "since_snapshot",
33132
+ title: "What changed since the last snapshot",
33133
+ description: "KPIs, added and dropped initiatives, score and risk moves since a snapshot.",
33134
+ example: "What changed since the last snapshot?",
33135
+ tools: ["compare_snapshots"],
33136
+ args: [{ name: "from", description: "Snapshot id or label to compare from (default the newest)", required: false }],
33137
+ body: (a) => {
33138
+ const from = a.from?.trim();
33139
+ return [
33140
+ `Show what changed since ${from ? `the snapshot "${from}"` : "the newest snapshot"}.`,
33141
+ `1. Call \`compare_snapshots\`${from ? ` with \`from\` "${from}"` : ""}.`,
33142
+ "Report: the KPI moves in one table; initiatives added and gone; the largest score changes (value and governance side by side); risks that rose. End with what deserves the board's attention and why."
33143
+ ].join("\n");
33144
+ }
33145
+ },
33146
+ {
33147
+ key: "initiative_brief",
33148
+ title: "One initiative, one page",
33149
+ description: "A one-page brief on one initiative: scores, economics, delivery, risks, capabilities and decisions.",
33150
+ example: "Give me a one-page brief on KYC Remediation.",
33151
+ tools: ["initiative_detail", "rank_initiatives"],
33152
+ args: [{ name: "initiative", description: "Initiative name or id", required: true }],
33153
+ body: (a) => {
33154
+ const name = a.initiative?.trim() ?? "";
33155
+ return [
33156
+ `Write a one-page brief on the initiative ${name}.`,
33157
+ `1. Call \`initiative_detail\` with \`initiative\` ${JSON.stringify(name)}.`,
33158
+ "2. Call `rank_initiatives` with `limit` 200 and find the initiative's row: rank, bucket, funded or parked and why. In Production, Retired and archived initiatives are not in the queue: then say so and take the bucket from initiative_detail (priorityBucket); never call it parked.",
33159
+ "Report: purpose and status in two sentences; scores and quadrant; economics; place in the cut line; milestones; open risks and follow-ups; capabilities it moves; the latest status report; the last decisions. End with the open questions for its sponsor."
33160
+ ].join("\n");
33161
+ }
33162
+ }
33163
+ ];
33164
+ function checkTemplateArgs(t, args = {}) {
33165
+ const errors = [];
33166
+ const out = {};
33167
+ for (const name of Object.keys(args)) {
33168
+ if (!t.args.some((a) => a.name === name)) errors.push(`${t.key} takes no argument "${name}"${t.args.length ? ` (it takes: ${t.args.map((a) => a.name).join(", ")})` : ""}`);
33169
+ }
33170
+ for (const a of t.args) {
33171
+ const raw = args[a.name];
33172
+ const v = raw === void 0 ? "" : String(raw).trim();
33173
+ if (!v) {
33174
+ if (a.required) errors.push(`${t.key} needs "${a.name}": ${a.description}`);
33175
+ continue;
33176
+ }
33177
+ if (a.number) {
33178
+ const n = Number(v);
33179
+ if (!Number.isInteger(n) || n < a.number.min || n > a.number.max) {
33180
+ errors.push(`"${a.name}" must be a whole number from ${a.number.min} to ${a.number.max}, not ${JSON.stringify(raw)}`);
33181
+ continue;
33182
+ }
33183
+ out[a.name] = String(n);
33184
+ } else {
33185
+ out[a.name] = v;
33186
+ }
33187
+ }
33188
+ return errors.length ? { ok: false, errors } : { ok: true, args: out };
33189
+ }
33190
+ function templateText(t, args = {}) {
33191
+ return `${t.title}
33192
+
33193
+ ${t.body(args)}
33194
+
33195
+ ${RULES}`;
33196
+ }
33197
+ function nextSteps() {
33198
+ return {
33199
+ hint: "Unless the user already asked something specific, give a two-line summary and offer these as a short numbered list (title and example), then wait for the choice. To run one, call get_analysis_template with its key and follow the text; an option with needs asks the user for those inputs first.",
33200
+ options: ANALYSIS_TEMPLATES.map((t) => {
33201
+ const needs = t.args.filter((a) => a.required).map((a) => a.name);
33202
+ return { key: t.key, title: t.title, example: t.example, ...needs.length ? { needs } : {} };
33203
+ })
33204
+ };
33205
+ }
32449
33206
 
32450
33207
  // src/analysis-schema.ts
32451
33208
  var portfolioId = () => external_exports.string().regex(/^pf_[0-9a-f]{16}$/, "a portfolioId looks like pf_<16 hex>, from load_portfolio").describe("The portfolioId load_portfolio returned");
@@ -32466,6 +33223,7 @@ var scopeFilter = () => external_exports.object({
32466
33223
  hasOverdueMilestone: external_exports.boolean().optional(),
32467
33224
  hasOverdueFollowUp: external_exports.boolean().optional(),
32468
33225
  hasSevereRisk: external_exports.boolean().optional().describe("live high/critical risk"),
33226
+ hasQualityGap: external_exports.boolean().optional().describe("a quality gap, any status"),
32469
33227
  ids: external_exports.array(external_exports.string()).optional()
32470
33228
  }).strict().optional().describe("Scope (AND; text matches ignore case; archived left out unless includeArchived; scores 0\u2013100, composite = Prioritisation's)");
32471
33229
  var loadPortfolioInputSchema = external_exports.object({
@@ -32534,6 +33292,15 @@ var skillAllocationInputSchema = external_exports.object({
32534
33292
  excludedIds: external_exports.array(external_exports.string()).optional().describe("What-if: initiative ids parked out of the queue (replaces the organisation's exclusions)"),
32535
33293
  limit: external_exports.number().int().min(1).max(ROW_LIMIT_MAX).optional().describe("Initiative rows per skill (default 50)")
32536
33294
  }).strict();
33295
+ var capabilityAllocationInputSchema = external_exports.object({
33296
+ portfolio: portfolioId(),
33297
+ capabilities: external_exports.array(external_exports.string().trim().min(1)).optional().describe('Capability ids, names or "Domain / Capability" (default every capability; a domain is not a capability, use domain)'),
33298
+ domain: external_exports.string().trim().min(1).optional().describe("Only the capabilities of this domain (by name)"),
33299
+ filter: scopeFilter(),
33300
+ excludedIds: external_exports.array(external_exports.string()).optional().describe("What-if: initiative ids parked out of the queue (replaces the organisation's exclusions)"),
33301
+ budgetCap: external_exports.number().positive().nullable().optional().describe("What-if: budget cap, above 0; null = no cap; default the organisation's envelope"),
33302
+ limit: external_exports.number().int().min(1).max(ROW_LIMIT_MAX).optional().describe("Initiative rows per capability (default 50)")
33303
+ }).strict();
32537
33304
 
32538
33305
  // src/server.ts
32539
33306
  var SERVER_NAME = "bicon-initiative-composer";
@@ -32570,12 +33337,13 @@ function createServer() {
32570
33337
  async () => text({
32571
33338
  capabilities: [
32572
33339
  "strict typed schemas: unknown keys anywhere in the spec are rejected with their exact path (never silently dropped)",
32573
- "sections (aiAct/economics/actuals/decisionLog/statusReports/requiredSkills/capabilityImpacts/milestones/risks/followUps) accept a JSON object/array or the same JSON as a string; row ids are minted by the Composer, and a follow-up's milestoneId / riskId names a milestone or risk of the same spec by the id label given there",
33340
+ "sections (aiAct/economics/actuals/decisionLog/statusReports/requiredSkills/capabilityImpacts/milestones/risks/followUps/qualityFactors) accept a JSON object/array or the same JSON as a string; row ids are minted by the Composer, and a follow-up's milestoneId / riskId names a milestone or risk of the same spec by the id label given there",
32574
33341
  "dates accepted as dd/mm/yyyy, dd.mm.yyyy or yyyy-mm-dd (stored as yyyy-mm-dd)",
32575
33342
  "get_initiative_schema: sectionShapes, aiActVocabularies, portfolio-mode wording overlays",
32576
33343
  "AI-Act vocabulary and id-format enforcement with the allowed values in the error",
32577
33344
  "one pipeline: export_initiative_file takes the same spec as assemble_initiative \u2014 scores are always derived; pass `id` to update an existing initiative",
32578
- "portfolio analysis: load_portfolio reads the app's full backup (by path) with the app's own sanitiser; query_initiatives, portfolio_readings, rank_initiatives, initiative_detail, compare_snapshots, board_slices and skill_allocation compute with the app's own rules (ranking and cut line, KPIs, concentration and HHI, operating model, capability impact, risk register, early warnings, Board Actions, snapshot deltas, the Board Memo's chapter data, skill allocation over time)",
33345
+ "portfolio analysis: load_portfolio reads the app's full backup (by path) with the app's own sanitiser; query_initiatives, portfolio_readings, rank_initiatives, initiative_detail, compare_snapshots, board_slices, skill_allocation and capability_allocation compute with the app's own rules (ranking and cut line, KPIs, concentration and HHI, operating model, capability impact, risk register, early warnings, Board Actions, snapshot deltas, the Board Memo's chapter data, skill allocation over time, capability allocation)",
33346
+ "ready-made analyses: seven recipes (board briefing, budget what-if, skill bottlenecks, capability gaps, risk review, change since a snapshot, one-page initiative brief) as prompts, as load_portfolio's nextSteps and by get_analysis_template",
32579
33347
  "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 / capability / update ids are validated (capabilities from the version-2 context file)"
32580
33348
  ]
32581
33349
  })
@@ -32650,14 +33418,14 @@ function createServer() {
32650
33418
  },
32651
33419
  async (input2) => {
32652
33420
  const r = loadPortfolio(input2);
32653
- return r.ok ? compact(fitAnswer({ ok: true, ...portfolioSummary(r.portfolio) })) : compact({ ok: false, errors: r.errors });
33421
+ return r.ok ? compact({ ...fitAnswer({ ok: true, ...portfolioSummary(r.portfolio) }), nextSteps: nextSteps() }) : compact({ ok: false, errors: r.errors });
32654
33422
  }
32655
33423
  );
32656
33424
  server.registerTool(
32657
33425
  "query_initiatives",
32658
33426
  {
32659
33427
  title: "Query initiatives",
32660
- description: "Filters the loaded portfolio's initiatives (status, business unit, category, driver, tags, saved portfolio, capability, skill, score ranges, overdue milestones or follow-ups, severe risks) and returns chosen columns per initiative, sorted, plus aggregates over the whole scope (count, budget sum, mean scores, count by status). Columns include the four dimension scores, composite, quadrant, priority bucket, NPV, risks, milestones, follow-ups, last status report, capabilities, skills and dependencies.",
33428
+ description: "Filters the loaded portfolio's initiatives (status, business unit, category, driver, tags, saved portfolio, capability, skill, score ranges, overdue milestones or follow-ups, severe risks, quality gaps) and returns chosen columns per initiative, sorted, plus aggregates over the whole scope (count, budget sum, mean scores, count by status). Columns include the four dimension scores, composite, quadrant, priority bucket, NPV, risks, milestones, follow-ups, quality gaps, last status report, capabilities, skills and dependencies.",
32661
33429
  inputSchema: queryInputSchema
32662
33430
  },
32663
33431
  async ({ portfolio, ...q }) => withPortfolio(portfolio, (p) => queryInitiatives(p, q))
@@ -32684,7 +33452,7 @@ function createServer() {
32684
33452
  "initiative_detail",
32685
33453
  {
32686
33454
  title: "Initiative detail",
32687
- description: "One initiative in full from the loaded portfolio, by id, externalId or name: every column of query_initiatives plus description, sub-scores, economics, milestones, risks, follow-ups, recent status reports and decisions, capability impacts and the initiatives that depend on it.",
33455
+ description: "One initiative in full from the loaded portfolio, by id, externalId or name: every column of query_initiatives plus description, sub-scores, economics, milestones, risks, follow-ups, quality factors, recent status reports and decisions, capability impacts and the initiatives that depend on it.",
32688
33456
  inputSchema: detailInputSchema
32689
33457
  },
32690
33458
  async ({ portfolio, initiative }) => withPortfolio(portfolio, (p) => initiativeDetail(p, initiative))
@@ -32702,7 +33470,7 @@ function createServer() {
32702
33470
  "board_slices",
32703
33471
  {
32704
33472
  title: "Board memo slices",
32705
- description: "The raw data of the Board Memo's chapters, from the same functions the memo and the board pack use: KPIs since the baseline snapshot, capacity (cut line totals, parked by reason, the must-do block), milestones (missed and due in the next 90 days), risks (heat map, top severe, issues and rises since the baseline) and capability impact (domains, top changes, uncovered gaps). Use it to draft a memo section, a committee note or slides; state the snapshot and export date the numbers stand on.",
33473
+ description: "The raw data of the Board Memo's chapters, from the same functions the memo and the board pack use: KPIs since the baseline snapshot, capacity (cut line totals, parked by reason, the must-do block), milestones (missed and due in the next 90 days), risks (heat map, top severe, issues and rises since the baseline), quality gaps (top 10, per factor) and capability impact (domains, top changes, uncovered gaps). Use it to draft a memo section, a committee note or slides; state the snapshot and export date the numbers stand on.",
32706
33474
  inputSchema: boardSlicesInputSchema
32707
33475
  },
32708
33476
  async ({ portfolio, ...q }) => withPortfolio(portfolio, (p) => boardSlices(p, q))
@@ -32716,6 +33484,40 @@ function createServer() {
32716
33484
  },
32717
33485
  async ({ portfolio, ...q }) => withPortfolio(portfolio, (p) => skillAllocation(p, q))
32718
33486
  );
33487
+ server.registerTool(
33488
+ "capability_allocation",
33489
+ {
33490
+ title: "Capability allocation",
33491
+ description: "Per capability of the map (or the ones named, or one domain): current, projected and target maturity, the gap, coverage, concentration and attributed spend, from the Capabilities page's own rules; every initiative linked to it (status, kind, the level it brings, planned quarter, spend, and its place in the baseline cut line: funded or parked and why), with atRisk when the projection needs parked pipeline work; the maturity path by quarter (the Timeline tab); and with excludedIds or budgetCap, which capabilities fall back, which gaps open and which initiatives are taken out (funded in the baseline but not in the what-if, or excluded).",
33492
+ inputSchema: capabilityAllocationInputSchema
33493
+ },
33494
+ async ({ portfolio, ...q }) => withPortfolio(portfolio, (p) => capabilityAllocation(p, q))
33495
+ );
33496
+ for (const t of ANALYSIS_TEMPLATES) {
33497
+ const argsSchema = Object.fromEntries(t.args.map((a) => [a.name, a.required ? external_exports.string().describe(a.description) : external_exports.string().optional().describe(a.description)]));
33498
+ server.registerPrompt(`bicon_${t.key}`, { title: `BICon: ${t.title}`, description: t.description, argsSchema }, (args) => {
33499
+ const c = checkTemplateArgs(t, args);
33500
+ if (!c.ok) throw new Error(c.errors.join("; "));
33501
+ return { messages: [{ role: "user", content: { type: "text", text: templateText(t, c.args) } }] };
33502
+ });
33503
+ }
33504
+ server.registerTool(
33505
+ "get_analysis_template",
33506
+ {
33507
+ title: "Ready-made analysis",
33508
+ description: `Returns the recipe of a ready-made portfolio analysis: which analysis tools to call with which inputs, what to report, and the rules (quote the tools' figures, name the export date). Follow it step by step. The same recipes are offered as prompts and as load_portfolio's nextSteps. Keys: ${ANALYSIS_TEMPLATES.map((t) => `${t.key} (${t.title})`).join("; ")}.`,
33509
+ inputSchema: external_exports.object({
33510
+ key: external_exports.enum(ANALYSIS_TEMPLATES.map((t) => t.key)).describe("Which analysis"),
33511
+ args: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number()])).optional().describe(`Inputs by name: ${ANALYSIS_TEMPLATES.filter((t) => t.args.length).map((t) => `${t.key}: ${t.args.map((a) => `${a.name}${a.required ? " (required)" : ""}`).join(", ")}`).join("; ")}`)
33512
+ }).strict()
33513
+ },
33514
+ async ({ key, args }) => {
33515
+ const t = ANALYSIS_TEMPLATES.find((x) => x.key === key);
33516
+ const c = checkTemplateArgs(t, args ?? {});
33517
+ if (!c.ok) return { ...compact({ ok: false, errors: c.errors }), isError: true };
33518
+ return compact({ ok: true, key: t.key, title: t.title, text: templateText(t, c.args) });
33519
+ }
33520
+ );
32719
33521
  return server;
32720
33522
  }
32721
33523