@unified-product-graph/core 0.26.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -135,6 +135,8 @@ __export(index_exports, {
135
135
  backfillSlug: () => backfillSlug,
136
136
  buildTypeAliases: () => buildTypeAliases,
137
137
  canBeChildOf: () => canBeChildOf,
138
+ checkHeaderSeal: () => checkHeaderSeal,
139
+ checkHeaderSealText: () => checkHeaderSealText,
138
140
  coerceProductStage: () => coerceProductStage,
139
141
  collectSlugsForType: () => collectSlugsForType,
140
142
  compareVersions: () => compareVersions,
@@ -143,6 +145,9 @@ __export(index_exports, {
143
145
  concernFor: () => concernFor,
144
146
  concernGatesFor: () => concernGatesFor,
145
147
  crossProductScope: () => crossProductScope,
148
+ deriveCounts: () => deriveCounts,
149
+ derivePortfolioCounts: () => derivePortfolioCounts,
150
+ deriveSingleCounts: () => deriveSingleCounts,
146
151
  describeTreePattern: () => describeTreePattern,
147
152
  edgeCarriesProperties: () => edgeCarriesProperties,
148
153
  evaluateAntiPatterns: () => evaluateAntiPatterns,
@@ -30160,6 +30165,28 @@ function portfolioBody(doc) {
30160
30165
  ...doc.signals && doc.signals.length > 0 ? { signals: sortNodes(doc.signals).map(canonicalNode) } : {}
30161
30166
  };
30162
30167
  }
30168
+ function deriveSingleCounts(doc) {
30169
+ return { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
30170
+ }
30171
+ function derivePortfolioCounts(doc) {
30172
+ const members = doc.products ?? [];
30173
+ const memberKindOf = (p) => p.member_kind ?? "product";
30174
+ const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
30175
+ const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
30176
+ const operatingFunctionCount = members.filter((p) => memberKindOf(p) === "operating_function").length;
30177
+ return {
30178
+ products: members.filter((p) => memberKindOf(p) === "product").length,
30179
+ ...watchedCount > 0 ? { watched_products: watchedCount } : {},
30180
+ ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
30181
+ ...operatingFunctionCount > 0 ? { operating_functions: operatingFunctionCount } : {},
30182
+ product_areas: doc.product_areas?.length ?? 0,
30183
+ portfolios: doc.portfolios?.length ?? 0,
30184
+ cross_edges: doc.cross_edges?.length ?? 0
30185
+ };
30186
+ }
30187
+ function deriveCounts(doc) {
30188
+ return isPortfolio(doc) ? derivePortfolioCounts(doc) : deriveSingleCounts(doc);
30189
+ }
30163
30190
  function computeBodyChecksum(doc) {
30164
30191
  const body = isPortfolio(doc) ? portfolioBody(doc) : singleBody(doc);
30165
30192
  const content = JSON.stringify(body);
@@ -30199,7 +30226,7 @@ function serializeSingleWithHeader(doc, opts) {
30199
30226
  };
30200
30227
  const summary = deriveSummary(product.description);
30201
30228
  if (summary) header.summary = summary;
30202
- header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
30229
+ header.counts = deriveSingleCounts(doc);
30203
30230
  if (doc.member_kind === "org_rollup" || doc.member_kind === "watched" || doc.member_kind === "operating_function") {
30204
30231
  header.member_kind = doc.member_kind;
30205
30232
  }
@@ -30222,20 +30249,7 @@ function serializePortfolioWithHeader(doc, opts) {
30222
30249
  };
30223
30250
  const summary = deriveSummary(org.description);
30224
30251
  if (summary) header.summary = summary;
30225
- const members = doc.products ?? [];
30226
- const memberKindOf = (p) => p.member_kind ?? "product";
30227
- const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
30228
- const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
30229
- const operatingFunctionCount = members.filter((p) => memberKindOf(p) === "operating_function").length;
30230
- header.counts = {
30231
- products: members.filter((p) => memberKindOf(p) === "product").length,
30232
- ...watchedCount > 0 ? { watched_products: watchedCount } : {},
30233
- ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
30234
- ...operatingFunctionCount > 0 ? { operating_functions: operatingFunctionCount } : {},
30235
- product_areas: doc.product_areas?.length ?? 0,
30236
- portfolios: doc.portfolios?.length ?? 0,
30237
- cross_edges: doc.cross_edges?.length ?? 0
30238
- };
30252
+ header.counts = derivePortfolioCounts(doc);
30239
30253
  header.provenance = buildProvenance(doc, opts);
30240
30254
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
30241
30255
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
@@ -30303,9 +30317,61 @@ function isCanonical(text) {
30303
30317
  const parsed = parseUpg(text);
30304
30318
  return text === serializeCanonical(parsed);
30305
30319
  }
30320
+ function checkHeaderSeal(obj) {
30321
+ const raw = obj;
30322
+ const header = raw?.$upg;
30323
+ const report = {
30324
+ header_present: !!header,
30325
+ counts_checked: false,
30326
+ integrity_checked: false,
30327
+ counts_drift: [],
30328
+ integrity_drift: []
30329
+ };
30330
+ if (!header) return report;
30331
+ if (header.format_version !== UPG_CANONICAL_FORMAT_VERSION) {
30332
+ report.skipped_reason = `format_version "${header.format_version}" is not this serialiser's "${UPG_CANONICAL_FORMAT_VERSION}"; counts keys and body layout are format-version contracts, so neither seal can be judged here.`;
30333
+ return report;
30334
+ }
30335
+ const doc = normalizeDocument(raw);
30336
+ const declaredCounts = header.counts ?? {};
30337
+ const actualCounts = deriveCounts(doc);
30338
+ if (header.counts === void 0) {
30339
+ report.skipped_reason = "header has no `counts` block to compare.";
30340
+ } else {
30341
+ report.counts_checked = true;
30342
+ const fields = /* @__PURE__ */ new Set([...Object.keys(declaredCounts), ...Object.keys(actualCounts)]);
30343
+ for (const field of [...fields].sort()) {
30344
+ const declaredRaw = declaredCounts[field];
30345
+ const declared = typeof declaredRaw === "number" ? declaredRaw : 0;
30346
+ const actual = actualCounts[field] ?? 0;
30347
+ if (declared !== actual) report.counts_drift.push({ field, declared, actual });
30348
+ }
30349
+ }
30350
+ const integrity = header.integrity;
30351
+ if (!integrity || typeof integrity.body !== "string") {
30352
+ report.skipped_reason = report.skipped_reason ?? "header has no `integrity.body` seal to compare.";
30353
+ } else if (integrity.algorithm !== INTEGRITY_ALGORITHM) {
30354
+ report.skipped_reason = report.skipped_reason ?? `integrity.algorithm "${integrity.algorithm}" is not "${INTEGRITY_ALGORITHM}"; the declared digest cannot be recomputed here.`;
30355
+ } else {
30356
+ report.integrity_checked = true;
30357
+ const computed = computeBodyChecksum(doc);
30358
+ if (computed !== integrity.body) {
30359
+ report.integrity_drift.push({
30360
+ algorithm: integrity.algorithm,
30361
+ declared: integrity.body,
30362
+ computed
30363
+ });
30364
+ }
30365
+ }
30366
+ return report;
30367
+ }
30368
+ function checkHeaderSealText(text) {
30369
+ const noBom = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
30370
+ return checkHeaderSeal(JSON.parse(noBom));
30371
+ }
30306
30372
 
30307
30373
  // src/index.ts
30308
- var UPG_VERSION = "0.26.0";
30374
+ var UPG_VERSION = "0.26.1";
30309
30375
  var UPG_FORMAT_VERSION = "0.4.0";
30310
30376
  var MARKDOWN_FORMAT_VERSION = "0.1";
30311
30377
  var UPG_TYPES = getTypes();
@@ -30475,6 +30541,8 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
30475
30541
  backfillSlug,
30476
30542
  buildTypeAliases,
30477
30543
  canBeChildOf,
30544
+ checkHeaderSeal,
30545
+ checkHeaderSealText,
30478
30546
  coerceProductStage,
30479
30547
  collectSlugsForType,
30480
30548
  compareVersions,
@@ -30483,6 +30551,9 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
30483
30551
  concernFor,
30484
30552
  concernGatesFor,
30485
30553
  crossProductScope,
30554
+ deriveCounts,
30555
+ derivePortfolioCounts,
30556
+ deriveSingleCounts,
30486
30557
  describeTreePattern,
30487
30558
  edgeCarriesProperties,
30488
30559
  evaluateAntiPatterns,