@empire-builder-kit/nx 1.0.0-rc.44 → 1.0.0-rc.46

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 (100) hide show
  1. package/package.json +3 -3
  2. package/src/blueprints/external-sandbox.d.ts +1 -1
  3. package/src/cli/main.d.ts +11 -5
  4. package/src/cli/main.js +88 -29
  5. package/src/container-images.d.ts +1 -1
  6. package/src/container-images.js +1 -1
  7. package/src/developer-agent/generator-planning.js +10 -3
  8. package/src/developer-agent/operation-registry.js +1 -1
  9. package/src/executors/deployment-plan/deployment-plan.js +5 -1
  10. package/src/executors/deployment-plan/schema.json +1 -1
  11. package/src/executors/fleet/fleet.js +4 -0
  12. package/src/executors/fleet/schema.json +1 -1
  13. package/src/executors/github-bootstrap/github-bootstrap.d.ts +3 -0
  14. package/src/executors/github-bootstrap/github-bootstrap.js +28 -0
  15. package/src/executors/github-bootstrap/schema.json +14 -0
  16. package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
  17. package/src/executors/local-deploy/federated-credential-transport.js +15 -5
  18. package/src/foundation/cost-estimate.d.ts +8 -3
  19. package/src/foundation/cost-estimate.js +42 -19
  20. package/src/foundation/index.d.ts +1 -0
  21. package/src/foundation/index.js +1 -0
  22. package/src/foundation/local-private-smoke-access.js +6 -0
  23. package/src/foundation/logging.d.ts +3 -1
  24. package/src/foundation/logging.js +8 -1
  25. package/src/foundation/observability.d.ts +42 -0
  26. package/src/foundation/observability.js +181 -0
  27. package/src/foundation/preflight.js +1 -1
  28. package/src/generators/function/function.js +5 -2
  29. package/src/generators/observability-infrastructure.d.ts +2 -0
  30. package/src/generators/observability-infrastructure.js +31 -0
  31. package/src/generators/observability-policy.d.ts +4 -0
  32. package/src/generators/observability-policy.js +23 -0
  33. package/src/generators/preset/dependency-overrides.d.ts +17 -6
  34. package/src/generators/preset/dependency-overrides.js +17 -6
  35. package/src/generators/preset/tool-versions.d.ts +2 -2
  36. package/src/generators/preset/tool-versions.js +2 -2
  37. package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
  38. package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
  39. package/src/generators/preset/workspace-files-agent-development.js +42 -0
  40. package/src/generators/preset/workspace-files-thin-deploy.js +17 -4
  41. package/src/generators/preset/workspace-files-thin-docs.js +10 -22
  42. package/src/generators/preset/workspace-files.d.ts +1 -0
  43. package/src/generators/preset/workspace-files.js +120 -35
  44. package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
  45. package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
  46. package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
  47. package/src/generators/slice/built-in-plan-data-files.js +10 -3
  48. package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
  49. package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
  50. package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
  51. package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
  52. package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
  53. package/src/generators/slice/built-in-plan-metadata-files.js +37 -2
  54. package/src/generators/slice/built-in-plan-operations-files.js +106 -6
  55. package/src/generators/slice/built-in-plan-project-files.js +1 -0
  56. package/src/generators/slice/built-in-plan-release-files.js +2 -44
  57. package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
  58. package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
  59. package/src/generators/slice/built-in-plan-shared.js +29 -2
  60. package/src/generators/slice/built-in-plan-verification-files.js +0 -1
  61. package/src/generators/slice/built-in-plan-web-files.js +22 -6
  62. package/src/generators/slice/built-in-plan.js +2 -2
  63. package/src/generators/slice/slice.js +2 -0
  64. package/src/generators/upgrade/upgrade.d.ts +4 -3
  65. package/src/generators/upgrade/upgrade.js +139 -29
  66. package/src/generators/workload/frozen-container.d.ts +1 -1
  67. package/src/generators/workload/reliability-registration.d.ts +1 -1
  68. package/src/generators/workload/reliability-registration.js +9 -9
  69. package/src/generators/workload/workload.d.ts +1 -1
  70. package/src/generators/workload/workload.js +32 -10
  71. package/src/git-policy/github-bootstrap.d.ts +18 -0
  72. package/src/git-policy/github-bootstrap.js +99 -6
  73. package/src/local-dev/dev-command-core.d.ts +24 -3
  74. package/src/local-dev/dev-command-core.js +124 -12
  75. package/src/local-dev/errors.d.ts +19 -1
  76. package/src/local-dev/errors.js +18 -1
  77. package/src/local-dev/fleet-client.d.ts +64 -6
  78. package/src/local-dev/fleet-client.js +189 -15
  79. package/src/local-dev/fleet-process.d.ts +47 -2
  80. package/src/local-dev/fleet-process.js +248 -31
  81. package/src/local-dev/fleet-runtime.d.ts +37 -0
  82. package/src/local-dev/fleet-runtime.js +127 -69
  83. package/src/local-dev/fleet-session.d.ts +1 -0
  84. package/src/local-dev/fleet-session.js +83 -7
  85. package/src/local-dev/fleet-state.d.ts +5 -0
  86. package/src/local-dev/fleet-state.js +11 -2
  87. package/src/local-dev/fleet-supervisor-daemon.js +1 -5
  88. package/src/local-dev/fleet-supervisor.d.ts +7 -0
  89. package/src/local-dev/fleet-supervisor.js +83 -12
  90. package/src/local-dev/slice-runner.d.ts +19 -1
  91. package/src/local-dev/slice-runner.js +290 -91
  92. package/src/repository-policy/deployment-plan.d.ts +1 -1
  93. package/src/repository-policy/deployment-plan.js +16 -1
  94. package/src/secrets/generated-aws-control-plane.d.ts +11 -1
  95. package/src/secrets/generated-aws-control-plane.js +92 -29
  96. package/src/secrets/operator.d.ts +3 -2
  97. package/src/secrets/operator.js +27 -5
  98. package/src/supply-chain/blueprint-verification-authority.js +24 -3
  99. package/src/supply-chain/generated-files.js +9 -0
  100. package/src/supply-chain/sbom.js +74 -12
@@ -11,7 +11,9 @@ exports.upgradeGenerator = upgradeGenerator;
11
11
  const deterministic_order_js_1 = require("../../deterministic-order.js");
12
12
  const capacity_migration_js_1 = require("../../repository-policy/capacity-migration.js");
13
13
  const dependency_overrides_js_1 = require("../preset/dependency-overrides.js");
14
+ const observability_js_1 = require("../../foundation/observability.js");
14
15
  const workspace_files_adopter_skills_js_1 = require("../preset/workspace-files-adopter-skills.js");
16
+ const observability_policy_js_1 = require("../observability-policy.js");
15
17
  const sql_seeded_reconciliation_js_1 = require("./sql-seeded-reconciliation.js");
16
18
  const devkit_1 = require("@nx/devkit");
