@unified-product-graph/core 0.28.0 → 0.29.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.
- package/CHANGELOG.md +31 -0
- package/dist/index.cjs +219 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +353 -12
- package/dist/index.d.ts +353 -12
- package/dist/index.js +212 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12679,7 +12679,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12679
12679
|
health_status: { type: "string", enum: ["on_track", "at_risk", "off_track"], description: "Overall health" },
|
|
12680
12680
|
url: { type: "string", description: "Where the product lives. Marketing site, app store URL, etc.", modifier: "volatile" },
|
|
12681
12681
|
logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
|
|
12682
|
-
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" }
|
|
12682
|
+
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
|
|
12683
|
+
described_configuration: { type: "string", description: `The product configuration this graph describes, named in plain language. A product's composition often differs by feature flag, plan tier, permission level or beta programme: surfaces appear, disappear, or are replaced by different surfaces with different occupants. A graph that models one of those without saying which is qualified by a condition nobody wrote down, and every fact in it inherits that silence. NAMING, NOT CONFIGURING. This is a label for readers. Nothing reads it to gate, filter, or alter how a tool interprets the graph, and it makes no claim about which configuration most customers are on. @example "Enterprise plan, split-navigation flag on" @example "Default: free tier, all flags off"` }
|
|
12683
12684
|
},
|
|
12684
12685
|
// ProductAreaProperties: ProductArea entity.
|
|
12685
12686
|
product_area: {
|
|
@@ -13207,7 +13208,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13207
13208
|
surface_kind: { type: "string", enum: ["shell", "tool", "pane", "region", "slot", "gutter", "action_bar", "overlay", "ambient"], description: "Structural kind. Determines what may legally nest inside this surface." },
|
|
13208
13209
|
persistence: { type: "string", enum: ["always", "conditional", "on_demand", "transient"], description: "How reliably the surface is present." },
|
|
13209
13210
|
visibility_condition: { type: "string", description: 'When the surface appears, in plain language. Pairs with `persistence: \'conditional\'`, which states *that* it is conditional; this states *what* the condition is. @example "A node is selected", "Only for workspace admins"' },
|
|
13210
|
-
capacity: { type: "number", description: 'How many occupants ONE INSTANCE of the surface holds at once, as a non-negative count. Count the instances themselves with `cardinality`. CAPACITY IS ALWAYS INTENT, NEVER OBSERVED BEHAVIOUR. It records the cap the design asserts, so a banner region declared `capacity: 1` keeps saying 1 even after someone finds it rendering four. Reality that has drifted from the declared intent is recorded as a trackable, assignable debt item on `surface_deviates_via_technical_debt_item`, not by quietly editing this number up to match the bug. Editing it to match would destroy the only record that a gap exists. ABSENT MEANS UNBOUNDED. UPG has no union-typed property primitive (`PropertyDefinition.type` is a single scalar kind), so the proposed `integer | unbounded` shape is expressed as an optional number whose absence carries the "no cap" reading, rather than a magic sentinel value that every consumer would have to special-case. `0` is a real cap meaning "nothing may occupy this surface" (a reserved place), not "unbounded".' },
|
|
13211
|
+
capacity: { type: "number", description: 'How many occupants ONE INSTANCE of the surface holds at once, as a non-negative count. Count the instances themselves with `cardinality`. CAPACITY IS ALWAYS INTENT, NEVER OBSERVED BEHAVIOUR. It records the cap the design asserts, so a banner region declared `capacity: 1` keeps saying 1 even after someone finds it rendering four. Reality that has drifted from the declared intent is recorded as a trackable, assignable debt item on `surface_deviates_via_technical_debt_item`, not by quietly editing this number up to match the bug. Editing it to match would destroy the only record that a gap exists. ABSENT MEANS UNBOUNDED. UPG has no union-typed property primitive (`PropertyDefinition.type` is a single scalar kind), so the proposed `integer | unbounded` shape is expressed as an optional number whose absence carries the "no cap" reading, rather than a magic sentinel value that every consumer would have to special-case. `0` is a real cap meaning "nothing may occupy this surface" (a reserved place), not "unbounded". THREE STATES, AND THEY ARE ALL DIFFERENT. Absent = unbounded, no cap stated. `0` = a reserved place nothing may occupy. `null` = neither of those, and nothing this field defines; it is what you get by writing an explicit null rather than removing the key, and no consumer reads it as a cap. To return this property to ABSENT, remove the key with `update_node`\'s or `batch_update_nodes`\' `unset_properties`, since a property merge preserves anything you omit. UNBOUNDED IS NOT AN EXEMPTION FROM SCRUTINY. The contention detector reads an absent capacity as a threshold of 1, on the reasoning that a surface which states no limit has stated no answer either, so several occupants is exactly the unrecorded decision worth naming. Declaring a real capacity is therefore the way to quiet the check honestly, and the only way that also records something true.' },
|
|
13211
13212
|
cardinality: { type: "string", enum: ["1", "0..1", "1..n", "0..n"], description: "How many instances of this surface exist. `capacity` counts occupants within one instance; this counts the instances." },
|
|
13212
13213
|
instance_scope: { type: "string", enum: ["global", "per_parent"], description: 'What an instance is scoped to: one shared instance for the product, or one per containing surface. Decides whether "the product has this surface" is a true sentence or a per-parent one.' },
|
|
13213
13214
|
composition_mode: { type: "string", enum: ["exclusive", "additive", "chained"], description: "How the occupants relate: one wins (`exclusive`), all coexist (`additive`), or each wraps the next (`chained`). Declaring `chained` exempts the surface from `contended-surface-without-arbitration`; leaving this unset does not." },
|
|
@@ -27049,7 +27050,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27049
27050
|
id: "contended-surface-without-arbitration",
|
|
27050
27051
|
since: "0.27.0",
|
|
27051
27052
|
name: "Contended surface without arbitration",
|
|
27052
|
-
description: '
|
|
27053
|
+
description: 'At least one surface in this graph has no recorded answer to "who wins here, and why". Either it holds more occupants than its stated `capacity` allows (or holds several while stating no limit at all) and carries no arbitration_rule, or it admits through arbitration_state that the answer is absent (`none`) or accidental (`safe_by_coincidence`). Left unrecorded, that answer lives in whoever remembers the last argument.',
|
|
27053
27054
|
// 0.28.0 (feedback 852a9721, from a 30-surface field audit). Three changes,
|
|
27054
27055
|
// each earned by a reported false positive or a reported blind spot:
|
|
27055
27056
|
//
|
|
@@ -27087,6 +27088,68 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27087
27088
|
// attribution — making this family noisier on its first real deployment,
|
|
27088
27089
|
// which is the exact failure (a) exists to fix. If field evidence later shows
|
|
27089
27090
|
// it needs its own severity, mint it then.
|
|
27091
|
+
//
|
|
27092
|
+
// 0.29.0 (feedback af9ae4c2, the same reporter's measured follow-up: 43
|
|
27093
|
+
// surfaces, 10 flagged, 7 rightly and 3 wrongly). Branch (a) moved from an
|
|
27094
|
+
// aggregate presence count to the per-node `edge_count_vs_property` form.
|
|
27095
|
+
//
|
|
27096
|
+
// (d) CAPACITY IS NOW READ. The old branch was a pure edge count: any
|
|
27097
|
+
// surface with more than one occupant and no rule. That flags a header
|
|
27098
|
+
// row declaring room for four and holding exactly four, which is not
|
|
27099
|
+
// contended but PARTITIONED — everyone fits, by design, so nothing is
|
|
27100
|
+
// displaced and there is nothing to decide. All three of the reporter's
|
|
27101
|
+
// false positives were this, and all three had occupancy at or below a
|
|
27102
|
+
// stated capacity. The rule is now `occupancy > (capacity ?? 1)`.
|
|
27103
|
+
//
|
|
27104
|
+
// ABSENT CAPACITY BEHAVES AS 1, NOT AS INFINITY. Absence means
|
|
27105
|
+
// unbounded, and it is tempting to read unbounded as "never flag". The
|
|
27106
|
+
// opposite is right: a surface that states no limit has stated no
|
|
27107
|
+
// answer, so two or more occupants is precisely the unrecorded decision
|
|
27108
|
+
// this pattern names. That keeps all four of the reporter's unbounded
|
|
27109
|
+
// true positives (7, 7, 10 and 3 occupants) firing.
|
|
27110
|
+
//
|
|
27111
|
+
// The check had to become per-node rather than two ANDed aggregates:
|
|
27112
|
+
// "some surface is over capacity" and "some surface has no rule" are
|
|
27113
|
+
// both true when they are DIFFERENT surfaces, which would have kept
|
|
27114
|
+
// every false positive alive. See `EdgeCountVsPropertyCheck.node_filter`.
|
|
27115
|
+
//
|
|
27116
|
+
// ADJUDICATED: DOES A CAPACITY-SATISFIED `additive` SURFACE STILL NEED AN
|
|
27117
|
+
// ORDERING RULE? The reporter's capacity rule and their own earlier report
|
|
27118
|
+
// pull in opposite directions here: 0.28.0 documented that `additive`
|
|
27119
|
+
// occupants raise a question of ORDER rather than victory and that the order
|
|
27120
|
+
// belongs in `arbitration_rule`, while the capacity rule says a surface
|
|
27121
|
+
// where everyone fits has nothing to settle. Ruling: BOTH ARE RIGHT, because
|
|
27122
|
+
// they are about different failures, and this detector owns only one of them.
|
|
27123
|
+
//
|
|
27124
|
+
// - Contention is about DISPLACEMENT: who is not rendered. Capacity settles
|
|
27125
|
+
// it. If everyone fits, nobody is displaced, and "who wins" is genuinely
|
|
27126
|
+
// moot. That is why reading capacity removes real false positives rather
|
|
27127
|
+
// than merely quieting the check.
|
|
27128
|
+
// - Ordering is about ARRANGEMENT: in what sequence coexisting occupants
|
|
27129
|
+
// appear. Capacity says nothing about it. The reporter's own four-occupant
|
|
27130
|
+
// header row, positioned by runtime width measurement, has a live ordering
|
|
27131
|
+
// question and no displacement question at all.
|
|
27132
|
+
//
|
|
27133
|
+
// So this check reads capacity and stops at displacement. The ordering concern
|
|
27134
|
+
// stays documented on `composition_mode: 'additive'` as guidance, NOT as a
|
|
27135
|
+
// detector: minting `additive-surface-without-ordering` today would repeat the
|
|
27136
|
+
// 0.28.0 mistake of shipping a second detector on no field evidence, and it
|
|
27137
|
+
// would double-report against this one. Mint it when a graph shows unrecorded
|
|
27138
|
+
// ordering causing a real defect, with its own severity and remediation.
|
|
27139
|
+
//
|
|
27140
|
+
// Known consequence, recorded rather than papered over: `arbitration_rule` is
|
|
27141
|
+
// now visibly OVERLOADED. It holds a displacement rule for `exclusive`
|
|
27142
|
+
// surfaces and an ordering rule for `additive` ones, and this check reads it
|
|
27143
|
+
// for only the first. Splitting it (`arbitration_rule` vs `ordering_rule`) is
|
|
27144
|
+
// the natural move if the ordering detector is ever minted; it is not worth a
|
|
27145
|
+
// breaking property change before then.
|
|
27146
|
+
//
|
|
27147
|
+
// STILL OPEN (banked at 0.28.0, deliberately NOT changed here): a `chained`
|
|
27148
|
+
// slot that honestly declares `arbitration_state: 'none'` re-fires through
|
|
27149
|
+
// branch (c), which carries no chained exemption. Current guidance stands:
|
|
27150
|
+
// leave `arbitration_state` unset on chained surfaces. Changing (b)/(c) was
|
|
27151
|
+
// out of scope for this release and still awaits the field confirmation the
|
|
27152
|
+
// reporter will produce populating their 14 chained slots.
|
|
27090
27153
|
structured_condition: {
|
|
27091
27154
|
operator: "and",
|
|
27092
27155
|
checks: [
|
|
@@ -27105,14 +27168,16 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27105
27168
|
checks: [
|
|
27106
27169
|
{
|
|
27107
27170
|
check: {
|
|
27108
|
-
type: "
|
|
27171
|
+
type: "edge_count_vs_property",
|
|
27109
27172
|
entity_type: "surface",
|
|
27110
|
-
|
|
27111
|
-
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
},
|
|
27173
|
+
edge_type: "feature_occupies_surface",
|
|
27174
|
+
direction: "inbound",
|
|
27175
|
+
property: "capacity",
|
|
27176
|
+
property_absent_default: 1,
|
|
27177
|
+
node_comparison: "gt",
|
|
27178
|
+
node_filter: { property: "arbitration_rule", present: false },
|
|
27179
|
+
except_property: "composition_mode",
|
|
27180
|
+
except_value: "chained",
|
|
27116
27181
|
comparison: "nonzero"
|
|
27117
27182
|
}
|
|
27118
27183
|
},
|
|
@@ -27137,7 +27202,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27137
27202
|
]
|
|
27138
27203
|
},
|
|
27139
27204
|
why_it_matters: "Contention over a UI place is settled every time it comes up, and an unrecorded settlement is re-argued at the next feature. The cost is paid in repeated decisions, not in a visible defect. A surface that is safe only by coincidence pays it all at once instead, the first time an occupant is added.",
|
|
27140
|
-
remediation: "Read `arbitration_state` first: it says what the work actually is. `enforced_undocumented` means the rule already exists in code and needs transcribing into `arbitration_rule`: ten minutes, no meeting. `none` means nobody has decided and someone has to. `safe_by_coincidence` means nothing is enforcing anything and the surface only looks settled; that one wants a guard in code, not just prose.
|
|
27205
|
+
remediation: "The violation names the offending surfaces in `target_node_ids`; start there rather than re-reading the whole roster. Read `arbitration_state` first: it says what the work actually is. `enforced_undocumented` means the rule already exists in code and needs transcribing into `arbitration_rule`: ten minutes, no meeting. `none` means nobody has decided and someone has to. `safe_by_coincidence` means nothing is enforcing anything and the surface only looks settled; that one wants a guard in code, not just prose. If a flagged surface genuinely holds everything it was designed to hold, the honest fix is to state its `capacity`, not to invent a rule: a surface whose occupancy is at or below a stated capacity is partitioned rather than contended, and it stops firing. Where a design system already answers the question, link the source with `surface_governed_by_design_guideline`. Surfaces whose occupants wrap one another rather than compete should declare `composition_mode: 'chained'`, which exempts them: that is a factual claim about how the code composes, not a way to quiet the check.",
|
|
27141
27206
|
stages: ["build", "beta", "launch", "growth", "mature", "maintenance"],
|
|
27142
27207
|
severity: "medium",
|
|
27143
27208
|
source: { kind: "fundamental" }
|
|
@@ -27227,6 +27292,95 @@ var UPG_PRESENCE_EXCEPT_SPECS = (() => {
|
|
|
27227
27292
|
function presenceExceptKey(property, exceptProperty, exceptValue) {
|
|
27228
27293
|
return `${property}!${exceptProperty}=${exceptValue}`;
|
|
27229
27294
|
}
|
|
27295
|
+
function edgeCountSpecKey(spec) {
|
|
27296
|
+
const filter = spec.node_filter ? `${spec.node_filter.property}=${spec.node_filter.present ? "present" : "absent"}` : "*";
|
|
27297
|
+
const except = spec.except_property !== void 0 && spec.except_value !== void 0 ? `${spec.except_property}=${spec.except_value}` : "*";
|
|
27298
|
+
return [
|
|
27299
|
+
spec.entity_type,
|
|
27300
|
+
spec.edge_type,
|
|
27301
|
+
spec.direction,
|
|
27302
|
+
spec.property,
|
|
27303
|
+
String(spec.property_absent_default),
|
|
27304
|
+
spec.node_comparison,
|
|
27305
|
+
filter,
|
|
27306
|
+
except
|
|
27307
|
+
].join("|");
|
|
27308
|
+
}
|
|
27309
|
+
function entityFilterKey(entityType, filter) {
|
|
27310
|
+
const kind = classifyEntityFilter(filter);
|
|
27311
|
+
switch (kind) {
|
|
27312
|
+
case "presence":
|
|
27313
|
+
return `${entityType}|${filter.property}=${filter.present ? "present" : "absent"}`;
|
|
27314
|
+
case "value":
|
|
27315
|
+
return `${entityType}|${filter.property}=${filter.value}`;
|
|
27316
|
+
case "status":
|
|
27317
|
+
return `${entityType}|status=${filter.status}`;
|
|
27318
|
+
case "unrecognized":
|
|
27319
|
+
return `${entityType}|<unrecognized>`;
|
|
27320
|
+
}
|
|
27321
|
+
}
|
|
27322
|
+
function classifyEntityFilter(filter) {
|
|
27323
|
+
const hasProperty = typeof filter.property === "string";
|
|
27324
|
+
if (hasProperty && typeof filter.present === "boolean") return "presence";
|
|
27325
|
+
if (hasProperty && typeof filter.value === "string") return "value";
|
|
27326
|
+
if (typeof filter.status === "string") return "status";
|
|
27327
|
+
return "unrecognized";
|
|
27328
|
+
}
|
|
27329
|
+
function walkForEntityFilterSpecs(cond, out) {
|
|
27330
|
+
if ("operator" in cond) {
|
|
27331
|
+
for (const child of cond.checks) walkForEntityFilterSpecs(child, out);
|
|
27332
|
+
return;
|
|
27333
|
+
}
|
|
27334
|
+
const check = cond.check;
|
|
27335
|
+
if (check.type !== "entity_count" || !check.filter) return;
|
|
27336
|
+
const kind = classifyEntityFilter(check.filter);
|
|
27337
|
+
if (kind === "unrecognized") {
|
|
27338
|
+
throw new Error(
|
|
27339
|
+
`Unrecognized entity_count filter shape on "${check.entity_type}": ${JSON.stringify(check.filter)}. Recognised shapes are { property, present }, { property, value } and { status }. Teach classifyEntityFilter + the collector before declaring a new one.`
|
|
27340
|
+
);
|
|
27341
|
+
}
|
|
27342
|
+
const key = entityFilterKey(check.entity_type, check.filter);
|
|
27343
|
+
if (out.some((s) => entityFilterKey(s.entity_type, s.filter) === key)) return;
|
|
27344
|
+
out.push({ entity_type: check.entity_type, filter: check.filter, kind });
|
|
27345
|
+
}
|
|
27346
|
+
var UPG_ENTITY_FILTER_SPECS = (() => {
|
|
27347
|
+
const out = [];
|
|
27348
|
+
for (const ap of UPG_ANTI_PATTERNS) {
|
|
27349
|
+
if (ap.structured_condition) walkForEntityFilterSpecs(ap.structured_condition, out);
|
|
27350
|
+
}
|
|
27351
|
+
return out;
|
|
27352
|
+
})();
|
|
27353
|
+
function checkToEdgeCountSpec(check) {
|
|
27354
|
+
return {
|
|
27355
|
+
entity_type: check.entity_type,
|
|
27356
|
+
edge_type: check.edge_type,
|
|
27357
|
+
direction: check.direction ?? "inbound",
|
|
27358
|
+
property: check.property,
|
|
27359
|
+
property_absent_default: check.property_absent_default,
|
|
27360
|
+
node_comparison: check.node_comparison,
|
|
27361
|
+
node_filter: check.node_filter,
|
|
27362
|
+
except_property: check.except_property,
|
|
27363
|
+
except_value: check.except_value
|
|
27364
|
+
};
|
|
27365
|
+
}
|
|
27366
|
+
function walkForEdgeCountSpecs(cond, out) {
|
|
27367
|
+
if ("operator" in cond) {
|
|
27368
|
+
for (const child of cond.checks) walkForEdgeCountSpecs(child, out);
|
|
27369
|
+
return;
|
|
27370
|
+
}
|
|
27371
|
+
const check = cond.check;
|
|
27372
|
+
if (check.type !== "edge_count_vs_property") return;
|
|
27373
|
+
const spec = checkToEdgeCountSpec(check);
|
|
27374
|
+
const key = edgeCountSpecKey(spec);
|
|
27375
|
+
if (!out.some((s) => edgeCountSpecKey(s) === key)) out.push(spec);
|
|
27376
|
+
}
|
|
27377
|
+
var UPG_EDGE_COUNT_SPECS = (() => {
|
|
27378
|
+
const out = [];
|
|
27379
|
+
for (const ap of UPG_ANTI_PATTERNS) {
|
|
27380
|
+
if (ap.structured_condition) walkForEdgeCountSpecs(ap.structured_condition, out);
|
|
27381
|
+
}
|
|
27382
|
+
return out;
|
|
27383
|
+
})();
|
|
27230
27384
|
function getAntiPatternById(id) {
|
|
27231
27385
|
return UPG_ANTI_PATTERNS.find((ap) => ap.id === id);
|
|
27232
27386
|
}
|
|
@@ -27303,7 +27457,11 @@ function evaluateAntiPatterns(inputs, options) {
|
|
|
27303
27457
|
const concern = concernFor(ap.id);
|
|
27304
27458
|
if (!concernEvaluatedFor(inputs.memberKind, concern)) continue;
|
|
27305
27459
|
if (evaluateCondition(ap.structured_condition, inputs)) {
|
|
27306
|
-
|
|
27460
|
+
const ids = /* @__PURE__ */ new Set();
|
|
27461
|
+
collectAttribution(ap.structured_condition, inputs, ids);
|
|
27462
|
+
const violation = buildViolation(ap, concern);
|
|
27463
|
+
if (ids.size > 0) violation.target_node_ids = [...ids].sort();
|
|
27464
|
+
fires.push(violation);
|
|
27307
27465
|
}
|
|
27308
27466
|
}
|
|
27309
27467
|
fires.sort((a, b) => {
|
|
@@ -27313,6 +27471,25 @@ function evaluateAntiPatterns(inputs, options) {
|
|
|
27313
27471
|
});
|
|
27314
27472
|
return fires;
|
|
27315
27473
|
}
|
|
27474
|
+
function collectAttribution(cond, inputs, out) {
|
|
27475
|
+
if ("operator" in cond) {
|
|
27476
|
+
for (const child of cond.checks) {
|
|
27477
|
+
if (cond.operator === "or" && !evaluateCondition(child, inputs)) continue;
|
|
27478
|
+
collectAttribution(child, inputs, out);
|
|
27479
|
+
}
|
|
27480
|
+
return;
|
|
27481
|
+
}
|
|
27482
|
+
const check = cond.check;
|
|
27483
|
+
if (check.type === "edge_count_vs_property") {
|
|
27484
|
+
const key = edgeCountSpecKey(checkToEdgeCountSpec(check));
|
|
27485
|
+
for (const id of inputs.nodesByEdgeCountSpec?.[key] ?? []) out.add(id);
|
|
27486
|
+
return;
|
|
27487
|
+
}
|
|
27488
|
+
if (check.type === "entity_count" && check.filter) {
|
|
27489
|
+
const key = entityFilterKey(check.entity_type, check.filter);
|
|
27490
|
+
for (const id of inputs.nodesByEntityFilter?.[key] ?? []) out.add(id);
|
|
27491
|
+
}
|
|
27492
|
+
}
|
|
27316
27493
|
function evaluateCondition(cond, inputs) {
|
|
27317
27494
|
if ("check" in cond) {
|
|
27318
27495
|
return evaluateLeaf(cond.check, inputs);
|
|
@@ -27332,6 +27509,8 @@ function evaluateLeaf(check, inputs) {
|
|
|
27332
27509
|
switch (check.type) {
|
|
27333
27510
|
case "entity_count":
|
|
27334
27511
|
return evaluateEntityCount(check, inputs);
|
|
27512
|
+
case "edge_count_vs_property":
|
|
27513
|
+
return evaluateEdgeCountVsProperty(check, inputs);
|
|
27335
27514
|
case "relationship":
|
|
27336
27515
|
return evaluateRelationship(check, inputs);
|
|
27337
27516
|
case "benchmark":
|
|
@@ -27392,6 +27571,18 @@ function evaluateEntityCount(check, inputs) {
|
|
|
27392
27571
|
}
|
|
27393
27572
|
return compareNumber(count, check.comparison, check.threshold);
|
|
27394
27573
|
}
|
|
27574
|
+
function evaluateEdgeCountVsProperty(check, inputs) {
|
|
27575
|
+
const key = edgeCountSpecKey(checkToEdgeCountSpec(check));
|
|
27576
|
+
const matched = inputs.nodesByEdgeCountSpec?.[key];
|
|
27577
|
+
if (matched === void 0) {
|
|
27578
|
+
return compareNumber(
|
|
27579
|
+
inputs.countsByType[check.entity_type] ?? 0,
|
|
27580
|
+
check.comparison,
|
|
27581
|
+
check.threshold
|
|
27582
|
+
);
|
|
27583
|
+
}
|
|
27584
|
+
return compareNumber(matched.length, check.comparison, check.threshold);
|
|
27585
|
+
}
|
|
27395
27586
|
function relationshipKey(source_type, edge_type, target_type) {
|
|
27396
27587
|
return `${source_type}|${edge_type}|${target_type}`;
|
|
27397
27588
|
}
|
|
@@ -27474,7 +27665,7 @@ function collectTargetEntities(cond) {
|
|
|
27474
27665
|
function walk(c) {
|
|
27475
27666
|
if ("check" in c) {
|
|
27476
27667
|
const leaf = c.check;
|
|
27477
|
-
if (leaf.type === "entity_count" || leaf.type === "benchmark") {
|
|
27668
|
+
if (leaf.type === "entity_count" || leaf.type === "benchmark" || leaf.type === "edge_count_vs_property") {
|
|
27478
27669
|
types.add(leaf.entity_type);
|
|
27479
27670
|
} else if (leaf.type === "relationship") {
|
|
27480
27671
|
types.add(leaf.source_type);
|
|
@@ -30501,7 +30692,7 @@ function checkHeaderSealText(text) {
|
|
|
30501
30692
|
}
|
|
30502
30693
|
|
|
30503
30694
|
// src/index.ts
|
|
30504
|
-
var UPG_VERSION = "0.
|
|
30695
|
+
var UPG_VERSION = "0.29.0";
|
|
30505
30696
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
30506
30697
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
30507
30698
|
var UPG_TYPES = getTypes();
|
|
@@ -30615,10 +30806,12 @@ export {
|
|
|
30615
30806
|
UPG_DOMAIN_RINGS,
|
|
30616
30807
|
UPG_EDGE_CATALOG,
|
|
30617
30808
|
UPG_EDGE_COUNT,
|
|
30809
|
+
UPG_EDGE_COUNT_SPECS,
|
|
30618
30810
|
UPG_EDGE_MIGRATIONS,
|
|
30619
30811
|
UPG_EDGE_PAIR_MAP,
|
|
30620
30812
|
UPG_EDGE_TYPES,
|
|
30621
30813
|
UPG_ENTITY_COUNT,
|
|
30814
|
+
UPG_ENTITY_FILTER_SPECS,
|
|
30622
30815
|
UPG_ENTITY_META,
|
|
30623
30816
|
UPG_ENTITY_META_BY_ID,
|
|
30624
30817
|
UPG_ENTITY_META_BY_NAME,
|
|
@@ -30673,8 +30866,11 @@ export {
|
|
|
30673
30866
|
canBeChildOf,
|
|
30674
30867
|
checkHeaderSeal,
|
|
30675
30868
|
checkHeaderSealText,
|
|
30869
|
+
checkToEdgeCountSpec,
|
|
30870
|
+
classifyEntityFilter,
|
|
30676
30871
|
coerceProductStage,
|
|
30677
30872
|
collectSlugsForType,
|
|
30873
|
+
collectTargetEntities,
|
|
30678
30874
|
compareVersions,
|
|
30679
30875
|
computeBodyChecksum,
|
|
30680
30876
|
concernEvaluatedFor,
|
|
@@ -30686,6 +30882,8 @@ export {
|
|
|
30686
30882
|
deriveSingleCounts,
|
|
30687
30883
|
describeTreePattern,
|
|
30688
30884
|
edgeCarriesProperties,
|
|
30885
|
+
edgeCountSpecKey,
|
|
30886
|
+
entityFilterKey,
|
|
30689
30887
|
evaluateAntiPatterns,
|
|
30690
30888
|
findInvalidStatusMigrationTargets,
|
|
30691
30889
|
formatUpgText,
|