@unified-product-graph/core 0.28.0 → 0.30.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 +48 -0
- package/README.md +1 -1
- package/dist/index.cjs +654 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +742 -15
- package/dist/index.d.ts +742 -15
- package/dist/index.js +636 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,11 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ACTIVE_WHEN_PROPERTY: () => ACTIVE_WHEN_PROPERTY,
|
|
33
34
|
ANALYTICS_DATA_PLAYBOOK: () => ANALYTICS_DATA_PLAYBOOK,
|
|
34
35
|
BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK: () => BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
|
|
35
36
|
BUSINESS_GTM_GROWTH_PLAYBOOK: () => BUSINESS_GTM_GROWTH_PLAYBOOK,
|
|
36
37
|
BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK: () => BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
|
|
37
38
|
CLASSIFICATION_EDGE_PROPERTY_SCHEMA: () => CLASSIFICATION_EDGE_PROPERTY_SCHEMA,
|
|
39
|
+
CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA: () => CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA,
|
|
40
|
+
CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA: () => CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA,
|
|
38
41
|
CONTENT_DEPTH_WARNING_RULES: () => CONTENT_DEPTH_WARNING_RULES,
|
|
39
42
|
COVERAGE_ANTI_PATTERNS: () => COVERAGE_ANTI_PATTERNS,
|
|
40
43
|
DEFAULT_ENTITY_EMOJI: () => DEFAULT_ENTITY_EMOJI,
|
|
@@ -49,10 +52,12 @@ __export(index_exports, {
|
|
|
49
52
|
MARKDOWN_FORMAT_VERSION: () => MARKDOWN_FORMAT_VERSION,
|
|
50
53
|
MARKET_COMPETITIVE_PLAYBOOK: () => MARKET_COMPETITIVE_PLAYBOOK,
|
|
51
54
|
OPERATIONS_QUALITY_PLAYBOOK: () => OPERATIONS_QUALITY_PLAYBOOK,
|
|
55
|
+
PRESENT_UNDER_PROPERTY: () => PRESENT_UNDER_PROPERTY,
|
|
52
56
|
PRODUCT_DELIVERY_PLAYBOOK: () => PRODUCT_DELIVERY_PLAYBOOK,
|
|
53
57
|
PROPERTY_MODIFIERS: () => PROPERTY_MODIFIERS,
|
|
54
58
|
PROPERTY_MODIFIER_SEMANTICS: () => PROPERTY_MODIFIER_SEMANTICS,
|
|
55
59
|
PROPERTY_SCALE_MAP: () => PROPERTY_SCALE_MAP,
|
|
60
|
+
QUALIFIABLE_EDGE_TYPES: () => QUALIFIABLE_EDGE_TYPES,
|
|
56
61
|
RECORD_ENTITY_TYPES: () => RECORD_ENTITY_TYPES,
|
|
57
62
|
REFLECT_MODES: () => REFLECT_MODES,
|
|
58
63
|
REGISTRY_PRODUCT_ID: () => REGISTRY_PRODUCT_ID,
|
|
@@ -80,10 +85,12 @@ __export(index_exports, {
|
|
|
80
85
|
UPG_DOMAIN_RINGS: () => UPG_DOMAIN_RINGS,
|
|
81
86
|
UPG_EDGE_CATALOG: () => UPG_EDGE_CATALOG,
|
|
82
87
|
UPG_EDGE_COUNT: () => UPG_EDGE_COUNT,
|
|
88
|
+
UPG_EDGE_COUNT_SPECS: () => UPG_EDGE_COUNT_SPECS,
|
|
83
89
|
UPG_EDGE_MIGRATIONS: () => UPG_EDGE_MIGRATIONS,
|
|
84
90
|
UPG_EDGE_PAIR_MAP: () => UPG_EDGE_PAIR_MAP,
|
|
85
91
|
UPG_EDGE_TYPES: () => UPG_EDGE_TYPES,
|
|
86
92
|
UPG_ENTITY_COUNT: () => UPG_ENTITY_COUNT,
|
|
93
|
+
UPG_ENTITY_FILTER_SPECS: () => UPG_ENTITY_FILTER_SPECS,
|
|
87
94
|
UPG_ENTITY_META: () => UPG_ENTITY_META,
|
|
88
95
|
UPG_ENTITY_META_BY_ID: () => UPG_ENTITY_META_BY_ID,
|
|
89
96
|
UPG_ENTITY_META_BY_NAME: () => UPG_ENTITY_META_BY_NAME,
|
|
@@ -133,13 +140,18 @@ __export(index_exports, {
|
|
|
133
140
|
UPG_WILDCARD_ENDPOINT: () => UPG_WILDCARD_ENDPOINT,
|
|
134
141
|
USERS_NEEDS_PLAYBOOK: () => USERS_NEEDS_PLAYBOOK,
|
|
135
142
|
UnknownEntityTypeError: () => UnknownEntityTypeError,
|
|
143
|
+
VARIES_BY_EDGE: () => VARIES_BY_EDGE,
|
|
136
144
|
backfillSlug: () => backfillSlug,
|
|
137
145
|
buildTypeAliases: () => buildTypeAliases,
|
|
138
146
|
canBeChildOf: () => canBeChildOf,
|
|
147
|
+
checkConfigurationDrift: () => checkConfigurationDrift,
|
|
139
148
|
checkHeaderSeal: () => checkHeaderSeal,
|
|
140
149
|
checkHeaderSealText: () => checkHeaderSealText,
|
|
150
|
+
checkToEdgeCountSpec: () => checkToEdgeCountSpec,
|
|
151
|
+
classifyEntityFilter: () => classifyEntityFilter,
|
|
141
152
|
coerceProductStage: () => coerceProductStage,
|
|
142
153
|
collectSlugsForType: () => collectSlugsForType,
|
|
154
|
+
collectTargetEntities: () => collectTargetEntities,
|
|
143
155
|
compareVersions: () => compareVersions,
|
|
144
156
|
computeBodyChecksum: () => computeBodyChecksum,
|
|
145
157
|
concernEvaluatedFor: () => concernEvaluatedFor,
|
|
@@ -151,6 +163,9 @@ __export(index_exports, {
|
|
|
151
163
|
deriveSingleCounts: () => deriveSingleCounts,
|
|
152
164
|
describeTreePattern: () => describeTreePattern,
|
|
153
165
|
edgeCarriesProperties: () => edgeCarriesProperties,
|
|
166
|
+
edgeCountSpecKey: () => edgeCountSpecKey,
|
|
167
|
+
entityFilterKey: () => entityFilterKey,
|
|
168
|
+
enumerateProjections: () => enumerateProjections,
|
|
154
169
|
evaluateAntiPatterns: () => evaluateAntiPatterns,
|
|
155
170
|
findInvalidStatusMigrationTargets: () => findInvalidStatusMigrationTargets,
|
|
156
171
|
formatUpgText: () => formatUpgText,
|
|
@@ -242,6 +257,9 @@ __export(index_exports, {
|
|
|
242
257
|
parseUpg: () => parseUpg,
|
|
243
258
|
pickCanonicalEdge: () => pickCanonicalEdge,
|
|
244
259
|
presenceExceptKey: () => presenceExceptKey,
|
|
260
|
+
projectGraph: () => projectGraph,
|
|
261
|
+
readActiveWhen: () => readActiveWhen,
|
|
262
|
+
readStringArray: () => readStringArray,
|
|
245
263
|
resolveAllEdges: () => resolveAllEdges,
|
|
246
264
|
resolveContainmentEdge: () => resolveContainmentEdge,
|
|
247
265
|
resolveEntityType: () => resolveEntityType,
|
|
@@ -368,7 +386,8 @@ var UPG_DOMAINS = [
|
|
|
368
386
|
"roadmap_item",
|
|
369
387
|
"roadmap_theme",
|
|
370
388
|
"changelog",
|
|
371
|
-
"planning_cycle"
|
|
389
|
+
"planning_cycle",
|
|
390
|
+
"configuration_axis"
|
|
372
391
|
]
|
|
373
392
|
},
|
|
374
393
|
{
|
|
@@ -908,6 +927,7 @@ var UPG_ENTITY_META = [
|
|
|
908
927
|
// it, carrying its occupants and the rule that arbitrates between them.
|
|
909
928
|
// Enters `proposed` per the maturity rubric (precedent: planning_cycle 0.20.0).
|
|
910
929
|
{ name: "surface", type_id: "ent_359", maturity: "proposed", since: "0.27.0" },
|
|
930
|
+
{ name: "configuration_axis", type_id: "ent_360", maturity: "proposed", since: "0.30.0" },
|
|
911
931
|
{ name: "journey_phase", type_id: "ent_330", maturity: "proposed", since: "0.2.0" },
|
|
912
932
|
{ name: "journey_action", type_id: "ent_331", maturity: "proposed", since: "0.2.0" },
|
|
913
933
|
{ name: "design_decision", type_id: "ent_319", maturity: "deprecated", since: "0.2.0", deprecated_in: "0.2.0", replacement: "decision" },
|
|
@@ -1309,6 +1329,29 @@ var DEFER_EDGE_PROPERTY_SCHEMA = {
|
|
|
1309
1329
|
description: 'Target period the parked work is deferred to. Freeform, mirroring strategic_theme.time_horizon. @example "Q4 2026", "FY27", "next release".'
|
|
1310
1330
|
}
|
|
1311
1331
|
};
|
|
1332
|
+
var CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA = {
|
|
1333
|
+
present_under: {
|
|
1334
|
+
type: "string[]",
|
|
1335
|
+
description: 'Values of the axis under which this surface exists. Every entry must name a member of the axis\'s closed `values` list, and an empty list is a modelling error rather than a way to say "never" (a surface that exists under no configuration should be deleted, not declared). Omitting the edge entirely is how a surface says it is present in every configuration. @example ["legacy_nav"]'
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA = {
|
|
1339
|
+
active_when: {
|
|
1340
|
+
type: "object",
|
|
1341
|
+
description: 'Configuration condition under which this relationship holds. The edge is present in a projection that sets `axis` to one of `values`, and absent otherwise. Omit the key entirely for an invariant relationship: absence means "always", never "unknown".',
|
|
1342
|
+
properties: {
|
|
1343
|
+
axis: {
|
|
1344
|
+
type: "string",
|
|
1345
|
+
description: "Node id of the `configuration_axis` this condition reads."
|
|
1346
|
+
},
|
|
1347
|
+
values: {
|
|
1348
|
+
type: "string[]",
|
|
1349
|
+
description: "Values of that axis under which the relationship holds. Every entry must be a member of the axis's closed `values` list."
|
|
1350
|
+
}
|
|
1351
|
+
},
|
|
1352
|
+
required: ["axis", "values"]
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1312
1355
|
var UPG_EDGE_CATALOG = {
|
|
1313
1356
|
// ── Part 1: Core Narrative Spine (Ring 1) ──────────────────────────────────
|
|
1314
1357
|
// 1.1 User Domain
|
|
@@ -1835,7 +1878,7 @@ var UPG_EDGE_CATALOG = {
|
|
|
1835
1878
|
// portfolio_shared, so the reading it names commonly lives in the
|
|
1836
1879
|
// registry rather than in the product graph.
|
|
1837
1880
|
// supersedes → semantic (prompt_version_supersedes_prompt_version).
|
|
1838
|
-
surface_contains_surface: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "surface", target_type: "surface" },
|
|
1881
|
+
surface_contains_surface: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "surface", target_type: "surface", carries_properties: true, property_schema: CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA },
|
|
1839
1882
|
surface_serves_job: { forward_verb: "serves", reverse_verb: "served_by", classification: "cross-domain", source_type: "surface", target_type: "job" },
|
|
1840
1883
|
surface_governed_by_design_guideline: { forward_verb: "governed_by", reverse_verb: "governs", classification: "cross-domain", source_type: "surface", target_type: "design_guideline", cross_product_eligible: true },
|
|
1841
1884
|
surface_renders_design_component: { forward_verb: "renders", reverse_verb: "rendered_on", classification: "hierarchy", source_type: "surface", target_type: "design_component", cross_product_eligible: true },
|
|
@@ -1870,11 +1913,53 @@ var UPG_EDGE_CATALOG = {
|
|
|
1870
1913
|
// not cross_product_eligible → a deviation is a fact about one product's
|
|
1871
1914
|
// code, and `surface` is not portfolio_shared in the first place.
|
|
1872
1915
|
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" },
|
|
1916
|
+
// ── surface, 0.30.0: composition that varies by configuration ───────────────
|
|
1917
|
+
// A product's surface tree is not one tree; it is a family of trees selected
|
|
1918
|
+
// by a configuration lever. The stored graph is the UNION of that family and
|
|
1919
|
+
// a single configuration is a PROJECTION of it, so these two edges are how a
|
|
1920
|
+
// graph says which member a fact belongs to. Silence still means "every
|
|
1921
|
+
// member", which is why a graph written before 0.30.0 needs no migration.
|
|
1922
|
+
//
|
|
1923
|
+
// varies_by — CONDITIONAL EXISTENCE. The surface exists only under the
|
|
1924
|
+
// values in `present_under`. An edge qualifier cannot say
|
|
1925
|
+
// this: the reported case was a set of named chips REPLACED
|
|
1926
|
+
// by one generic badge, where which surface EXISTS changes,
|
|
1927
|
+
// and the two carry different capacities and occupants.
|
|
1928
|
+
// alternates — "one of these, depending" as distinct from "both,
|
|
1929
|
+
// together". Semantic, matching its sibling
|
|
1930
|
+
// `surface_supersedes_surface`: neither surface owns the
|
|
1931
|
+
// other, and a hierarchy classification would demand a
|
|
1932
|
+
// UPG_VALID_CHILDREN pair that would be false. Explicit and
|
|
1933
|
+
// validator-checked (same axis, disjoint present_under);
|
|
1934
|
+
// never auto-derived, because disjointness is necessary for
|
|
1935
|
+
// alternation and nowhere near sufficient.
|
|
1936
|
+
//
|
|
1937
|
+
// Direction on `alternates_with` is a CONVENTION, not an enforcement: declare
|
|
1938
|
+
// it once, sourced from the surface present under the axis's `default_value`.
|
|
1939
|
+
// An axis with no default is legal modelling, so a check keyed on the
|
|
1940
|
+
// convention would fire on a correct graph, which is the one thing a check
|
|
1941
|
+
// must never do.
|
|
1942
|
+
// ── configuration_axis, 0.30.0: the lever itself ───────────────────────────
|
|
1943
|
+
// Three layers, three owners, and keeping them apart is what makes the model
|
|
1944
|
+
// honest: `engineering` owns the MECHANISM (`feature_flag`, with its key and
|
|
1945
|
+
// rollout percentage), `product_spec` owns the LEVER (the named dimension
|
|
1946
|
+
// along which what-you-ship differs), `ux_design` owns the PLACE (`surface`).
|
|
1947
|
+
// Two code flags that move together are ONE lever with two values.
|
|
1948
|
+
//
|
|
1949
|
+
// `defines` is hierarchy so the axis has a home in `get_tree`. It earns the
|
|
1950
|
+
// UPG_VALID_CHILDREN.product pair (guardrail G2b) on an honest verb: an axis
|
|
1951
|
+
// exists only within the product whose composition it varies. The rejected
|
|
1952
|
+
// alternative mirrors `classification_axis_owned_by_product`, a cross-domain
|
|
1953
|
+
// ownership edge that leaves the node unparented and invisible in every tree.
|
|
1954
|
+
product_defines_configuration_axis: { forward_verb: "defines", reverse_verb: "defined_by", classification: "hierarchy", source_type: "product", target_type: "configuration_axis" },
|
|
1955
|
+
feature_flag_drives_configuration_axis: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "feature_flag", target_type: "configuration_axis" },
|
|
1956
|
+
surface_varies_by_configuration_axis: { forward_verb: "varies_by", reverse_verb: "varies", classification: "cross-domain", source_type: "surface", target_type: "configuration_axis", carries_properties: true, property_schema: CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA },
|
|
1957
|
+
surface_alternates_with_surface: { forward_verb: "alternates_with", reverse_verb: "alternates_with", classification: "semantic", source_type: "surface", target_type: "surface" },
|
|
1873
1958
|
// Inbound. `feature_occupies_surface` is the guest list: the edge that makes
|
|
1874
1959
|
// "what else lives here?" answerable, and the one the contention anti-pattern
|
|
1875
1960
|
// counts. cross-domain (product_spec → ux_design), like screen_surfaces_feature
|
|
1876
1961
|
// running the other way.
|
|
1877
|
-
feature_occupies_surface: { forward_verb: "occupies", reverse_verb: "occupied_by", classification: "cross-domain", source_type: "feature", target_type: "surface" },
|
|
1962
|
+
feature_occupies_surface: { forward_verb: "occupies", reverse_verb: "occupied_by", classification: "cross-domain", source_type: "feature", target_type: "surface", carries_properties: true, property_schema: CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA },
|
|
1878
1963
|
screen_renders_surface: { forward_verb: "renders", reverse_verb: "rendered_by", classification: "hierarchy", source_type: "screen", target_type: "surface" },
|
|
1879
1964
|
decision_affects_surface: { forward_verb: "affects", reverse_verb: "affected_by", classification: "cross-domain", source_type: "decision", target_type: "surface" },
|
|
1880
1965
|
// The finer-grained sibling of journey_step_shown_on_screen: a step happens on
|
|
@@ -3940,6 +4025,10 @@ var UPG_VALID_CHILDREN = {
|
|
|
3940
4025
|
"roadmap",
|
|
3941
4026
|
"roadmap_theme",
|
|
3942
4027
|
"planning_cycle",
|
|
4028
|
+
// Configuration (0.30.0): the levers whose values select which surface tree
|
|
4029
|
+
// the product renders. Few per product, and top-level because an axis is a
|
|
4030
|
+
// product-wide fact rather than a child of any one feature or surface.
|
|
4031
|
+
"configuration_axis",
|
|
3943
4032
|
// Engineering
|
|
3944
4033
|
"bounded_context",
|
|
3945
4034
|
"code_repository",
|
|
@@ -4563,6 +4652,289 @@ function isContainmentFreeType(entityType) {
|
|
|
4563
4652
|
return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
|
|
4564
4653
|
}
|
|
4565
4654
|
|
|
4655
|
+
// src/grammar/projection.ts
|
|
4656
|
+
var VARIES_BY_EDGE = "surface_varies_by_configuration_axis";
|
|
4657
|
+
var PRESENT_UNDER_PROPERTY = "present_under";
|
|
4658
|
+
var ACTIVE_WHEN_PROPERTY = "active_when";
|
|
4659
|
+
var QUALIFIABLE_EDGE_TYPES = [
|
|
4660
|
+
"surface_contains_surface",
|
|
4661
|
+
"feature_occupies_surface"
|
|
4662
|
+
];
|
|
4663
|
+
function readStringArray(properties, key) {
|
|
4664
|
+
const raw = properties?.[key];
|
|
4665
|
+
if (!Array.isArray(raw)) return void 0;
|
|
4666
|
+
return raw.filter((v) => typeof v === "string");
|
|
4667
|
+
}
|
|
4668
|
+
function readActiveWhen(edge) {
|
|
4669
|
+
const raw = edge.properties?.[ACTIVE_WHEN_PROPERTY];
|
|
4670
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
4671
|
+
const axis = raw.axis;
|
|
4672
|
+
const values = readStringArray(raw, "values");
|
|
4673
|
+
if (typeof axis !== "string" || axis.length === 0 || !values || values.length === 0) {
|
|
4674
|
+
return void 0;
|
|
4675
|
+
}
|
|
4676
|
+
return { axis, values };
|
|
4677
|
+
}
|
|
4678
|
+
function projectGraph(nodes, edges, configuration) {
|
|
4679
|
+
const axes = Object.keys(configuration);
|
|
4680
|
+
if (axes.length === 0) {
|
|
4681
|
+
return {
|
|
4682
|
+
nodes: [...nodes],
|
|
4683
|
+
edges: [...edges],
|
|
4684
|
+
excluded_node_ids: [],
|
|
4685
|
+
deactivated_edge_count: 0,
|
|
4686
|
+
dangling_edge_count: 0
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
4690
|
+
for (const edge of edges) {
|
|
4691
|
+
if (edge.type !== VARIES_BY_EDGE) continue;
|
|
4692
|
+
const chosen = configuration[edge.target];
|
|
4693
|
+
if (chosen === void 0) continue;
|
|
4694
|
+
const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
|
|
4695
|
+
if (!presentUnder) continue;
|
|
4696
|
+
if (!presentUnder.includes(chosen)) excluded.add(edge.source);
|
|
4697
|
+
}
|
|
4698
|
+
const survivingNodes = excluded.size === 0 ? [...nodes] : nodes.filter((n) => !excluded.has(n.id));
|
|
4699
|
+
let deactivated = 0;
|
|
4700
|
+
let dangling = 0;
|
|
4701
|
+
const survivingEdges = [];
|
|
4702
|
+
for (const edge of edges) {
|
|
4703
|
+
const qualifier = readActiveWhen(edge);
|
|
4704
|
+
if (qualifier) {
|
|
4705
|
+
const chosen = configuration[qualifier.axis];
|
|
4706
|
+
if (chosen !== void 0 && !qualifier.values.includes(chosen)) {
|
|
4707
|
+
deactivated++;
|
|
4708
|
+
continue;
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
if (excluded.has(edge.source) || excluded.has(edge.target)) {
|
|
4712
|
+
dangling++;
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
survivingEdges.push(edge);
|
|
4716
|
+
}
|
|
4717
|
+
return {
|
|
4718
|
+
nodes: survivingNodes,
|
|
4719
|
+
edges: survivingEdges,
|
|
4720
|
+
excluded_node_ids: [...excluded].sort(),
|
|
4721
|
+
deactivated_edge_count: deactivated,
|
|
4722
|
+
dangling_edge_count: dangling
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
function enumerateProjections(nodes) {
|
|
4726
|
+
const out = [
|
|
4727
|
+
{ configuration: {} }
|
|
4728
|
+
];
|
|
4729
|
+
for (const node of nodes) {
|
|
4730
|
+
if (node.type !== "configuration_axis") continue;
|
|
4731
|
+
const values = readStringArray(node.properties, "values");
|
|
4732
|
+
if (!values) continue;
|
|
4733
|
+
for (const value of values) {
|
|
4734
|
+
out.push({ axis: node.id, value, configuration: { [node.id]: value } });
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
return out;
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
// src/grammar/configuration-drift.ts
|
|
4741
|
+
var ALTERNATES_EDGE = "surface_alternates_with_surface";
|
|
4742
|
+
var PARENT_EDGE_TYPES = /* @__PURE__ */ new Set(["surface_contains_surface", "screen_renders_surface"]);
|
|
4743
|
+
function edgeLabel(edge) {
|
|
4744
|
+
return edge.id ?? `${edge.source} -> ${edge.target}`;
|
|
4745
|
+
}
|
|
4746
|
+
function checkConfigurationDrift(nodes, edges) {
|
|
4747
|
+
const findings = [];
|
|
4748
|
+
const axisValues = /* @__PURE__ */ new Map();
|
|
4749
|
+
for (const node of nodes) {
|
|
4750
|
+
if (node.type !== "configuration_axis") continue;
|
|
4751
|
+
const values = readStringArray(node.properties, "values");
|
|
4752
|
+
if (!values || values.length === 0) {
|
|
4753
|
+
findings.push({
|
|
4754
|
+
kind: "axis_values_empty",
|
|
4755
|
+
severity: "error",
|
|
4756
|
+
node_id: node.id,
|
|
4757
|
+
axis_id: node.id,
|
|
4758
|
+
message: "Configuration axis declares no values. An axis with no values selects nothing, so no projection can be taken along it."
|
|
4759
|
+
});
|
|
4760
|
+
axisValues.set(node.id, []);
|
|
4761
|
+
continue;
|
|
4762
|
+
}
|
|
4763
|
+
axisValues.set(node.id, values);
|
|
4764
|
+
const rawDefault = node.properties?.default_value;
|
|
4765
|
+
if (typeof rawDefault === "string" && rawDefault.length > 0 && !values.includes(rawDefault)) {
|
|
4766
|
+
findings.push({
|
|
4767
|
+
kind: "axis_default_not_a_value",
|
|
4768
|
+
severity: "error",
|
|
4769
|
+
node_id: node.id,
|
|
4770
|
+
axis_id: node.id,
|
|
4771
|
+
message: `default_value "${rawDefault}" is not one of the axis values [${values.join(", ")}]. It names a value of this axis, so it has to be one the axis declares.`
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
for (const edge of edges) {
|
|
4776
|
+
if (edge.type !== VARIES_BY_EDGE) continue;
|
|
4777
|
+
const values = axisValues.get(edge.target);
|
|
4778
|
+
const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
|
|
4779
|
+
if (!presentUnder || presentUnder.length === 0) {
|
|
4780
|
+
findings.push({
|
|
4781
|
+
kind: "present_under_empty",
|
|
4782
|
+
severity: "error",
|
|
4783
|
+
edge_id: edge.id,
|
|
4784
|
+
node_id: edge.source,
|
|
4785
|
+
axis_id: edge.target,
|
|
4786
|
+
message: `${edgeLabel(edge)} declares variance without a non-empty present_under. A surface that exists under no configuration should be deleted rather than declared; a surface that exists under all of them should carry no varies_by edge at all.`
|
|
4787
|
+
});
|
|
4788
|
+
continue;
|
|
4789
|
+
}
|
|
4790
|
+
if (!values) continue;
|
|
4791
|
+
for (const value of presentUnder) {
|
|
4792
|
+
if (!values.includes(value)) {
|
|
4793
|
+
findings.push({
|
|
4794
|
+
kind: "present_under_unknown_value",
|
|
4795
|
+
severity: "error",
|
|
4796
|
+
edge_id: edge.id,
|
|
4797
|
+
node_id: edge.source,
|
|
4798
|
+
axis_id: edge.target,
|
|
4799
|
+
message: `present_under names "${value}", which the axis does not declare. Its values are [${values.join(", ")}].`
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
for (const edge of edges) {
|
|
4805
|
+
const hasQualifierKey = edge.properties !== void 0 && Object.prototype.hasOwnProperty.call(edge.properties, ACTIVE_WHEN_PROPERTY);
|
|
4806
|
+
if (!hasQualifierKey) continue;
|
|
4807
|
+
if (edge.type !== void 0 && !QUALIFIABLE_EDGE_TYPES.includes(edge.type)) {
|
|
4808
|
+
findings.push({
|
|
4809
|
+
kind: "qualifier_on_illegal_edge",
|
|
4810
|
+
severity: "error",
|
|
4811
|
+
edge_id: edge.id,
|
|
4812
|
+
message: `active_when is not legal on ${edge.type}. The qualifier is scoped to [${QUALIFIABLE_EDGE_TYPES.join(", ")}] deliberately: conditional composition is the question this release answers, and a general modality system is not.`
|
|
4813
|
+
});
|
|
4814
|
+
continue;
|
|
4815
|
+
}
|
|
4816
|
+
const qualifier = readActiveWhen(edge);
|
|
4817
|
+
if (!qualifier) {
|
|
4818
|
+
const rawQualifier = edge.properties?.[ACTIVE_WHEN_PROPERTY];
|
|
4819
|
+
const rawValues = rawQualifier && typeof rawQualifier === "object" && !Array.isArray(rawQualifier) ? rawQualifier.values : void 0;
|
|
4820
|
+
if (Array.isArray(rawValues) && rawValues.length === 0) {
|
|
4821
|
+
findings.push({
|
|
4822
|
+
kind: "qualifier_values_empty",
|
|
4823
|
+
severity: "error",
|
|
4824
|
+
edge_id: edge.id,
|
|
4825
|
+
message: `${edgeLabel(edge)} carries an active_when with an empty values list. Read literally that would remove the relationship from every configuration, which is a way to delete an edge by writing a property; it is treated as absent instead. To say a relationship never holds, delete the edge.`
|
|
4826
|
+
});
|
|
4827
|
+
continue;
|
|
4828
|
+
}
|
|
4829
|
+
findings.push({
|
|
4830
|
+
kind: "qualifier_axis_unresolved",
|
|
4831
|
+
severity: "error",
|
|
4832
|
+
edge_id: edge.id,
|
|
4833
|
+
message: `${edgeLabel(edge)} carries a malformed active_when. It must be an object with an axis id and a non-empty values array; anything else reads as absent, which silently makes the relationship invariant.`
|
|
4834
|
+
});
|
|
4835
|
+
continue;
|
|
4836
|
+
}
|
|
4837
|
+
const values = axisValues.get(qualifier.axis);
|
|
4838
|
+
if (!values) {
|
|
4839
|
+
findings.push({
|
|
4840
|
+
kind: "qualifier_axis_unresolved",
|
|
4841
|
+
severity: "error",
|
|
4842
|
+
edge_id: edge.id,
|
|
4843
|
+
axis_id: qualifier.axis,
|
|
4844
|
+
message: `active_when names axis "${qualifier.axis}", which is not a configuration_axis node in this graph.`
|
|
4845
|
+
});
|
|
4846
|
+
continue;
|
|
4847
|
+
}
|
|
4848
|
+
for (const value of qualifier.values) {
|
|
4849
|
+
if (!values.includes(value)) {
|
|
4850
|
+
findings.push({
|
|
4851
|
+
kind: "qualifier_unknown_value",
|
|
4852
|
+
severity: "error",
|
|
4853
|
+
edge_id: edge.id,
|
|
4854
|
+
axis_id: qualifier.axis,
|
|
4855
|
+
message: `active_when names value "${value}", which axis "${qualifier.axis}" does not declare. Its values are [${values.join(", ")}].`
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
const varianceBySource = /* @__PURE__ */ new Map();
|
|
4861
|
+
for (const edge of edges) {
|
|
4862
|
+
if (edge.type !== VARIES_BY_EDGE) continue;
|
|
4863
|
+
const values = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY) ?? [];
|
|
4864
|
+
const list = varianceBySource.get(edge.source) ?? [];
|
|
4865
|
+
list.push({ axis: edge.target, values });
|
|
4866
|
+
varianceBySource.set(edge.source, list);
|
|
4867
|
+
}
|
|
4868
|
+
for (const edge of edges) {
|
|
4869
|
+
if (edge.type !== ALTERNATES_EDGE) continue;
|
|
4870
|
+
const left = varianceBySource.get(edge.source) ?? [];
|
|
4871
|
+
const right = varianceBySource.get(edge.target) ?? [];
|
|
4872
|
+
if (left.length === 0 || right.length === 0) {
|
|
4873
|
+
findings.push({
|
|
4874
|
+
kind: "alternation_axis_mismatch",
|
|
4875
|
+
severity: "error",
|
|
4876
|
+
edge_id: edge.id,
|
|
4877
|
+
message: `${edgeLabel(edge)} declares an alternation, but at least one endpoint declares no variance at all. Two surfaces that are both always present are not alternatives; they co-exist.`
|
|
4878
|
+
});
|
|
4879
|
+
continue;
|
|
4880
|
+
}
|
|
4881
|
+
const sharedAxes = left.filter((l) => right.some((r) => r.axis === l.axis)).map((l) => l.axis);
|
|
4882
|
+
if (sharedAxes.length === 0) {
|
|
4883
|
+
findings.push({
|
|
4884
|
+
kind: "alternation_axis_mismatch",
|
|
4885
|
+
severity: "error",
|
|
4886
|
+
edge_id: edge.id,
|
|
4887
|
+
message: `${edgeLabel(edge)} declares an alternation between surfaces that vary on different axes. Alternatives are selected by ONE lever; surfaces on different levers can both be present at once.`
|
|
4888
|
+
});
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4891
|
+
for (const axis of sharedAxes) {
|
|
4892
|
+
const leftValues = left.find((l) => l.axis === axis)?.values ?? [];
|
|
4893
|
+
const rightValues = right.find((r) => r.axis === axis)?.values ?? [];
|
|
4894
|
+
const overlap = leftValues.filter((v) => rightValues.includes(v));
|
|
4895
|
+
if (overlap.length > 0) {
|
|
4896
|
+
findings.push({
|
|
4897
|
+
kind: "alternation_overlap",
|
|
4898
|
+
severity: "error",
|
|
4899
|
+
edge_id: edge.id,
|
|
4900
|
+
axis_id: axis,
|
|
4901
|
+
message: `${edgeLabel(edge)} declares an alternation, but both surfaces are present under [${overlap.join(", ")}] on axis "${axis}". They co-exist there, so the graph is asserting "one of these" and "both, together" about the same pair.`
|
|
4902
|
+
});
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
const parentsByChild = /* @__PURE__ */ new Map();
|
|
4907
|
+
for (const edge of edges) {
|
|
4908
|
+
if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
|
|
4909
|
+
parentsByChild.set(edge.target, (parentsByChild.get(edge.target) ?? 0) + 1);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
for (const [axisId, values] of axisValues) {
|
|
4913
|
+
for (const value of values) {
|
|
4914
|
+
const projected = projectGraph(nodes, edges, { [axisId]: value });
|
|
4915
|
+
const survivingParents = /* @__PURE__ */ new Map();
|
|
4916
|
+
for (const edge of projected.edges) {
|
|
4917
|
+
if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
|
|
4918
|
+
survivingParents.set(edge.target, (survivingParents.get(edge.target) ?? 0) + 1);
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
for (const node of projected.nodes) {
|
|
4922
|
+
if (node.type !== "surface") continue;
|
|
4923
|
+
if ((parentsByChild.get(node.id) ?? 0) === 0) continue;
|
|
4924
|
+
if ((survivingParents.get(node.id) ?? 0) > 0) continue;
|
|
4925
|
+
findings.push({
|
|
4926
|
+
kind: "orphaned_under_projection",
|
|
4927
|
+
severity: "warning",
|
|
4928
|
+
node_id: node.id,
|
|
4929
|
+
axis_id: axisId,
|
|
4930
|
+
message: `Surface is present under ${axisId} = "${value}" but every containment parent it has is absent there. Either it needs a parent in that configuration, or it should declare that it is absent from it too.`
|
|
4931
|
+
});
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
return findings;
|
|
4936
|
+
}
|
|
4937
|
+
|
|
4566
4938
|
// src/grammar/lifecycles.ts
|
|
4567
4939
|
var PRODUCT_LIFECYCLE = {
|
|
4568
4940
|
entity_type: "product",
|
|
@@ -6813,6 +7185,10 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
|
|
|
6813
7185
|
"market_segment",
|
|
6814
7186
|
"classification_axis",
|
|
6815
7187
|
"classification_value",
|
|
7188
|
+
// ── Product Specification (1): a configuration_axis is a structural
|
|
7189
|
+
// DECLARATION (a lever and its closed value set), not a thing that
|
|
7190
|
+
// progresses through states. Same reading as classification_axis above.
|
|
7191
|
+
"configuration_axis",
|
|
6816
7192
|
// ── User Research (5 of 8): participants, quotes, observations,
|
|
6817
7193
|
// clusters, survey responses are immutable data points ────────────────
|
|
6818
7194
|
"participant",
|
|
@@ -11322,6 +11698,12 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11322
11698
|
regulation: { type: "string", enum: ["gdpr", "ccpa", "hipaa", "soc2", "iso27001", "pci_dss", "other"], description: "Regulation or standard this requirement derives from" },
|
|
11323
11699
|
compliance_status: { type: "string", enum: ["compliant", "non_compliant", "in_progress", "not_applicable"], description: "Current compliance posture" }
|
|
11324
11700
|
},
|
|
11701
|
+
// ConfigurationAxisProperties: A named dimension along which the product's composition differs.
|
|
11702
|
+
configuration_axis: {
|
|
11703
|
+
values: { type: "string[]", description: "The closed set of values this axis can take. Required: an axis with no values selects nothing and cannot be projected along. Two values is the common case and three is not unusual (a plan ladder). Order is not significant; the axis is categorical, not ordinal. Where the ordering does matter (an entitlement ladder in which each tier includes the one below), that is a classification question, and `classification_axis` with `axis_kind: 'ordinal'` is the instrument for it." },
|
|
11704
|
+
default_value: { type: "string", description: "The value this axis is understood to sit at when nobody says otherwise. Must be a member of `values`. NOTHING APPLIES IT AUTOMATICALLY. Omitting `configuration` on a read returns the UNION, not this projection, because the union is the honest answer to an unqualified question: it is every configuration at once, and silently substituting one of them would hide the others from a reader who did not know to ask. What the field does carry is the declaration convention for `surface_alternates_with_surface` (declare the edge from the surface present under the default) and a documented anchor for tools that later want to offer a starting configuration. It is a claim about the model, not about deployment: it says which value most of the graph was written against, not which configuration most customers are on." },
|
|
11705
|
+
kind: { type: "string", enum: ["feature_flag", "plan_tier", "permission_level", "beta_program", "other"], description: "What kind of lever this is. Names the mechanism family." }
|
|
11706
|
+
},
|
|
11325
11707
|
// ConstraintProperties: Constraint: a named limitation or boundary on product creation.
|
|
11326
11708
|
constraint: {
|
|
11327
11709
|
constraint_kind: { type: "string", enum: ["resource", "technical", "regulatory", "temporal", "compliance", "other"], description: "Limitation category" },
|
|
@@ -12941,7 +13323,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12941
13323
|
health_status: { type: "string", enum: ["on_track", "at_risk", "off_track"], description: "Overall health" },
|
|
12942
13324
|
url: { type: "string", description: "Where the product lives. Marketing site, app store URL, etc.", modifier: "volatile" },
|
|
12943
13325
|
logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
|
|
12944
|
-
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" }
|
|
13326
|
+
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
|
|
13327
|
+
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"` }
|
|
12945
13328
|
},
|
|
12946
13329
|
// ProductAreaProperties: ProductArea entity.
|
|
12947
13330
|
product_area: {
|
|
@@ -13469,7 +13852,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13469
13852
|
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." },
|
|
13470
13853
|
persistence: { type: "string", enum: ["always", "conditional", "on_demand", "transient"], description: "How reliably the surface is present." },
|
|
13471
13854
|
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"' },
|
|
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".' },
|
|
13855
|
+
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.' },
|
|
13473
13856
|
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
13857
|
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
13858
|
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." },
|
|
@@ -20189,6 +20572,44 @@ function validateValue(key, def, val) {
|
|
|
20189
20572
|
}
|
|
20190
20573
|
break;
|
|
20191
20574
|
}
|
|
20575
|
+
case "string[]": {
|
|
20576
|
+
if (!Array.isArray(val)) {
|
|
20577
|
+
errors.push(`property "${key}" must be an array of strings`);
|
|
20578
|
+
break;
|
|
20579
|
+
}
|
|
20580
|
+
if (val.some((v) => typeof v !== "string")) {
|
|
20581
|
+
errors.push(`property "${key}" must contain only strings`);
|
|
20582
|
+
}
|
|
20583
|
+
if (val.length === 0) {
|
|
20584
|
+
errors.push(`property "${key}" must not be empty`);
|
|
20585
|
+
}
|
|
20586
|
+
break;
|
|
20587
|
+
}
|
|
20588
|
+
case "object": {
|
|
20589
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) {
|
|
20590
|
+
errors.push(`property "${key}" must be an object`);
|
|
20591
|
+
break;
|
|
20592
|
+
}
|
|
20593
|
+
const obj = val;
|
|
20594
|
+
for (const req2 of def.required ?? []) {
|
|
20595
|
+
if (obj[req2] === void 0 || obj[req2] === null) {
|
|
20596
|
+
errors.push(`property "${key}" is missing required key "${req2}"`);
|
|
20597
|
+
}
|
|
20598
|
+
}
|
|
20599
|
+
for (const [subKey, subDef] of Object.entries(def.properties ?? {})) {
|
|
20600
|
+
const subVal = obj[subKey];
|
|
20601
|
+
if (subVal === void 0 || subVal === null) continue;
|
|
20602
|
+
errors.push(...validateValue(`${key}.${subKey}`, subDef, subVal));
|
|
20603
|
+
}
|
|
20604
|
+
for (const presentKey of Object.keys(obj)) {
|
|
20605
|
+
if (def.properties && !(presentKey in def.properties)) {
|
|
20606
|
+
errors.push(
|
|
20607
|
+
`property "${key}" has unknown key "${presentKey}" (allowed: ${Object.keys(def.properties).join(", ")})`
|
|
20608
|
+
);
|
|
20609
|
+
}
|
|
20610
|
+
}
|
|
20611
|
+
break;
|
|
20612
|
+
}
|
|
20192
20613
|
}
|
|
20193
20614
|
return errors;
|
|
20194
20615
|
}
|
|
@@ -20407,6 +20828,7 @@ var ENTITY_EMOJI = {
|
|
|
20407
20828
|
pipeline_sales: "\u{1FA88}",
|
|
20408
20829
|
pipeline_stage: "\u{1F9FD}",
|
|
20409
20830
|
planning_cycle: "\u{1F5D3}\uFE0F",
|
|
20831
|
+
configuration_axis: "\u{1F39A}\uFE0F",
|
|
20410
20832
|
playbook: "\u{1F4D2}",
|
|
20411
20833
|
portfolio: "\u{1F9F8}",
|
|
20412
20834
|
positioning: "\u{1F3AA}",
|
|
@@ -21101,6 +21523,7 @@ var STANDARD_LABELS = {
|
|
|
21101
21523
|
roadmap_theme: { alt_labels: ["product theme", "roadmap theme"] },
|
|
21102
21524
|
// UPG-660: renamed from bare 'theme' (N6 lineage)
|
|
21103
21525
|
planning_cycle: { alt_labels: ["sprint", "iteration", "cycle", "program increment", "pi", "cadence", "time-box", "cooldown", "quarter"] },
|
|
21526
|
+
configuration_axis: { alt_labels: ["configuration", "config axis", "lever", "variant axis", "plan axis", "flag axis", "entitlement axis"] },
|
|
21104
21527
|
// Engineering layer
|
|
21105
21528
|
bounded_context: { alt_labels: ["context", "domain boundary", "module boundary"] },
|
|
21106
21529
|
service: { alt_labels: ["microservice", "backend service", "api service"] },
|
|
@@ -23622,7 +24045,7 @@ var PRODUCT_SPEC_GUIDE = {
|
|
|
23622
24045
|
// registered to product_spec but missing from the navigation order).
|
|
23623
24046
|
// `changelog` lives here because it is a structural product-shipping
|
|
23624
24047
|
// artefact; content domain references it only via cross-domain bridges.
|
|
23625
|
-
creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "roadmap_theme", "changelog", "planning_cycle"],
|
|
24048
|
+
creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "roadmap_theme", "changelog", "planning_cycle", "configuration_axis"],
|
|
23626
24049
|
patterns: [
|
|
23627
24050
|
{
|
|
23628
24051
|
name: "Feature Decomposition",
|
|
@@ -27311,7 +27734,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27311
27734
|
id: "contended-surface-without-arbitration",
|
|
27312
27735
|
since: "0.27.0",
|
|
27313
27736
|
name: "Contended surface without arbitration",
|
|
27314
|
-
description: '
|
|
27737
|
+
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.',
|
|
27315
27738
|
// 0.28.0 (feedback 852a9721, from a 30-surface field audit). Three changes,
|
|
27316
27739
|
// each earned by a reported false positive or a reported blind spot:
|
|
27317
27740
|
//
|
|
@@ -27349,6 +27772,68 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27349
27772
|
// attribution — making this family noisier on its first real deployment,
|
|
27350
27773
|
// which is the exact failure (a) exists to fix. If field evidence later shows
|
|
27351
27774
|
// it needs its own severity, mint it then.
|
|
27775
|
+
//
|
|
27776
|
+
// 0.29.0 (feedback af9ae4c2, the same reporter's measured follow-up: 43
|
|
27777
|
+
// surfaces, 10 flagged, 7 rightly and 3 wrongly). Branch (a) moved from an
|
|
27778
|
+
// aggregate presence count to the per-node `edge_count_vs_property` form.
|
|
27779
|
+
//
|
|
27780
|
+
// (d) CAPACITY IS NOW READ. The old branch was a pure edge count: any
|
|
27781
|
+
// surface with more than one occupant and no rule. That flags a header
|
|
27782
|
+
// row declaring room for four and holding exactly four, which is not
|
|
27783
|
+
// contended but PARTITIONED — everyone fits, by design, so nothing is
|
|
27784
|
+
// displaced and there is nothing to decide. All three of the reporter's
|
|
27785
|
+
// false positives were this, and all three had occupancy at or below a
|
|
27786
|
+
// stated capacity. The rule is now `occupancy > (capacity ?? 1)`.
|
|
27787
|
+
//
|
|
27788
|
+
// ABSENT CAPACITY BEHAVES AS 1, NOT AS INFINITY. Absence means
|
|
27789
|
+
// unbounded, and it is tempting to read unbounded as "never flag". The
|
|
27790
|
+
// opposite is right: a surface that states no limit has stated no
|
|
27791
|
+
// answer, so two or more occupants is precisely the unrecorded decision
|
|
27792
|
+
// this pattern names. That keeps all four of the reporter's unbounded
|
|
27793
|
+
// true positives (7, 7, 10 and 3 occupants) firing.
|
|
27794
|
+
//
|
|
27795
|
+
// The check had to become per-node rather than two ANDed aggregates:
|
|
27796
|
+
// "some surface is over capacity" and "some surface has no rule" are
|
|
27797
|
+
// both true when they are DIFFERENT surfaces, which would have kept
|
|
27798
|
+
// every false positive alive. See `EdgeCountVsPropertyCheck.node_filter`.
|
|
27799
|
+
//
|
|
27800
|
+
// ADJUDICATED: DOES A CAPACITY-SATISFIED `additive` SURFACE STILL NEED AN
|
|
27801
|
+
// ORDERING RULE? The reporter's capacity rule and their own earlier report
|
|
27802
|
+
// pull in opposite directions here: 0.28.0 documented that `additive`
|
|
27803
|
+
// occupants raise a question of ORDER rather than victory and that the order
|
|
27804
|
+
// belongs in `arbitration_rule`, while the capacity rule says a surface
|
|
27805
|
+
// where everyone fits has nothing to settle. Ruling: BOTH ARE RIGHT, because
|
|
27806
|
+
// they are about different failures, and this detector owns only one of them.
|
|
27807
|
+
//
|
|
27808
|
+
// - Contention is about DISPLACEMENT: who is not rendered. Capacity settles
|
|
27809
|
+
// it. If everyone fits, nobody is displaced, and "who wins" is genuinely
|
|
27810
|
+
// moot. That is why reading capacity removes real false positives rather
|
|
27811
|
+
// than merely quieting the check.
|
|
27812
|
+
// - Ordering is about ARRANGEMENT: in what sequence coexisting occupants
|
|
27813
|
+
// appear. Capacity says nothing about it. The reporter's own four-occupant
|
|
27814
|
+
// header row, positioned by runtime width measurement, has a live ordering
|
|
27815
|
+
// question and no displacement question at all.
|
|
27816
|
+
//
|
|
27817
|
+
// So this check reads capacity and stops at displacement. The ordering concern
|
|
27818
|
+
// stays documented on `composition_mode: 'additive'` as guidance, NOT as a
|
|
27819
|
+
// detector: minting `additive-surface-without-ordering` today would repeat the
|
|
27820
|
+
// 0.28.0 mistake of shipping a second detector on no field evidence, and it
|
|
27821
|
+
// would double-report against this one. Mint it when a graph shows unrecorded
|
|
27822
|
+
// ordering causing a real defect, with its own severity and remediation.
|
|
27823
|
+
//
|
|
27824
|
+
// Known consequence, recorded rather than papered over: `arbitration_rule` is
|
|
27825
|
+
// now visibly OVERLOADED. It holds a displacement rule for `exclusive`
|
|
27826
|
+
// surfaces and an ordering rule for `additive` ones, and this check reads it
|
|
27827
|
+
// for only the first. Splitting it (`arbitration_rule` vs `ordering_rule`) is
|
|
27828
|
+
// the natural move if the ordering detector is ever minted; it is not worth a
|
|
27829
|
+
// breaking property change before then.
|
|
27830
|
+
//
|
|
27831
|
+
// STILL OPEN (banked at 0.28.0, deliberately NOT changed here): a `chained`
|
|
27832
|
+
// slot that honestly declares `arbitration_state: 'none'` re-fires through
|
|
27833
|
+
// branch (c), which carries no chained exemption. Current guidance stands:
|
|
27834
|
+
// leave `arbitration_state` unset on chained surfaces. Changing (b)/(c) was
|
|
27835
|
+
// out of scope for this release and still awaits the field confirmation the
|
|
27836
|
+
// reporter will produce populating their 14 chained slots.
|
|
27352
27837
|
structured_condition: {
|
|
27353
27838
|
operator: "and",
|
|
27354
27839
|
checks: [
|
|
@@ -27367,14 +27852,16 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27367
27852
|
checks: [
|
|
27368
27853
|
{
|
|
27369
27854
|
check: {
|
|
27370
|
-
type: "
|
|
27855
|
+
type: "edge_count_vs_property",
|
|
27371
27856
|
entity_type: "surface",
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
},
|
|
27857
|
+
edge_type: "feature_occupies_surface",
|
|
27858
|
+
direction: "inbound",
|
|
27859
|
+
property: "capacity",
|
|
27860
|
+
property_absent_default: 1,
|
|
27861
|
+
node_comparison: "gt",
|
|
27862
|
+
node_filter: { property: "arbitration_rule", present: false },
|
|
27863
|
+
except_property: "composition_mode",
|
|
27864
|
+
except_value: "chained",
|
|
27378
27865
|
comparison: "nonzero"
|
|
27379
27866
|
}
|
|
27380
27867
|
},
|
|
@@ -27399,7 +27886,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27399
27886
|
]
|
|
27400
27887
|
},
|
|
27401
27888
|
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.
|
|
27889
|
+
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.",
|
|
27403
27890
|
stages: ["build", "beta", "launch", "growth", "mature", "maintenance"],
|
|
27404
27891
|
severity: "medium",
|
|
27405
27892
|
source: { kind: "fundamental" }
|
|
@@ -27489,6 +27976,95 @@ var UPG_PRESENCE_EXCEPT_SPECS = (() => {
|
|
|
27489
27976
|
function presenceExceptKey(property, exceptProperty, exceptValue) {
|
|
27490
27977
|
return `${property}!${exceptProperty}=${exceptValue}`;
|
|
27491
27978
|
}
|
|
27979
|
+
function edgeCountSpecKey(spec) {
|
|
27980
|
+
const filter = spec.node_filter ? `${spec.node_filter.property}=${spec.node_filter.present ? "present" : "absent"}` : "*";
|
|
27981
|
+
const except = spec.except_property !== void 0 && spec.except_value !== void 0 ? `${spec.except_property}=${spec.except_value}` : "*";
|
|
27982
|
+
return [
|
|
27983
|
+
spec.entity_type,
|
|
27984
|
+
spec.edge_type,
|
|
27985
|
+
spec.direction,
|
|
27986
|
+
spec.property,
|
|
27987
|
+
String(spec.property_absent_default),
|
|
27988
|
+
spec.node_comparison,
|
|
27989
|
+
filter,
|
|
27990
|
+
except
|
|
27991
|
+
].join("|");
|
|
27992
|
+
}
|
|
27993
|
+
function entityFilterKey(entityType, filter) {
|
|
27994
|
+
const kind = classifyEntityFilter(filter);
|
|
27995
|
+
switch (kind) {
|
|
27996
|
+
case "presence":
|
|
27997
|
+
return `${entityType}|${filter.property}=${filter.present ? "present" : "absent"}`;
|
|
27998
|
+
case "value":
|
|
27999
|
+
return `${entityType}|${filter.property}=${filter.value}`;
|
|
28000
|
+
case "status":
|
|
28001
|
+
return `${entityType}|status=${filter.status}`;
|
|
28002
|
+
case "unrecognized":
|
|
28003
|
+
return `${entityType}|<unrecognized>`;
|
|
28004
|
+
}
|
|
28005
|
+
}
|
|
28006
|
+
function classifyEntityFilter(filter) {
|
|
28007
|
+
const hasProperty = typeof filter.property === "string";
|
|
28008
|
+
if (hasProperty && typeof filter.present === "boolean") return "presence";
|
|
28009
|
+
if (hasProperty && typeof filter.value === "string") return "value";
|
|
28010
|
+
if (typeof filter.status === "string") return "status";
|
|
28011
|
+
return "unrecognized";
|
|
28012
|
+
}
|
|
28013
|
+
function walkForEntityFilterSpecs(cond, out) {
|
|
28014
|
+
if ("operator" in cond) {
|
|
28015
|
+
for (const child of cond.checks) walkForEntityFilterSpecs(child, out);
|
|
28016
|
+
return;
|
|
28017
|
+
}
|
|
28018
|
+
const check = cond.check;
|
|
28019
|
+
if (check.type !== "entity_count" || !check.filter) return;
|
|
28020
|
+
const kind = classifyEntityFilter(check.filter);
|
|
28021
|
+
if (kind === "unrecognized") {
|
|
28022
|
+
throw new Error(
|
|
28023
|
+
`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.`
|
|
28024
|
+
);
|
|
28025
|
+
}
|
|
28026
|
+
const key = entityFilterKey(check.entity_type, check.filter);
|
|
28027
|
+
if (out.some((s) => entityFilterKey(s.entity_type, s.filter) === key)) return;
|
|
28028
|
+
out.push({ entity_type: check.entity_type, filter: check.filter, kind });
|
|
28029
|
+
}
|
|
28030
|
+
var UPG_ENTITY_FILTER_SPECS = (() => {
|
|
28031
|
+
const out = [];
|
|
28032
|
+
for (const ap of UPG_ANTI_PATTERNS) {
|
|
28033
|
+
if (ap.structured_condition) walkForEntityFilterSpecs(ap.structured_condition, out);
|
|
28034
|
+
}
|
|
28035
|
+
return out;
|
|
28036
|
+
})();
|
|
28037
|
+
function checkToEdgeCountSpec(check) {
|
|
28038
|
+
return {
|
|
28039
|
+
entity_type: check.entity_type,
|
|
28040
|
+
edge_type: check.edge_type,
|
|
28041
|
+
direction: check.direction ?? "inbound",
|
|
28042
|
+
property: check.property,
|
|
28043
|
+
property_absent_default: check.property_absent_default,
|
|
28044
|
+
node_comparison: check.node_comparison,
|
|
28045
|
+
node_filter: check.node_filter,
|
|
28046
|
+
except_property: check.except_property,
|
|
28047
|
+
except_value: check.except_value
|
|
28048
|
+
};
|
|
28049
|
+
}
|
|
28050
|
+
function walkForEdgeCountSpecs(cond, out) {
|
|
28051
|
+
if ("operator" in cond) {
|
|
28052
|
+
for (const child of cond.checks) walkForEdgeCountSpecs(child, out);
|
|
28053
|
+
return;
|
|
28054
|
+
}
|
|
28055
|
+
const check = cond.check;
|
|
28056
|
+
if (check.type !== "edge_count_vs_property") return;
|
|
28057
|
+
const spec = checkToEdgeCountSpec(check);
|
|
28058
|
+
const key = edgeCountSpecKey(spec);
|
|
28059
|
+
if (!out.some((s) => edgeCountSpecKey(s) === key)) out.push(spec);
|
|
28060
|
+
}
|
|
28061
|
+
var UPG_EDGE_COUNT_SPECS = (() => {
|
|
28062
|
+
const out = [];
|
|
28063
|
+
for (const ap of UPG_ANTI_PATTERNS) {
|
|
28064
|
+
if (ap.structured_condition) walkForEdgeCountSpecs(ap.structured_condition, out);
|
|
28065
|
+
}
|
|
28066
|
+
return out;
|
|
28067
|
+
})();
|
|
27492
28068
|
function getAntiPatternById(id) {
|
|
27493
28069
|
return UPG_ANTI_PATTERNS.find((ap) => ap.id === id);
|
|
27494
28070
|
}
|
|
@@ -27565,7 +28141,11 @@ function evaluateAntiPatterns(inputs, options) {
|
|
|
27565
28141
|
const concern = concernFor(ap.id);
|
|
27566
28142
|
if (!concernEvaluatedFor(inputs.memberKind, concern)) continue;
|
|
27567
28143
|
if (evaluateCondition(ap.structured_condition, inputs)) {
|
|
27568
|
-
|
|
28144
|
+
const ids = /* @__PURE__ */ new Set();
|
|
28145
|
+
collectAttribution(ap.structured_condition, inputs, ids);
|
|
28146
|
+
const violation = buildViolation(ap, concern);
|
|
28147
|
+
if (ids.size > 0) violation.target_node_ids = [...ids].sort();
|
|
28148
|
+
fires.push(violation);
|
|
27569
28149
|
}
|
|
27570
28150
|
}
|
|
27571
28151
|
fires.sort((a, b) => {
|
|
@@ -27575,6 +28155,25 @@ function evaluateAntiPatterns(inputs, options) {
|
|
|
27575
28155
|
});
|
|
27576
28156
|
return fires;
|
|
27577
28157
|
}
|
|
28158
|
+
function collectAttribution(cond, inputs, out) {
|
|
28159
|
+
if ("operator" in cond) {
|
|
28160
|
+
for (const child of cond.checks) {
|
|
28161
|
+
if (cond.operator === "or" && !evaluateCondition(child, inputs)) continue;
|
|
28162
|
+
collectAttribution(child, inputs, out);
|
|
28163
|
+
}
|
|
28164
|
+
return;
|
|
28165
|
+
}
|
|
28166
|
+
const check = cond.check;
|
|
28167
|
+
if (check.type === "edge_count_vs_property") {
|
|
28168
|
+
const key = edgeCountSpecKey(checkToEdgeCountSpec(check));
|
|
28169
|
+
for (const id of inputs.nodesByEdgeCountSpec?.[key] ?? []) out.add(id);
|
|
28170
|
+
return;
|
|
28171
|
+
}
|
|
28172
|
+
if (check.type === "entity_count" && check.filter) {
|
|
28173
|
+
const key = entityFilterKey(check.entity_type, check.filter);
|
|
28174
|
+
for (const id of inputs.nodesByEntityFilter?.[key] ?? []) out.add(id);
|
|
28175
|
+
}
|
|
28176
|
+
}
|
|
27578
28177
|
function evaluateCondition(cond, inputs) {
|
|
27579
28178
|
if ("check" in cond) {
|
|
27580
28179
|
return evaluateLeaf(cond.check, inputs);
|
|
@@ -27594,6 +28193,8 @@ function evaluateLeaf(check, inputs) {
|
|
|
27594
28193
|
switch (check.type) {
|
|
27595
28194
|
case "entity_count":
|
|
27596
28195
|
return evaluateEntityCount(check, inputs);
|
|
28196
|
+
case "edge_count_vs_property":
|
|
28197
|
+
return evaluateEdgeCountVsProperty(check, inputs);
|
|
27597
28198
|
case "relationship":
|
|
27598
28199
|
return evaluateRelationship(check, inputs);
|
|
27599
28200
|
case "benchmark":
|
|
@@ -27654,6 +28255,18 @@ function evaluateEntityCount(check, inputs) {
|
|
|
27654
28255
|
}
|
|
27655
28256
|
return compareNumber(count, check.comparison, check.threshold);
|
|
27656
28257
|
}
|
|
28258
|
+
function evaluateEdgeCountVsProperty(check, inputs) {
|
|
28259
|
+
const key = edgeCountSpecKey(checkToEdgeCountSpec(check));
|
|
28260
|
+
const matched = inputs.nodesByEdgeCountSpec?.[key];
|
|
28261
|
+
if (matched === void 0) {
|
|
28262
|
+
return compareNumber(
|
|
28263
|
+
inputs.countsByType[check.entity_type] ?? 0,
|
|
28264
|
+
check.comparison,
|
|
28265
|
+
check.threshold
|
|
28266
|
+
);
|
|
28267
|
+
}
|
|
28268
|
+
return compareNumber(matched.length, check.comparison, check.threshold);
|
|
28269
|
+
}
|
|
27657
28270
|
function relationshipKey(source_type, edge_type, target_type) {
|
|
27658
28271
|
return `${source_type}|${edge_type}|${target_type}`;
|
|
27659
28272
|
}
|
|
@@ -27736,7 +28349,7 @@ function collectTargetEntities(cond) {
|
|
|
27736
28349
|
function walk(c) {
|
|
27737
28350
|
if ("check" in c) {
|
|
27738
28351
|
const leaf = c.check;
|
|
27739
|
-
if (leaf.type === "entity_count" || leaf.type === "benchmark") {
|
|
28352
|
+
if (leaf.type === "entity_count" || leaf.type === "benchmark" || leaf.type === "edge_count_vs_property") {
|
|
27740
28353
|
types.add(leaf.entity_type);
|
|
27741
28354
|
} else if (leaf.type === "relationship") {
|
|
27742
28355
|
types.add(leaf.source_type);
|
|
@@ -28847,6 +29460,11 @@ var UPG_REGIONS = [
|
|
|
28847
29460
|
type: "planning_cycle",
|
|
28848
29461
|
role: "container",
|
|
28849
29462
|
notes: "self-nesting cadence axis"
|
|
29463
|
+
},
|
|
29464
|
+
{
|
|
29465
|
+
type: "configuration_axis",
|
|
29466
|
+
role: "leaf",
|
|
29467
|
+
notes: "the configuration lever whose values select which surface tree the product renders"
|
|
28850
29468
|
}
|
|
28851
29469
|
],
|
|
28852
29470
|
anchor: {
|
|
@@ -30763,7 +31381,7 @@ function checkHeaderSealText(text) {
|
|
|
30763
31381
|
}
|
|
30764
31382
|
|
|
30765
31383
|
// src/index.ts
|
|
30766
|
-
var UPG_VERSION = "0.
|
|
31384
|
+
var UPG_VERSION = "0.30.0";
|
|
30767
31385
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
30768
31386
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
30769
31387
|
var UPG_TYPES = getTypes();
|
|
@@ -30828,11 +31446,14 @@ var UPG_EDGE_COUNT = UPG_EDGE_TYPES.length;
|
|
|
30828
31446
|
var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
30829
31447
|
// Annotate the CommonJS export names for ESM import in node:
|
|
30830
31448
|
0 && (module.exports = {
|
|
31449
|
+
ACTIVE_WHEN_PROPERTY,
|
|
30831
31450
|
ANALYTICS_DATA_PLAYBOOK,
|
|
30832
31451
|
BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
|
|
30833
31452
|
BUSINESS_GTM_GROWTH_PLAYBOOK,
|
|
30834
31453
|
BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
|
|
30835
31454
|
CLASSIFICATION_EDGE_PROPERTY_SCHEMA,
|
|
31455
|
+
CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA,
|
|
31456
|
+
CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA,
|
|
30836
31457
|
CONTENT_DEPTH_WARNING_RULES,
|
|
30837
31458
|
COVERAGE_ANTI_PATTERNS,
|
|
30838
31459
|
DEFAULT_ENTITY_EMOJI,
|
|
@@ -30847,10 +31468,12 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
30847
31468
|
MARKDOWN_FORMAT_VERSION,
|
|
30848
31469
|
MARKET_COMPETITIVE_PLAYBOOK,
|
|
30849
31470
|
OPERATIONS_QUALITY_PLAYBOOK,
|
|
31471
|
+
PRESENT_UNDER_PROPERTY,
|
|
30850
31472
|
PRODUCT_DELIVERY_PLAYBOOK,
|
|
30851
31473
|
PROPERTY_MODIFIERS,
|
|
30852
31474
|
PROPERTY_MODIFIER_SEMANTICS,
|
|
30853
31475
|
PROPERTY_SCALE_MAP,
|
|
31476
|
+
QUALIFIABLE_EDGE_TYPES,
|
|
30854
31477
|
RECORD_ENTITY_TYPES,
|
|
30855
31478
|
REFLECT_MODES,
|
|
30856
31479
|
REGISTRY_PRODUCT_ID,
|
|
@@ -30878,10 +31501,12 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
30878
31501
|
UPG_DOMAIN_RINGS,
|
|
30879
31502
|
UPG_EDGE_CATALOG,
|
|
30880
31503
|
UPG_EDGE_COUNT,
|
|
31504
|
+
UPG_EDGE_COUNT_SPECS,
|
|
30881
31505
|
UPG_EDGE_MIGRATIONS,
|
|
30882
31506
|
UPG_EDGE_PAIR_MAP,
|
|
30883
31507
|
UPG_EDGE_TYPES,
|
|
30884
31508
|
UPG_ENTITY_COUNT,
|
|
31509
|
+
UPG_ENTITY_FILTER_SPECS,
|
|
30885
31510
|
UPG_ENTITY_META,
|
|
30886
31511
|
UPG_ENTITY_META_BY_ID,
|
|
30887
31512
|
UPG_ENTITY_META_BY_NAME,
|
|
@@ -30931,13 +31556,18 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
30931
31556
|
UPG_WILDCARD_ENDPOINT,
|
|
30932
31557
|
USERS_NEEDS_PLAYBOOK,
|
|
30933
31558
|
UnknownEntityTypeError,
|
|
31559
|
+
VARIES_BY_EDGE,
|
|
30934
31560
|
backfillSlug,
|
|
30935
31561
|
buildTypeAliases,
|
|
30936
31562
|
canBeChildOf,
|
|
31563
|
+
checkConfigurationDrift,
|
|
30937
31564
|
checkHeaderSeal,
|
|
30938
31565
|
checkHeaderSealText,
|
|
31566
|
+
checkToEdgeCountSpec,
|
|
31567
|
+
classifyEntityFilter,
|
|
30939
31568
|
coerceProductStage,
|
|
30940
31569
|
collectSlugsForType,
|
|
31570
|
+
collectTargetEntities,
|
|
30941
31571
|
compareVersions,
|
|
30942
31572
|
computeBodyChecksum,
|
|
30943
31573
|
concernEvaluatedFor,
|
|
@@ -30949,6 +31579,9 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
30949
31579
|
deriveSingleCounts,
|
|
30950
31580
|
describeTreePattern,
|
|
30951
31581
|
edgeCarriesProperties,
|
|
31582
|
+
edgeCountSpecKey,
|
|
31583
|
+
entityFilterKey,
|
|
31584
|
+
enumerateProjections,
|
|
30952
31585
|
evaluateAntiPatterns,
|
|
30953
31586
|
findInvalidStatusMigrationTargets,
|
|
30954
31587
|
formatUpgText,
|
|
@@ -31040,6 +31673,9 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
31040
31673
|
parseUpg,
|
|
31041
31674
|
pickCanonicalEdge,
|
|
31042
31675
|
presenceExceptKey,
|
|
31676
|
+
projectGraph,
|
|
31677
|
+
readActiveWhen,
|
|
31678
|
+
readStringArray,
|
|
31043
31679
|
resolveAllEdges,
|
|
31044
31680
|
resolveContainmentEdge,
|
|
31045
31681
|
resolveEntityType,
|