17
19
  const node_buffer_1 = require("node:buffer");
@@ -25,7 +27,8 @@ const index_js_2 = require("../../api-contract/index.js");
25
27
  const index_js_3 = require("../../foundation/index.js");
26
28
  const capability_plan_js_1 = require("../../foundation/capability-plan.js");
27
29
  const persistence_contract_js_1 = require("../slice/persistence-contract.js");
28
- const index_js_4 = require("../../ownership/index.js");
30
+ const index_js_4 = require("../../repository-policy/index.js");
31
+ const index_js_5 = require("../../ownership/index.js");
29
32
  const function_js_1 = require("../function/function.js");
30
33
  const logging_registry_js_1 = require("../logging-registry.js");
31
34
  const feature_registry_js_1 = require("../feature-registry.js");
@@ -380,6 +383,15 @@ const SEEDED_RECONCILIATION = {
380
383
  'journey-authority',
381
384
  ],
382
385
  },
386
+ 'reliability/metric-declarations.ts': {
387
+ category: 'reliability-metrics',
388
+ guidance: 'Review existing custom recordReliabilityOperation calls and declare each finite deployment-owned component/feature/operation tuple in reliability/metric-declarations.ts. Undeclared custom series are dropped by the managed recorder. Never derive declarations from request, user, resource, or correlation IDs. Preserve existing declarations, verify custom metric emission, retain evidence, and resolve the seeded reconciliation marker before protected promotion.',
389
+ implications: [
390
+ 'custom-metric-series-allowlist',
391
+ 'finite-deployment-owned-dimensions',
392
+ 'undeclared-custom-series-are-dropped',
393
+ ],
394
+ },
383
395
  'secrets/declarations.ts': {
384
396
  category: 'secret-contract',
385
397
  implications: [
@@ -458,6 +470,89 @@ function appendSeededReconciliationMarker(tree, root, report) {
458
470
  return;
459
471
  tree.write(anchor, `${current.replace(/\s*$/, '')}\n\n${SEEDED_RECONCILIATION_MARKER}\n`);
460
472
  }
473
+ function isJsonObject(value) {
474
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
475
+ }
476
+ function appendServiceTaskPresenceReconciliation(currentFiles, currentOwnership, targetOwnership, report) {
477
+ const services = currentOwnership.compositions
478
+ .filter(({ kind }) => kind === 'service')
479
+ .map(({ name }) => name)
480
+ .sort(deterministic_order_js_1.compareUtf16);
481
+ if (services.length === 0)
482
+ return;
483
+ const registryPath = 'operations/reliability.json';
484
+ const source = currentFiles.get(registryPath);
485
+ if (source === null || source === undefined)
486
+ return;
487
+ let operations;
488
+ try {
489
+ operations = JSON.parse(source);
490
+ }
491
+ catch {
492
+ throw new Error('Retained Service reliability registry must be valid JSON before upgrade.');
493
+ }
494
+ if (!isJsonObject(operations) ||
495
+ !Array.isArray(operations.deployedAlarmResources)) {
496
+ throw new Error('Retained Service reliability registry must expose deployedAlarmResources before upgrade.');
497
+ }
498
+ const resources = operations.deployedAlarmResources;
499
+ const append = (path, guidance) => {
500
+ const current = currentFiles.get(path);
501
+ const targetEntry = targetOwnership.paths.find((entry) => entry.path === path);
502
+ const currentEntry = currentOwnership.paths.find((entry) => entry.path === path);
503
+ if (current === null ||
504
+ current === undefined ||
505
+ targetEntry?.mode !== 'seeded')
506
+ return;
507
+ const existing = report.seededReconciliation.find((entry) => entry.path === path);
508
+ if (existing) {
509
+ existing.guidance = `${existing.guidance ? `${existing.guidance}\n\n` : ''}${guidance}`;
510
+ existing.implications = [
511
+ ...new Set([...existing.implications, 'service-live-task-presence']),
512
+ ].sort(deterministic_order_js_1.compareUtf16);
513
+ return;
514
+ }
515
+ report.seededReconciliation.push({
516
+ adopterContentSha256: (0, index_js_5.hashOwnedContent)(current, currentEntry ?? targetEntry),
517
+ category: 'reliability-operations',
518
+ currentFrameworkSha256: currentEntry?.sha256 ?? null,
519
+ guidance,
520
+ implications: ['alarm-registration', 'service-live-task-presence'],
521
+ markerReference: SEEDED_RECONCILIATION_MARKER_REFERENCE,
522
+ path,
523
+ requiredAction: 'review-and-reconcile-preserving-adopter-content',
524
+ targetFrameworkSha256: targetEntry.sha256,
525
+ });
526
+ };
527
+ for (const name of services) {
528
+ const id = `service-${name}-health`;
529
+ const legacy = resources.some((entry) => {
530
+ if (!isJsonObject(entry))
531
+ return false;
532
+ const expected = {
533
+ comparisonOperator: 'LessThanThreshold',
534
+ evaluationPeriods: 1,
535
+ id,
536
+ metricName: 'RunningTaskCount',
537
+ name: `${currentOwnership.slice}-{stage}-service-${name}-running`,
538
+ namespace: 'ECS/ContainerInsights',
539
+ period: 60,
540
+ signal: 'metric',
541
+ statistic: 'Sum',
542
+ threshold: 1,
543
+ treatMissingData: 'breaching',
544
+ };
545
+ return (Object.entries(expected).every(([key, value]) => entry[key] === value) &&
546
+ JSON.stringify(entry.dimensionNames) ===
547
+ JSON.stringify(['ClusterName', 'ServiceName']));
548
+ });
549
+ if (!legacy)
550
+ continue;
551
+ append(registryPath, `For recorded Service ${name}, reconcile only deployedAlarmResources[id=${id}]: namespace ECS/ContainerInsights -> AWS/ECS, metricName RunningTaskCount -> LiveTaskCount, statistic Sum -> Minimum. Keep its ID, physical name, actions, dimensions, threshold 1, LessThanThreshold, 60-second period, one evaluation period and breaching missing-data policy. In alarmRegistrations[id=${id}], set missingData to breaching and review businessSymptom, condition and both expected risk fields for task presence or missing telemetry, startup/emission-gap false positives, and activating/draining/replacement crash-loop false negatives. Preserve unrelated registrations and every adopter-owned field. Review services/${name}/RUNBOOK.md, run the declared reliability verification after authorized deployment, retain evidence, then resolve the existing seeded reconciliation marker.`);
552
+ append(`services/${name}/RUNBOOK.md`, `Review the retained Service ${name} runbook for AWS/ECS LiveTaskCount Minimum: ACTIVATING, RUNNING and DEACTIVATING task presence, not readiness. Missing telemetry breaches but does not prove zero tasks. Document startup/emission-gap false positives and activating/draining/replacement crash-loop false negatives, preserve custom recovery instructions, verify the critical journey and alarm recovery after an authorized roll-forward, retain evidence, then resolve the existing seeded reconciliation marker.`);
553
+ }
554
+ report.seededReconciliation.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.path, right.path));
555
+ }
461
556
  function currentContractConsumers(tree, root) {
462
557
  const source = tree.read(node_path_1.posix.join(root, 'contracts/dependencies.json'), 'utf8');
463
558
  if (source === null)
@@ -667,7 +762,7 @@ function preserveBuiltInDerivedApiArtifacts(tree, root, currentOwnership, target
667
762
  entry?.mode === 'managed' &&
668
763
  entry.origin === 'blueprint' &&
669
764
  entry.managedJsonPointers === undefined &&
670
- (0, index_js_4.hashOwnedContent)(current, entry) === entry.sha256);
765
+ (0, index_js_5.hashOwnedContent)(current, entry) === entry.sha256);
671
766
  if (!completeCleanPair)
