@unified-product-graph/core 0.27.0 → 0.28.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/README.md +1 -1
- package/dist/index.cjs +153 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +205 -3
- package/dist/index.d.ts +205 -3
- package/dist/index.js +151 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
The TypeScript core for the **Unified Product Graph (UPG)**, an open specification that gives product tools a shared vocabulary for entities, edges, and frameworks.
|
|
9
9
|
|
|
10
|
-
321 entity types across 37 domains.
|
|
10
|
+
321 entity types across 37 domains. 1069 typed edges. 46 canonical framework definitions, with more added as each is reviewed.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
package/dist/index.cjs
CHANGED
|
@@ -104,6 +104,7 @@ __export(index_exports, {
|
|
|
104
104
|
UPG_POLYMORPHIC_EDGE_KEYS: () => UPG_POLYMORPHIC_EDGE_KEYS,
|
|
105
105
|
UPG_PORTFOLIO_KINDS: () => UPG_PORTFOLIO_KINDS,
|
|
106
106
|
UPG_PORTFOLIO_SHARED_TYPES: () => UPG_PORTFOLIO_SHARED_TYPES,
|
|
107
|
+
UPG_PRESENCE_EXCEPT_SPECS: () => UPG_PRESENCE_EXCEPT_SPECS,
|
|
107
108
|
UPG_PRODUCT_STAGES: () => UPG_PRODUCT_STAGES,
|
|
108
109
|
UPG_PRODUCT_STAGE_COERCION_MAP: () => UPG_PRODUCT_STAGE_COERCION_MAP,
|
|
109
110
|
UPG_PROPERTY_MIGRATIONS: () => UPG_PROPERTY_MIGRATIONS,
|
|
@@ -240,6 +241,7 @@ __export(index_exports, {
|
|
|
240
241
|
normalizeDocument: () => normalizeDocument,
|
|
241
242
|
parseUpg: () => parseUpg,
|
|
242
243
|
pickCanonicalEdge: () => pickCanonicalEdge,
|
|
244
|
+
presenceExceptKey: () => presenceExceptKey,
|
|
243
245
|
resolveAllEdges: () => resolveAllEdges,
|
|
244
246
|
resolveContainmentEdge: () => resolveContainmentEdge,
|
|
245
247
|
resolveEntityType: () => resolveEntityType,
|
|
@@ -1800,9 +1802,10 @@ var UPG_EDGE_CATALOG = {
|
|
|
1800
1802
|
journey_action_realised_by_feature: { forward_verb: "realised_by", reverse_verb: "realises", classification: "cross-domain", source_type: "journey_action", target_type: "feature" },
|
|
1801
1803
|
// ── surface (0.27.0): the place inside a screen ─────────────────────────
|
|
1802
1804
|
// `screen` is route-level; `surface` is the contested place within it. The
|
|
1803
|
-
//
|
|
1805
|
+
// eleven edges below give a surface its spine (nesting), its guest list, its
|
|
1804
1806
|
// purpose, its governing rule, its rendering vocabulary, its measurement,
|
|
1805
|
-
//
|
|
1807
|
+
// its replacement path, and (0.28.0) the gap between what it intends and
|
|
1808
|
+
// what it actually does.
|
|
1806
1809
|
//
|
|
1807
1810
|
// Classification adjudications, each anchored on the nearest precedent:
|
|
1808
1811
|
// contains → hierarchy (feature_area_contains_feature_area, planning_cycle_contains_planning_cycle)
|
|
@@ -1838,6 +1841,35 @@ var UPG_EDGE_CATALOG = {
|
|
|
1838
1841
|
surface_renders_design_component: { forward_verb: "renders", reverse_verb: "rendered_on", classification: "hierarchy", source_type: "surface", target_type: "design_component", cross_product_eligible: true },
|
|
1839
1842
|
surface_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "semantic", source_type: "surface", target_type: "metric", cross_product_eligible: true },
|
|
1840
1843
|
surface_supersedes_surface: { forward_verb: "supersedes", reverse_verb: "superseded_by", classification: "semantic", source_type: "surface", target_type: "surface" },
|
|
1844
|
+
// ── surface, 0.28.0 addition: intent versus reality ─────────────────────────
|
|
1845
|
+
// Every surface property records what the design INTENDS. `capacity: 1` on a
|
|
1846
|
+
// banner region is an assertion, and a real product will eventually be found
|
|
1847
|
+
// rendering four. Without somewhere to put that fact, the only ways to record
|
|
1848
|
+
// it are to edit `capacity` up to 4 (which destroys the intent, and with it
|
|
1849
|
+
// any evidence that a gap exists) or to leave the graph quietly wrong.
|
|
1850
|
+
//
|
|
1851
|
+
// This edge is the third option, and it is what lets `capacity` mean intent
|
|
1852
|
+
// unconditionally: the deviation hangs off a `technical_debt_item`, which is
|
|
1853
|
+
// already a trackable, assignable, prioritisable entity with an owner and a
|
|
1854
|
+
// remediation cost. The gap becomes work rather than a discrepancy.
|
|
1855
|
+
//
|
|
1856
|
+
// classification → cross-domain (ux_design → technical_debt). Crossing INTO
|
|
1857
|
+
// the debt domain from another ring is the established shape:
|
|
1858
|
+
// risk_manifests_as_technical_debt_item is cross-domain on exactly that
|
|
1859
|
+
// reasoning. NOT `causal`: `decision_incurs_technical_debt_item` is causal
|
|
1860
|
+
// because a decision BRINGS the debt into existence, whereas a surface
|
|
1861
|
+
// does not cause its own drift, it exhibits it. NOT `hierarchy`: that
|
|
1862
|
+
// would oblige a surface → technical_debt_item containment pair in
|
|
1863
|
+
// UPG_VALID_CHILDREN (guardrail G2b), and a surface does not own the debt
|
|
1864
|
+
// that afflicts it — service_carries_technical_debt_item can be hierarchy
|
|
1865
|
+
// because a service genuinely owns its backlog.
|
|
1866
|
+
// verbs → `deviates_via` / `causes_deviation_in`. The forward verb names
|
|
1867
|
+
// what the SOURCE does (the surface deviates), qualified by the debt that
|
|
1868
|
+
// accounts for it, so the edge key reads as the sentence it asserts. The
|
|
1869
|
+
// reverse says what the debt does to the place.
|
|
1870
|
+
// not cross_product_eligible → a deviation is a fact about one product's
|
|
1871
|
+
// code, and `surface` is not portfolio_shared in the first place.
|
|
1872
|
+
surface_deviates_via_technical_debt_item: { forward_verb: "deviates_via", reverse_verb: "causes_deviation_in", classification: "cross-domain", source_type: "surface", target_type: "technical_debt_item" },
|
|
1841
1873
|
// Inbound. `feature_occupies_surface` is the guest list: the edge that makes
|
|
1842
1874
|
// "what else lives here?" answerable, and the one the contention anti-pattern
|
|
1843
1875
|
// counts. cross-domain (product_spec → ux_design), like screen_surfaces_feature
|
|
@@ -13437,8 +13469,12 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13437
13469
|
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." },
|
|
13438
13470
|
persistence: { type: "string", enum: ["always", "conditional", "on_demand", "transient"], description: "How reliably the surface is present." },
|
|
13439
13471
|
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"' },
|
|
13440
|
-
capacity: { type: "number", description: 'How many occupants the surface holds at once, as a non-negative count. 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".' },
|
|
13472
|
+
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".' },
|
|
13473
|
+
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." },
|
|
13474
|
+
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.' },
|
|
13475
|
+
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." },
|
|
13441
13476
|
arbitration_rule: { type: "string", description: 'Who wins when more occupants want the surface than `capacity` allows, and why. ABSENCE IS MEANINGFUL: a null or empty `arbitration_rule` on a contested surface means nobody decided, which is exactly what the `contended-surface-without-arbitration` anti-pattern detects. Do not fill this in with a placeholder to silence the check. @example "Highest priority wins; ties break to the most recently updated."' },
|
|
13477
|
+
arbitration_state: { type: "string", enum: ["enforced_documented", "enforced_undocumented", "safe_by_coincidence", "none"], description: 'Whether the arbitration answer is enforced, written down, both, or neither. Separates "enforced in code but never transcribed" (ten minutes of typing) from "never decided" (a design meeting) from "safe only because nothing has collided yet" (the dangerous one).' },
|
|
13442
13478
|
extensibility: { type: "string", enum: ["closed", "plugin_registerable", "user_configurable"], description: "Who may add occupants to the surface." },
|
|
13443
13479
|
mutates_content: { type: "boolean", description: "Whether occupying this surface can change the underlying content, as opposed to only selecting or revealing it. The selector-versus-mutator distinction: a gutter that toggles a value is a mutator, a gutter that jumps the cursor is not." },
|
|
13444
13480
|
dimensional_constraint: { type: "string", description: 'The hard spatial budget the surface imposes on its occupants, in whatever unit the design system speaks. @example "292px wide", "25px per field", "two grid columns"' }
|
|
@@ -27275,7 +27311,44 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27275
27311
|
id: "contended-surface-without-arbitration",
|
|
27276
27312
|
since: "0.27.0",
|
|
27277
27313
|
name: "Contended surface without arbitration",
|
|
27278
|
-
description: 'Features occupy surfaces in this graph,
|
|
27314
|
+
description: 'Features occupy surfaces in this graph, and at least one surface has no recorded answer to "who wins here, and why". Either it carries no arbitration_rule and has not declared itself `chained`, 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.',
|
|
27315
|
+
// 0.28.0 (feedback 852a9721, from a 30-surface field audit). Three changes,
|
|
27316
|
+
// each earned by a reported false positive or a reported blind spot:
|
|
27317
|
+
//
|
|
27318
|
+
// (a) rule-absence, EXCEPT chained. `composition_mode: 'chained'` means each
|
|
27319
|
+
// occupant wraps the next rather than competing with it. Many occupants
|
|
27320
|
+
// and no arbitration rule is the designed shape, so the reporter's 14
|
|
27321
|
+
// chained slots were 14 standing false positives — and because
|
|
27322
|
+
// `get_anti_pattern_violations_for` attributes by TYPE, they kept the
|
|
27323
|
+
// whole surface roster lit no matter how diligently the genuinely
|
|
27324
|
+
// contended surfaces were documented. The check could never be silenced
|
|
27325
|
+
// by correct modelling, which is the property a check has to have.
|
|
27326
|
+
// Exemption is DECLARE-TO-EARN: an unset `composition_mode` still fires,
|
|
27327
|
+
// so the default posture stays suspicious and silence must be claimed.
|
|
27328
|
+
// (b) `safe_by_coincidence` fires on its own, with no chained exemption. It
|
|
27329
|
+
// is the one state that a written `arbitration_rule` can mask: the field
|
|
27330
|
+
// audit found surfaces whose prose described disjoint enum values doing
|
|
27331
|
+
// the arbitrating with nothing guarding them. Branch (a) cannot see
|
|
27332
|
+
// those, because the rule text is present.
|
|
27333
|
+
// (c) `none` fires on its own too. A graph that says "nobody decided" while
|
|
27334
|
+
// carrying rule text is contradicting itself, and the admission is the
|
|
27335
|
+
// half to believe.
|
|
27336
|
+
//
|
|
27337
|
+
// `enforced_undocumented` deliberately gets NO branch and NO suppression. It
|
|
27338
|
+
// needs none: a surface enforcing an untranscribed rule has no
|
|
27339
|
+
// `arbitration_rule`, so branch (a) already fires. Nor does it earn a
|
|
27340
|
+
// downgrade — severity is per-anti-pattern, not per-violation, so there is
|
|
27341
|
+
// nowhere to put one; and the harm this pattern names is that the settlement
|
|
27342
|
+
// is unrecorded, which is exactly what `enforced_undocumented` admits. The
|
|
27343
|
+
// graph is the record; code is not. What the state buys is triage, so the
|
|
27344
|
+
// remediation text below names the two costs separately.
|
|
27345
|
+
//
|
|
27346
|
+
// No new anti-pattern was minted for `safe_by_coincidence`. It is a value of
|
|
27347
|
+
// the arbitration question, not a different question, and a second detector
|
|
27348
|
+
// would fire on the same graphs and double-report through type-keyed
|
|
27349
|
+
// attribution — making this family noisier on its first real deployment,
|
|
27350
|
+
// which is the exact failure (a) exists to fix. If field evidence later shows
|
|
27351
|
+
// it needs its own severity, mint it then.
|
|
27279
27352
|
structured_condition: {
|
|
27280
27353
|
operator: "and",
|
|
27281
27354
|
checks: [
|
|
@@ -27290,17 +27363,43 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27290
27363
|
}
|
|
27291
27364
|
},
|
|
27292
27365
|
{
|
|
27293
|
-
|
|
27294
|
-
|
|
27295
|
-
|
|
27296
|
-
|
|
27297
|
-
|
|
27298
|
-
|
|
27366
|
+
operator: "or",
|
|
27367
|
+
checks: [
|
|
27368
|
+
{
|
|
27369
|
+
check: {
|
|
27370
|
+
type: "entity_count",
|
|
27371
|
+
entity_type: "surface",
|
|
27372
|
+
filter: {
|
|
27373
|
+
property: "arbitration_rule",
|
|
27374
|
+
present: false,
|
|
27375
|
+
except_property: "composition_mode",
|
|
27376
|
+
except_value: "chained"
|
|
27377
|
+
},
|
|
27378
|
+
comparison: "nonzero"
|
|
27379
|
+
}
|
|
27380
|
+
},
|
|
27381
|
+
{
|
|
27382
|
+
check: {
|
|
27383
|
+
type: "entity_count",
|
|
27384
|
+
entity_type: "surface",
|
|
27385
|
+
filter: { property: "arbitration_state", value: "safe_by_coincidence" },
|
|
27386
|
+
comparison: "nonzero"
|
|
27387
|
+
}
|
|
27388
|
+
},
|
|
27389
|
+
{
|
|
27390
|
+
check: {
|
|
27391
|
+
type: "entity_count",
|
|
27392
|
+
entity_type: "surface",
|
|
27393
|
+
filter: { property: "arbitration_state", value: "none" },
|
|
27394
|
+
comparison: "nonzero"
|
|
27395
|
+
}
|
|
27396
|
+
}
|
|
27397
|
+
]
|
|
27299
27398
|
}
|
|
27300
27399
|
]
|
|
27301
27400
|
},
|
|
27302
|
-
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.",
|
|
27303
|
-
remediation: "
|
|
27401
|
+
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.",
|
|
27402
|
+
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. Record the constraint the rule works within (`capacity`, `dimensional_constraint`), and where a design system already answers it, 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.",
|
|
27304
27403
|
stages: ["build", "beta", "launch", "growth", "mature", "maintenance"],
|
|
27305
27404
|
severity: "medium",
|
|
27306
27405
|
source: { kind: "fundamental" }
|
|
@@ -27358,6 +27457,38 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27358
27457
|
source: { kind: "fundamental" }
|
|
27359
27458
|
}
|
|
27360
27459
|
];
|
|
27460
|
+
function walkForExceptSpecs(cond, out) {
|
|
27461
|
+
if ("operator" in cond) {
|
|
27462
|
+
for (const child of cond.checks) walkForExceptSpecs(child, out);
|
|
27463
|
+
return;
|
|
27464
|
+
}
|
|
27465
|
+
const check = cond.check;
|
|
27466
|
+
if (check.type !== "entity_count" || !check.filter) return;
|
|
27467
|
+
const f = check.filter;
|
|
27468
|
+
if (typeof f.property !== "string" || typeof f.except_property !== "string" || typeof f.except_value !== "string") {
|
|
27469
|
+
return;
|
|
27470
|
+
}
|
|
27471
|
+
const spec = {
|
|
27472
|
+
entity_type: check.entity_type,
|
|
27473
|
+
property: f.property,
|
|
27474
|
+
except_property: f.except_property,
|
|
27475
|
+
except_value: f.except_value
|
|
27476
|
+
};
|
|
27477
|
+
const seen = out.some(
|
|
27478
|
+
(s) => s.entity_type === spec.entity_type && s.property === spec.property && s.except_property === spec.except_property && s.except_value === spec.except_value
|
|
27479
|
+
);
|
|
27480
|
+
if (!seen) out.push(spec);
|
|
27481
|
+
}
|
|
27482
|
+
var UPG_PRESENCE_EXCEPT_SPECS = (() => {
|
|
27483
|
+
const out = [];
|
|
27484
|
+
for (const ap of UPG_ANTI_PATTERNS) {
|
|
27485
|
+
if (ap.structured_condition) walkForExceptSpecs(ap.structured_condition, out);
|
|
27486
|
+
}
|
|
27487
|
+
return out;
|
|
27488
|
+
})();
|
|
27489
|
+
function presenceExceptKey(property, exceptProperty, exceptValue) {
|
|
27490
|
+
return `${property}!${exceptProperty}=${exceptValue}`;
|
|
27491
|
+
}
|
|
27361
27492
|
function getAntiPatternById(id) {
|
|
27362
27493
|
return UPG_ANTI_PATTERNS.find((ap) => ap.id === id);
|
|
27363
27494
|
}
|
|
@@ -27505,7 +27636,13 @@ function compareNumber(value, comparison, threshold) {
|
|
|
27505
27636
|
function evaluateEntityCount(check, inputs) {
|
|
27506
27637
|
const filter = check.filter;
|
|
27507
27638
|
let count = 0;
|
|
27508
|
-
if (filter && typeof filter.property === "string" && typeof filter.present === "boolean") {
|
|
27639
|
+
if (filter && typeof filter.property === "string" && typeof filter.present === "boolean" && typeof filter.except_property === "string" && typeof filter.except_value === "string") {
|
|
27640
|
+
const key = presenceExceptKey(filter.property, filter.except_property, filter.except_value);
|
|
27641
|
+
const exempt = inputs.countsByTypeAndProperty?.[check.entity_type]?.[filter.except_property]?.[filter.except_value] ?? 0;
|
|
27642
|
+
const eligible = Math.max(0, (inputs.countsByType[check.entity_type] ?? 0) - exempt);
|
|
27643
|
+
const withValue = inputs.countsByTypeAndPropertyPresenceExcept?.[check.entity_type]?.[key] ?? 0;
|
|
27644
|
+
count = filter.present ? Math.min(withValue, eligible) : Math.max(0, eligible - withValue);
|
|
27645
|
+
} else if (filter && typeof filter.property === "string" && typeof filter.present === "boolean") {
|
|
27509
27646
|
const withValue = inputs.countsByTypeAndPropertyPresence?.[check.entity_type]?.[filter.property] ?? 0;
|
|
27510
27647
|
count = filter.present ? withValue : Math.max(0, (inputs.countsByType[check.entity_type] ?? 0) - withValue);
|
|
27511
27648
|
} else if (filter && typeof filter.property === "string" && typeof filter.value === "string") {
|
|
@@ -30626,7 +30763,7 @@ function checkHeaderSealText(text) {
|
|
|
30626
30763
|
}
|
|
30627
30764
|
|
|
30628
30765
|
// src/index.ts
|
|
30629
|
-
var UPG_VERSION = "0.
|
|
30766
|
+
var UPG_VERSION = "0.28.0";
|
|
30630
30767
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
30631
30768
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
30632
30769
|
var UPG_TYPES = getTypes();
|
|
@@ -30765,6 +30902,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
30765
30902
|
UPG_POLYMORPHIC_EDGE_KEYS,
|
|
30766
30903
|
UPG_PORTFOLIO_KINDS,
|
|
30767
30904
|
UPG_PORTFOLIO_SHARED_TYPES,
|
|
30905
|
+
UPG_PRESENCE_EXCEPT_SPECS,
|
|
30768
30906
|
UPG_PRODUCT_STAGES,
|
|
30769
30907
|
UPG_PRODUCT_STAGE_COERCION_MAP,
|
|
30770
30908
|
UPG_PROPERTY_MIGRATIONS,
|
|
@@ -30901,6 +31039,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
30901
31039
|
normalizeDocument,
|
|
30902
31040
|
parseUpg,
|
|
30903
31041
|
pickCanonicalEdge,
|
|
31042
|
+
presenceExceptKey,
|
|
30904
31043
|
resolveAllEdges,
|
|
30905
31044
|
resolveContainmentEdge,
|
|
30906
31045
|
resolveEntityType,
|