672
767
  return targetFiles;
673
768
  const retained = { ...targetFiles };
@@ -821,16 +916,17 @@ function ownershipValidationMessage(label, issues) {
821
916
  }
822
917
  function requireUpgradePrerequisites(tree, target) {
823
918
  const installedVersion = (0, generator_js_1.frameworkPackageVersion)();
824
- const workspaceResult = (0, index_js_4.validateWorkspaceOwnershipRecord)(readJson(tree, '.ebk/ownership.json', 'Workspace ownership metadata'));
919
+ const workspaceResult = (0, index_js_5.validateWorkspaceOwnershipRecord)(readJson(tree, '.ebk/ownership.json', 'Workspace ownership metadata'));
825
920
  if (!workspaceResult.valid) {
826
921
  throw new Error(ownershipValidationMessage('Workspace ownership metadata', workspaceResult.issues));
827
922
  }
828
923
  if (workspaceResult.value.frameworkVersion !== installedVersion) {
829
924
  throw new Error(`Workspace tooling is recorded at ${workspaceResult.value.frameworkVersion}, but the installed @empire-builder-kit/nx version is ${installedVersion}. Report and apply the workspace upgrade before upgrading the ${target === 'foundation' ? 'Standard Foundation' : 'Product Slice'}.`);
830
925
  }
926
+ (0, observability_policy_js_1.requireObservabilityPolicyFromTree)(tree);
831
927
  if (target === 'foundation')
832
928
  return;
833
- const foundationResult = (0, index_js_4.validateFoundationOwnershipRecord)(readJson(tree, 'packages/foundation/.ebk/ownership.json', 'Standard Foundation ownership metadata'));
929
+ const foundationResult = (0, index_js_5.validateFoundationOwnershipRecord)(readJson(tree, 'packages/foundation/.ebk/ownership.json', 'Standard Foundation ownership metadata'));
834
930
  if (!foundationResult.valid) {
835
931
  throw new Error(ownershipValidationMessage('Standard Foundation ownership metadata', foundationResult.issues));
836
932
  }
@@ -959,21 +1055,29 @@ function createUpgradeReport(currentFiles, currentOwnership, targetFiles, target
959
1055
  path.startsWith('database/migrations/')) {
960
1056
  report.incompatibleSeededMigrations.push(path);
961
1057
  }
962
- else if (current !== null &&
963
- currentEntry?.sha256 !== targetSha256 &&
1058
+ else if (((current !== null && currentEntry?.sha256 !== targetSha256) ||
1059
+ (current === null &&
1060
+ path === 'reliability/metric-declarations.ts')) &&
964
1061
  seededReconciliationDefinition(path) !== null) {
965
1062
  const reconciliation = seededReconciliationDefinition(path);
966
1063
  if (reconciliation === null) {
967
1064
  throw new Error(`Missing seeded reconciliation definition for ${path}.`);
968
1065
  }
969
1066
  report.seededReconciliation.push({
970
- adopterContentSha256: (0, index_js_4.hashOwnedContent)(current, currentEntry ?? targetEntry),
1067
+ adopterContentSha256: current === null
1068
+ ? null
1069
+ : (0, index_js_5.hashOwnedContent)(current, currentEntry ?? targetEntry),
971
1070
  category: reconciliation.category,
972
1071
  currentFrameworkSha256: currentEntry?.sha256 ?? null,
1072
+ ...('guidance' in reconciliation
1073
+ ? { guidance: reconciliation.guidance }
1074
+ : {}),
973
1075
  implications: [...reconciliation.implications],
974
1076
  markerReference: SEEDED_RECONCILIATION_MARKER_REFERENCE,
975
1077
  path,
976
- requiredAction: 'review-and-reconcile-preserving-adopter-content',
1078
+ requiredAction: current === null
1079
+ ? 'declare-existing-custom-metric-series-before-promotion'
1080
+ : 'review-and-reconcile-preserving-adopter-content',
977
1081
  targetFrameworkSha256: targetSha256,
978
1082
  });
979
1083
  }
@@ -982,7 +1086,7 @@ function createUpgradeReport(currentFiles, currentOwnership, targetFiles, target
982
1086
  report.missing.push(path);
983
1087
  }
984
1088
  else if (currentEntry !== undefined &&
985
- (0, index_js_4.hashOwnedContent)(current, currentEntry) !== currentEntry.sha256) {
1089
+ (0, index_js_5.hashOwnedContent)(current, currentEntry) !== currentEntry.sha256) {
986
1090
  report.conflicting.push(path);
987
1091
  }
988
1092
  else if (current === target) {
@@ -1005,7 +1109,7 @@ function createUpgradeReport(currentFiles, currentOwnership, targetFiles, target
1005
1109
  const current = currentFiles.get(path) ?? null;
1006
1110
  if (current === null)
1007
1111
  continue;
1008
- if ((0, index_js_4.hashOwnedContent)(current, entry) === sha256) {
1112
+ if ((0, index_js_5.hashOwnedContent)(current, entry) === sha256) {
1009
1113
  report.obsolete.push(path);
1010
1114
  }
1011
1115
  else if (!report.conflicting.includes(path)) {
@@ -1060,7 +1164,7 @@ function mergeStructuredTargetFiles(currentFiles, currentOwnership, targetFiles,
1060
1164
  if (!targetEntry.managedJsonPointers)
1061
1165
  continue;
1062
1166
  if (currentEntry?.managedJsonPointers) {
1063
- const transition = (0, index_js_4.analyzeManagedJsonPointerTransition)(current, target, currentEntry.managedJsonPointers, targetEntry.managedJsonPointers);
1167
+ const transition = (0, index_js_5.analyzeManagedJsonPointerTransition)(current, target, currentEntry.managedJsonPointers, targetEntry.managedJsonPointers);
1064
1168
  changes.claimedJsonMembers.push(...transition.claimed.map((pointer) => `${targetEntry.path}#${pointer}`));
1065
1169
  changes.conflictingJsonMembers.push(...transition.conflictingClaims.map((pointer) => `${targetEntry.path}#${pointer}`));
1066
1170
  changes.releasedJsonMembers.push(...transition.released.map((pointer) => `${targetEntry.path}#${pointer}`));
@@ -1069,7 +1173,7 @@ function mergeStructuredTargetFiles(currentFiles, currentOwnership, targetFiles,
1069
1173
  continue;
1070
1174
  }
1071
1175
  }
1072
- files.set(targetEntry.path, (0, index_js_4.mergeManagedJsonContents)(current, target, targetEntry.managedJsonPointers));
1176
+ files.set(targetEntry.path, (0, index_js_5.mergeManagedJsonContents)(current, target, targetEntry.managedJsonPointers));
1073
1177
  }
1074
1178
  return { files, ...changes };
1075
1179
  }
@@ -1089,7 +1193,7 @@ async function formatMergedManagedJsonFiles(tree, root, merge, ownership) {
1089
1193
  }));
1090
1194
  return { ...merge, files };
1091
1195
  }
1092
- function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serviceAdapterOwner) {
1196
+ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serviceAdapterOwner, deploymentProfile) {
1093
1197
  const compositionPaths = ownership.paths.filter(({ origin }) => origin === 'composition');
1094
1198
  const files = {};
1095
1199
  const paths = new Map();
@@ -1107,7 +1211,7 @@ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serv
1107
1211
  mode: entry.mode ?? 'managed',
1108
1212
  origin: 'composition',
1109
1213
  path: entry.path,
1110
- sha256: (0, index_js_4.hashOwnedContent)(contents, entry),
1214
+ sha256: (0, index_js_5.hashOwnedContent)(contents, entry),
1111
1215
  });
1112
1216
  }
1113
1217
  };
@@ -1118,7 +1222,7 @@ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serv
1118
1222
  }
1119
1223
  else {
1120
1224
  const plan = (0, workload_js_1.createWorkloadFilePlan)(workspace, ownership.slice, composition.name, composition.kind, runtimeVersion, composition.feature, composition.kind === 'service' &&
1121
- composition.name === serviceAdapterOwner);
1225
+ composition.name === serviceAdapterOwner, [(0, index_js_4.productionRehearsalStage)(deploymentProfile), 'production']);
1122
1226
  addPlan(plan.files, plan.paths);
1123
1227
  }
1124
1228
  }
@@ -1251,7 +1355,7 @@ async function upgradeFoundation(tree, options, packageJson, output, foundationR
1251
1355
  throw new Error('Standard Foundation upgrades do not accept Blueprint or input options.');
1252
1356
  }
1253
1357
  const root = 'packages/foundation';
1254
- const ownershipResult = (0, index_js_4.validateFoundationOwnershipRecord)(readJson(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Standard Foundation ownership metadata'));
1358
+ const ownershipResult = (0, index_js_5.validateFoundationOwnershipRecord)(readJson(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Standard Foundation ownership metadata'));
1255
1359
  if (!ownershipResult.valid) {
1256
1360
  throw new Error(ownershipValidationMessage('Standard Foundation ownership metadata', ownershipResult.issues));
1257
1361
  }
@@ -1271,7 +1375,7 @@ async function upgradeFoundation(tree, options, packageJson, output, foundationR
1271
1375
  ...renderedTarget,
1272
1376
  files: await formatRenderedFiles(tree, renderedTarget.files, root),
1273
1377
  };
1274
- const targetValidation = (0, index_js_4.validateFoundationOwnershipRecord)(target.ownership);
1378
+ const targetValidation = (0, index_js_5.validateFoundationOwnershipRecord)(target.ownership);
1275
1379
  if (!targetValidation.valid) {
1276
1380
  throw new Error(`Target Standard Foundation ownership metadata is invalid:\n${targetValidation.issues
1277
1381
  .map((issue) => `- ${issue.path}: ${issue.message}`)
@@ -1312,7 +1416,7 @@ async function upgradeFoundation(tree, options, packageJson, output, foundationR
1312
1416
  }
1313
1417
  tree.write(node_path_1.posix.join(root, path), contents);
1314
1418
  }
1315
- (0, index_js_4.restampFoundationOwnership)(tree);
1419
+ (0, index_js_5.restampFoundationOwnership)(tree);
1316
1420
  // Ownership bytes are part of the upgrade result, including agent previews
1317
1421
  // that skip workspace formatting. Canonicalize before binding those bytes.
1318
1422
  await formatOwnershipRecord(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Standard Foundation');
@@ -1339,7 +1443,7 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
1339
1443
  if (options.blueprint !== undefined || options.inputs !== undefined) {
1340
1444
  throw new Error('Workspace upgrades do not accept Blueprint or input options.');
1341
1445
  }
1342
- const ownershipResult = (0, index_js_4.validateWorkspaceOwnershipRecord)(readJson(tree, '.ebk/ownership.json', 'Workspace ownership metadata'));
1446
+ const ownershipResult = (0, index_js_5.validateWorkspaceOwnershipRecord)(readJson(tree, '.ebk/ownership.json', 'Workspace ownership metadata'));
1343
1447
  if (!ownershipResult.valid) {
1344
1448
  throw new Error(ownershipValidationMessage('Workspace ownership metadata', ownershipResult.issues));
1345
1449
  }
@@ -1354,6 +1458,9 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
1354
1458
  dependencyOverrides: tree.exists(dependency_overrides_js_1.DEPENDENCY_OVERRIDES_PATH)
1355
1459
  ? readJson(tree, dependency_overrides_js_1.DEPENDENCY_OVERRIDES_PATH, 'Dependency overrides')
1356
1460
  : undefined,
1461
+ observabilityPolicy: tree.exists(observability_js_1.OBSERVABILITY_POLICY_PATH)
1462
+ ? readJson(tree, observability_js_1.OBSERVABILITY_POLICY_PATH, 'Observability policy')
1463
+ : (0, observability_js_1.defaultObservabilityPolicy)((0, index_js_4.activeDeploymentStages)(deployment.deploymentProfile ?? 'starter'), { preserveLegacy: true }),
1357
1464
  frameworkVersion: (0, generator_js_1.frameworkPackageVersion)(),
1358
1465
  name: packageJson.name,
1359
1466
  });
@@ -1361,7 +1468,7 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
1361
1468
  ...renderedTarget,
1362
1469
  files: await formatRenderedFiles(tree, renderedTarget.files),
1363
1470
  };
1364
- const targetValidation = (0, index_js_4.validateWorkspaceOwnershipRecord)(target.ownership);
1471
+ const targetValidation = (0, index_js_5.validateWorkspaceOwnershipRecord)(target.ownership);
1365
1472
  if (!targetValidation.valid) {
1366
1473
  throw new Error('Target workspace ownership metadata is invalid.');
1367
1474
  }
@@ -1417,7 +1524,7 @@ async function upgradeWorkspace(tree, options, packageJson, output) {
1417
1524
  if (developerAgentInstructionAppend !== null) {
1418
1525
  tree.write(developerAgentInstructionAppend.path, developerAgentInstructionAppend.contents);
1419
1526
  }
1420
- (0, index_js_4.restampWorkspaceOwnership)(tree);
1527
+ (0, index_js_5.restampWorkspaceOwnership)(tree);
1421
1528
  await formatOwnershipRecord(tree, '.ebk/ownership.json', 'Workspace');
1422
1529
  }
1423
1530
  async function executeUpgradeGenerator(tree, options, output = devkit_1.logger, executionOptions = {}) {
@@ -1445,7 +1552,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1445
1552
  if (recorded?.blueprint
1446
1553
  ?.name !== index_js_1.BUILTIN_BLUEPRINT_NAME)
1447
1554
  continue;
1448
- const parsed = (0, index_js_4.validateGeneratedOwnershipRecord)(recorded);
1555
+ const parsed = (0, index_js_5.validateGeneratedOwnershipRecord)(recorded);
1449
1556
  if (!parsed.valid)
1450
1557
  throw new Error(ownershipValidationMessage('Product Slice ownership metadata', parsed.issues));
1451
1558
  (0, persistence_contract_js_1.standardPersistenceContract)({
@@ -1462,7 +1569,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1462
1569
  }
1463
1570
  requireUpgradePrerequisites(tree, 'product-slice');
1464
1571
  const root = node_path_1.posix.join('packages', name);
1465
- const ownershipResult = (0, index_js_4.validateGeneratedOwnershipRecord)(readJson(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Product Slice ownership metadata'));
1572
+ const ownershipResult = (0, index_js_5.validateGeneratedOwnershipRecord)(readJson(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Product Slice ownership metadata'));
1466
1573
  if (!ownershipResult.valid) {
1467
1574
  throw new Error(ownershipValidationMessage('Product Slice ownership metadata', ownershipResult.issues));
1468
1575
  }
@@ -1609,7 +1716,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1609
1716
  mode,
1610
1717
  origin: 'blueprint',
1611
1718
  path,
1612
- sha256: (0, index_js_4.hashOwnedContent)(contents, { managedJsonPointers }),
1719
+ sha256: (0, index_js_5.hashOwnedContent)(contents, { managedJsonPointers }),
1613
1720
  }));
1614
1721
  paths.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.path, right.path));
1615
1722
  targetOwnership = {
@@ -1623,7 +1730,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1623
1730
  inputs: inputResult.value,
1624
1731
  paths,
1625
1732
  projects: generated.projects,
1626
- schemaVersion: index_js_4.GENERATED_OWNERSHIP_SCHEMA_VERSION,
1733
+ schemaVersion: index_js_5.GENERATED_OWNERSHIP_SCHEMA_VERSION,
1627
1734
  slice: name,
1628
1735
  verification: blueprint.manifest.verification,
1629
1736
  };
@@ -1648,7 +1755,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1648
1755
  targetFiles = preserveBuiltInDerivedApiArtifacts(tree, root, currentOwnership, targetFiles);
1649
1756
  }
1650
1757
  const targetPathSet = new Set(targetOwnership.paths.map(({ path }) => path));
1651
- const compositionPlan = renderCompositionUpgradePlan(currentOwnership, packageJson.name ?? '', packageJson.dependencies?.['@empire-builder-kit/runtime'] ?? '', serviceAdapterOwner);
1758
+ const compositionPlan = renderCompositionUpgradePlan(currentOwnership, packageJson.name ?? '', packageJson.dependencies?.['@empire-builder-kit/runtime'] ?? '', serviceAdapterOwner, (0, tree_policy_js_1.readRepositoryOperatingPolicyFromTree)(tree).deploymentProfile);
1652
1759
  const compositionPaths = compositionPlan.paths.filter(({ path }) => !targetPathSet.has(path));
1653
1760
  if (externalBlueprintDestinationPaths) {
1654
1761
  (0, index_js_1.assertBlueprintDestinationPaths)(tree.root, root, [
@@ -1677,7 +1784,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1677
1784
  if (contents === undefined) {
1678
1785
  throw new Error(`Target Blueprint did not render ${entry.path}.`);
1679
1786
  }
1680
- return { ...entry, sha256: (0, index_js_4.hashOwnedContent)(contents, entry) };
1787
+ return { ...entry, sha256: (0, index_js_5.hashOwnedContent)(contents, entry) };
1681
1788
  }),
1682
1789
  projects: [
1683
1790
  ...new Set([...targetOwnership.projects, ...currentOwnership.projects]),
@@ -1687,7 +1794,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1687
1794
  ...targetFiles,
1688
1795
  '.ebk/ownership.json': json(targetOwnership),
1689
1796
  };
1690
- const targetValidation = (0, index_js_4.validateGeneratedOwnershipRecord)(targetOwnership);
1797
+ const targetValidation = (0, index_js_5.validateGeneratedOwnershipRecord)(targetOwnership);
1691
1798
  if (!targetValidation.valid) {
1692
1799
  throw new Error(`Target ownership metadata is invalid:\n${targetValidation.issues
1693
1800
  .map((issue) => `- ${issue.path}: ${issue.message}`)
@@ -1721,6 +1828,9 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1721
1828
  mergedTarget.files.set(logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH, await (0, formatting_js_1.formatTreeContentsToFixedPoint)(loggingAppend, formatting, workspacePath));
1722
1829
  }
1723
1830
  const report = createUpgradeReport(currentFiles, currentOwnership, mergedTarget.files, targetOwnership, mergedTarget);
1831
+ if (blueprint.source === 'built-in') {
1832
+ appendServiceTaskPresenceReconciliation(currentFiles, currentOwnership, targetOwnership, report);
1833
+ }
1724
1834
  if (loggingAppend !== null) {
1725
1835
  report.preservedSeeded = report.preservedSeeded.filter((path) => path !== logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH);
1726
1836
  report.appendedSeeded.push(logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH);
@@ -1774,7 +1884,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1774
1884
  await (0, formatting_js_1.formatTreeFileToFixedPoint)(tree, path);
1775
1885
  }
1776
1886
  }
1777
- (0, index_js_4.restampProductSliceOwnership)(tree, name, { preserveSeeded: true });
1887
+ (0, index_js_5.restampProductSliceOwnership)(tree, name, { preserveSeeded: true });
1778
1888
  await formatOwnershipRecord(tree, node_path_1.posix.join(root, '.ebk/ownership.json'), 'Product Slice');
1779
1889
  return options.skipInstall
1780
1890
  ? undefined
@@ -1,4 +1,4 @@
1
- export declare const WORKLOAD_CONTAINER_NODE_IMAGE: "node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402";
1
+ export declare const WORKLOAD_CONTAINER_NODE_IMAGE: "node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914";
2
2
  export declare const WORKLOAD_CONTAINER_PNPM_VERSION: "11.20.0";
3
3
  export declare const WORKLOAD_CONTAINER_PNPM_DESCRIPTOR: "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee";
4
4
  export declare const WORKLOAD_CONTAINER_PNPM_STORE = "/workspace/.ebk/cache/pnpm-store";
@@ -3,7 +3,7 @@ export declare const WORKLOAD_RELIABILITY_REGISTRY_PATH: "operations/reliability
3
3
  export interface WorkloadReliabilityPlan {
4
4
  alarmId: string;
5
5
  contents: string;
6
- metricName: 'MatchedEvents' | 'RunningTaskCount';
6
+ metricName: 'LiveTaskCount' | 'MatchedEvents';
7
7
  path: string;
8
8
  runbookRef: string;
9
9
  }
@@ -61,21 +61,21 @@ function planWorkloadReliabilityRegistration(tree, input) {
61
61
  ...operations.alarmRegistrations,
62
62
  (0, reliability_1.validateAlarmRegistrationMetadata)({
63
63
  businessSymptom: input.kind === 'service'
64
- ? `${input.name} Service health checks are failing.`
64
+ ? `${input.name} Service task presence is below one or telemetry is missing.`
65
65
  : `${input.name} Job executions are failing.`,
66
66
  condition: input.kind === 'service'
67
- ? 'The AWS-native ECS RunningTaskCount signal is below one or missing for the exact Service.'
67
+ ? 'The AWS/ECS LiveTaskCount minimum is below one or missing for the exact Service; it counts ACTIVATING, RUNNING, and DEACTIVATING tasks.'
68
68
  : 'An AWS-native ECS task-state event reports an essential container exit for the exact Job task family.',
69
69
  dependencyAlarmIds: [],
70
70
  escalationRouteKey: `${input.slice}-on-call`,
71
71
  expectedFalseNegativeRisk: input.kind === 'service'
72
- ? 'Container Insights delivery can be delayed; missing data is therefore treated as breaching.'
72
+ ? 'Activating, draining, or repeatedly replaced tasks can keep LiveTaskCount positive while readiness or useful work fails.'
73
73
  : 'ECS task-state events are durable but can be delayed before EventBridge reports a match.',
74
74
  expectedFalsePositiveRisk: input.kind === 'service'
75
- ? 'A synthetic health client can fail while the Service remains available.'
75
+ ? 'Metric emission gaps, startup, or lifecycle transitions can breach the missing-data policy while tasks remain present.'
76
76
  : 'A retried Job attempt can fail before a later attempt succeeds.',
77
77
  id: alarmId,
78
- missingData: 'missing',
78
+ missingData: input.kind === 'service' ? 'breaching' : 'missing',
79
79
  owner,
80
80
  runbookRef: `runbook:${runbookRef}`,
81
81
  severity: 'high',
@@ -90,15 +90,15 @@ function planWorkloadReliabilityRegistration(tree, input) {
90
90
  dimensionNames: input.kind === 'service' ? ['ClusterName', 'ServiceName'] : ['RuleName'],
91
91
  evaluationPeriods: 1,
92
92
  id: alarmId,
93
- metricName: input.kind === 'service' ? 'RunningTaskCount' : 'MatchedEvents',
93
+ metricName: input.kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents',
94
94
  name: `${input.slice}-{stage}-${input.kind}-${input.name}-${input.kind === 'service' ? 'running' : 'hard-crash'}`,
95
- namespace: input.kind === 'service' ? 'ECS/ContainerInsights' : 'AWS/Events',
95
+ namespace: input.kind === 'service' ? 'AWS/ECS' : 'AWS/Events',
96
96
  owner,
97
97
  period: 60,
98
98
  route: `${input.slice}-on-call`,
99
99
  runbook: runbookRef,
100
100
  signal: 'metric',
101
- statistic: 'Sum',
101
+ statistic: input.kind === 'service' ? 'Minimum' : 'Sum',
102
102
  threshold: 1,
103
103
  treatMissingData: input.kind === 'service' ? 'breaching' : 'notBreaching',
104
104
  };
@@ -142,7 +142,7 @@ function planWorkloadReliabilityRegistration(tree, input) {
142
142
  return {
143
143
  alarmId,
144
144
  contents: `${JSON.stringify(operations, null, 2)}\n`,
145
- metricName: input.kind === 'service' ? 'RunningTaskCount' : 'MatchedEvents',
145
+ metricName: input.kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents',
146
146
  path,
147
147
  runbookRef,
148
148
  };
@@ -13,5 +13,5 @@ export interface WorkloadFilePlan {
13
13
  }>;
14
14
  project: string;
15
15
  }
16
- export declare function createWorkloadFilePlan(workspace: string, slice: string, workload: string, kind: WorkloadKind, runtimeVersion: string, feature: string, ownsProtectedLoadAdapter: boolean, protectedRuntimeStages?: readonly ('development' | 'production' | 'staging')[]): WorkloadFilePlan;
16
+ export declare function createWorkloadFilePlan(workspace: string, slice: string, workload: string, kind: WorkloadKind, runtimeVersion: string, feature: string, ownsProtectedLoadAdapter: boolean, protectedRuntimeStages: readonly ('development' | 'production' | 'staging')[]): WorkloadFilePlan;
17
17
  export declare function workloadGenerator(tree: Tree, options: WorkloadGeneratorSchema, kind: WorkloadKind): Promise<GeneratorCallback | void>;
@@ -4,6 +4,8 @@ exports.SERVICE_WORKLOAD_PACKAGE_MANAGED_JSON_POINTERS = exports.WORKLOAD_PACKAG
4
4
  exports.createWorkloadFilePlan = createWorkloadFilePlan;
5
5
  exports.workloadGenerator = workloadGenerator;
6
6
  const deterministic_order_js_1 = require("../../deterministic-order.js");
7
+ const observability_infrastructure_js_1 = require("../observability-infrastructure.js");
8
+ const observability_policy_js_1 = require("../observability-policy.js");
7
9
  const node_path_1 = require("node:path");
8
10
  const index_js_1 = require("../../ownership/index.js");
9
11
  const capability_plan_js_1 = require("../../foundation/capability-plan.js");
@@ -137,7 +139,7 @@ exports.SERVICE_WORKLOAD_PACKAGE_MANAGED_JSON_POINTERS = [
137
139
  '/dependencies/@aws-sdk~1client-dynamodb',
138
140
  '/dependencies/sst',
139
141
  ];
140
- function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion, feature, ownsProtectedLoadAdapter, protectedRuntimeStages = ['staging', 'production']) {
142
+ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion, feature, ownsProtectedLoadAdapter, protectedRuntimeStages) {
141
143
  const plural = kind === 'service' ? 'services' : 'jobs';
142
144
  const relativeRoot = node_path_1.posix.join(plural, workload);
143
145
  const className = `${pascal(slice)}${pascal(workload)}${pascal(kind)}`;
@@ -159,7 +161,7 @@ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion
159
161
  version: '0.0.0',
160
162
  scripts: {
161
163
  build: 'tsc -p tsconfig.json',
162
- 'docker-build': `docker build -f packages/${slice}/${relativeRoot}/Dockerfile ` +
164
+ 'docker-build': 'docker build -f Dockerfile ' +
163
165
  `-t ${slice}-${kind}-${workload}:local ../../../..`,
164
166
  dev: `node --experimental-strip-types ${kind === 'service' ? 'src/main.ts' : 'src/run.ts'}`,
165
167
  lint: 'eslint .',
@@ -210,7 +212,7 @@ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion
210
212
  [`${relativeRoot}/src/workload.spec.ts`]: kind === 'service'
211
213
  ? `import { describe, expect, it } from 'vitest';\nimport { healthResponse } from './main.js';\n\ndescribe('${workload} service', () => {\n it('reports healthy only on the health endpoint', () => {\n expect(healthResponse('/health')).toEqual({ body: { service: '${workload}', status: 'ok' }, status: 200 });\n expect(healthResponse('/missing').status).toBe(404);\n });\n});\n`
212
214
  : `import { createLogger, type LogEntry } from '@empire-builder-kit/runtime/logging';\nimport { describe, expect, it } from 'vitest';\nimport { run } from './run.js';\n\ndescribe('${workload} job', () => {\n it('emits canonical start and completion records', async () => {\n const entries: LogEntry[] = [];\n const logger = createLogger({\n component: 'job-${workload}', feature: '${feature}', operation: 'run',\n slice: '${slice}', stage: 'test', workspace: '${workspace}',\n sink: (entry) => entries.push(entry as LogEntry),\n });\n await expect(run(logger)).resolves.toEqual({\n ok: true,\n providerDeliveryAssigned: false,\n });\n expect(entries.map(({ message, outcome }) => ({ message, outcome }))).toEqual([\n { message: '${workload} job started', outcome: undefined },\n { message: '${workload} job completed', outcome: 'success' },\n ]);\n });\n});\n`,
213
- [`${relativeRoot}/infra.ts`]: `import { createManagedLogGroupContract } from '@empire-builder-kit/nx/foundation';\nimport { standardSliceExtensionTaskFamily } from '../../infra/workload-roles/effective-intent.js';\n\nexport function ${factoryName}(\n cluster: any,\n alertTopicArn: any,\n roles?: { executionRole: string; taskRole: string },\n links: any[] = [],\n configuration?: {\n applicationId: any;\n deployment: any;\n environmentId: any;\n profileId: any;\n },\n) {\n const contract = createManagedLogGroupContract({\n component: '${kind}-${workload}',\n slice: '${slice}',\n stage: $app.stage,\n workspace: '${workspace}',\n });\n const workload = new sst.aws.${resource}('${className}', {\n ...(roles ?? {}),\n cluster,\n environment: {\n EBK_STAGE: $app.stage,\n ...(configuration\n ? {\n EBK_APPCONFIG_APPLICATION_ID: configuration.applicationId,\n EBK_APPCONFIG_ENVIRONMENT_ID: configuration.environmentId,\n EBK_APPCONFIG_PROFILE_ID: configuration.profileId,\n }\n : {}),\n },\n link: links,\n ${kind === 'service' ? "image: { context: './services/" + workload + "', dockerfile: 'Dockerfile' },\n health: { command: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1'] },\n logging: { name: contract.name, retention: '1 month' },\n serviceRegistry: { port: 3000 }," : "image: { context: './jobs/" + workload + "', dockerfile: 'Dockerfile' },\n logging: { name: contract.name, retention: '1 month' },"}\n transform: {\n logGroup: {\n logGroupClass: contract.logGroupClass,\n tags: contract.tags,\n },\n taskDefinition: {\n family: standardSliceExtensionTaskFamily(\n $app.stage,\n '${workload}',\n '${kind}',\n ),\n },\n },\n }, configuration?.deployment\n ? { dependsOn: [configuration.deployment] }\n : {});\n const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(\n '${className}LogDataProtection',\n {\n logGroupName: contract.name,\n policyDocument: JSON.stringify(contract.dataProtectionPolicy),\n },\n { dependsOn: [workload.nodes.taskDefinition] },\n );\n const dataProtectionAlarm = new aws.cloudwatch.MetricAlarm(\n '${className}LogDataProtectionFindings',\n {\n alarmActions: [alertTopicArn],\n comparisonOperator: 'GreaterThanOrEqualToThreshold',\n dimensions: { LogGroupName: contract.name },\n evaluationPeriods: 1,\n metricName: 'LogEventsWithFindings',\n namespace: 'AWS/Logs',\n period: 300,\n statistic: 'Sum',\n threshold: 1,\n treatMissingData: 'notBreaching',\n },\n { dependsOn: [dataProtection] },\n );\n void dataProtectionAlarm;\n return workload;\n}\n`,
215
+ [`${relativeRoot}/infra.ts`]: `import { createManagedLogGroupContract } from '@empire-builder-kit/nx/foundation';\nimport { standardSliceExtensionTaskFamily } from '../../infra/workload-roles/effective-intent.js';\n\nexport function ${factoryName}(\n cluster: any,\n alertTopicArn: any,\n roles?: { executionRole: string; taskRole: string },\n links: any[] = [],\n configuration?: {\n applicationId: any;\n deployment: any;\n environmentId: any;\n profileId: any;\n },\n) {\n const contract = createManagedLogGroupContract({\n component: '${kind}-${workload}',\n slice: '${slice}',\n stage: $app.stage,\n workspace: '${workspace}',\n });\n const workload = new sst.aws.${resource}('${className}', {\n ...(roles ?? {}),\n cluster,\n environment: {\n EBK_STAGE: $app.stage,\n ...(configuration\n ? {\n EBK_APPCONFIG_APPLICATION_ID: configuration.applicationId,\n EBK_APPCONFIG_ENVIRONMENT_ID: configuration.environmentId,\n EBK_APPCONFIG_PROFILE_ID: configuration.profileId,\n }\n : {}),\n },\n link: links,\n ${kind === 'service' ? "image: { context: './services/" + workload + "', dockerfile: 'Dockerfile' },\n health: { command: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1'] },\n logging: { name: contract.name, retention: '1 month' },\n serviceRegistry: { port: 3000 }," : "image: { context: './jobs/" + workload + "', dockerfile: 'Dockerfile' },\n logging: { name: contract.name, retention: '1 month' },"}\n transform: {\n logGroup: {\n logGroupClass: contract.logGroupClass,\n tags: contract.tags,\n },\n taskDefinition: {\n family: standardSliceExtensionTaskFamily(\n $app.stage,\n '${workload}',\n '${kind}',\n ),\n },\n },\n }, configuration?.deployment\n ? { dependsOn: [configuration.deployment] }\n : {});\n${kind === 'service' ? ' if ($dev) return workload;\n' : ''} const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(\n '${className}LogDataProtection',\n {\n logGroupName: contract.name,\n policyDocument: JSON.stringify(contract.dataProtectionPolicy),\n },\n { dependsOn: [workload.nodes.taskDefinition] },\n );\n const dataProtectionAlarm = new aws.cloudwatch.MetricAlarm(\n '${className}LogDataProtectionFindings',\n {\n alarmActions: [alertTopicArn],\n comparisonOperator: 'GreaterThanOrEqualToThreshold',\n dimensions: { LogGroupName: contract.name },\n evaluationPeriods: 1,\n metricName: 'LogEventsWithFindings',\n namespace: 'AWS/Logs',\n period: 300,\n statistic: 'Sum',\n threshold: 1,\n treatMissingData: 'notBreaching',\n },\n { dependsOn: [dataProtection] },\n );\n void dataProtectionAlarm;\n return workload;\n}\n`,
214
216
  };
215
217
  if (kind === 'service') {
216
218
  const serviceMainPath = `${relativeRoot}/src/main.ts`;
@@ -411,7 +413,7 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
411
413
  alarmDescription: JSON.stringify({
412
414
  businessSymptom:
413
415
  '${kind === 'service'
414
- ? `${workload} Service has no running task.`
416
+ ? `${workload} Service task presence is below one or telemetry is missing.`
415
417
  : `${workload} Job essential container exited.`}',
416
418
  id: '${kind === 'service'
417
419
  ? `service-${workload}-health`
@@ -426,14 +428,14 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
426
428
  ? 'LessThanThreshold'
427
429
  : 'GreaterThanOrEqualToThreshold'}',
428
430
  dimensions: ${kind === 'service'
429
- ? `{ ClusterName: cluster.name, ServiceName: workload.name }`
431
+ ? `{ ClusterName: cluster.nodes.cluster.name, ServiceName: workload.nodes.service.name }`
430
432
  : `{ RuleName: reliabilityTaskStopped.name }`},
431
433
  evaluationPeriods: 1,
432
- metricName: '${kind === 'service' ? 'RunningTaskCount' : 'MatchedEvents'}',
434
+ metricName: '${kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents'}',
433
435
  name: '${slice}-' + $app.stage + '-${kind}-${workload}-${kind === 'service' ? 'running' : 'hard-crash'}',
434
- namespace: '${kind === 'service' ? 'ECS/ContainerInsights' : 'AWS/Events'}',
436
+ namespace: '${kind === 'service' ? 'AWS/ECS' : 'AWS/Events'}',
435
437
  period: 60,
436
- statistic: 'Sum',
438
+ statistic: '${kind === 'service' ? 'Minimum' : 'Sum'}',
437
439
  tags: standardTags,
438
440
  threshold: 1,
439
441
  treatMissingData: '${kind === 'service' ? 'breaching' : 'notBreaching'}',
@@ -539,7 +541,8 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
539
541
  }
540
542
  const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(`);
541
543
  }
542
- files[infrastructurePath] = infrastructureSource;
544
+ files[infrastructurePath] =
545
+ (0, observability_infrastructure_js_1.renderExtensionObservability)(infrastructureSource);
543
546
  files[`${relativeRoot}/RUNBOOK.md`] = `# ${workload} ${kind} recovery
544
547
 
545
548
  Confirm the exact Slice, stage, release, configuration, alarm, and workload
@@ -548,7 +551,25 @@ through the read-only diagnostic path. Do not restart, scale, retry, or replace
548
551
  protected resources without the named approval.
549
552
 
550
553
  ${kind === 'service'
551
- ? 'Prefer a verified roll-forward; verify readiness, dependency protection, and the critical Service journey after recovery.'
554
+ ? `The native AWS/ECS LiveTaskCount alarm uses Minimum over 60 seconds and
555
+ counts tasks in ACTIVATING, RUNNING, and DEACTIVATING states. It breaches below
556
+ one or when telemetry is missing. ECS can emit no metrics when there are no
557
+ RUNNING tasks; missing telemetry is actionable but does not prove zero tasks.
558
+
559
+ Startup, emission gaps, and lifecycle transitions can cause false positives.
560
+ Activating, draining, or repeatedly replaced tasks can keep the count positive
561
+ while readiness or useful work fails. ALARM is not a diagnosis. OK does not
562
+ prove readiness, freedom from crash loops, or successful business traffic.
563
+
564
+ Inspect ECS events, task states and exit reasons, the existing container health
565
+ check, and the Slice API health and burn alarms. Do not infer that every Service
566
+ has a load-balancer readiness alarm. Prefer a verified roll-forward; verify
567
+ stable tasks, readiness, dependency protection, and the critical Service journey
568
+ after recovery, then confirm telemetry and alarm recovery.
569
+
570
+ LiveTaskCount adds no metric collection charge and needs no Container Insights
571
+ opt-in. Existing alarm, compute, log, and request charges still apply; this is
572
+ not a measured account-cost claim.`
552
573
  : 'Preserve idempotency and checkpoint state; verify the business deadline and postconditions before retrying or declaring recovery.'}
553
574
  `;
554
575
  files[`${relativeRoot}/src/reliability.ts`] = `import {
@@ -1038,6 +1059,7 @@ async function executeWorkloadGenerator(tree, options, kind) {
1038
1059
  const slice = name(options.slice, 'Product Slice name');
1039
1060
  const workload = name(options.name, kind === 'service' ? 'Service name' : 'Job name');
1040
1061
  const requestedFeature = name(options.feature, 'Feature');
1062
+ (0, observability_policy_js_1.requireObservabilityPolicyFromTree)(tree);
1041
1063
  const plural = kind === 'service' ? 'services' : 'jobs';
1042
1064
  const sliceRoot = node_path_1.posix.join('packages', slice);
1043
1065
  const root = node_path_1.posix.join(sliceRoot, plural, workload);
@@ -72,5 +72,23 @@ export interface GitHubRepositoryIdentity {
72
72
  repositoryId: string;
73
73
  subjectPrefix: string;
74
74
  }
75
+ export interface FrameworkNpmStableBootstrapOptions {
76
+ npmStable: true;
77
+ apiBaseUrl?: string;
78
+ apply?: boolean;
79
+ repository: string;
80
+ token: string;
81
+ npmStableReviewerIds: number[];
82
+ npmStablePreventSelfReview: boolean;
83
+ }
84
+ export interface FrameworkNpmStableBootstrapReport {
85
+ kind: 'framework-npm-stable';
86
+ repository: string;
87
+ ready: boolean;
88
+ blockers: string[];
89
+ changes: string[];
90
+ warnings: string[];
91
+ }
92
+ export declare function runGitHubBootstrap(options: FrameworkNpmStableBootstrapOptions): Promise<FrameworkNpmStableBootstrapReport>;
75
93
  export declare function runGitHubBootstrap(options: GitHubBootstrapOptions): Promise<GitHubBootstrapReport>;
76
94
  export {};