@unified-product-graph/core 0.29.0 → 0.31.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 +45 -0
- package/README.md +1 -1
- package/dist/index.cjs +2192 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1601 -175
- package/dist/index.d.ts +1601 -175
- package/dist/index.js +2177 -201
- 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,
|
|
@@ -135,9 +140,12 @@ __export(index_exports, {
|
|
|
135
140
|
UPG_WILDCARD_ENDPOINT: () => UPG_WILDCARD_ENDPOINT,
|
|
136
141
|
USERS_NEEDS_PLAYBOOK: () => USERS_NEEDS_PLAYBOOK,
|
|
137
142
|
UnknownEntityTypeError: () => UnknownEntityTypeError,
|
|
143
|
+
VARIES_BY_EDGE: () => VARIES_BY_EDGE,
|
|
144
|
+
WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA: () => WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA,
|
|
138
145
|
backfillSlug: () => backfillSlug,
|
|
139
146
|
buildTypeAliases: () => buildTypeAliases,
|
|
140
147
|
canBeChildOf: () => canBeChildOf,
|
|
148
|
+
checkConfigurationDrift: () => checkConfigurationDrift,
|
|
141
149
|
checkHeaderSeal: () => checkHeaderSeal,
|
|
142
150
|
checkHeaderSealText: () => checkHeaderSealText,
|
|
143
151
|
checkToEdgeCountSpec: () => checkToEdgeCountSpec,
|
|
@@ -158,6 +166,7 @@ __export(index_exports, {
|
|
|
158
166
|
edgeCarriesProperties: () => edgeCarriesProperties,
|
|
159
167
|
edgeCountSpecKey: () => edgeCountSpecKey,
|
|
160
168
|
entityFilterKey: () => entityFilterKey,
|
|
169
|
+
enumerateProjections: () => enumerateProjections,
|
|
161
170
|
evaluateAntiPatterns: () => evaluateAntiPatterns,
|
|
162
171
|
findInvalidStatusMigrationTargets: () => findInvalidStatusMigrationTargets,
|
|
163
172
|
formatUpgText: () => formatUpgText,
|
|
@@ -248,11 +257,17 @@ __export(index_exports, {
|
|
|
248
257
|
normalizeDocument: () => normalizeDocument,
|
|
249
258
|
parseUpg: () => parseUpg,
|
|
250
259
|
pickCanonicalEdge: () => pickCanonicalEdge,
|
|
260
|
+
predicatesProvablyDisjoint: () => predicatesProvablyDisjoint,
|
|
251
261
|
presenceExceptKey: () => presenceExceptKey,
|
|
262
|
+
projectGraph: () => projectGraph,
|
|
263
|
+
readActiveWhen: () => readActiveWhen,
|
|
264
|
+
readStringArray: () => readStringArray,
|
|
265
|
+
relationalCoveredEntityTypes: () => relationalCoveredEntityTypes,
|
|
252
266
|
resolveAllEdges: () => resolveAllEdges,
|
|
253
267
|
resolveContainmentEdge: () => resolveContainmentEdge,
|
|
254
268
|
resolveEntityType: () => resolveEntityType,
|
|
255
269
|
resolveLabel: () => resolveLabel,
|
|
270
|
+
resolveRelationalPath: () => resolveRelationalPath,
|
|
256
271
|
resolveSlugCollision: () => resolveSlugCollision,
|
|
257
272
|
resolveTreePatternEdges: () => resolveTreePatternEdges,
|
|
258
273
|
ringOrderedDomainIds: () => ringOrderedDomainIds,
|
|
@@ -375,7 +390,8 @@ var UPG_DOMAINS = [
|
|
|
375
390
|
"roadmap_item",
|
|
376
391
|
"roadmap_theme",
|
|
377
392
|
"changelog",
|
|
378
|
-
"planning_cycle"
|
|
393
|
+
"planning_cycle",
|
|
394
|
+
"configuration_axis"
|
|
379
395
|
]
|
|
380
396
|
},
|
|
381
397
|
{
|
|
@@ -662,7 +678,7 @@ var UPG_DOMAINS = [
|
|
|
662
678
|
{
|
|
663
679
|
id: "program_mgmt",
|
|
664
680
|
label: "Program Management",
|
|
665
|
-
description: "Coordinating delivery across teams and timelines. Programs are the highest container. Projects break programs down. Milestones mark key dates. Risk registers track threats to delivery. Change requests manage scope changes. Deliverables define what ships. Resource allocations assign effort. Status reports communicate progress. Connects Product Specification (what to deliver) to Team & Organisation (who delivers).",
|
|
681
|
+
description: "Coordinating delivery across teams and timelines. Programs are the highest container. Projects break programs down. Milestones mark key dates, and hang from either a project or, where a product owns the date outright with no program above it, from the product itself. Risk registers track threats to delivery. Change requests manage scope changes. Deliverables define what ships. Resource allocations assign effort. Status reports communicate progress. Connects Product Specification (what to deliver) to Team & Organisation (who delivers).",
|
|
666
682
|
types: [
|
|
667
683
|
"program",
|
|
668
684
|
"project",
|
|
@@ -758,8 +774,8 @@ var UPG_DOMAINS = [
|
|
|
758
774
|
{
|
|
759
775
|
id: "workspace",
|
|
760
776
|
label: "Workspace",
|
|
761
|
-
description: "Spatial thinking spaces for arranging entities, debating decisions, and committing to the graph. Workspaces are transient canvases that sit alongside all other domains, letting you compose and explore relationships before they become permanent graph structure. A framework exercise is a structured workspace: one run of a framework (MoSCoW, RICE, Kano, \u2026) applied to a chosen set of entities, with each entity's result recorded on the exercise-to-entity edge rather than the entity itself.",
|
|
762
|
-
types: ["workspace", "framework_exercise"]
|
|
777
|
+
description: "Spatial thinking spaces for arranging entities, debating decisions, and committing to the graph. Workspaces are transient canvases that sit alongside all other domains, letting you compose and explore relationships before they become permanent graph structure. A framework exercise is a structured workspace: one run of a framework (MoSCoW, RICE, Kano, \u2026) applied to a chosen set of entities, with each entity's result recorded on the exercise-to-entity edge rather than the entity itself. A composition is the durable other half: a named, published view assembled from a canvas, with a stable slug people link to and a revision history, whose frozen arrangement holds pointers rather than copied content.",
|
|
778
|
+
types: ["workspace", "framework_exercise", "composition"]
|
|
763
779
|
},
|
|
764
780
|
{
|
|
765
781
|
id: "foundations",
|
|
@@ -915,6 +931,7 @@ var UPG_ENTITY_META = [
|
|
|
915
931
|
// it, carrying its occupants and the rule that arbitrates between them.
|
|
916
932
|
// Enters `proposed` per the maturity rubric (precedent: planning_cycle 0.20.0).
|
|
917
933
|
{ name: "surface", type_id: "ent_359", maturity: "proposed", since: "0.27.0" },
|
|
934
|
+
{ name: "configuration_axis", type_id: "ent_360", maturity: "proposed", since: "0.30.0" },
|
|
918
935
|
{ name: "journey_phase", type_id: "ent_330", maturity: "proposed", since: "0.2.0" },
|
|
919
936
|
{ name: "journey_action", type_id: "ent_331", maturity: "proposed", since: "0.2.0" },
|
|
920
937
|
{ name: "design_decision", type_id: "ent_319", maturity: "deprecated", since: "0.2.0", deprecated_in: "0.2.0", replacement: "decision" },
|
|
@@ -1245,7 +1262,8 @@ var UPG_ENTITY_META = [
|
|
|
1245
1262
|
{ name: "partner_revenue_share", type_id: "ent_312", maturity: "stable", since: "0.1.0" },
|
|
1246
1263
|
// ── Workspace ──
|
|
1247
1264
|
{ name: "workspace", type_id: "ent_328", maturity: "proposed", since: "0.2.0" },
|
|
1248
|
-
{ name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" }
|
|
1265
|
+
{ name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" },
|
|
1266
|
+
{ name: "composition", type_id: "ent_361", maturity: "proposed", since: "0.31.0" }
|
|
1249
1267
|
];
|
|
1250
1268
|
var DEFERRED_PROPOSED_BY_CONTRACT = {
|
|
1251
1269
|
// user_story (UPG-571) and experiment_run (UPG-664) graduated to stable in
|
|
@@ -1316,6 +1334,47 @@ var DEFER_EDGE_PROPERTY_SCHEMA = {
|
|
|
1316
1334
|
description: 'Target period the parked work is deferred to. Freeform, mirroring strategic_theme.time_horizon. @example "Q4 2026", "FY27", "next release".'
|
|
1317
1335
|
}
|
|
1318
1336
|
};
|
|
1337
|
+
var CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA = {
|
|
1338
|
+
present_under: {
|
|
1339
|
+
type: "string[]",
|
|
1340
|
+
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"]'
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA = {
|
|
1344
|
+
active_when: {
|
|
1345
|
+
type: "object",
|
|
1346
|
+
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".',
|
|
1347
|
+
properties: {
|
|
1348
|
+
axis: {
|
|
1349
|
+
type: "string",
|
|
1350
|
+
description: "Node id of the `configuration_axis` this condition reads."
|
|
1351
|
+
},
|
|
1352
|
+
values: {
|
|
1353
|
+
type: "string[]",
|
|
1354
|
+
description: "Values of that axis under which the relationship holds. Every entry must be a member of the axis's closed `values` list."
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
required: ["axis", "values"]
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
var WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA = {
|
|
1361
|
+
x: {
|
|
1362
|
+
type: "number",
|
|
1363
|
+
description: "Horizontal canvas coordinate of the card, in the canvas's own units. Expected on every arrangement: a placement without coordinates is not a placement."
|
|
1364
|
+
},
|
|
1365
|
+
y: {
|
|
1366
|
+
type: "number",
|
|
1367
|
+
description: "Vertical canvas coordinate of the card, in the canvas's own units. Expected on every arrangement: a placement without coordinates is not a placement."
|
|
1368
|
+
},
|
|
1369
|
+
expanded: {
|
|
1370
|
+
type: "boolean",
|
|
1371
|
+
description: "Whether the card is drawn expanded rather than collapsed. Presentation state that survives a reload, unlike selection."
|
|
1372
|
+
},
|
|
1373
|
+
frame_id: {
|
|
1374
|
+
type: "string",
|
|
1375
|
+
description: "Id of the canvas frame this card sits inside. The frame itself is opaque furniture and lives in `workspace.properties.canvas.frames`, so this names a bag member rather than a graph node."
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1319
1378
|
var UPG_EDGE_CATALOG = {
|
|
1320
1379
|
// ── Part 1: Core Narrative Spine (Ring 1) ──────────────────────────────────
|
|
1321
1380
|
// 1.1 User Domain
|
|
@@ -1842,7 +1901,7 @@ var UPG_EDGE_CATALOG = {
|
|
|
1842
1901
|
// portfolio_shared, so the reading it names commonly lives in the
|
|
1843
1902
|
// registry rather than in the product graph.
|
|
1844
1903
|
// supersedes → semantic (prompt_version_supersedes_prompt_version).
|
|
1845
|
-
surface_contains_surface: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "surface", target_type: "surface" },
|
|
1904
|
+
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 },
|
|
1846
1905
|
surface_serves_job: { forward_verb: "serves", reverse_verb: "served_by", classification: "cross-domain", source_type: "surface", target_type: "job" },
|
|
1847
1906
|
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 },
|
|
1848
1907
|
surface_renders_design_component: { forward_verb: "renders", reverse_verb: "rendered_on", classification: "hierarchy", source_type: "surface", target_type: "design_component", cross_product_eligible: true },
|
|
@@ -1877,11 +1936,53 @@ var UPG_EDGE_CATALOG = {
|
|
|
1877
1936
|
// not cross_product_eligible → a deviation is a fact about one product's
|
|
1878
1937
|
// code, and `surface` is not portfolio_shared in the first place.
|
|
1879
1938
|
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" },
|
|
1939
|
+
// ── surface, 0.30.0: composition that varies by configuration ───────────────
|
|
1940
|
+
// A product's surface tree is not one tree; it is a family of trees selected
|
|
1941
|
+
// by a configuration lever. The stored graph is the UNION of that family and
|
|
1942
|
+
// a single configuration is a PROJECTION of it, so these two edges are how a
|
|
1943
|
+
// graph says which member a fact belongs to. Silence still means "every
|
|
1944
|
+
// member", which is why a graph written before 0.30.0 needs no migration.
|
|
1945
|
+
//
|
|
1946
|
+
// varies_by — CONDITIONAL EXISTENCE. The surface exists only under the
|
|
1947
|
+
// values in `present_under`. An edge qualifier cannot say
|
|
1948
|
+
// this: the reported case was a set of named chips REPLACED
|
|
1949
|
+
// by one generic badge, where which surface EXISTS changes,
|
|
1950
|
+
// and the two carry different capacities and occupants.
|
|
1951
|
+
// alternates — "one of these, depending" as distinct from "both,
|
|
1952
|
+
// together". Semantic, matching its sibling
|
|
1953
|
+
// `surface_supersedes_surface`: neither surface owns the
|
|
1954
|
+
// other, and a hierarchy classification would demand a
|
|
1955
|
+
// UPG_VALID_CHILDREN pair that would be false. Explicit and
|
|
1956
|
+
// validator-checked (same axis, disjoint present_under);
|
|
1957
|
+
// never auto-derived, because disjointness is necessary for
|
|
1958
|
+
// alternation and nowhere near sufficient.
|
|
1959
|
+
//
|
|
1960
|
+
// Direction on `alternates_with` is a CONVENTION, not an enforcement: declare
|
|
1961
|
+
// it once, sourced from the surface present under the axis's `default_value`.
|
|
1962
|
+
// An axis with no default is legal modelling, so a check keyed on the
|
|
1963
|
+
// convention would fire on a correct graph, which is the one thing a check
|
|
1964
|
+
// must never do.
|
|
1965
|
+
// ── configuration_axis, 0.30.0: the lever itself ───────────────────────────
|
|
1966
|
+
// Three layers, three owners, and keeping them apart is what makes the model
|
|
1967
|
+
// honest: `engineering` owns the MECHANISM (`feature_flag`, with its key and
|
|
1968
|
+
// rollout percentage), `product_spec` owns the LEVER (the named dimension
|
|
1969
|
+
// along which what-you-ship differs), `ux_design` owns the PLACE (`surface`).
|
|
1970
|
+
// Two code flags that move together are ONE lever with two values.
|
|
1971
|
+
//
|
|
1972
|
+
// `defines` is hierarchy so the axis has a home in `get_tree`. It earns the
|
|
1973
|
+
// UPG_VALID_CHILDREN.product pair (guardrail G2b) on an honest verb: an axis
|
|
1974
|
+
// exists only within the product whose composition it varies. The rejected
|
|
1975
|
+
// alternative mirrors `classification_axis_owned_by_product`, a cross-domain
|
|
1976
|
+
// ownership edge that leaves the node unparented and invisible in every tree.
|
|
1977
|
+
product_defines_configuration_axis: { forward_verb: "defines", reverse_verb: "defined_by", classification: "hierarchy", source_type: "product", target_type: "configuration_axis" },
|
|
1978
|
+
feature_flag_drives_configuration_axis: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "feature_flag", target_type: "configuration_axis" },
|
|
1979
|
+
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 },
|
|
1980
|
+
surface_alternates_with_surface: { forward_verb: "alternates_with", reverse_verb: "alternates_with", classification: "semantic", source_type: "surface", target_type: "surface" },
|
|
1880
1981
|
// Inbound. `feature_occupies_surface` is the guest list: the edge that makes
|
|
1881
1982
|
// "what else lives here?" answerable, and the one the contention anti-pattern
|
|
1882
1983
|
// counts. cross-domain (product_spec → ux_design), like screen_surfaces_feature
|
|
1883
1984
|
// running the other way.
|
|
1884
|
-
feature_occupies_surface: { forward_verb: "occupies", reverse_verb: "occupied_by", classification: "cross-domain", source_type: "feature", target_type: "surface" },
|
|
1985
|
+
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 },
|
|
1885
1986
|
screen_renders_surface: { forward_verb: "renders", reverse_verb: "rendered_by", classification: "hierarchy", source_type: "screen", target_type: "surface" },
|
|
1886
1987
|
decision_affects_surface: { forward_verb: "affects", reverse_verb: "affected_by", classification: "cross-domain", source_type: "decision", target_type: "surface" },
|
|
1887
1988
|
// The finer-grained sibling of journey_step_shown_on_screen: a step happens on
|
|
@@ -2170,7 +2271,40 @@ var UPG_EDGE_CATALOG = {
|
|
|
2170
2271
|
// real edge, not just a string property — the domain's own anti-pattern is
|
|
2171
2272
|
// "version prompts like code".
|
|
2172
2273
|
prompt_version_supersedes_prompt_version: { forward_verb: "supersedes", reverse_verb: "superseded_by", classification: "semantic", source_type: "prompt_version", target_type: "prompt_version" },
|
|
2274
|
+
// The TYPED subject edge. Kept alongside the polymorphic `eval_benchmark_measures_node`
|
|
2275
|
+
// added in 0.31.0, on the same footing as `constraint_constrains_feature` beside
|
|
2276
|
+
// `node_constrains_node`: reach for THIS one when the subject is a feature, and for the
|
|
2277
|
+
// wildcard only when it is not. Two ways to say the same thing about a feature is the
|
|
2278
|
+
// cost of the pair, and it is paid deliberately rather than by accident.
|
|
2173
2279
|
eval_benchmark_measures_feature: { forward_verb: "measures", reverse_verb: "measured_by", classification: "cross-domain", source_type: "eval_benchmark", target_type: "feature" },
|
|
2280
|
+
/*
|
|
2281
|
+
* 0.31.0 — the benchmark SUBJECT, polymorphic.
|
|
2282
|
+
*
|
|
2283
|
+
* An eval measures a tool, a document, a check, an importer, a feature. Those
|
|
2284
|
+
* are unrelated types, so the subject relation is genuinely wildcard-shaped and
|
|
2285
|
+
* follows the decision-to-anything family (`decision_influences_node`).
|
|
2286
|
+
*
|
|
2287
|
+
* THE WIDTH IS OVER-WIDE, AND THAT IS ON THE LABEL RATHER THAN LAUNDERED.
|
|
2288
|
+
* The decision family's warrant is that a decision really is about anything. A
|
|
2289
|
+
* benchmark is not: it measures a MEASURABLE thing, and this edge will happily
|
|
2290
|
+
* claim a benchmark can measure a persona, which is meaningless. It ships as the
|
|
2291
|
+
* honest deferral it is — the alternative was minting typed edges for subject
|
|
2292
|
+
* types the spec does not yet have, which would answer "what is a tool in the
|
|
2293
|
+
* graph" as a side effect of building an eval harness. That question is a stated
|
|
2294
|
+
* non-goal, and deferring it is the point of choosing the wildcard.
|
|
2295
|
+
*
|
|
2296
|
+
* Registered in UPG_POLYMORPHIC_EDGE_KEYS; Audit 05 errors on any wildcard that
|
|
2297
|
+
* is not.
|
|
2298
|
+
*/
|
|
2299
|
+
eval_benchmark_measures_node: { forward_verb: "measures", reverse_verb: "measured_by", classification: "semantic", source_type: "eval_benchmark", target_type: "node" },
|
|
2300
|
+
/*
|
|
2301
|
+
* 0.31.0 — the labeled corpus a benchmark draws its cases from.
|
|
2302
|
+
*
|
|
2303
|
+
* `ai_dataset` reused rather than a new `corpus` type minted (Q5, ratified): the
|
|
2304
|
+
* existing type plus one join edge carries the need, and a corpus type would be
|
|
2305
|
+
* minted only if that demonstrably failed. It has not.
|
|
2306
|
+
*/
|
|
2307
|
+
eval_benchmark_draws_cases_from_ai_dataset: { forward_verb: "draws_cases_from", reverse_verb: "provides_cases_to", classification: "semantic", source_type: "eval_benchmark", target_type: "ai_dataset" },
|
|
2174
2308
|
ai_guardrail_enforces_security_policy: { forward_verb: "enforces", reverse_verb: "enforced_by", classification: "cross-domain", source_type: "ai_guardrail", target_type: "security_policy" },
|
|
2175
2309
|
model_comparison_informs_decision: { forward_verb: "informs", reverse_verb: "informed_by", classification: "cross-domain", source_type: "model_comparison", target_type: "decision" },
|
|
2176
2310
|
ai_cost_tracker_feeds_cost_structure: { forward_verb: "feeds", reverse_verb: "fed_by", classification: "cross-domain", source_type: "ai_cost_tracker", target_type: "cost_structure" },
|
|
@@ -2208,6 +2342,23 @@ var UPG_EDGE_CATALOG = {
|
|
|
2208
2342
|
agent_hook_triggers_ci_pipeline: { forward_verb: "triggers", reverse_verb: "triggered_by", classification: "cross-domain", source_type: "agent_hook", target_type: "ci_pipeline" },
|
|
2209
2343
|
workflow_template_defines_agent_task: { forward_verb: "defines", reverse_verb: "defined_by", classification: "hierarchy", source_type: "workflow_template", target_type: "agent_task" },
|
|
2210
2344
|
agent_definition_spawns_agent_task: { forward_verb: "spawns", reverse_verb: "spawned_by", classification: "hierarchy", source_type: "agent_definition", target_type: "agent_task" },
|
|
2345
|
+
// The delegation bridge (tasks-workflows-2026-08 §5). Before this, the whole
|
|
2346
|
+
// automation domain could reach product work only at coarse grain —
|
|
2347
|
+
// `workflow_run_implements_initiative` (initiative), `agent_skill_extends_feature`
|
|
2348
|
+
// (feature), `agent_session_creates_decision` (decision),
|
|
2349
|
+
// `workflow_artifact_references_deliverable` (deliverable) — and NOTHING reached
|
|
2350
|
+
// `task`, the atomic unit of product work that `feature_decomposes_into_task` /
|
|
2351
|
+
// `epic_decomposes_into_task` make the delivery hierarchy's leaf. So "this agent
|
|
2352
|
+
// work item IS the execution of that product task" was inexpressible, and a
|
|
2353
|
+
// human-vs-agent assignee could not be read off the graph.
|
|
2354
|
+
// Cross-domain: agent_task lives in `automation`, task in product delivery.
|
|
2355
|
+
// NOT hierarchy — the agent_task does not CONTAIN the task; both are
|
|
2356
|
+
// independently-parented work items (`workflow_template_defines_agent_task` /
|
|
2357
|
+
// `agent_definition_spawns_agent_task` own the agent side, feature/epic the
|
|
2358
|
+
// product side), and the delegation is a lateral reference between them.
|
|
2359
|
+
// Verb `executes` mirrors the domain's own vocabulary (`workflow_template`
|
|
2360
|
+
// is `executed_as` a run); reverse reads "this task is executed_by that agent task".
|
|
2361
|
+
agent_task_executes_task: { forward_verb: "executes", reverse_verb: "executed_by", classification: "cross-domain", source_type: "agent_task", target_type: "task" },
|
|
2211
2362
|
// ── Part 4: Scale Ring (Ring 4) ────────────────────────────────────────────
|
|
2212
2363
|
// 4.1 Growth Domain
|
|
2213
2364
|
product_measures_funnel: { forward_verb: "measures", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "product", target_type: "funnel" },
|
|
@@ -2558,6 +2709,25 @@ var UPG_EDGE_CATALOG = {
|
|
|
2558
2709
|
product_managed_via_program: { forward_verb: "managed_via", reverse_verb: "manages", classification: "hierarchy", source_type: "product", target_type: "program" },
|
|
2559
2710
|
program_contains_project: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "program", target_type: "project" },
|
|
2560
2711
|
project_targets_milestone: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "project", target_type: "milestone" },
|
|
2712
|
+
// Portfolio Phase 2 (ratified 2026-08-14): a milestone that a PRODUCT owns
|
|
2713
|
+
// outright, with no program/project above it. `milestone` had exactly one
|
|
2714
|
+
// inbound edge spec-wide (`project_targets_milestone`) and `project` was its
|
|
2715
|
+
// only declared parent, so a portfolio-grain milestone — which names a
|
|
2716
|
+
// product, not a project — was inexpressible. This is the 0.23.0 epic-twin
|
|
2717
|
+
// precedent: an existing leaf type gains a second, SHALLOWER parent, and the
|
|
2718
|
+
// name, verbs and classification are the deeper edge's, inherited unchanged
|
|
2719
|
+
// rather than invented.
|
|
2720
|
+
//
|
|
2721
|
+
// The mediated path was checked and rejected, not overlooked. The relation IS
|
|
2722
|
+
// reachable as product —managed_via→ program —contains→ project —targets→
|
|
2723
|
+
// milestone, but at three hops (past the 1-2-hop threshold), and the hop count
|
|
2724
|
+
// is not the real objection: mediation forces `program` and `project` nodes
|
|
2725
|
+
// into existence that the source data does not have.
|
|
2726
|
+
//
|
|
2727
|
+
// NOT cross_product_eligible: a milestone belongs to the graph of the product
|
|
2728
|
+
// that owns it. `UPG_VALID_CHILDREN.product` gains 'milestone' in step, which
|
|
2729
|
+
// the hierarchy-orphan guard requires.
|
|
2730
|
+
product_targets_milestone: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "product", target_type: "milestone" },
|
|
2561
2731
|
project_produces_deliverable: { forward_verb: "produces", reverse_verb: "produced_by", classification: "hierarchy", source_type: "project", target_type: "deliverable" },
|
|
2562
2732
|
program_tracked_via_risk_register: { forward_verb: "tracked_via", reverse_verb: "tracks", classification: "hierarchy", source_type: "program", target_type: "risk_register" },
|
|
2563
2733
|
risk_register_contains_risk: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "risk_register", target_type: "risk" },
|
|
@@ -2704,6 +2874,50 @@ var UPG_EDGE_CATALOG = {
|
|
|
2704
2874
|
// node) has no schema mechanism yet — deliberately deferred, see the WS3
|
|
2705
2875
|
// proposal doc, ripple map "surprises" #3.
|
|
2706
2876
|
workspace_produced_node: { forward_verb: "produced", reverse_verb: "produced_in", classification: "causal", source_type: "workspace", target_type: "node" },
|
|
2877
|
+
// Canvas arrangement: which entities are ON a workspace, and where. This is
|
|
2878
|
+
// the edge `WorkspaceProperties`' docstring promised for months without the
|
|
2879
|
+
// catalog having it; the promise is now true.
|
|
2880
|
+
//
|
|
2881
|
+
// Polymorphic per the enum-vs-polymorphism ADR (2026-06-16), on all three
|
|
2882
|
+
// arms: universality holds (any entity can be dragged onto a canvas, with no
|
|
2883
|
+
// principled stopping point), constraint value is nil (nobody could defend
|
|
2884
|
+
// "you may arrange features but not personas"), and there is NO STRUCTURAL
|
|
2885
|
+
// ROLE. That third arm is load-bearing rather than decorative.
|
|
2886
|
+
// `classification: 'cross-domain'` is therefore mandatory, not cosmetic: it
|
|
2887
|
+
// keeps the edge out of `UPG_VALID_CHILDREN` and `resolveContainmentEdge`, so
|
|
2888
|
+
// `get_tree` never walks it and a canvas placement can never masquerade as
|
|
2889
|
+
// containment. An arranged node keeps its real containment parent. Same
|
|
2890
|
+
// posture as `framework_exercise_includes_node`.
|
|
2891
|
+
//
|
|
2892
|
+
// The alternative was putting the whole canvas snapshot in one opaque node
|
|
2893
|
+
// property, which is cheaper and loses. A node id inside a blob is a foreign
|
|
2894
|
+
// key held as a scalar (the exact thing P14 forbids), the placed entities
|
|
2895
|
+
// become invisible to `get_node` and `query`, and a deleted node leaves a
|
|
2896
|
+
// stale reference that `repair_dangling_edges` cannot see. As an edge, that
|
|
2897
|
+
// guard is inherited for free. Only the furniture with no graph referent
|
|
2898
|
+
// stays opaque, in `workspace.properties.canvas`.
|
|
2899
|
+
//
|
|
2900
|
+
// NOT cross_product_eligible: deliberate scope limit mirroring the WS3
|
|
2901
|
+
// anchors, not an oversight.
|
|
2902
|
+
workspace_arranges_node: { forward_verb: "arranges", reverse_verb: "arranged_in", classification: "cross-domain", source_type: "workspace", target_type: "node", carries_properties: true, property_schema: WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA },
|
|
2903
|
+
// What a published composition SHOWS. Passes the same three-arm ADR test the
|
|
2904
|
+
// same way, and carries no properties: a focus is a bare reference.
|
|
2905
|
+
//
|
|
2906
|
+
// It earns its place by making a composition legible to a tool that cannot
|
|
2907
|
+
// parse the publishing tool's URLs. "Which published views show this
|
|
2908
|
+
// persona?" is a LEARN question the graph exists to answer, and the edge is
|
|
2909
|
+
// what makes a composition's graph references repairable when a focused node
|
|
2910
|
+
// is deleted. Adding it in a later release would be a second blast-radius
|
|
2911
|
+
// event on the same track.
|
|
2912
|
+
//
|
|
2913
|
+
// POPULATION IS BEST-EFFORT and an empty focus set is VALID: a composition
|
|
2914
|
+
// whose member hrefs the writing tool cannot resolve to node ids is still a
|
|
2915
|
+
// well-formed composition. That keeps the spec complete without blocking on
|
|
2916
|
+
// app work, and it is why no check fires on an unfocused composition.
|
|
2917
|
+
//
|
|
2918
|
+
// NOT cross_product_eligible: a portfolio-level composition spanning several
|
|
2919
|
+
// product graphs is conceivable but unevidenced. Same YAGNI call as above.
|
|
2920
|
+
composition_focuses_node: { forward_verb: "focuses", reverse_verb: "focused_in", classification: "cross-domain", source_type: "composition", target_type: "node" },
|
|
2707
2921
|
// ── P14 edges (replacing foreign-key properties) ─────────────────────────
|
|
2708
2922
|
feature_addresses_job: { forward_verb: "addresses", reverse_verb: "addressed_by", classification: "cross-domain", source_type: "feature", target_type: "job" },
|
|
2709
2923
|
feature_drives_key_result: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "feature", target_type: "key_result" },
|
|
@@ -3748,6 +3962,9 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
|
|
|
3748
3962
|
"framework_exercise_includes_node",
|
|
3749
3963
|
// Universal classification (any node classified against a classification_value)
|
|
3750
3964
|
"node_classified_as_classification_value",
|
|
3965
|
+
// Benchmark subject (0.31.0): an eval measures a tool, a doc, a check, an
|
|
3966
|
+
// importer, a feature. Over-wide by admission — see the definition's comment.
|
|
3967
|
+
"eval_benchmark_measures_node",
|
|
3751
3968
|
// Work-item issue links (0.20.0): endpoint-polymorphic over the work-item set
|
|
3752
3969
|
// {feature, epic, user_story, task, bug}. The `work_item_` key names the
|
|
3753
3970
|
// intended semantic domain; the endpoints are the `node` wildcard.
|
|
@@ -3756,7 +3973,12 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
|
|
|
3756
3973
|
"work_item_duplicates_work_item",
|
|
3757
3974
|
// Workspace provenance (WS3, 2026-07-05): a workspace can produce any
|
|
3758
3975
|
// entity type arranged in it. Widened from workspace_produced_decision.
|
|
3759
|
-
"workspace_produced_node"
|
|
3976
|
+
"workspace_produced_node",
|
|
3977
|
+
// Canvas arrangement and published-view focus: any entity can be dragged
|
|
3978
|
+
// onto a canvas or shown in a published view, and neither carries a
|
|
3979
|
+
// structural role, so both collapse polymorphic rather than enumerating.
|
|
3980
|
+
"workspace_arranges_node",
|
|
3981
|
+
"composition_focuses_node"
|
|
3760
3982
|
];
|
|
3761
3983
|
var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
|
|
3762
3984
|
function isPolymorphicEdge(key) {
|
|
@@ -3947,6 +4169,10 @@ var UPG_VALID_CHILDREN = {
|
|
|
3947
4169
|
"roadmap",
|
|
3948
4170
|
"roadmap_theme",
|
|
3949
4171
|
"planning_cycle",
|
|
4172
|
+
// Configuration (0.30.0): the levers whose values select which surface tree
|
|
4173
|
+
// the product renders. Few per product, and top-level because an axis is a
|
|
4174
|
+
// product-wide fact rather than a child of any one feature or surface.
|
|
4175
|
+
"configuration_axis",
|
|
3950
4176
|
// Engineering
|
|
3951
4177
|
"bounded_context",
|
|
3952
4178
|
"code_repository",
|
|
@@ -4037,7 +4263,12 @@ var UPG_VALID_CHILDREN = {
|
|
|
4037
4263
|
"subscription",
|
|
4038
4264
|
"forecast",
|
|
4039
4265
|
// Program Management
|
|
4266
|
+
// `milestone` is a SECOND, shallower parent here (Portfolio Phase 2,
|
|
4267
|
+
// 2026-08-14), alongside its existing `project` parent: a product can own a
|
|
4268
|
+
// milestone outright with no program/project above it. Required by the
|
|
4269
|
+
// hierarchy-orphan guard now that `product_targets_milestone` exists.
|
|
4040
4270
|
"program",
|
|
4271
|
+
"milestone",
|
|
4041
4272
|
// Marketing
|
|
4042
4273
|
"marketing_strategy",
|
|
4043
4274
|
"press_release",
|
|
@@ -4564,12 +4795,303 @@ var UPG_CONTAINMENT_FREE_TYPES = /* @__PURE__ */ new Set([
|
|
|
4564
4795
|
// containment. Modelling it as a product child would force a containment edge
|
|
4565
4796
|
// and read against the whole point of the exercise model (relational, not
|
|
4566
4797
|
// hierarchical). Same posture as `person`. See ADR 2026-06-02-framework-exercises.
|
|
4567
|
-
"framework_exercise"
|
|
4798
|
+
"framework_exercise",
|
|
4799
|
+
// A composition has no structural parent for the same reason a
|
|
4800
|
+
// framework_exercise does not: it anchors to what it SHOWS, relationally, via
|
|
4801
|
+
// `composition_focuses_node`. Modelling it as a product child would force a
|
|
4802
|
+
// containment edge that reads against the point — a published view is not a
|
|
4803
|
+
// part of the product, it is a lens onto one. This is also what keeps the
|
|
4804
|
+
// type to two edges rather than three: no provenance edge is minted (that is
|
|
4805
|
+
// `workspace_produced_node`, reused) and no parent edge is needed.
|
|
4806
|
+
"composition"
|
|
4568
4807
|
]);
|
|
4569
4808
|
function isContainmentFreeType(entityType) {
|
|
4570
4809
|
return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
|
|
4571
4810
|
}
|
|
4572
4811
|
|
|
4812
|
+
// src/grammar/projection.ts
|
|
4813
|
+
var VARIES_BY_EDGE = "surface_varies_by_configuration_axis";
|
|
4814
|
+
var PRESENT_UNDER_PROPERTY = "present_under";
|
|
4815
|
+
var ACTIVE_WHEN_PROPERTY = "active_when";
|
|
4816
|
+
var QUALIFIABLE_EDGE_TYPES = [
|
|
4817
|
+
"surface_contains_surface",
|
|
4818
|
+
"feature_occupies_surface"
|
|
4819
|
+
];
|
|
4820
|
+
function readStringArray(properties, key) {
|
|
4821
|
+
const raw = properties?.[key];
|
|
4822
|
+
if (!Array.isArray(raw)) return void 0;
|
|
4823
|
+
return raw.filter((v) => typeof v === "string");
|
|
4824
|
+
}
|
|
4825
|
+
function readActiveWhen(edge) {
|
|
4826
|
+
const raw = edge.properties?.[ACTIVE_WHEN_PROPERTY];
|
|
4827
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
4828
|
+
const axis = raw.axis;
|
|
4829
|
+
const values = readStringArray(raw, "values");
|
|
4830
|
+
if (typeof axis !== "string" || axis.length === 0 || !values || values.length === 0) {
|
|
4831
|
+
return void 0;
|
|
4832
|
+
}
|
|
4833
|
+
return { axis, values };
|
|
4834
|
+
}
|
|
4835
|
+
function projectGraph(nodes, edges, configuration) {
|
|
4836
|
+
const axes = Object.keys(configuration);
|
|
4837
|
+
if (axes.length === 0) {
|
|
4838
|
+
return {
|
|
4839
|
+
nodes: [...nodes],
|
|
4840
|
+
edges: [...edges],
|
|
4841
|
+
excluded_node_ids: [],
|
|
4842
|
+
deactivated_edge_count: 0,
|
|
4843
|
+
dangling_edge_count: 0
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4846
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
4847
|
+
for (const edge of edges) {
|
|
4848
|
+
if (edge.type !== VARIES_BY_EDGE) continue;
|
|
4849
|
+
const chosen = configuration[edge.target];
|
|
4850
|
+
if (chosen === void 0) continue;
|
|
4851
|
+
const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
|
|
4852
|
+
if (!presentUnder) continue;
|
|
4853
|
+
if (!presentUnder.includes(chosen)) excluded.add(edge.source);
|
|
4854
|
+
}
|
|
4855
|
+
const survivingNodes = excluded.size === 0 ? [...nodes] : nodes.filter((n) => !excluded.has(n.id));
|
|
4856
|
+
let deactivated = 0;
|
|
4857
|
+
let dangling = 0;
|
|
4858
|
+
const survivingEdges = [];
|
|
4859
|
+
for (const edge of edges) {
|
|
4860
|
+
const qualifier = readActiveWhen(edge);
|
|
4861
|
+
if (qualifier) {
|
|
4862
|
+
const chosen = configuration[qualifier.axis];
|
|
4863
|
+
if (chosen !== void 0 && !qualifier.values.includes(chosen)) {
|
|
4864
|
+
deactivated++;
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
if (excluded.has(edge.source) || excluded.has(edge.target)) {
|
|
4869
|
+
dangling++;
|
|
4870
|
+
continue;
|
|
4871
|
+
}
|
|
4872
|
+
survivingEdges.push(edge);
|
|
4873
|
+
}
|
|
4874
|
+
return {
|
|
4875
|
+
nodes: survivingNodes,
|
|
4876
|
+
edges: survivingEdges,
|
|
4877
|
+
excluded_node_ids: [...excluded].sort(),
|
|
4878
|
+
deactivated_edge_count: deactivated,
|
|
4879
|
+
dangling_edge_count: dangling
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
function enumerateProjections(nodes) {
|
|
4883
|
+
const out = [
|
|
4884
|
+
{ configuration: {} }
|
|
4885
|
+
];
|
|
4886
|
+
for (const node of nodes) {
|
|
4887
|
+
if (node.type !== "configuration_axis") continue;
|
|
4888
|
+
const values = readStringArray(node.properties, "values");
|
|
4889
|
+
if (!values) continue;
|
|
4890
|
+
for (const value of values) {
|
|
4891
|
+
out.push({ axis: node.id, value, configuration: { [node.id]: value } });
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
return out;
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
// src/grammar/configuration-drift.ts
|
|
4898
|
+
var ALTERNATES_EDGE = "surface_alternates_with_surface";
|
|
4899
|
+
var PARENT_EDGE_TYPES = /* @__PURE__ */ new Set(["surface_contains_surface", "screen_renders_surface"]);
|
|
4900
|
+
function edgeLabel(edge) {
|
|
4901
|
+
return edge.id ?? `${edge.source} -> ${edge.target}`;
|
|
4902
|
+
}
|
|
4903
|
+
function checkConfigurationDrift(nodes, edges) {
|
|
4904
|
+
const findings = [];
|
|
4905
|
+
const axisValues = /* @__PURE__ */ new Map();
|
|
4906
|
+
for (const node of nodes) {
|
|
4907
|
+
if (node.type !== "configuration_axis") continue;
|
|
4908
|
+
const values = readStringArray(node.properties, "values");
|
|
4909
|
+
if (!values || values.length === 0) {
|
|
4910
|
+
findings.push({
|
|
4911
|
+
kind: "axis_values_empty",
|
|
4912
|
+
severity: "error",
|
|
4913
|
+
node_id: node.id,
|
|
4914
|
+
axis_id: node.id,
|
|
4915
|
+
message: "Configuration axis declares no values. An axis with no values selects nothing, so no projection can be taken along it."
|
|
4916
|
+
});
|
|
4917
|
+
axisValues.set(node.id, []);
|
|
4918
|
+
continue;
|
|
4919
|
+
}
|
|
4920
|
+
axisValues.set(node.id, values);
|
|
4921
|
+
const rawDefault = node.properties?.default_value;
|
|
4922
|
+
if (typeof rawDefault === "string" && rawDefault.length > 0 && !values.includes(rawDefault)) {
|
|
4923
|
+
findings.push({
|
|
4924
|
+
kind: "axis_default_not_a_value",
|
|
4925
|
+
severity: "error",
|
|
4926
|
+
node_id: node.id,
|
|
4927
|
+
axis_id: node.id,
|
|
4928
|
+
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.`
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
for (const edge of edges) {
|
|
4933
|
+
if (edge.type !== VARIES_BY_EDGE) continue;
|
|
4934
|
+
const values = axisValues.get(edge.target);
|
|
4935
|
+
const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
|
|
4936
|
+
if (!presentUnder || presentUnder.length === 0) {
|
|
4937
|
+
findings.push({
|
|
4938
|
+
kind: "present_under_empty",
|
|
4939
|
+
severity: "error",
|
|
4940
|
+
edge_id: edge.id,
|
|
4941
|
+
node_id: edge.source,
|
|
4942
|
+
axis_id: edge.target,
|
|
4943
|
+
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.`
|
|
4944
|
+
});
|
|
4945
|
+
continue;
|
|
4946
|
+
}
|
|
4947
|
+
if (!values) continue;
|
|
4948
|
+
for (const value of presentUnder) {
|
|
4949
|
+
if (!values.includes(value)) {
|
|
4950
|
+
findings.push({
|
|
4951
|
+
kind: "present_under_unknown_value",
|
|
4952
|
+
severity: "error",
|
|
4953
|
+
edge_id: edge.id,
|
|
4954
|
+
node_id: edge.source,
|
|
4955
|
+
axis_id: edge.target,
|
|
4956
|
+
message: `present_under names "${value}", which the axis does not declare. Its values are [${values.join(", ")}].`
|
|
4957
|
+
});
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
for (const edge of edges) {
|
|
4962
|
+
const hasQualifierKey = edge.properties !== void 0 && Object.prototype.hasOwnProperty.call(edge.properties, ACTIVE_WHEN_PROPERTY);
|
|
4963
|
+
if (!hasQualifierKey) continue;
|
|
4964
|
+
if (edge.type !== void 0 && !QUALIFIABLE_EDGE_TYPES.includes(edge.type)) {
|
|
4965
|
+
findings.push({
|
|
4966
|
+
kind: "qualifier_on_illegal_edge",
|
|
4967
|
+
severity: "error",
|
|
4968
|
+
edge_id: edge.id,
|
|
4969
|
+
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.`
|
|
4970
|
+
});
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4973
|
+
const qualifier = readActiveWhen(edge);
|
|
4974
|
+
if (!qualifier) {
|
|
4975
|
+
const rawQualifier = edge.properties?.[ACTIVE_WHEN_PROPERTY];
|
|
4976
|
+
const rawValues = rawQualifier && typeof rawQualifier === "object" && !Array.isArray(rawQualifier) ? rawQualifier.values : void 0;
|
|
4977
|
+
if (Array.isArray(rawValues) && rawValues.length === 0) {
|
|
4978
|
+
findings.push({
|
|
4979
|
+
kind: "qualifier_values_empty",
|
|
4980
|
+
severity: "error",
|
|
4981
|
+
edge_id: edge.id,
|
|
4982
|
+
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.`
|
|
4983
|
+
});
|
|
4984
|
+
continue;
|
|
4985
|
+
}
|
|
4986
|
+
findings.push({
|
|
4987
|
+
kind: "qualifier_axis_unresolved",
|
|
4988
|
+
severity: "error",
|
|
4989
|
+
edge_id: edge.id,
|
|
4990
|
+
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.`
|
|
4991
|
+
});
|
|
4992
|
+
continue;
|
|
4993
|
+
}
|
|
4994
|
+
const values = axisValues.get(qualifier.axis);
|
|
4995
|
+
if (!values) {
|
|
4996
|
+
findings.push({
|
|
4997
|
+
kind: "qualifier_axis_unresolved",
|
|
4998
|
+
severity: "error",
|
|
4999
|
+
edge_id: edge.id,
|
|
5000
|
+
axis_id: qualifier.axis,
|
|
5001
|
+
message: `active_when names axis "${qualifier.axis}", which is not a configuration_axis node in this graph.`
|
|
5002
|
+
});
|
|
5003
|
+
continue;
|
|
5004
|
+
}
|
|
5005
|
+
for (const value of qualifier.values) {
|
|
5006
|
+
if (!values.includes(value)) {
|
|
5007
|
+
findings.push({
|
|
5008
|
+
kind: "qualifier_unknown_value",
|
|
5009
|
+
severity: "error",
|
|
5010
|
+
edge_id: edge.id,
|
|
5011
|
+
axis_id: qualifier.axis,
|
|
5012
|
+
message: `active_when names value "${value}", which axis "${qualifier.axis}" does not declare. Its values are [${values.join(", ")}].`
|
|
5013
|
+
});
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
const varianceBySource = /* @__PURE__ */ new Map();
|
|
5018
|
+
for (const edge of edges) {
|
|
5019
|
+
if (edge.type !== VARIES_BY_EDGE) continue;
|
|
5020
|
+
const values = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY) ?? [];
|
|
5021
|
+
const list = varianceBySource.get(edge.source) ?? [];
|
|
5022
|
+
list.push({ axis: edge.target, values });
|
|
5023
|
+
varianceBySource.set(edge.source, list);
|
|
5024
|
+
}
|
|
5025
|
+
for (const edge of edges) {
|
|
5026
|
+
if (edge.type !== ALTERNATES_EDGE) continue;
|
|
5027
|
+
const left = varianceBySource.get(edge.source) ?? [];
|
|
5028
|
+
const right = varianceBySource.get(edge.target) ?? [];
|
|
5029
|
+
if (left.length === 0 || right.length === 0) {
|
|
5030
|
+
findings.push({
|
|
5031
|
+
kind: "alternation_axis_mismatch",
|
|
5032
|
+
severity: "error",
|
|
5033
|
+
edge_id: edge.id,
|
|
5034
|
+
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.`
|
|
5035
|
+
});
|
|
5036
|
+
continue;
|
|
5037
|
+
}
|
|
5038
|
+
const sharedAxes = left.filter((l) => right.some((r) => r.axis === l.axis)).map((l) => l.axis);
|
|
5039
|
+
if (sharedAxes.length === 0) {
|
|
5040
|
+
findings.push({
|
|
5041
|
+
kind: "alternation_axis_mismatch",
|
|
5042
|
+
severity: "error",
|
|
5043
|
+
edge_id: edge.id,
|
|
5044
|
+
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.`
|
|
5045
|
+
});
|
|
5046
|
+
continue;
|
|
5047
|
+
}
|
|
5048
|
+
for (const axis of sharedAxes) {
|
|
5049
|
+
const leftValues = left.find((l) => l.axis === axis)?.values ?? [];
|
|
5050
|
+
const rightValues = right.find((r) => r.axis === axis)?.values ?? [];
|
|
5051
|
+
const overlap = leftValues.filter((v) => rightValues.includes(v));
|
|
5052
|
+
if (overlap.length > 0) {
|
|
5053
|
+
findings.push({
|
|
5054
|
+
kind: "alternation_overlap",
|
|
5055
|
+
severity: "error",
|
|
5056
|
+
edge_id: edge.id,
|
|
5057
|
+
axis_id: axis,
|
|
5058
|
+
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.`
|
|
5059
|
+
});
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
const parentsByChild = /* @__PURE__ */ new Map();
|
|
5064
|
+
for (const edge of edges) {
|
|
5065
|
+
if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
|
|
5066
|
+
parentsByChild.set(edge.target, (parentsByChild.get(edge.target) ?? 0) + 1);
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
for (const [axisId, values] of axisValues) {
|
|
5070
|
+
for (const value of values) {
|
|
5071
|
+
const projected = projectGraph(nodes, edges, { [axisId]: value });
|
|
5072
|
+
const survivingParents = /* @__PURE__ */ new Map();
|
|
5073
|
+
for (const edge of projected.edges) {
|
|
5074
|
+
if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
|
|
5075
|
+
survivingParents.set(edge.target, (survivingParents.get(edge.target) ?? 0) + 1);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
for (const node of projected.nodes) {
|
|
5079
|
+
if (node.type !== "surface") continue;
|
|
5080
|
+
if ((parentsByChild.get(node.id) ?? 0) === 0) continue;
|
|
5081
|
+
if ((survivingParents.get(node.id) ?? 0) > 0) continue;
|
|
5082
|
+
findings.push({
|
|
5083
|
+
kind: "orphaned_under_projection",
|
|
5084
|
+
severity: "warning",
|
|
5085
|
+
node_id: node.id,
|
|
5086
|
+
axis_id: axisId,
|
|
5087
|
+
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.`
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
return findings;
|
|
5093
|
+
}
|
|
5094
|
+
|
|
4573
5095
|
// src/grammar/lifecycles.ts
|
|
4574
5096
|
var PRODUCT_LIFECYCLE = {
|
|
4575
5097
|
entity_type: "product",
|
|
@@ -6657,6 +7179,34 @@ var FRAMEWORK_EXERCISE_LIFECYCLE = {
|
|
|
6657
7179
|
}
|
|
6658
7180
|
]
|
|
6659
7181
|
};
|
|
7182
|
+
var COMPOSITION_LIFECYCLE = {
|
|
7183
|
+
entity_type: "composition",
|
|
7184
|
+
initial_phase: "draft",
|
|
7185
|
+
terminal_phases: ["archived"],
|
|
7186
|
+
phases: [
|
|
7187
|
+
{
|
|
7188
|
+
id: "draft",
|
|
7189
|
+
status_category: "started",
|
|
7190
|
+
label: "Draft",
|
|
7191
|
+
description: "Being arranged. It has a slug reserved and members placed, but has never been published, so nothing resolves at its address yet.",
|
|
7192
|
+
transitions_to: ["published", "archived"]
|
|
7193
|
+
},
|
|
7194
|
+
{
|
|
7195
|
+
id: "published",
|
|
7196
|
+
status_category: "completed",
|
|
7197
|
+
label: "Published",
|
|
7198
|
+
description: "Live at its slug and linkable. Republishing the same slug stays in this phase and bumps the revision rather than moving the composition.",
|
|
7199
|
+
transitions_to: ["archived", "draft"]
|
|
7200
|
+
},
|
|
7201
|
+
{
|
|
7202
|
+
id: "archived",
|
|
7203
|
+
status_category: "completed",
|
|
7204
|
+
label: "Archived",
|
|
7205
|
+
description: "Withdrawn from the published set but retained, so an existing link resolves to a view marked out of date rather than to nothing. Can be republished.",
|
|
7206
|
+
transitions_to: ["published", "draft"]
|
|
7207
|
+
}
|
|
7208
|
+
]
|
|
7209
|
+
};
|
|
6660
7210
|
var SPECIFICATION_LIFECYCLE = {
|
|
6661
7211
|
entity_type: "specification",
|
|
6662
7212
|
initial_phase: "draft",
|
|
@@ -6788,6 +7338,7 @@ var UPG_LIFECYCLES = [
|
|
|
6788
7338
|
GROWTH_CAMPAIGN_LIFECYCLE,
|
|
6789
7339
|
// Workspace
|
|
6790
7340
|
FRAMEWORK_EXERCISE_LIFECYCLE,
|
|
7341
|
+
COMPOSITION_LIFECYCLE,
|
|
6791
7342
|
// ── Template-generated lifecycles ─────────────────────────────────
|
|
6792
7343
|
...TEMPLATE_LIFECYCLES
|
|
6793
7344
|
];
|
|
@@ -6820,6 +7371,10 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
|
|
|
6820
7371
|
"market_segment",
|
|
6821
7372
|
"classification_axis",
|
|
6822
7373
|
"classification_value",
|
|
7374
|
+
// ── Product Specification (1): a configuration_axis is a structural
|
|
7375
|
+
// DECLARATION (a lever and its closed value set), not a thing that
|
|
7376
|
+
// progresses through states. Same reading as classification_axis above.
|
|
7377
|
+
"configuration_axis",
|
|
6823
7378
|
// ── User Research (5 of 8): participants, quotes, observations,
|
|
6824
7379
|
// clusters, survey responses are immutable data points ────────────────
|
|
6825
7380
|
"participant",
|
|
@@ -7675,8 +8230,7 @@ var UPG_MIGRATIONS = {
|
|
|
7675
8230
|
{
|
|
7676
8231
|
from: "metric_definition",
|
|
7677
8232
|
to: "metric",
|
|
7678
|
-
|
|
7679
|
-
reason: "A metric definition is a metric without implementation. Lifecycle property, not a separate type."
|
|
8233
|
+
reason: "A metric definition is a metric without implementation. Lifecycle state, not a separate type. (Corrected in v0.26.0: the default previously stamped `has_implementation: false`, a property `metric` has never declared. Dropped rather than declared, because a metric with no live reading simply has no `current_value`, and a boolean restating that would immediately drift from it.)"
|
|
7680
8234
|
},
|
|
7681
8235
|
// ── Experiment consolidation ────────────────────────────────────────────
|
|
7682
8236
|
{
|
|
@@ -7701,8 +8255,8 @@ var UPG_MIGRATIONS = {
|
|
|
7701
8255
|
{
|
|
7702
8256
|
from: "risk_item",
|
|
7703
8257
|
to: "risk",
|
|
7704
|
-
defaults: {
|
|
7705
|
-
reason: "risk_item (Program Mgmt) is identical to risk (Legal). Consolidated
|
|
8258
|
+
defaults: { risk_type: "program" },
|
|
8259
|
+
reason: "risk_item (Program Mgmt) is identical to risk (Legal). Consolidated onto the single kind axis, `risk_type`. (Corrected in v0.26.0: the default previously named `risk_domain`, a property `risk` has never declared; `program` was added to the `risk_type` enum rather than giving risk a second, drift-prone classification vocabulary.)"
|
|
7706
8260
|
},
|
|
7707
8261
|
{
|
|
7708
8262
|
from: "security_incident",
|
|
@@ -7713,8 +8267,8 @@ var UPG_MIGRATIONS = {
|
|
|
7713
8267
|
{
|
|
7714
8268
|
from: "defect_report",
|
|
7715
8269
|
to: "support_ticket",
|
|
7716
|
-
defaults: {
|
|
7717
|
-
reason: "Defect report is a support ticket with
|
|
8270
|
+
defaults: { ticket_type: "bug" },
|
|
8271
|
+
reason: "Defect report is a support ticket with ticket_type=bug. Same signal interface. (Corrected: the default previously named `ticket_designation`, a property `support_ticket` has never declared, so every migrated node was stamped with an undeclared field.)"
|
|
7718
8272
|
},
|
|
7719
8273
|
{
|
|
7720
8274
|
from: "onboarding_flow",
|
|
@@ -10736,7 +11290,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
10736
11290
|
acceptance_criterion: {
|
|
10737
11291
|
condition: { type: "string", description: "Required condition (Given/When/Then or plain text)" },
|
|
10738
11292
|
test_type: { type: "string", enum: ["manual", "automated"], description: "Test mode" },
|
|
10739
|
-
pass_status: { type: "string", enum: ["untested", "pass", "fail"], description: "Current
|
|
11293
|
+
pass_status: { type: "string", enum: ["untested", "pass", "fail", "regressed", "blocked"], description: "Current verification state of this criterion. `untested` means never attempted; `blocked` means attempted but not verifiable for an environmental reason; `regressed` means previously passing, now failing.", notes: '`blocked` is a distinct state from `untested` (a missing credential or an unreachable dependency is not the same as nobody having tried), which the earlier three-value enum conflated into a silent gap. `regressed` is a criterion-level state and is NOT derivable here, because `acceptance_criterion` stores current state only and keeps no result history. The history lives on the `test_case` to `test_result` series, where a single execution\'s outcome is `TestResultProperties.result_status` and can never itself be "regressed".' }
|
|
10740
11294
|
},
|
|
10741
11295
|
// AccessPolicyProperties: Access policy.
|
|
10742
11296
|
access_policy: {
|
|
@@ -11183,14 +11737,14 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11183
11737
|
// ClassificationAxisProperties: ClassificationAxis: a dimension along which subjects are classified.
|
|
11184
11738
|
classification_axis: {
|
|
11185
11739
|
axis_kind: { type: "string", enum: ["categorical", "ordinal", "continuous"], description: "Structural kind of values on this axis. `categorical` = discrete, unordered (most common; CMS architectures). `ordinal` = discrete, ordered (maturity tiers, T-shirt sizes). `continuous` = numeric range (latency budget, price points)." },
|
|
11186
|
-
cardinality: { type: "string", enum: ["single", "multi"], description:
|
|
11740
|
+
cardinality: { type: "string", enum: ["single", "multi"], description: "How many values a subject may hold on this axis at once. `single` (the default) means re-classifying SUPERSEDES the prior value; `multi` means it ADDS one.", notes: 'Under `single` the classify writer retires the old same-axis edge and records the move in the reclassification history, so the change is traceable rather than silent. `multi` suits an axis like "supported frameworks", where holding several values at once is the truth. A separate axis from `axis_kind`: an axis can be `categorical` (unordered) yet single-select, or `categorical` yet multi-select. The two answer different questions and neither implies the other.' }
|
|
11187
11741
|
},
|
|
11188
11742
|
// ClassificationValueProperties: ClassificationValue: a value on a classification axis.
|
|
11189
11743
|
classification_value: {
|
|
11190
11744
|
rationale: { type: "string", description: "Short paragraph: why this value earns its own row/column. Longer narrative belongs in `summary_md` or attached `content_piece` nodes." },
|
|
11191
11745
|
exemplars: { type: "string[]", description: "Free-text examples of products occupying this value. For queryable occupancy, prefer `competitor` nodes with `classified_as` edges. @example ['Nimbus', 'Larch', 'Prism']" },
|
|
11192
|
-
commitments: { type: "object[]", description: "The
|
|
11193
|
-
capabilities: { type: "object[]", description: "Structured capability bullets across the six canonical surfaces. Each surface appears at most once per `classification_value`.
|
|
11746
|
+
commitments: { type: "object[]", description: "The load-bearing commitments that define this category, typically two to four. Each is a structural axis: removing it changes the category.", notes: "Pairs with `capabilities`, and the distinction matters when deciding where a bullet belongs: commitments are the load-bearing DEFINITION, capabilities are the surface OFFERING. A capability can be dropped without the category changing; a commitment cannot. A worked set: [ { name: 'Typed content graph', description: 'Schemas in code; references as edges; content is structured data.' }, { name: 'Real-time backend', description: 'Live queries, CRDT collaboration, sub-second propagation.' }, { name: 'Embeddable studio', description: 'Editor is a library hosted inside the team app, not an external portal.' }, ]" },
|
|
11747
|
+
capabilities: { type: "object[]", description: "Structured capability bullets across the six canonical surfaces. Each surface appears at most once per `classification_value`. @example [ { surface: 'delivery', bullets: ['NQL', 'GraphQL', 'REST', 'Live Sync API', 'Asset CDN'] }, { surface: 'extensibility', bullets: ['Custom input components', 'editor plugins'] }, ]", notes: "Pairs with `commitments`, and the distinction decides where a bullet belongs: capabilities describe what the category OFFERS, commitments describe what DEFINES it. Dropping a capability leaves the category intact; dropping a commitment does not." }
|
|
11194
11748
|
},
|
|
11195
11749
|
// CodeRepositoryProperties: Code repository.
|
|
11196
11750
|
code_repository: {
|
|
@@ -11225,7 +11779,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11225
11779
|
analysis_type: { type: "string", enum: ["feature_comparison", "positioning", "swot", "pricing"], description: "Type of analysis. `feature_comparison` = side-by-side matrix. `positioning` = competitor positioning relative to each other. `swot` = strengths, weaknesses, opportunities, threats. `pricing` = pricing structure comparison." },
|
|
11226
11780
|
analysis_date: { type: "string", description: 'ISO date conducted. Competitive intelligence decays quickly; track snapshot age. @example "2026-03-15"' },
|
|
11227
11781
|
framework_id: { type: "string", description: 'Framework ID (references `UPGFramework.id`). @example "porter-five-forces", "swot-analysis", "competitive-matrix"' },
|
|
11228
|
-
empty_cells: { type: "object[]", description: "Empty cells in a
|
|
11782
|
+
empty_cells: { type: "object[]", description: "Empty cells in a two-axis classification matrix that earn explicit commentary, and only the strategically interesting ones. Opportunity-kind cells are the most valuable: they identify unoccupied strategic space.", notes: "Each entry references two `classification_value` nodes by id, one from each `classification_axis` child of this `competitive_analysis`. `validate_graph` enforces that the refs resolve to `classification_value` nodes whose parents are distinct `classification_axis` instances, so a cell cannot name two values from the same axis. See `ClassificationValueProperties.commitments` for the inverse \"occupied cell\" structural-definition shape. A worked pair, one of each rationale kind: [ { axis_a_value_ref: 'val-git-based', axis_b_value_ref: 'val-structured-text', rationale_kind: 'opportunity', rationale_md: 'No technical reason; only adoption inertia. Watching brief.' }, { axis_a_value_ref: 'val-composable', axis_b_value_ref: 'val-wysiwyg', rationale_kind: 'structural', rationale_md: 'Composable rejects HTML blobs; WYSIWYG requires them.' }, ]" },
|
|
11229
11783
|
last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. Distinct from `analysis_date` (when the analysis was conducted). @example "2026-06-13"' },
|
|
11230
11784
|
source: { type: "string", description: 'Provenance: where this was observed. A changelog, pricing, or docs URL, an analyst report, or a research note. @example "https://docs.larch.example/changelog/"' },
|
|
11231
11785
|
confidence: {
|
|
@@ -11295,7 +11849,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11295
11849
|
// CompetitorSignalProperties: Competitor signal: a dated competitor move mapped onto our portfolio.
|
|
11296
11850
|
competitor_signal: {
|
|
11297
11851
|
observed_at: { type: "string", description: 'ISO date-time the move was observed. @example "2026-06-10"' },
|
|
11298
|
-
signal_type: { type: "string", enum: ["feature_launch", "pricing_change", "acquisition", "partnership", "market_entry", "reclassification"], description: "Kind of move
|
|
11852
|
+
signal_type: { type: "string", enum: ["feature_launch", "pricing_change", "acquisition", "partnership", "market_entry", "reclassification"], description: "Kind of move: a shipped `feature_launch`, a `pricing_change`, the strategic `acquisition` / `partnership` / `market_entry`, or a `reclassification` when the competitor moves between classification cells on an axis.", notes: "`reclassification` is auto-emitted at the classify-write chokepoint rather than authored, and carries `axis`, `from_value`, `to_value` and `competitor` so the move is reconstructable without diffing two snapshots." },
|
|
11299
11853
|
summary: { type: "string", description: "One-line factual summary of the move (what shipped, not marketing copy)." },
|
|
11300
11854
|
impact: { type: "string", enum: ["high", "medium", "low"], description: "Expected impact on our position." },
|
|
11301
11855
|
competitor: { type: "string", description: "Reclassification only. The qualified id of the competitor that moved, as the classify cross-edge source (e.g. `p_rival/n_acme`). Identifies both the subject and its owning product, so `diff_classification({ product })` can filter the history stream." },
|
|
@@ -11327,7 +11881,21 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11327
11881
|
// ComplianceRequirementProperties: Compliance requirement.
|
|
11328
11882
|
compliance_requirement: {
|
|
11329
11883
|
regulation: { type: "string", enum: ["gdpr", "ccpa", "hipaa", "soc2", "iso27001", "pci_dss", "other"], description: "Regulation or standard this requirement derives from" },
|
|
11330
|
-
compliance_status: { type: "string", enum: ["compliant", "non_compliant", "in_progress", "not_applicable"], description: "Current compliance posture" }
|
|
11884
|
+
compliance_status: { type: "string", enum: ["compliant", "non_compliant", "in_progress", "not_applicable"], description: "Current compliance posture" },
|
|
11885
|
+
owner: { type: "string", description: "Accountable person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." }
|
|
11886
|
+
},
|
|
11887
|
+
// CompositionProperties
|
|
11888
|
+
composition: {
|
|
11889
|
+
members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
|
|
11890
|
+
rev: { type: "number", description: "Monotonic revision, bumped on each republish of the same slug.", notes: "A published revision is a fact about the composition, user-visible as a new print from the same plate, so it is serialised. Worth distinguishing from a store's concurrency token, which shares the name in some backends but is a fact about the table rather than about the thing, and is not spec data." },
|
|
11891
|
+
published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
|
|
11892
|
+
published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." }
|
|
11893
|
+
},
|
|
11894
|
+
// ConfigurationAxisProperties: A named dimension along which the product's composition differs.
|
|
11895
|
+
configuration_axis: {
|
|
11896
|
+
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. Every `present_under` and `active_when.values` entry must name one of them.", notes: "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." },
|
|
11897
|
+
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: an unqualified read returns the union, not this projection.", notes: "The union is the honest answer to an unqualified question, because it is every configuration at once; 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." },
|
|
11898
|
+
kind: { type: "string", enum: ["feature_flag", "plan_tier", "permission_level", "beta_program", "other"], description: "What kind of lever this is. Names the mechanism family." }
|
|
11331
11899
|
},
|
|
11332
11900
|
// ConstraintProperties: Constraint: a named limitation or boundary on product creation.
|
|
11333
11901
|
constraint: {
|
|
@@ -11342,7 +11910,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11342
11910
|
contact: {
|
|
11343
11911
|
contact_role: { type: "string", description: "Job title or role within the account" },
|
|
11344
11912
|
is_decision_maker: { type: "boolean", description: "Whether this person has purchasing authority" },
|
|
11345
|
-
buying_role: { type: "string", enum: ["champion", "economic_buyer", "technical_evaluator", "end_user", "detractor", "influencer", "procurement", "legal", "security"], description: "Role this contact plays in the buying committee
|
|
11913
|
+
buying_role: { type: "string", enum: ["champion", "economic_buyer", "technical_evaluator", "end_user", "detractor", "influencer", "procurement", "legal", "security"], description: "Role this contact plays in the buying committee, the decision-making unit. @example 'economic_buyer'", notes: "The substrate of enterprise multi-threading and of qualification frameworks such as MEDDICC and SPICED: a deal with no `champion` and no `economic_buyer` mapped is single-threaded and at risk, which is a fact the graph can answer rather than a judgement someone has to make. `is_decision_maker` becomes largely derivable from this (`buying_role = economic_buyer`) but is kept for back-compat." }
|
|
11346
11914
|
},
|
|
11347
11915
|
// ContentCalendarProperties: Content calendar.
|
|
11348
11916
|
content_calendar: {
|
|
@@ -11463,7 +12031,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11463
12031
|
data_contract: {
|
|
11464
12032
|
retention_period: { type: "string", description: "How long data is retained before deletion" },
|
|
11465
12033
|
deletion_policy: { type: "string", description: "Policy governing data deletion" },
|
|
11466
|
-
third_party_sharing: { type: "boolean", description: "Whether data is shared with third parties" }
|
|
12034
|
+
third_party_sharing: { type: "boolean", description: "Whether data is shared with third parties" },
|
|
12035
|
+
owner: { type: "string", description: "Owning person or team accountable for the contract. Promote to a `node_owned_by_team` edge if ownership must be queryable." }
|
|
11467
12036
|
},
|
|
11468
12037
|
// DataDomainProperties: DataDomain. A coarse-grained grouping of related data assets (sources,
|
|
11469
12038
|
data_domain: {
|
|
@@ -11509,7 +12078,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11509
12078
|
// DataProductProperties: DataProduct entity.
|
|
11510
12079
|
data_product: {
|
|
11511
12080
|
data_product_type: { type: "string", enum: ["report", "dataset", "stream", "api", "ml_feature", "other"], description: "Classification of the data product (UPG-579 Option B)." },
|
|
11512
|
-
sla_freshness: { type: "string", description: 'Freshness SLA commitment (e.g. "< 1 hour")' }
|
|
12081
|
+
sla_freshness: { type: "string", description: 'Freshness SLA commitment (e.g. "< 1 hour")' },
|
|
12082
|
+
owner: { type: "string", description: "Owning person or team. Sibling of `data_domain.steward`: use `owner` for the accountable party of a single data product, `steward` for a whole domain. Promote to a `node_owned_by_team` edge if ownership must be queryable." }
|
|
11513
12083
|
},
|
|
11514
12084
|
// DataQualityRuleProperties: DataQualityRule entity.
|
|
11515
12085
|
data_quality_rule: {
|
|
@@ -11539,7 +12109,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11539
12109
|
deal_value: { type: "number", description: "Monetary value of the deal" },
|
|
11540
12110
|
close_date: { type: "string", description: "Expected close date (ISO format)" },
|
|
11541
12111
|
probability: { type: "number", description: "Likelihood of closing (0-100%)" },
|
|
11542
|
-
deal_outcome: { type: "string", enum: ["won", "lost", "no_decision"], description: "Terminal result of the deal. An Event-axis outcome (the verdict on a one-time event)
|
|
12112
|
+
deal_outcome: { type: "string", enum: ["won", "lost", "no_decision"], description: "Terminal result of the deal: the win or loss verdict a closed deal carries. @example 'won'", notes: "An Event-axis outcome (the verdict on a one-time event) and NOT a lifecycle phase, so it is `deal_outcome` rather than `deal_status`, per status-convention Rule 3. Lifecycle (open, won and lost as phases) belongs on the base `status` slot. This field is what gives `deal_lost_to_competitor` and win-loss study derivations their anchor." },
|
|
11543
12113
|
deal_type: { type: "string", enum: ["new_business", "expansion", "renewal"], description: "Motion this deal belongs to. Mirrors `PipelineSales.pipeline_type` at the deal grain; `renewal` is the value `subscription_renews_via_deal` points at. @example 'expansion'" },
|
|
11544
12114
|
next_step: { type: "string", description: "The single most-used CRM field: the next concrete action to move the deal. Free text on purpose (a coordination note, not a structured task). @example 'Send security questionnaire to procurement'" },
|
|
11545
12115
|
next_step_date: { type: "string", description: "When the `next_step` is due (ISO format)." },
|
|
@@ -11555,7 +12125,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11555
12125
|
normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
|
|
11556
12126
|
},
|
|
11557
12127
|
required: ["value", "label"]
|
|
11558
|
-
}
|
|
12128
|
+
},
|
|
12129
|
+
owner: { type: "string", description: "Accountable person or team carrying the deal. Promote to a `node_owned_by_person` edge if ownership must be queryable." }
|
|
11559
12130
|
},
|
|
11560
12131
|
// DecisionProperties: Decision record. Strategic, product, engineering, or design.
|
|
11561
12132
|
decision: {
|
|
@@ -11768,7 +12339,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11768
12339
|
},
|
|
11769
12340
|
// EpicProperties: A collection of related user stories that delivers a feature or capability.
|
|
11770
12341
|
epic: {
|
|
11771
|
-
|
|
12342
|
+
effort: { type: "string", description: 'Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team. Canonical name for the work-item size family (`epic`, `user_story`, `task`).' },
|
|
12343
|
+
estimate: { type: "string", description: 'Rough size estimate (e.g. "3 sprints", "L", "13 points"). @deprecated STAGED, release number assigned at release prep (docket Track "Docket wave 2"). Use `effort`, the family-uniform name already carried by `user_story` and `task`. `estimate` was epic\'s lone divergent spelling of the same concept and it caused real misreads (the app read `estimate` on `user_story`, where only `effort` is declared). Kept (not removed) for back-compat; removal is a later major. Writers: emit `effort`. Readers: prefer `effort`, fall back to `estimate`.' },
|
|
11772
12344
|
priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Task-level priority" },
|
|
11773
12345
|
owner: { type: "string", description: "Responsible person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
|
|
11774
12346
|
start_date: { type: "string", description: "ISO date work begins" },
|
|
@@ -11785,7 +12357,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11785
12357
|
},
|
|
11786
12358
|
// EvalBenchmarkProperties: Evaluation benchmark.
|
|
11787
12359
|
eval_benchmark: {
|
|
11788
|
-
benchmark_type: { type: "string", enum: ["accuracy", "latency", "cost", "safety", "custom"], description: "Measured dimension" },
|
|
12360
|
+
benchmark_type: { type: "string", enum: ["accuracy", "latency", "cost", "safety", "precision_recall", "task_success", "coherence", "custom"], description: "Measured dimension.", notes: 'Widened in 0.31.0 with the three dimensions an eval suite actually reports and this set could not express: `precision_recall` (two numbers, not one accuracy figure, and the pair is the point, because a detector that fires on everything scores perfectly on recall alone), `task_success` (did the agent achieve the goal), and `coherence` (rubric-graded, for multi-step runs). THIS ENUM IS A DIMENSION AXIS, AND IT SHOULD STAY ONE. An eval suite\'s families are a SUBJECT axis (what is under test), and a dimension cannot separate subjects, which is why several families legitimately share `task_success`. The subject is expressed on the EDGE, not here: `eval_benchmark_measures_node` names what a benchmark measures by pointing at it. So "which benchmarks cover the importer" is a traversal, and this property stays a clean answer to a different question, "what dimension does this benchmark report". The commissioning brief hoped the enum would carry both; splitting them across the enum and the edge is the better answer, and it needs no third mechanism. Corollary worth stating, since it is load-bearing: do NOT add subject-shaped values here. `tool_use` or `documentation` would encode on this axis what the edge already carries, and the two would drift the first time a benchmark measured something its enum value did not admit.' },
|
|
11789
12361
|
test_case_count: { type: "number", description: "Test cases in the suite", modifier: "derived" },
|
|
11790
12362
|
passing_threshold: { type: "number", description: "Minimum passing score" },
|
|
11791
12363
|
last_run: { type: "string", description: "ISO date of the most recent run" }
|
|
@@ -11801,7 +12373,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11801
12373
|
output_token_count: { type: "number", description: "Output tokens" },
|
|
11802
12374
|
cost: { type: "number", description: "Total run cost" },
|
|
11803
12375
|
error_rate: { type: "number", description: "Percentage of test cases that errored" },
|
|
11804
|
-
feedback_scores: { type: "string", description: "Feedback score summary (human or automated)" }
|
|
12376
|
+
feedback_scores: { type: "string", description: "Feedback score summary (human or automated)" },
|
|
12377
|
+
metric_scores: { type: "object[]", description: "Per-metric scores, one entry per measured metric.", notes: "Added in 0.31.0 because `score` is a single aggregate and the forcing case needs two numbers: precision and recall, reported separately. An aggregate hides the only interesting failure mode, which is a detector that catches everything by firing on everything. `sample_size` is not optional decoration, and as of 0.31.0 the type says so: it is a REQUIRED member of `MetricScore`. A sampled run reports its sample size beside its score, always, so a comparison across runs can never quietly compare different sample sizes. A number without its denominator is not a smaller measurement, it is not a measurement, and a remark saying `always` over a field typed optional is a contract only the careful reader honours. The entry shape is the exported `MetricScore` interface rather than an anonymous literal, so a consumer can name the thing it is building. This is one of two `object[]` properties in the spec; the other, `composition.members`, already exports `CompositionMember`. The rejected alternative was two runs per benchmark, which needs no spec change and makes every question about a benchmark's quality a join, encoding a measurement artifact as graph structure." }
|
|
11805
12378
|
},
|
|
11806
12379
|
// EventProperties: Event.
|
|
11807
12380
|
event: {
|
|
@@ -11905,6 +12478,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11905
12478
|
},
|
|
11906
12479
|
// ExperimentRunProperties: Execution evidence for a structured test of a hypothesis (UCS pattern P6: event-occurrence).
|
|
11907
12480
|
experiment_run: {
|
|
12481
|
+
experiment_type: { type: "string", enum: ["ab_test", "growth", "pricing"], description: "What sort of experiment this run is: `ab_test` is a controlled split between variants, `growth` an acquisition or activation experiment, `pricing` a packaging or willingness-to-pay one. Distinct from `disposition`, which is the outcome axis.", notes: "Three retired types (`ab_test`, `growth_experiment`, `pricing_experiment`) collapsed into `experiment_run` precisely along this axis, and the migration defaults stamp it so the distinction survives consolidation. Extensible: further kinds may be added in a minor release." },
|
|
11908
12482
|
actual_start_date: { type: "string", description: "ISO actual start date (may differ from the plan's `planned_start_date`)" },
|
|
11909
12483
|
actual_end_date: { type: "string", description: "ISO actual end date" },
|
|
11910
12484
|
actual_reach: { type: "number", description: "Observed reach: how many people the run actually touched" },
|
|
@@ -12072,7 +12646,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12072
12646
|
// FrameworkExerciseProperties: Framework exercise: one run of a framework over a set of entities.
|
|
12073
12647
|
framework_exercise: {
|
|
12074
12648
|
framework_id: { type: "string", description: "Which framework this exercise runs: a framework id (e.g. 'moscow', 'rice-scoring', 'kano-model'). Resolves against the framework catalog." },
|
|
12075
|
-
inputs_snapshot: { type: "object", description: "Optional frozen copy of the framework's input spec at apply time, so a historical exercise still renders correctly if the framework definition later evolves (inputs added, removed, or rescaled)." }
|
|
12649
|
+
inputs_snapshot: { type: "object", description: "Optional frozen copy of the framework's input spec at apply time, so a historical exercise still renders correctly if the framework definition later evolves (inputs added, removed, or rescaled)." },
|
|
12650
|
+
input_weights: { type: "object", description: "Relative multiplier per framework input for this run, keyed by input id. @example const properties: FrameworkExerciseProperties = { framework_id: 'rice-scoring', input_weights: { reach: 1, impact: 2, confidence: 1, effort: 1.5 }, }", notes: "A weight belongs to the RUN, not to any entity it scores: one weight per input, shared by every scored entity, which is why it sits here and not on the `framework_exercise_includes_node` edge alongside the per-entity result. Named `input_weights` rather than a bare `weight` deliberately, and the collision is live rather than hypothetical: `getPropertyDefaultScale` keys on property NAME alone and ignores entity type, and `PROPERTY_SCALE_MAP` already maps `weight` to the `importance_5` ordinal. A bare `weight` here would silently resolve to a 1-5 assessment scale, which is the wrong type, range and meaning for a multiplier. The plural also matches the spec's own vocabulary: a framework declares *inputs*, not dimensions." }
|
|
12076
12651
|
},
|
|
12077
12652
|
// FunnelProperties: Funnel entity.
|
|
12078
12653
|
funnel: {
|
|
@@ -12089,7 +12664,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12089
12664
|
// GlossaryTermProperties: GlossaryTerm entity.
|
|
12090
12665
|
glossary_term: {
|
|
12091
12666
|
term_definition: { type: "string", description: "Plain-language definition of the term" },
|
|
12092
|
-
synonyms: { type: "string[]", description: "Alternative names or abbreviations for this term" }
|
|
12667
|
+
synonyms: { type: "string[]", description: "Alternative names or abbreviations for this term" },
|
|
12668
|
+
owner: { type: "string", description: "Person or team accountable for keeping the definition current. Promote to a `node_owned_by_person` edge if ownership must be queryable." }
|
|
12093
12669
|
},
|
|
12094
12670
|
// GrowthCampaignProperties: GrowthCampaign entity.
|
|
12095
12671
|
growth_campaign: {
|
|
@@ -12162,7 +12738,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12162
12738
|
// HypothesisEvidenceProperties
|
|
12163
12739
|
hypothesis_evidence: {
|
|
12164
12740
|
evidence_type: { type: "string", enum: ["experiment_run", "observation", "quote", "metric_change", "market_data", "interview"], description: "Kind of evidence. Drives renderer + filter UI. The provenance edge (`derived_from_*`) carries the actual source node reference per P14; this enum is typing/UI metadata." },
|
|
12165
|
-
direction: { type: "string", enum: ["supports", "refutes", "neutral"], description: "Direction relative to the parent claim
|
|
12741
|
+
direction: { type: "string", enum: ["supports", "refutes", "neutral"], description: "Direction relative to the parent claim: `supports` and `refutes` pair with the edges of the same name, `neutral` means the data is insufficient or noisy. BREAKING in v0.4.0: `confirms`, `disconfirms` and `inconclusive` no longer type-check. Migration is one-to-one: confirms to supports, disconfirms to refutes, inconclusive to neutral.", notes: "Aligned to `Evidence.direction` so every direction-of-evidence property in the spec shares one vocabulary." },
|
|
12166
12742
|
weight: {
|
|
12167
12743
|
type: "assessment",
|
|
12168
12744
|
scale_id: "importance_5",
|
|
@@ -12232,6 +12808,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12232
12808
|
novelty: { type: "string", enum: ["known", "surprising", "contradictory"], description: "Novelty against existing knowledge. `known` = confirms what we already believed. `surprising` = challenges or extends our understanding. `contradictory` = directly conflicts with a prior assumption." },
|
|
12233
12809
|
actionability: { type: "string", enum: ["immediate", "needs_validation", "informational"], description: "Current actionability. `immediate` = clear action, no further research needed. `needs_validation` = promising but requires more evidence. `informational` = important context, no direct action." },
|
|
12234
12810
|
source_method: { type: "string", description: 'Producing research method. @example "usability_study", "interview_series", "survey"' },
|
|
12811
|
+
source_domain: { type: "string", description: 'Which discipline the insight came out of. The second provenance axis, symmetrical with `source_method`: that one is HOW the insight was produced, this one is WHICH PRACTICE produced it, and neither substitutes for the other. @example "ux", "support", "sales", "data_science"', notes: "Declared in v0.26.0 so the retired `ux_insight` type's migration default (`source_domain: 'ux'`) lands in a typed field rather than being lost." },
|
|
12235
12812
|
statement: { type: "string", description: 'Insight statement in plain language. Write as an active, present-tense assertion. @example "Users consistently skip the tutorial because they trust their ability to explore independently."' },
|
|
12236
12813
|
implications: { type: "string", description: "Product implications. The so-what." }
|
|
12237
12814
|
},
|
|
@@ -12253,8 +12830,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12253
12830
|
},
|
|
12254
12831
|
// InternalDocProperties: Internal document.
|
|
12255
12832
|
internal_doc: {
|
|
12256
|
-
doc_type: { type: "string", enum: ["rfc", "runbook", "guide", "spec", "onboarding", "other"], description: "Classification of the document" },
|
|
12257
|
-
url: { type: "string", description: "URL or path to the document" }
|
|
12833
|
+
doc_type: { type: "string", enum: ["rfc", "runbook", "guide", "spec", "onboarding", "other"], description: "Classification of the document. @deprecated use `document.document_type`" },
|
|
12834
|
+
url: { type: "string", description: "URL or path to the document. @deprecated use `document.source_url`" }
|
|
12258
12835
|
},
|
|
12259
12836
|
// InterviewGuideProperties: Interview guide document.
|
|
12260
12837
|
interview_guide: {
|
|
@@ -12612,7 +13189,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12612
13189
|
range_max: { type: "number", description: "Maximum expected or ceiling value" },
|
|
12613
13190
|
cadence: { type: "string", enum: ["continuous", "hourly", "daily", "weekly", "monthly", "quarterly", "yearly", "on_demand", "other"], description: "Measurement cadence. Canonical `Cadence` since v0.4.0. `'realtime'` migrates to `'continuous'`; all other values 1:1." },
|
|
12614
13191
|
owner: { type: "string", description: "Person or team responsible for tracking this metric. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
|
|
12615
|
-
metric_health: { type: "string", enum: ["healthy", "at_risk", "unhealthy", "unknown"], description: "Universal health rollup
|
|
13192
|
+
metric_health: { type: "string", enum: ["healthy", "at_risk", "unhealthy", "unknown"], description: "Universal health rollup, applying to every metric regardless of `designation`: `healthy` is inside range, `at_risk` is drifting toward a breach or shortfall, `unhealthy` has missed or breached, `unknown` has no current reading.", notes: "Orthogonal to lifecycle and to `guardrail_status`. For guardrails specifically, `guardrail_status` remains the breach-specific signal (`safe` / `warning` / `breached`), so a guardrail carries both: one says how it is trending, the other says whether it has been crossed." },
|
|
12616
13193
|
guardrail_threshold_min: { type: "number", description: "Lower bound for guardrail safety (below this = breach)" },
|
|
12617
13194
|
guardrail_threshold_max: { type: "number", description: "Upper bound for guardrail safety (above this = breach)" },
|
|
12618
13195
|
guardrail_status: { type: "string", enum: ["safe", "warning", "breached"], description: "Current guardrail health state" }
|
|
@@ -12632,7 +13209,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12632
13209
|
},
|
|
12633
13210
|
// MilestoneProperties: Milestone.
|
|
12634
13211
|
milestone: {
|
|
12635
|
-
milestone_order: { type: "number", description: "Display order of this milestone within its parent
|
|
13212
|
+
milestone_order: { type: "number", description: "Display order of this milestone within its parent (0-indexed). The scalar ordering convention shared with `journey_step.step_order` and `journey_action.action_order` (UPG-663 / UPG-674). Orders the delivery milestones a parent moves through, independent of `due_date`.", notes: "The parent is whichever of the two milestone parents the graph actually uses: a `project` via `project_targets_milestone`, or a `product` directly via `product_targets_milestone` (Portfolio Phase 2, for milestones a product owns outright with no program or project above them). The order is scoped to that one parent, never global." },
|
|
12636
13213
|
due_date: { type: "string", description: "Target due date (ISO format)" },
|
|
12637
13214
|
met_on_time: { type: "boolean", description: "Whether the milestone was met on time" }
|
|
12638
13215
|
},
|
|
@@ -12862,8 +13439,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12862
13439
|
motivation: { type: "string", description: 'Primary motivation or driving need @example "Making confident, evidence-based decisions"' },
|
|
12863
13440
|
tech_comfort: { type: "string", enum: ["low", "medium", "high", "expert", "other"], description: "Tech comfort. Closed set so personas across products compare on the same axis. Free-text colour belongs in `context` or `motivation`." },
|
|
12864
13441
|
domain_expertise: { type: "string", description: 'Industry or domain knowledge this persona brings @example "10+ years SaaS experience", "New to healthcare IT"' },
|
|
12865
|
-
audience_role: { type: "string", enum: ["buyer", "user", "champion", "influencer", "partner"], description: 'Role in the buying
|
|
12866
|
-
actor_kind: { type: "string", enum: ["human", "agent", "system"], description: 'Who performs the work: a human archetype
|
|
13442
|
+
audience_role: { type: "string", enum: ["buyer", "user", "champion", "influencer", "partner"], description: 'Role in the buying or adoption decision: who signs (`buyer`), who uses (`user`), who advocates internally (`champion`), who shapes the choice (`influencer`), and who delivers it (`partner`). A closed set, so roles compare across products. @example "buyer"', notes: "A portfolio must separate the economic buyer from the practitioner user: they are distinct personas with distinct jobs, and collapsing them is how a product ends up built for whoever was easiest to interview." },
|
|
13443
|
+
actor_kind: { type: "string", enum: ["human", "agent", "system"], description: 'Who performs the work: a human archetype, an autonomous AI agent, or a non-agentic platform service. Absent means `human`, so existing personas need no migration. @example "agent"', notes: "An `agent` persona is a first-class actor: it participates in journeys through the same persona machinery, and humans delegate to it via `persona_delegates_to_persona`. Human-coverage and segmentation metrics count `human` only, so agent and system personas are opt-in rather than silently inflating a coverage number." }
|
|
12867
13444
|
},
|
|
12868
13445
|
// PipelineSalesProperties: Sales pipeline.
|
|
12869
13446
|
pipeline_sales: {
|
|
@@ -12949,7 +13526,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12949
13526
|
url: { type: "string", description: "Where the product lives. Marketing site, app store URL, etc.", modifier: "volatile" },
|
|
12950
13527
|
logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
|
|
12951
13528
|
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
|
|
12952
|
-
described_configuration: { type: "string", description:
|
|
13529
|
+
described_configuration: { type: "string", description: 'The product configuration this graph describes, named in plain language. A label for readers: nothing reads it to gate, filter, or alter how a tool interprets the graph. @example "Enterprise plan, split-navigation flag on"', notes: "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. It makes no claim about which configuration most customers are on. Where a product genuinely ships several and the differences matter, declare a `configuration_axis` instead and let each fact say which values it holds under." }
|
|
12953
13530
|
},
|
|
12954
13531
|
// ProductAreaProperties: ProductArea entity.
|
|
12955
13532
|
product_area: {
|
|
@@ -13001,7 +13578,10 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13001
13578
|
qa_session: {
|
|
13002
13579
|
session_type: { type: "string", enum: ["exploratory", "regression", "smoke", "uat"], description: "Type of QA session" },
|
|
13003
13580
|
duration_minutes: { type: "number", description: "Duration of the session in minutes" },
|
|
13004
|
-
bugs_found: { type: "number", description: "Number of bugs found during the session" }
|
|
13581
|
+
bugs_found: { type: "number", description: "Number of bugs found during the session" },
|
|
13582
|
+
environment: { type: "string", enum: ["local", "ci", "staging", "sandbox", "production_mirror"], description: "Environment the session was run against. Single-valued, unlike `TestPlanProperties.environments` (a plan spans several); the enum is the same one, mirroring `TestEnvironmentProperties.env_type`." },
|
|
13583
|
+
outcome_summary: { type: "string", description: "Plain-English outcome of the session: what it established, not how many bugs it counted (`bugs_found`). Named `outcome_summary`, matching `ExperimentRunProperties.outcome_summary`, because a bare `outcome` would collide with the `outcome` entity type." },
|
|
13584
|
+
executed_at: { type: "string", description: "ISO timestamp of when the session was run. Mirrors `TestResultProperties.executed_at`." }
|
|
13005
13585
|
},
|
|
13006
13586
|
// QueueTopicProperties: Message queue or topic.
|
|
13007
13587
|
queue_topic: {
|
|
@@ -13068,7 +13648,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13068
13648
|
},
|
|
13069
13649
|
// ReleaseStrategyProperties: Release strategy.
|
|
13070
13650
|
release_strategy: {
|
|
13071
|
-
strategy_type: { type: "string", enum: ["blue_green", "canary", "rolling", "recreate", "feature_flag"], description: 'How a new version reaches production.
|
|
13651
|
+
strategy_type: { type: "string", enum: ["blue_green", "canary", "rolling", "recreate", "feature_flag"], description: 'How a new version reaches production. @example "canary" for gradual rollout, "blue_green" for instant switch with instant rollback', notes: "`blue_green` switches instantly between two identical environments. `canary` rolls out by percentage. `rolling` replaces instances incrementally. `recreate` takes the old down before bringing the new up. `feature_flag` ships the code with the features gated." },
|
|
13072
13652
|
canary_percentage: { type: "number", description: "Traffic routed to canary. Applies when `strategy_type === 'canary'`. @example 5 (5% initial canary before full rollout)" },
|
|
13073
13653
|
rollback_criteria: { type: "string", description: 'Rollback triggers, automatic or manual. @example "Error rate > 1% over 5 minutes", "Latency p99 > 2x baseline"' },
|
|
13074
13654
|
bake_time: { type: "string", description: 'Soak time before promoting to full production. @example "30m", "2h", "24h"' },
|
|
@@ -13129,7 +13709,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13129
13709
|
},
|
|
13130
13710
|
// RiskProperties: Risk.
|
|
13131
13711
|
risk: {
|
|
13132
|
-
risk_type: { type: "string", enum: ["technical", "business", "legal", "security", "operational"], description: "Domain the risk belongs to" },
|
|
13712
|
+
risk_type: { type: "string", enum: ["technical", "business", "legal", "security", "operational", "program"], description: "Domain the risk belongs to. The single kind axis for `risk`: there is no second classification vocabulary. `program` added in v0.26.0 so the retired `risk_item` (Program Management) type has a home on the canonical `risk` after consolidation, rather than a parallel `risk_domain` field free to drift from this one." },
|
|
13133
13713
|
probability: {
|
|
13134
13714
|
type: "assessment",
|
|
13135
13715
|
scale_id: "confidence_5",
|
|
@@ -13277,7 +13857,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13277
13857
|
effective_date: { type: "string", description: "ISO effective date" },
|
|
13278
13858
|
url: { type: "string", description: "Policy document URL" },
|
|
13279
13859
|
policy_status: { type: "string", enum: ["draft", "active", "under_review", "retired"], description: "Lifecycle status" },
|
|
13280
|
-
rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Imperative force" }
|
|
13860
|
+
rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Imperative force" },
|
|
13861
|
+
owner: { type: "string", description: "Owning person or team accountable for the policy. Promote to a `node_owned_by_team` edge if ownership must be queryable." }
|
|
13281
13862
|
},
|
|
13282
13863
|
// SecurityReviewProperties: Security review.
|
|
13283
13864
|
security_review: {
|
|
@@ -13477,12 +14058,12 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13477
14058
|
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." },
|
|
13478
14059
|
persistence: { type: "string", enum: ["always", "conditional", "on_demand", "transient"], description: "How reliably the surface is present." },
|
|
13479
14060
|
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"' },
|
|
13480
|
-
capacity: { type: "number", description:
|
|
14061
|
+
capacity: { type: "number", description: "How many occupants ONE INSTANCE of the surface holds at once, as a non-negative count. Absent means unbounded, `0` means a reserved place nothing may occupy. Always the cap the design INTENDS, never what was observed rendering. Count the instances themselves with `cardinality`.", notes: "Intent, not observation: a banner region declared `capacity: 1` keeps saying 1 even after someone finds it rendering four. Reality that has drifted from the declared intent belongs on `surface_deviates_via_technical_debt_item` as a trackable, assignable debt item. Editing this number up to match the bug would destroy the only record that a gap exists. Why absence rather than a sentinel: UPG has no union-typed property primitive, so `integer | unbounded` is expressed as an optional number whose absence carries the \"no cap\" reading, instead of a magic value every consumer would have to special-case. Three states, all different. ABSENT is unbounded, no cap stated. `0` is a reserved place. `null` is neither, and nothing this field defines: it is what an explicit null write leaves behind, and no consumer reads it as a cap. To return the property to absent, remove the key with `update_node`'s or `batch_update_nodes`' `unset_properties`, since a property merge preserves anything omitted. Unbounded is not an exemption from scrutiny. The contention detector reads an absent capacity as a threshold of 1, because a surface that states no limit has stated no answer either, so several occupants is exactly the unrecorded decision worth naming. Declaring a real capacity is the way to quiet the check honestly, and the only way that also records something true." },
|
|
13481
14062
|
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." },
|
|
13482
14063
|
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.' },
|
|
13483
14064
|
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." },
|
|
13484
|
-
arbitration_rule: { type: "string", description: 'Who wins when more occupants want the surface than `capacity` allows, and why.
|
|
13485
|
-
arbitration_state: { type: "string", enum: ["enforced_documented", "enforced_undocumented", "safe_by_coincidence", "none"], description:
|
|
14065
|
+
arbitration_rule: { type: "string", description: 'Who wins when more occupants want the surface than `capacity` allows, and why. Absence is meaningful: on a contested surface it means nobody decided. @example "Highest priority wins; ties break to the most recently updated."', notes: "An empty or absent rule on a contested surface is exactly what `contended-surface-without-arbitration` detects. Do not fill it in with a placeholder to silence the check: the check exists to find the unrecorded decision, and a placeholder hides it without settling anything. The field is overloaded across composition modes, which is worth knowing before writing one. On an `exclusive` surface it records DISPLACEMENT (who is not rendered). On an `additive` surface everyone fits, so what it records is ORDER. Only the displacement reading is what the contention detector reads." },
|
|
14066
|
+
arbitration_state: { type: "string", enum: ["enforced_documented", "enforced_undocumented", "safe_by_coincidence", "none", "no_contention_by_design"], description: "Whether the arbitration answer is enforced, written down, both, neither, or not owed at all. Leaving the field unset means unassessed, and always did.", notes: 'The separations are the whole point of the field, because absence alone conflates four situations with four different remediations: "enforced in code but never transcribed" is ten minutes of typing, "never decided" is a design meeting, "safe only because nothing has collided yet" is the dangerous one, and "nothing to decide, the occupants never compete" is a chained surface, which owes no arbitration rule at all.' },
|
|
13486
14067
|
extensibility: { type: "string", enum: ["closed", "plugin_registerable", "user_configurable"], description: "Who may add occupants to the surface." },
|
|
13487
14068
|
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." },
|
|
13488
14069
|
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"' }
|
|
@@ -13650,7 +14231,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13650
14231
|
},
|
|
13651
14232
|
// TestSuiteProperties: Test suite.
|
|
13652
14233
|
test_suite: {
|
|
13653
|
-
suite_type: { type: "string", enum: ["unit", "integration", "e2e", "performance", "security", "accessibility"], description: "Category of test suite" },
|
|
14234
|
+
suite_type: { type: "string", enum: ["unit", "integration", "e2e", "performance", "security", "accessibility", "visual"], description: "Category of test suite. The single canonical verification-method vocabulary in the spec: do not mint a second one at criterion level.", notes: "`visual` covers visual-regression and screenshot-diff suites, whose evidence is a rendered-output comparison rather than an assertion over behaviour. Before it existed, producers mapped visual suites onto `integration` as the least-wrong stock value, which made them indistinguishable from genuine integration coverage." },
|
|
13654
14235
|
test_count: { type: "number", description: "Number of tests in the suite", modifier: "derived" },
|
|
13655
14236
|
pass_rate: { type: "number", description: "Percentage of tests passing (0-100)", modifier: "snapshot" },
|
|
13656
14237
|
last_run: { type: "string", description: "ISO date of last execution" },
|
|
@@ -13693,7 +14274,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13693
14274
|
},
|
|
13694
14275
|
// ThreatModelProperties: Threat model.
|
|
13695
14276
|
threat_model: {
|
|
13696
|
-
methodology: { type: "string", enum: ["stride", "dread", "pasta", "attack_tree", "other"], description: '
|
|
14277
|
+
methodology: { type: "string", enum: ["stride", "dread", "pasta", "attack_tree", "other"], description: 'Threat-modelling methodology: `stride`, `dread`, `pasta`, or `attack_tree`. @example "stride" is the most widely used methodology for web applications', notes: "`stride` is the OWASP standard (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege). `dread` is numeric scoring (Damage, Reproducibility, Exploitability, Affected users, Discoverability). `pasta` is the seven-stage Process for Attack Simulation and Threat Analysis. `attack_tree` is a hierarchical tree of attack paths." },
|
|
13697
14278
|
scope: { type: "string", description: 'What system, feature, or data flow is being analysed. @example "user authentication", "payment processing", "admin API"' },
|
|
13698
14279
|
last_reviewed: { type: "string", description: 'ISO date last reviewed. Models become stale as systems evolve. @example "2026-03-01"' },
|
|
13699
14280
|
participants: { type: "string", description: 'Participants in the exercise. Promote individuals to `node_owned_by_person` edges if participation must be queryable. @example "Alice Chen (security lead), Bob Park (backend engineer), Carol Liu (architect)"' },
|
|
@@ -13745,6 +14326,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13745
14326
|
},
|
|
13746
14327
|
// UserFlowProperties: User flow.
|
|
13747
14328
|
user_flow: {
|
|
14329
|
+
flow_type: { type: "string", description: 'What sort of flow this is. Free-form rather than a closed enum, because flow kinds are product-specific and the spec has no complete vocabulary for them. @example "onboarding", "checkout", "recovery", "upgrade"', notes: "`user_flow` otherwise carries only structure (`trigger`, `steps`, `success_state`, `failure_state`) and no classification. The retired `onboarding_flow` type collapsed into `user_flow` along this axis, and its migration default stamps the value so the distinction survives." },
|
|
13748
14330
|
flow_order: { type: "number", description: "Display order of this flow among sibling flows (0-indexed). The scalar ordering convention shared with `journey_step.step_order` and `journey_action.action_order` (UPG-663 / UPG-674). The free-text `steps` array below still captures the within-flow narrative; this scalar makes the flow itself a deterministically orderable sibling." },
|
|
13749
14331
|
trigger: { type: "string", description: "Initiating event" },
|
|
13750
14332
|
steps: { type: "string[]", description: "Ordered steps" },
|
|
@@ -13807,7 +14389,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13807
14389
|
severity: {
|
|
13808
14390
|
type: "assessment",
|
|
13809
14391
|
scale_id: "severity_5",
|
|
13810
|
-
description: "Categorical severity derived from CVSS
|
|
14392
|
+
description: "Categorical severity derived from CVSS, as a UPGAssessment on the `severity_5` scale. Score alone is insufficient for triage: severity is what drives filtering and prioritisation. @example value 5, label 'Critical', scale_id 'severity_5' (a remotely exploitable, no-auth vuln)",
|
|
14393
|
+
notes: "Migrated from the inline `critical|high|medium|low|informational` enum (UPG-579 Option C): map `critical` to 5, `high` to 4, `medium` to 3, `low` to 2, `informational` to 1, carrying the old word in `label` so the original vocabulary survives the move.",
|
|
13811
14394
|
properties: {
|
|
13812
14395
|
value: { type: "number", description: "The numeric value, used for computation." },
|
|
13813
14396
|
label: { type: "string", description: "The qualitative label (what the assessor meant)." },
|
|
@@ -13879,12 +14462,14 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13879
14462
|
workspace: {
|
|
13880
14463
|
visibility: { type: "string", enum: ["private", "shared", "public"], description: "Who can see this workspace" },
|
|
13881
14464
|
purpose: { type: "string", description: "Free-text description. Pairs with the closed-enum `workspace_purpose`." },
|
|
13882
|
-
workspace_purpose: { type: "string", enum: ["discovery", "planning", "retrospective", "design", "research", "strategy", "general"], description: "
|
|
14465
|
+
workspace_purpose: { type: "string", enum: ["discovery", "planning", "retrospective", "design", "research", "strategy", "general"], description: "What the workspace is for. Drives template suggestions and surfaces in workspace browsers.", notes: "`discovery` is persona, job and opportunity exploration. `planning` covers roadmap and decision sessions. `retrospective` is reflection on shipped work. `design` is experience or UI exploration. `research` organises study data and synthesis. `strategy` is high-level direction setting. `general` is the catch-all." },
|
|
13883
14466
|
owner: { type: "string", description: "Workspace owner (handle or email). Display label; canonical owner is `team_owns_workspace` or `persona_owns_workspace`." },
|
|
13884
14467
|
member_count: { type: "number", description: "Snapshot count. `team_works_in_workspace` edges are the source of truth.", modifier: "derived" },
|
|
13885
14468
|
archived: { type: "boolean", description: "Archived. Archived workspaces remain queryable but hidden from default views." },
|
|
13886
14469
|
archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`." },
|
|
13887
|
-
icon: { type: "string", description: "Display icon (emoji or icon name)" }
|
|
14470
|
+
icon: { type: "string", description: "Display icon (emoji or icon name)" },
|
|
14471
|
+
retention: { type: "string", enum: ["transient", "durable"], description: "Retention intent. Absent means `transient`.", notes: "A workspace is a free-form thinking space and is transient by default, so the spec makes both intents EXPRESSIBLE and takes no position on which reaches the file. Whether to write a transient canvas at all is a tool decision, and the recommended posture is not to: a scratch canvas nobody named has no business in a shared, git-tracked graph, where it lands in everyone's diff. Writing only `durable` workspaces is what keeps a canonical graph entity comfortable with a transient-by-default object." },
|
|
14472
|
+
canvas: { type: "object", description: "Opaque canvas furniture: the parts of a canvas with no graph referent. Preserved verbatim on round-trip and never interpreted. Tool extension keys are namespaced `<tool>:<key>` with a colon, and no consumer interprets a key it does not own.", notes: "The cut here is UPG principle P14 applied literally. Anything that is a REFERENCE TO A GRAPH NODE is an edge, which is why placed entities ride `workspace_arranges_node` and not this bag: a node id held as a scalar inside a blob is a foreign key in disguise, and a deleted node would leave a stale reference nothing can detect. Anything that is pure UI chrome with no graph referent stays here, because minting `annotation` and `frame` entity types would add catalog surface that is meaningless to every consumer outside the tool that drew it. WHY THE COLON, and why the rule is in the summary rather than buried here. An underscore key is indistinguishable from an ordinary property name, so a migration that targets namespaced keys cannot match it and no validator can detect one that should have been namespaced. That undetectability is why enforcement lives in the type and the documentation instead of in a check: a check would report clean on a bag full of underscore keys, which is worse than no check. Two conventions were already coexisting when this was ruled (`entopo_views` in a live writer, `entopo:view_blocks` in the contract test certifying preservation), which is how a cheap rule becomes a migration. PRESERVATION IS NOT PERMISSION TO RENDER. Preserving every byte says nothing about meaning: a field-measured canvas carried `excluded: true` tombstones, and a consumer that preserved them faithfully while rendering every entry showed the user images they had deleted." }
|
|
13888
14473
|
}
|
|
13889
14474
|
};
|
|
13890
14475
|
function getPropertySchema(entityType) {
|
|
@@ -14928,134 +15513,384 @@ var UPG_FRAMEWORKS = [
|
|
|
14928
15513
|
}
|
|
14929
15514
|
},
|
|
14930
15515
|
{
|
|
14931
|
-
"id": "
|
|
14932
|
-
"
|
|
14933
|
-
"inspect"
|
|
14934
|
-
],
|
|
14935
|
-
"name": "Porter Five Forces",
|
|
15516
|
+
"id": "lean-canvas",
|
|
15517
|
+
"name": "Lean Canvas",
|
|
14936
15518
|
"version": "1.0.0",
|
|
14937
|
-
"description": "
|
|
14938
|
-
"category": "
|
|
15519
|
+
"description": "Startup-focused adaptation of the BMC. Replaces partners/resources with problem/solution/unfair advantage.",
|
|
15520
|
+
"category": "business_model",
|
|
14939
15521
|
"origin": {
|
|
14940
15522
|
"type": "practitioner",
|
|
14941
|
-
"attribution": "
|
|
14942
|
-
"description": "Published in
|
|
14943
|
-
"
|
|
14944
|
-
"
|
|
15523
|
+
"attribution": "Ash Maurya",
|
|
15524
|
+
"description": "Adapted from BMC for startups. Published in Running Lean (O'Reilly). Replaces partners/resources with problem/solution.",
|
|
15525
|
+
"url": "https://leanstack.com/lean-canvas",
|
|
15526
|
+
"year": 2012,
|
|
15527
|
+
"license": "published_methodology"
|
|
14945
15528
|
},
|
|
14946
15529
|
"tags": [
|
|
14947
|
-
"
|
|
14948
|
-
"
|
|
15530
|
+
"business_model",
|
|
15531
|
+
"matrix"
|
|
14949
15532
|
],
|
|
14950
15533
|
"slots": [
|
|
14951
15534
|
{
|
|
14952
|
-
"label": "
|
|
14953
|
-
"
|
|
14954
|
-
"
|
|
14955
|
-
"description": "Intensity of competition among existing players"
|
|
15535
|
+
"label": "Problem",
|
|
15536
|
+
"entityTypeId": "need",
|
|
15537
|
+
"description": "Top 3 problems your customers face"
|
|
14956
15538
|
},
|
|
14957
15539
|
{
|
|
14958
|
-
"label": "
|
|
14959
|
-
"
|
|
14960
|
-
"
|
|
14961
|
-
"description": "How easy is it for new competitors to enter?"
|
|
15540
|
+
"label": "Solution",
|
|
15541
|
+
"entityTypeId": "solution",
|
|
15542
|
+
"description": "Top 3 features addressing each problem"
|
|
14962
15543
|
},
|
|
14963
15544
|
{
|
|
14964
|
-
"label": "
|
|
14965
|
-
"
|
|
14966
|
-
"
|
|
14967
|
-
"description": "Can customers switch to alternatives?"
|
|
15545
|
+
"label": "Key Metrics",
|
|
15546
|
+
"entityTypeId": "metric",
|
|
15547
|
+
"description": "Key numbers that tell you how your business is doing"
|
|
14968
15548
|
},
|
|
14969
15549
|
{
|
|
14970
|
-
"label": "
|
|
14971
|
-
"
|
|
14972
|
-
"
|
|
14973
|
-
"description": "How much leverage do buyers have?"
|
|
15550
|
+
"label": "Unique Value Prop",
|
|
15551
|
+
"entityTypeId": "value_proposition",
|
|
15552
|
+
"description": "Single, clear, compelling message"
|
|
14974
15553
|
},
|
|
14975
15554
|
{
|
|
14976
|
-
"label": "
|
|
14977
|
-
"
|
|
15555
|
+
"label": "Unfair Advantage",
|
|
15556
|
+
"entityTypeId": "capability",
|
|
15557
|
+
"description": "Something that cannot be easily copied"
|
|
15558
|
+
},
|
|
15559
|
+
{
|
|
15560
|
+
"label": "Channels",
|
|
15561
|
+
"entityTypeId": "acquisition_channel",
|
|
15562
|
+
"description": "Path to customers"
|
|
15563
|
+
},
|
|
15564
|
+
{
|
|
15565
|
+
"label": "Customer Segments",
|
|
14978
15566
|
"entityTypeId": "persona",
|
|
14979
|
-
"description": "
|
|
15567
|
+
"description": "Target customers"
|
|
15568
|
+
},
|
|
15569
|
+
{
|
|
15570
|
+
"label": "Existing Alternatives",
|
|
15571
|
+
"entityTypeId": "competitor",
|
|
15572
|
+
"description": "What do customers use today?"
|
|
15573
|
+
},
|
|
15574
|
+
{
|
|
15575
|
+
"label": "Early Adopters",
|
|
15576
|
+
"entityTypeId": "behavioral_segment",
|
|
15577
|
+
"description": "Your first target customers"
|
|
15578
|
+
},
|
|
15579
|
+
{
|
|
15580
|
+
"label": "Cost Structure",
|
|
15581
|
+
"entityTypeId": "cost_structure",
|
|
15582
|
+
"description": "Customer acquisition costs, hosting, etc."
|
|
15583
|
+
},
|
|
15584
|
+
{
|
|
15585
|
+
"label": "Revenue Streams",
|
|
15586
|
+
"entityTypeId": "revenue_stream",
|
|
15587
|
+
"description": "Revenue model, lifetime value, margins"
|
|
14980
15588
|
}
|
|
14981
15589
|
],
|
|
14982
15590
|
"data": {
|
|
14983
15591
|
"entity_types": [
|
|
15592
|
+
{
|
|
15593
|
+
"type": "need",
|
|
15594
|
+
"role": "bucket"
|
|
15595
|
+
},
|
|
15596
|
+
{
|
|
15597
|
+
"type": "solution",
|
|
15598
|
+
"role": "bucket"
|
|
15599
|
+
},
|
|
15600
|
+
{
|
|
15601
|
+
"type": "value_proposition",
|
|
15602
|
+
"role": "bucket"
|
|
15603
|
+
},
|
|
14984
15604
|
{
|
|
14985
15605
|
"type": "competitor",
|
|
14986
|
-
"role": "
|
|
15606
|
+
"role": "bucket"
|
|
14987
15607
|
},
|
|
14988
15608
|
{
|
|
14989
15609
|
"type": "persona",
|
|
15610
|
+
"role": "bucket"
|
|
15611
|
+
},
|
|
15612
|
+
{
|
|
15613
|
+
"type": "behavioral_segment",
|
|
15614
|
+
"role": "bucket"
|
|
15615
|
+
},
|
|
15616
|
+
{
|
|
15617
|
+
"type": "metric",
|
|
15618
|
+
"role": "bucket"
|
|
15619
|
+
},
|
|
15620
|
+
{
|
|
15621
|
+
"type": "acquisition_channel",
|
|
15622
|
+
"role": "bucket"
|
|
15623
|
+
},
|
|
15624
|
+
{
|
|
15625
|
+
"type": "cost_structure",
|
|
15626
|
+
"role": "bucket"
|
|
15627
|
+
},
|
|
15628
|
+
{
|
|
15629
|
+
"type": "revenue_stream",
|
|
15630
|
+
"role": "bucket"
|
|
15631
|
+
},
|
|
15632
|
+
{
|
|
15633
|
+
"type": "capability",
|
|
14990
15634
|
"role": "item"
|
|
14991
15635
|
}
|
|
14992
15636
|
],
|
|
14993
15637
|
"required_properties": {}
|
|
14994
15638
|
},
|
|
14995
15639
|
"structure": {
|
|
14996
|
-
"pattern": "
|
|
15640
|
+
"pattern": "matrix"
|
|
14997
15641
|
},
|
|
14998
15642
|
"presentation": {
|
|
14999
15643
|
"layout": {
|
|
15000
|
-
"type": "
|
|
15001
|
-
"
|
|
15644
|
+
"type": "matrix",
|
|
15645
|
+
"rows": 3,
|
|
15646
|
+
"cols": 4
|
|
15002
15647
|
},
|
|
15003
15648
|
"sort_by": {
|
|
15004
15649
|
"property": "title",
|
|
15005
15650
|
"direction": "asc"
|
|
15006
15651
|
},
|
|
15007
|
-
"colour_by": "
|
|
15652
|
+
"colour_by": "group",
|
|
15008
15653
|
"card_fields": [
|
|
15009
15654
|
"title",
|
|
15010
15655
|
"description"
|
|
15011
15656
|
]
|
|
15012
15657
|
},
|
|
15013
15658
|
"education": {
|
|
15014
|
-
"purpose": "
|
|
15015
|
-
"core_question": "What
|
|
15659
|
+
"purpose": "Capture an entire business model hypothesis on a single page so founders can test assumptions rapidly without writing a full business plan.",
|
|
15660
|
+
"core_question": "What are the riskiest assumptions in our business model, and how can we test them quickly?",
|
|
15016
15661
|
"when_to_use": [
|
|
15017
|
-
"You
|
|
15018
|
-
"
|
|
15019
|
-
"
|
|
15662
|
+
"You are designing or redesigning how the business creates and captures value",
|
|
15663
|
+
"You need to communicate the business model to stakeholders or investors",
|
|
15664
|
+
"You want to identify risks and assumptions in your business model"
|
|
15020
15665
|
],
|
|
15021
15666
|
"when_not_to_use": [
|
|
15022
|
-
"
|
|
15023
|
-
"
|
|
15667
|
+
"The business model is mature and well-understood by all stakeholders",
|
|
15668
|
+
"You are focused on tactical execution rather than model design"
|
|
15024
15669
|
]
|
|
15025
15670
|
}
|
|
15026
15671
|
},
|
|
15027
15672
|
{
|
|
15028
|
-
"id": "
|
|
15673
|
+
"id": "competitive-landscape",
|
|
15029
15674
|
"approach_ids": [
|
|
15030
15675
|
"inspect"
|
|
15031
15676
|
],
|
|
15032
|
-
"name": "
|
|
15677
|
+
"name": "Competitive Landscape",
|
|
15033
15678
|
"version": "1.0.0",
|
|
15034
|
-
"description": "
|
|
15035
|
-
"category": "
|
|
15679
|
+
"description": "Survey the whole competitive environment on one surface: who competes, what they ship, where the market is moving, and how you are positioned within it.",
|
|
15680
|
+
"category": "competitive",
|
|
15036
15681
|
"origin": {
|
|
15037
|
-
"type": "
|
|
15038
|
-
"
|
|
15039
|
-
"
|
|
15682
|
+
"type": "custom",
|
|
15683
|
+
"attribution": "The Product Creator",
|
|
15684
|
+
"description": "Original to the Unified Product Graph. Where competitor-profile studies one rival in depth, the landscape holds the market-level view: the competitor set, their features, the trends acting on all of them, and your own positioning read against the whole. Assembled from four entity types the catalog already models rather than adapted from a published methodology.",
|
|
15685
|
+
"license": "open_attribution"
|
|
15040
15686
|
},
|
|
15041
15687
|
"tags": [
|
|
15042
|
-
"
|
|
15688
|
+
"competitive",
|
|
15043
15689
|
"matrix"
|
|
15044
15690
|
],
|
|
15045
15691
|
"slots": [
|
|
15046
15692
|
{
|
|
15047
|
-
"label": "
|
|
15048
|
-
"entityTypeId": "
|
|
15049
|
-
"description": "
|
|
15693
|
+
"label": "Competitors",
|
|
15694
|
+
"entityTypeId": "competitor",
|
|
15695
|
+
"description": "The rivals in the market you are surveying"
|
|
15050
15696
|
},
|
|
15051
15697
|
{
|
|
15052
|
-
"label": "
|
|
15053
|
-
"entityTypeId": "
|
|
15054
|
-
"description": "
|
|
15698
|
+
"label": "Competitor Features",
|
|
15699
|
+
"entityTypeId": "competitor_feature",
|
|
15700
|
+
"description": "What those competitors actually ship"
|
|
15055
15701
|
},
|
|
15056
15702
|
{
|
|
15057
|
-
"label": "
|
|
15058
|
-
"entityTypeId": "
|
|
15703
|
+
"label": "Market Trends",
|
|
15704
|
+
"entityTypeId": "market_trend",
|
|
15705
|
+
"description": "Forces acting on the whole market, not on one rival"
|
|
15706
|
+
},
|
|
15707
|
+
{
|
|
15708
|
+
"label": "Positioning",
|
|
15709
|
+
"entityTypeId": "positioning",
|
|
15710
|
+
"description": "Where you stand relative to the field"
|
|
15711
|
+
}
|
|
15712
|
+
],
|
|
15713
|
+
"data": {
|
|
15714
|
+
"entity_types": [
|
|
15715
|
+
{
|
|
15716
|
+
"type": "competitor",
|
|
15717
|
+
"role": "item"
|
|
15718
|
+
},
|
|
15719
|
+
{
|
|
15720
|
+
"type": "competitor_feature",
|
|
15721
|
+
"role": "item"
|
|
15722
|
+
},
|
|
15723
|
+
{
|
|
15724
|
+
"type": "market_trend",
|
|
15725
|
+
"role": "item"
|
|
15726
|
+
},
|
|
15727
|
+
{
|
|
15728
|
+
"type": "positioning",
|
|
15729
|
+
"role": "item"
|
|
15730
|
+
}
|
|
15731
|
+
],
|
|
15732
|
+
"required_properties": {}
|
|
15733
|
+
},
|
|
15734
|
+
"structure": {
|
|
15735
|
+
"pattern": "matrix"
|
|
15736
|
+
},
|
|
15737
|
+
"presentation": {
|
|
15738
|
+
"layout": {
|
|
15739
|
+
"type": "matrix",
|
|
15740
|
+
"rows": 2,
|
|
15741
|
+
"cols": 2
|
|
15742
|
+
},
|
|
15743
|
+
"colour_by": "type",
|
|
15744
|
+
"card_fields": [
|
|
15745
|
+
"title",
|
|
15746
|
+
"description",
|
|
15747
|
+
"status"
|
|
15748
|
+
]
|
|
15749
|
+
},
|
|
15750
|
+
"education": {
|
|
15751
|
+
"purpose": "Hold the market-level competitive picture in one place: the field of rivals, what they ship, the trends moving the whole category, and your own position read against all three.",
|
|
15752
|
+
"core_question": "What does the field look like from above, and where do we sit in it?",
|
|
15753
|
+
"when_to_use": [
|
|
15754
|
+
"You need the shape of a whole market before choosing which rival to study in depth",
|
|
15755
|
+
"Trends are moving the category and you need them beside the competitor set, not in a separate document",
|
|
15756
|
+
"A positioning statement needs the competitive evidence sitting next to it"
|
|
15757
|
+
],
|
|
15758
|
+
"when_not_to_use": [
|
|
15759
|
+
"You already know which single rival matters and want depth on them: use competitor-profile",
|
|
15760
|
+
"You are comparing capabilities feature-by-feature against named rivals: use competitive-matrix",
|
|
15761
|
+
"You need to place rivals positionally on two scored axes: use positioning-map"
|
|
15762
|
+
]
|
|
15763
|
+
}
|
|
15764
|
+
},
|
|
15765
|
+
{
|
|
15766
|
+
"id": "porter-five-forces",
|
|
15767
|
+
"approach_ids": [
|
|
15768
|
+
"inspect"
|
|
15769
|
+
],
|
|
15770
|
+
"name": "Porter Five Forces",
|
|
15771
|
+
"version": "1.0.0",
|
|
15772
|
+
"description": "Analyse industry competitiveness through five forces: rivalry, new entrants, substitutes, buyer power, and supplier power.",
|
|
15773
|
+
"category": "strategy",
|
|
15774
|
+
"origin": {
|
|
15775
|
+
"type": "practitioner",
|
|
15776
|
+
"attribution": "Michael Porter",
|
|
15777
|
+
"description": "Published in Competitive Strategy (Free Press). The foundational framework for industry analysis.",
|
|
15778
|
+
"year": 1979,
|
|
15779
|
+
"license": "public_domain"
|
|
15780
|
+
},
|
|
15781
|
+
"tags": [
|
|
15782
|
+
"strategy",
|
|
15783
|
+
"collection"
|
|
15784
|
+
],
|
|
15785
|
+
"slots": [
|
|
15786
|
+
{
|
|
15787
|
+
"label": "Competitive Rivalry",
|
|
15788
|
+
"role": "competitive_rivalry",
|
|
15789
|
+
"entityTypeId": "competitor",
|
|
15790
|
+
"description": "Intensity of competition among existing players"
|
|
15791
|
+
},
|
|
15792
|
+
{
|
|
15793
|
+
"label": "Threat of New Entrants",
|
|
15794
|
+
"role": "new_entrants",
|
|
15795
|
+
"entityTypeId": "competitor",
|
|
15796
|
+
"description": "How easy is it for new competitors to enter?"
|
|
15797
|
+
},
|
|
15798
|
+
{
|
|
15799
|
+
"label": "Threat of Substitutes",
|
|
15800
|
+
"role": "substitutes",
|
|
15801
|
+
"entityTypeId": "competitor",
|
|
15802
|
+
"description": "Can customers switch to alternatives?"
|
|
15803
|
+
},
|
|
15804
|
+
{
|
|
15805
|
+
"label": "Buyer Power",
|
|
15806
|
+
"role": "buyer_power",
|
|
15807
|
+
"entityTypeId": "persona",
|
|
15808
|
+
"description": "How much leverage do buyers have?"
|
|
15809
|
+
},
|
|
15810
|
+
{
|
|
15811
|
+
"label": "Supplier Power",
|
|
15812
|
+
"role": "supplier_power",
|
|
15813
|
+
"entityTypeId": "persona",
|
|
15814
|
+
"description": "How much leverage do suppliers have?"
|
|
15815
|
+
}
|
|
15816
|
+
],
|
|
15817
|
+
"data": {
|
|
15818
|
+
"entity_types": [
|
|
15819
|
+
{
|
|
15820
|
+
"type": "competitor",
|
|
15821
|
+
"role": "item"
|
|
15822
|
+
},
|
|
15823
|
+
{
|
|
15824
|
+
"type": "persona",
|
|
15825
|
+
"role": "item"
|
|
15826
|
+
}
|
|
15827
|
+
],
|
|
15828
|
+
"required_properties": {}
|
|
15829
|
+
},
|
|
15830
|
+
"structure": {
|
|
15831
|
+
"pattern": "collection"
|
|
15832
|
+
},
|
|
15833
|
+
"presentation": {
|
|
15834
|
+
"layout": {
|
|
15835
|
+
"type": "grid",
|
|
15836
|
+
"groupBy": "type"
|
|
15837
|
+
},
|
|
15838
|
+
"sort_by": {
|
|
15839
|
+
"property": "title",
|
|
15840
|
+
"direction": "asc"
|
|
15841
|
+
},
|
|
15842
|
+
"colour_by": "type",
|
|
15843
|
+
"card_fields": [
|
|
15844
|
+
"title",
|
|
15845
|
+
"description"
|
|
15846
|
+
]
|
|
15847
|
+
},
|
|
15848
|
+
"education": {
|
|
15849
|
+
"purpose": "Analyse the competitive dynamics of an industry through five structural forces that determine profitability, revealing where power lies and where threats emerge.",
|
|
15850
|
+
"core_question": "What structural forces shape competition in our industry, and how do they affect our ability to capture value?",
|
|
15851
|
+
"when_to_use": [
|
|
15852
|
+
"You need to align the team on long-term direction",
|
|
15853
|
+
"Market conditions are shifting and you need to reassess positioning",
|
|
15854
|
+
"Leadership needs a structured view of strategic options"
|
|
15855
|
+
],
|
|
15856
|
+
"when_not_to_use": [
|
|
15857
|
+
"You are in pure execution mode with a clear strategy already set",
|
|
15858
|
+
"The team is too early-stage to commit to strategic constraints"
|
|
15859
|
+
]
|
|
15860
|
+
}
|
|
15861
|
+
},
|
|
15862
|
+
{
|
|
15863
|
+
"id": "swot-analysis",
|
|
15864
|
+
"approach_ids": [
|
|
15865
|
+
"inspect"
|
|
15866
|
+
],
|
|
15867
|
+
"name": "SWOT Analysis",
|
|
15868
|
+
"version": "1.0.0",
|
|
15869
|
+
"description": "Map Strengths, Weaknesses, Opportunities, and Threats in a 2x2 grid. Internal vs external, helpful vs harmful.",
|
|
15870
|
+
"category": "strategy",
|
|
15871
|
+
"origin": {
|
|
15872
|
+
"type": "practitioner",
|
|
15873
|
+
"description": "Widely attributed to Albert Humphrey at Stanford Research Institute. One of the most commonly used strategy frameworks.",
|
|
15874
|
+
"license": "public_domain"
|
|
15875
|
+
},
|
|
15876
|
+
"tags": [
|
|
15877
|
+
"strategy",
|
|
15878
|
+
"matrix"
|
|
15879
|
+
],
|
|
15880
|
+
"slots": [
|
|
15881
|
+
{
|
|
15882
|
+
"label": "Strengths",
|
|
15883
|
+
"entityTypeId": "capability",
|
|
15884
|
+
"description": "Internal advantages and core competencies"
|
|
15885
|
+
},
|
|
15886
|
+
{
|
|
15887
|
+
"label": "Weaknesses",
|
|
15888
|
+
"entityTypeId": "need",
|
|
15889
|
+
"description": "Internal limitations and gaps"
|
|
15890
|
+
},
|
|
15891
|
+
{
|
|
15892
|
+
"label": "Opportunities",
|
|
15893
|
+
"entityTypeId": "opportunity",
|
|
15059
15894
|
"description": "External trends and openings"
|
|
15060
15895
|
},
|
|
15061
15896
|
{
|
|
@@ -15219,6 +16054,108 @@ var UPG_FRAMEWORKS = [
|
|
|
15219
16054
|
]
|
|
15220
16055
|
}
|
|
15221
16056
|
},
|
|
16057
|
+
{
|
|
16058
|
+
"id": "product-vision-board",
|
|
16059
|
+
"approach_ids": [
|
|
16060
|
+
"plan"
|
|
16061
|
+
],
|
|
16062
|
+
"name": "Product Vision Board",
|
|
16063
|
+
"version": "1.0.0",
|
|
16064
|
+
"description": "Roman Pichler's one-page vision canvas. A vision at the top narrows through target group, needs, product and business goals, so the reason for building and the thing being built stay on the same page.",
|
|
16065
|
+
"category": "strategy",
|
|
16066
|
+
"origin": {
|
|
16067
|
+
"type": "practitioner",
|
|
16068
|
+
"attribution": "Roman Pichler",
|
|
16069
|
+
"description": 'Created by Roman Pichler as a one-page tool for capturing and sharing a product vision, and published in "Strategize" (2016). The board deliberately keeps the vision, the people it serves, their needs, the product and the business goals in a single vertical read, so a change to any one is visible against the other four.',
|
|
16070
|
+
"license": "published_methodology"
|
|
16071
|
+
},
|
|
16072
|
+
"tags": [
|
|
16073
|
+
"strategy",
|
|
16074
|
+
"matrix"
|
|
16075
|
+
],
|
|
16076
|
+
"slots": [
|
|
16077
|
+
{
|
|
16078
|
+
"label": "Vision",
|
|
16079
|
+
"entityTypeId": "vision",
|
|
16080
|
+
"description": "The long-term change the product exists to bring about"
|
|
16081
|
+
},
|
|
16082
|
+
{
|
|
16083
|
+
"label": "Target Group",
|
|
16084
|
+
"entityTypeId": "persona",
|
|
16085
|
+
"description": "The customers and users the vision is for"
|
|
16086
|
+
},
|
|
16087
|
+
{
|
|
16088
|
+
"label": "Needs",
|
|
16089
|
+
"entityTypeId": "need",
|
|
16090
|
+
"description": "The problem the product solves or the benefit it provides for that group"
|
|
16091
|
+
},
|
|
16092
|
+
{
|
|
16093
|
+
"label": "Product",
|
|
16094
|
+
"entityTypeId": "solution",
|
|
16095
|
+
"description": "What the product is, and what makes it stand out"
|
|
16096
|
+
},
|
|
16097
|
+
{
|
|
16098
|
+
"label": "Business Goals",
|
|
16099
|
+
"entityTypeId": "outcome",
|
|
16100
|
+
"description": "How the product benefits the company that builds it"
|
|
16101
|
+
}
|
|
16102
|
+
],
|
|
16103
|
+
"data": {
|
|
16104
|
+
"entity_types": [
|
|
16105
|
+
{
|
|
16106
|
+
"type": "vision",
|
|
16107
|
+
"role": "item"
|
|
16108
|
+
},
|
|
16109
|
+
{
|
|
16110
|
+
"type": "persona",
|
|
16111
|
+
"role": "item"
|
|
16112
|
+
},
|
|
16113
|
+
{
|
|
16114
|
+
"type": "need",
|
|
16115
|
+
"role": "item"
|
|
16116
|
+
},
|
|
16117
|
+
{
|
|
16118
|
+
"type": "solution",
|
|
16119
|
+
"role": "item"
|
|
16120
|
+
},
|
|
16121
|
+
{
|
|
16122
|
+
"type": "outcome",
|
|
16123
|
+
"role": "item"
|
|
16124
|
+
}
|
|
16125
|
+
],
|
|
16126
|
+
"required_properties": {}
|
|
16127
|
+
},
|
|
16128
|
+
"structure": {
|
|
16129
|
+
"pattern": "matrix"
|
|
16130
|
+
},
|
|
16131
|
+
"presentation": {
|
|
16132
|
+
"layout": {
|
|
16133
|
+
"type": "matrix",
|
|
16134
|
+
"rows": 5,
|
|
16135
|
+
"cols": 1
|
|
16136
|
+
},
|
|
16137
|
+
"colour_by": "type",
|
|
16138
|
+
"card_fields": [
|
|
16139
|
+
"title",
|
|
16140
|
+
"description",
|
|
16141
|
+
"status"
|
|
16142
|
+
]
|
|
16143
|
+
},
|
|
16144
|
+
"education": {
|
|
16145
|
+
"purpose": "Capture a product vision on one page together with the group it serves, their needs, the product itself and the business goals it advances.",
|
|
16146
|
+
"core_question": "What change are we trying to bring about, for whom, and how does building it pay off?",
|
|
16147
|
+
"when_to_use": [
|
|
16148
|
+
"A new product or a major new direction needs a shared vision before any roadmap exists",
|
|
16149
|
+
"The team can describe what it is building but not why anyone should want it",
|
|
16150
|
+
"Vision, target group and business goals are living in three different documents and drifting apart"
|
|
16151
|
+
],
|
|
16152
|
+
"when_not_to_use": [
|
|
16153
|
+
"The vision is settled and the open question is sequencing: use goal-oriented-roadmap",
|
|
16154
|
+
"You need the causal chain from vision down to daily work: use strategic-cascade",
|
|
16155
|
+
"The question is which business model captures the value: use business-model-canvas"
|
|
16156
|
+
]
|
|
16157
|
+
}
|
|
16158
|
+
},
|
|
15222
16159
|
{
|
|
15223
16160
|
"id": "three-horizons",
|
|
15224
16161
|
"approach_ids": [
|
|
@@ -15804,25 +16741,95 @@ var UPG_FRAMEWORKS = [
|
|
|
15804
16741
|
"label": "Must-haves",
|
|
15805
16742
|
"role": "must_have",
|
|
15806
16743
|
"entityTypeId": "feature",
|
|
15807
|
-
"description": "Expected features: absence causes dissatisfaction"
|
|
16744
|
+
"description": "Expected features: absence causes dissatisfaction",
|
|
16745
|
+
"predicate": [
|
|
16746
|
+
{
|
|
16747
|
+
"scope": "framework",
|
|
16748
|
+
"property": "functional_response",
|
|
16749
|
+
"op": "in",
|
|
16750
|
+
"value": [
|
|
16751
|
+
"i_expect_it",
|
|
16752
|
+
"i_am_neutral",
|
|
16753
|
+
"i_can_tolerate_it"
|
|
16754
|
+
]
|
|
16755
|
+
},
|
|
16756
|
+
{
|
|
16757
|
+
"scope": "framework",
|
|
16758
|
+
"property": "dysfunctional_response",
|
|
16759
|
+
"op": "eq",
|
|
16760
|
+
"value": "i_dislike_it"
|
|
16761
|
+
}
|
|
16762
|
+
]
|
|
15808
16763
|
},
|
|
15809
16764
|
{
|
|
15810
16765
|
"label": "Performance",
|
|
15811
16766
|
"role": "performance",
|
|
15812
16767
|
"entityTypeId": "feature",
|
|
15813
|
-
"description": "More is better: linear satisfaction increase"
|
|
16768
|
+
"description": "More is better: linear satisfaction increase",
|
|
16769
|
+
"predicate": [
|
|
16770
|
+
{
|
|
16771
|
+
"scope": "framework",
|
|
16772
|
+
"property": "functional_response",
|
|
16773
|
+
"op": "eq",
|
|
16774
|
+
"value": "i_like_it"
|
|
16775
|
+
},
|
|
16776
|
+
{
|
|
16777
|
+
"scope": "framework",
|
|
16778
|
+
"property": "dysfunctional_response",
|
|
16779
|
+
"op": "eq",
|
|
16780
|
+
"value": "i_dislike_it"
|
|
16781
|
+
}
|
|
16782
|
+
]
|
|
15814
16783
|
},
|
|
15815
16784
|
{
|
|
15816
16785
|
"label": "Delighters",
|
|
15817
16786
|
"role": "delighter",
|
|
15818
16787
|
"entityTypeId": "feature",
|
|
15819
|
-
"description": "Unexpected features: presence creates delight"
|
|
16788
|
+
"description": "Unexpected features: presence creates delight",
|
|
16789
|
+
"predicate": [
|
|
16790
|
+
{
|
|
16791
|
+
"scope": "framework",
|
|
16792
|
+
"property": "functional_response",
|
|
16793
|
+
"op": "eq",
|
|
16794
|
+
"value": "i_like_it"
|
|
16795
|
+
},
|
|
16796
|
+
{
|
|
16797
|
+
"scope": "framework",
|
|
16798
|
+
"property": "dysfunctional_response",
|
|
16799
|
+
"op": "in",
|
|
16800
|
+
"value": [
|
|
16801
|
+
"i_expect_it",
|
|
16802
|
+
"i_am_neutral",
|
|
16803
|
+
"i_can_tolerate_it"
|
|
16804
|
+
]
|
|
16805
|
+
}
|
|
16806
|
+
]
|
|
15820
16807
|
},
|
|
15821
16808
|
{
|
|
15822
16809
|
"label": "Indifferent",
|
|
15823
16810
|
"role": "indifferent",
|
|
15824
16811
|
"entityTypeId": "feature",
|
|
15825
|
-
"description": "Features users don't care about either way"
|
|
16812
|
+
"description": "Features users don't care about either way",
|
|
16813
|
+
"predicate": [
|
|
16814
|
+
{
|
|
16815
|
+
"scope": "framework",
|
|
16816
|
+
"property": "functional_response",
|
|
16817
|
+
"op": "in",
|
|
16818
|
+
"value": [
|
|
16819
|
+
"i_am_neutral",
|
|
16820
|
+
"i_can_tolerate_it"
|
|
16821
|
+
]
|
|
16822
|
+
},
|
|
16823
|
+
{
|
|
16824
|
+
"scope": "framework",
|
|
16825
|
+
"property": "dysfunctional_response",
|
|
16826
|
+
"op": "in",
|
|
16827
|
+
"value": [
|
|
16828
|
+
"i_am_neutral",
|
|
16829
|
+
"i_can_tolerate_it"
|
|
16830
|
+
]
|
|
16831
|
+
}
|
|
16832
|
+
]
|
|
15826
16833
|
}
|
|
15827
16834
|
],
|
|
15828
16835
|
"data": {
|
|
@@ -16157,32 +17164,213 @@ var UPG_FRAMEWORKS = [
|
|
|
16157
17164
|
"entityTypeId": "feature",
|
|
16158
17165
|
"description": "Features or requirements sorted into the four MoSCoW buckets"
|
|
16159
17166
|
}
|
|
16160
|
-
],
|
|
17167
|
+
],
|
|
17168
|
+
"data": {
|
|
17169
|
+
"entity_types": [
|
|
17170
|
+
{
|
|
17171
|
+
"type": "feature",
|
|
17172
|
+
"role": "scored_item"
|
|
17173
|
+
}
|
|
17174
|
+
],
|
|
17175
|
+
"required_properties": {
|
|
17176
|
+
"feature": [
|
|
17177
|
+
{
|
|
17178
|
+
"property": "moscow",
|
|
17179
|
+
"type": "enum",
|
|
17180
|
+
"required": true,
|
|
17181
|
+
"scope": "framework",
|
|
17182
|
+
"label": "MoSCoW priority",
|
|
17183
|
+
"description": "Which scope bucket this requirement falls into for the current release",
|
|
17184
|
+
"enum_values": [
|
|
17185
|
+
"must",
|
|
17186
|
+
"should",
|
|
17187
|
+
"could",
|
|
17188
|
+
"wont"
|
|
17189
|
+
]
|
|
17190
|
+
}
|
|
17191
|
+
]
|
|
17192
|
+
}
|
|
17193
|
+
},
|
|
17194
|
+
"structure": {
|
|
17195
|
+
"pattern": "table"
|
|
17196
|
+
},
|
|
17197
|
+
"presentation": {
|
|
17198
|
+
"layout": {
|
|
17199
|
+
"type": "table",
|
|
17200
|
+
"columns": [
|
|
17201
|
+
{
|
|
17202
|
+
"property": "title",
|
|
17203
|
+
"label": "Requirement",
|
|
17204
|
+
"sortable": true
|
|
17205
|
+
},
|
|
17206
|
+
{
|
|
17207
|
+
"property": "moscow",
|
|
17208
|
+
"label": "Priority",
|
|
17209
|
+
"sortable": true,
|
|
17210
|
+
"format": "badge"
|
|
17211
|
+
}
|
|
17212
|
+
]
|
|
17213
|
+
},
|
|
17214
|
+
"sort_by": {
|
|
17215
|
+
"property": "moscow",
|
|
17216
|
+
"direction": "asc"
|
|
17217
|
+
},
|
|
17218
|
+
"colour_by": "type",
|
|
17219
|
+
"card_fields": [
|
|
17220
|
+
"title",
|
|
17221
|
+
"description",
|
|
17222
|
+
"status"
|
|
17223
|
+
]
|
|
17224
|
+
},
|
|
17225
|
+
"education": {
|
|
17226
|
+
"purpose": "Categorise requirements into Must, Should, Could, and Won't buckets so stakeholders agree on scope boundaries before development begins.",
|
|
17227
|
+
"core_question": "Which requirements are truly non-negotiable for this release, and which can be deferred without blocking the goal?",
|
|
17228
|
+
"when_to_use": [
|
|
17229
|
+
"You have more ideas or features than capacity to build them",
|
|
17230
|
+
"Stakeholders disagree on what to build next",
|
|
17231
|
+
"You need a transparent, defensible prioritisation process"
|
|
17232
|
+
],
|
|
17233
|
+
"when_not_to_use": [
|
|
17234
|
+
"You have a single obvious next step with no contention",
|
|
17235
|
+
"The backlog is small enough to sequence intuitively"
|
|
17236
|
+
]
|
|
17237
|
+
}
|
|
17238
|
+
},
|
|
17239
|
+
{
|
|
17240
|
+
"id": "experiment-tracker",
|
|
17241
|
+
"name": "Experiment Tracker",
|
|
17242
|
+
"version": "1.0.0",
|
|
17243
|
+
"description": "Track experiments from design through execution to results. Velocity-focused.",
|
|
17244
|
+
"category": "validation",
|
|
17245
|
+
"origin": {
|
|
17246
|
+
"type": "practitioner",
|
|
17247
|
+
"description": "Common in growth and product teams running continuous experiments.",
|
|
17248
|
+
"license": "cc_by"
|
|
17249
|
+
},
|
|
17250
|
+
"tags": [
|
|
17251
|
+
"validation",
|
|
17252
|
+
"table"
|
|
17253
|
+
],
|
|
17254
|
+
"relational": {
|
|
17255
|
+
"spine": "experiment_run",
|
|
17256
|
+
"columns": [
|
|
17257
|
+
{
|
|
17258
|
+
"kind": "field",
|
|
17259
|
+
"id": "experiment",
|
|
17260
|
+
"label": "Experiment",
|
|
17261
|
+
"property": "title",
|
|
17262
|
+
"sortable": true
|
|
17263
|
+
},
|
|
17264
|
+
{
|
|
17265
|
+
"kind": "projection",
|
|
17266
|
+
"id": "hypothesis",
|
|
17267
|
+
"label": "Hypothesis",
|
|
17268
|
+
"path": [
|
|
17269
|
+
{
|
|
17270
|
+
"edge": "experiment_plan_ran_as_experiment_run",
|
|
17271
|
+
"direction": "reverse"
|
|
17272
|
+
},
|
|
17273
|
+
{
|
|
17274
|
+
"edge": "hypothesis_requires_experiment_plan",
|
|
17275
|
+
"direction": "reverse"
|
|
17276
|
+
}
|
|
17277
|
+
],
|
|
17278
|
+
"fields": [
|
|
17279
|
+
"title"
|
|
17280
|
+
],
|
|
17281
|
+
"sortable": true
|
|
17282
|
+
},
|
|
17283
|
+
{
|
|
17284
|
+
"kind": "projection",
|
|
17285
|
+
"id": "success_metric",
|
|
17286
|
+
"label": "Success Metric",
|
|
17287
|
+
"path": [
|
|
17288
|
+
{
|
|
17289
|
+
"edge": "experiment_run_measures_metric",
|
|
17290
|
+
"direction": "forward"
|
|
17291
|
+
}
|
|
17292
|
+
],
|
|
17293
|
+
"fields": [
|
|
17294
|
+
"title",
|
|
17295
|
+
"current_value",
|
|
17296
|
+
"target_value"
|
|
17297
|
+
]
|
|
17298
|
+
},
|
|
17299
|
+
{
|
|
17300
|
+
"kind": "field",
|
|
17301
|
+
"id": "stage",
|
|
17302
|
+
"label": "Stage",
|
|
17303
|
+
"property": "status"
|
|
17304
|
+
},
|
|
17305
|
+
{
|
|
17306
|
+
"kind": "field",
|
|
17307
|
+
"id": "verdict",
|
|
17308
|
+
"label": "Verdict",
|
|
17309
|
+
"property": "disposition"
|
|
17310
|
+
},
|
|
17311
|
+
{
|
|
17312
|
+
"kind": "projection",
|
|
17313
|
+
"id": "result",
|
|
17314
|
+
"label": "Result",
|
|
17315
|
+
"path": [
|
|
17316
|
+
{
|
|
17317
|
+
"edge": "experiment_run_produces_learning",
|
|
17318
|
+
"direction": "forward"
|
|
17319
|
+
}
|
|
17320
|
+
],
|
|
17321
|
+
"fields": [
|
|
17322
|
+
"title",
|
|
17323
|
+
"result_direction"
|
|
17324
|
+
]
|
|
17325
|
+
},
|
|
17326
|
+
{
|
|
17327
|
+
"kind": "projection",
|
|
17328
|
+
"id": "evidence",
|
|
17329
|
+
"label": "Evidence",
|
|
17330
|
+
"path": [
|
|
17331
|
+
{
|
|
17332
|
+
"edge": "experiment_run_yields_evidence",
|
|
17333
|
+
"direction": "forward"
|
|
17334
|
+
}
|
|
17335
|
+
],
|
|
17336
|
+
"fields": [
|
|
17337
|
+
"title"
|
|
17338
|
+
]
|
|
17339
|
+
}
|
|
17340
|
+
],
|
|
17341
|
+
"sort": {
|
|
17342
|
+
"column": "experiment",
|
|
17343
|
+
"direction": "asc"
|
|
17344
|
+
}
|
|
17345
|
+
},
|
|
16161
17346
|
"data": {
|
|
16162
17347
|
"entity_types": [
|
|
16163
17348
|
{
|
|
16164
|
-
"type": "
|
|
16165
|
-
"role": "
|
|
17349
|
+
"type": "experiment_run",
|
|
17350
|
+
"role": "item"
|
|
17351
|
+
},
|
|
17352
|
+
{
|
|
17353
|
+
"type": "experiment_plan",
|
|
17354
|
+
"role": "item"
|
|
17355
|
+
},
|
|
17356
|
+
{
|
|
17357
|
+
"type": "hypothesis",
|
|
17358
|
+
"role": "item"
|
|
17359
|
+
},
|
|
17360
|
+
{
|
|
17361
|
+
"type": "metric",
|
|
17362
|
+
"role": "item"
|
|
17363
|
+
},
|
|
17364
|
+
{
|
|
17365
|
+
"type": "learning",
|
|
17366
|
+
"role": "item"
|
|
17367
|
+
},
|
|
17368
|
+
{
|
|
17369
|
+
"type": "evidence",
|
|
17370
|
+
"role": "item"
|
|
16166
17371
|
}
|
|
16167
17372
|
],
|
|
16168
|
-
"required_properties": {
|
|
16169
|
-
"feature": [
|
|
16170
|
-
{
|
|
16171
|
-
"property": "moscow",
|
|
16172
|
-
"type": "enum",
|
|
16173
|
-
"required": true,
|
|
16174
|
-
"scope": "framework",
|
|
16175
|
-
"label": "MoSCoW priority",
|
|
16176
|
-
"description": "Which scope bucket this requirement falls into for the current release",
|
|
16177
|
-
"enum_values": [
|
|
16178
|
-
"must",
|
|
16179
|
-
"should",
|
|
16180
|
-
"could",
|
|
16181
|
-
"wont"
|
|
16182
|
-
]
|
|
16183
|
-
}
|
|
16184
|
-
]
|
|
16185
|
-
}
|
|
17373
|
+
"required_properties": {}
|
|
16186
17374
|
},
|
|
16187
17375
|
"structure": {
|
|
16188
17376
|
"pattern": "table"
|
|
@@ -16190,22 +17378,10 @@ var UPG_FRAMEWORKS = [
|
|
|
16190
17378
|
"presentation": {
|
|
16191
17379
|
"layout": {
|
|
16192
17380
|
"type": "table",
|
|
16193
|
-
"columns": [
|
|
16194
|
-
{
|
|
16195
|
-
"property": "title",
|
|
16196
|
-
"label": "Requirement",
|
|
16197
|
-
"sortable": true
|
|
16198
|
-
},
|
|
16199
|
-
{
|
|
16200
|
-
"property": "moscow",
|
|
16201
|
-
"label": "Priority",
|
|
16202
|
-
"sortable": true,
|
|
16203
|
-
"format": "badge"
|
|
16204
|
-
}
|
|
16205
|
-
]
|
|
17381
|
+
"columns": []
|
|
16206
17382
|
},
|
|
16207
17383
|
"sort_by": {
|
|
16208
|
-
"property": "
|
|
17384
|
+
"property": "title",
|
|
16209
17385
|
"direction": "asc"
|
|
16210
17386
|
},
|
|
16211
17387
|
"colour_by": "type",
|
|
@@ -16216,16 +17392,16 @@ var UPG_FRAMEWORKS = [
|
|
|
16216
17392
|
]
|
|
16217
17393
|
},
|
|
16218
17394
|
"education": {
|
|
16219
|
-
"purpose": "
|
|
16220
|
-
"core_question": "
|
|
17395
|
+
"purpose": "Track all running and completed experiments in one place: hypothesis, method, status, result, learnings. The team builds a cumulative record of validated knowledge.",
|
|
17396
|
+
"core_question": "What experiments are running, what have we learned from completed experiments, and are we applying those learnings to new hypotheses?",
|
|
16221
17397
|
"when_to_use": [
|
|
16222
|
-
"You have
|
|
16223
|
-
"
|
|
16224
|
-
"
|
|
17398
|
+
"You have hypotheses about user needs or solutions that need testing",
|
|
17399
|
+
"You want to reduce risk before committing engineering resources",
|
|
17400
|
+
"The team is debating assumptions that can be tested empirically"
|
|
16225
17401
|
],
|
|
16226
17402
|
"when_not_to_use": [
|
|
16227
|
-
"
|
|
16228
|
-
"
|
|
17403
|
+
"The solution is already validated through real usage data",
|
|
17404
|
+
"Speed of shipping matters more than certainty about assumptions"
|
|
16229
17405
|
]
|
|
16230
17406
|
}
|
|
16231
17407
|
},
|
|
@@ -16555,6 +17731,117 @@ var UPG_FRAMEWORKS = [
|
|
|
16555
17731
|
]
|
|
16556
17732
|
}
|
|
16557
17733
|
},
|
|
17734
|
+
{
|
|
17735
|
+
"id": "threat-model-canvas",
|
|
17736
|
+
"approach_ids": [
|
|
17737
|
+
"inspect"
|
|
17738
|
+
],
|
|
17739
|
+
"name": "Threat Model Canvas",
|
|
17740
|
+
"version": "1.0.0",
|
|
17741
|
+
"description": "One surface for a product's security posture: the threat models in play, the threats and vulnerabilities they name, and the controls, policies and reviews answering them.",
|
|
17742
|
+
"category": "security",
|
|
17743
|
+
"origin": {
|
|
17744
|
+
"type": "custom",
|
|
17745
|
+
"attribution": "The Product Creator",
|
|
17746
|
+
"description": "Original to the Unified Product Graph. Deliberately NOT a STRIDE record: STRIDE is a taxonomy of six threat categories (spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege), whereas this canvas lays out the security lifecycle: models, then threats, then vulnerabilities, then the controls, policies and reviews that answer them. A STRIDE record would be a different framework with six slots of one type, and is not this one. Assembled from six entity types the catalog already models.",
|
|
17747
|
+
"license": "open_attribution"
|
|
17748
|
+
},
|
|
17749
|
+
"tags": [
|
|
17750
|
+
"security",
|
|
17751
|
+
"matrix"
|
|
17752
|
+
],
|
|
17753
|
+
"slots": [
|
|
17754
|
+
{
|
|
17755
|
+
"label": "Threat Models",
|
|
17756
|
+
"entityTypeId": "threat_model",
|
|
17757
|
+
"description": "The modelling exercises themselves: scope, system, assumptions"
|
|
17758
|
+
},
|
|
17759
|
+
{
|
|
17760
|
+
"label": "Threats",
|
|
17761
|
+
"entityTypeId": "threat",
|
|
17762
|
+
"description": "What an adversary could attempt against the system"
|
|
17763
|
+
},
|
|
17764
|
+
{
|
|
17765
|
+
"label": "Vulnerabilities",
|
|
17766
|
+
"entityTypeId": "vulnerability",
|
|
17767
|
+
"description": "Weaknesses that make a threat realisable"
|
|
17768
|
+
},
|
|
17769
|
+
{
|
|
17770
|
+
"label": "Controls",
|
|
17771
|
+
"entityTypeId": "security_control",
|
|
17772
|
+
"description": "Technical and procedural measures that reduce risk"
|
|
17773
|
+
},
|
|
17774
|
+
{
|
|
17775
|
+
"label": "Policies",
|
|
17776
|
+
"entityTypeId": "security_policy",
|
|
17777
|
+
"description": "The standing rules the controls implement"
|
|
17778
|
+
},
|
|
17779
|
+
{
|
|
17780
|
+
"label": "Reviews",
|
|
17781
|
+
"entityTypeId": "security_review",
|
|
17782
|
+
"description": "Assessments that check the posture actually holds"
|
|
17783
|
+
}
|
|
17784
|
+
],
|
|
17785
|
+
"data": {
|
|
17786
|
+
"entity_types": [
|
|
17787
|
+
{
|
|
17788
|
+
"type": "threat_model",
|
|
17789
|
+
"role": "item"
|
|
17790
|
+
},
|
|
17791
|
+
{
|
|
17792
|
+
"type": "threat",
|
|
17793
|
+
"role": "item"
|
|
17794
|
+
},
|
|
17795
|
+
{
|
|
17796
|
+
"type": "vulnerability",
|
|
17797
|
+
"role": "item"
|
|
17798
|
+
},
|
|
17799
|
+
{
|
|
17800
|
+
"type": "security_control",
|
|
17801
|
+
"role": "item"
|
|
17802
|
+
},
|
|
17803
|
+
{
|
|
17804
|
+
"type": "security_policy",
|
|
17805
|
+
"role": "item"
|
|
17806
|
+
},
|
|
17807
|
+
{
|
|
17808
|
+
"type": "security_review",
|
|
17809
|
+
"role": "item"
|
|
17810
|
+
}
|
|
17811
|
+
],
|
|
17812
|
+
"required_properties": {}
|
|
17813
|
+
},
|
|
17814
|
+
"structure": {
|
|
17815
|
+
"pattern": "matrix"
|
|
17816
|
+
},
|
|
17817
|
+
"presentation": {
|
|
17818
|
+
"layout": {
|
|
17819
|
+
"type": "matrix",
|
|
17820
|
+
"rows": 2,
|
|
17821
|
+
"cols": 3
|
|
17822
|
+
},
|
|
17823
|
+
"colour_by": "type",
|
|
17824
|
+
"card_fields": [
|
|
17825
|
+
"title",
|
|
17826
|
+
"description",
|
|
17827
|
+
"status"
|
|
17828
|
+
]
|
|
17829
|
+
},
|
|
17830
|
+
"education": {
|
|
17831
|
+
"purpose": "Hold a product's whole security posture on one surface, so a named threat and the control that answers it are read together rather than tracked in separate systems.",
|
|
17832
|
+
"core_question": "What could go wrong, what makes it possible, and what have we actually put in place against it?",
|
|
17833
|
+
"when_to_use": [
|
|
17834
|
+
"A threat modelling session needs somewhere to land that is not a document nobody reopens",
|
|
17835
|
+
"Controls and policies exist but nobody can point to the threats they answer",
|
|
17836
|
+
"An audit or review asks you to show posture, not just a list of findings"
|
|
17837
|
+
],
|
|
17838
|
+
"when_not_to_use": [
|
|
17839
|
+
"You want STRIDE's six threat categories specifically. This canvas is a lifecycle, not that taxonomy",
|
|
17840
|
+
"You are triaging a live incident rather than modelling posture",
|
|
17841
|
+
"The work is scoring and ranking risks against each other rather than laying out what exists"
|
|
17842
|
+
]
|
|
17843
|
+
}
|
|
17844
|
+
},
|
|
16558
17845
|
{
|
|
16559
17846
|
"id": "atomic-design",
|
|
16560
17847
|
"approach_ids": [
|
|
@@ -17349,11 +18636,107 @@ var UPG_FRAMEWORKS = [
|
|
|
17349
18636
|
"category": "go_to_market",
|
|
17350
18637
|
"origin": {
|
|
17351
18638
|
"type": "practitioner",
|
|
17352
|
-
"attribution": "Wes Bush",
|
|
17353
|
-
"description": 'Coined and formalised by Wes Bush in "Product-Led Growth" (2019). The framework describes how the product itself drives acquisition, activation, and expansion, reducing dependence on sales-led motions.',
|
|
17354
|
-
"url": "https://www.productled.com/",
|
|
17355
|
-
"year": 2019,
|
|
17356
|
-
"license": "published_methodology"
|
|
18639
|
+
"attribution": "Wes Bush",
|
|
18640
|
+
"description": 'Coined and formalised by Wes Bush in "Product-Led Growth" (2019). The framework describes how the product itself drives acquisition, activation, and expansion, reducing dependence on sales-led motions.',
|
|
18641
|
+
"url": "https://www.productled.com/",
|
|
18642
|
+
"year": 2019,
|
|
18643
|
+
"license": "published_methodology"
|
|
18644
|
+
},
|
|
18645
|
+
"tags": [
|
|
18646
|
+
"go_to_market",
|
|
18647
|
+
"flow"
|
|
18648
|
+
],
|
|
18649
|
+
"slots": [
|
|
18650
|
+
{
|
|
18651
|
+
"label": "Free Entry",
|
|
18652
|
+
"role": "free_entry",
|
|
18653
|
+
"entityTypeId": "gtm_strategy",
|
|
18654
|
+
"description": "How users access the product for free"
|
|
18655
|
+
},
|
|
18656
|
+
{
|
|
18657
|
+
"label": "Aha Moment",
|
|
18658
|
+
"role": "aha_moment",
|
|
18659
|
+
"entityTypeId": "gtm_strategy",
|
|
18660
|
+
"description": "First experience of core value"
|
|
18661
|
+
},
|
|
18662
|
+
{
|
|
18663
|
+
"label": "Expansion Motion",
|
|
18664
|
+
"role": "expansion_motion",
|
|
18665
|
+
"entityTypeId": "sales_motion",
|
|
18666
|
+
"description": "How usage spreads within accounts"
|
|
18667
|
+
},
|
|
18668
|
+
{
|
|
18669
|
+
"label": "Monetisation Trigger",
|
|
18670
|
+
"role": "monetisation_trigger",
|
|
18671
|
+
"entityTypeId": "gtm_strategy",
|
|
18672
|
+
"description": "When and how to convert to paid"
|
|
18673
|
+
},
|
|
18674
|
+
{
|
|
18675
|
+
"label": "Metric",
|
|
18676
|
+
"role": "metric",
|
|
18677
|
+
"entityTypeId": "metric",
|
|
18678
|
+
"description": "Key metric driving the PLG motion"
|
|
18679
|
+
}
|
|
18680
|
+
],
|
|
18681
|
+
"data": {
|
|
18682
|
+
"entity_types": [
|
|
18683
|
+
{
|
|
18684
|
+
"type": "gtm_strategy",
|
|
18685
|
+
"role": "item"
|
|
18686
|
+
},
|
|
18687
|
+
{
|
|
18688
|
+
"type": "sales_motion",
|
|
18689
|
+
"role": "item"
|
|
18690
|
+
},
|
|
18691
|
+
{
|
|
18692
|
+
"type": "metric",
|
|
18693
|
+
"role": "item"
|
|
18694
|
+
}
|
|
18695
|
+
],
|
|
18696
|
+
"required_properties": {}
|
|
18697
|
+
},
|
|
18698
|
+
"structure": {
|
|
18699
|
+
"pattern": "flow"
|
|
18700
|
+
},
|
|
18701
|
+
"presentation": {
|
|
18702
|
+
"layout": {
|
|
18703
|
+
"type": "flow",
|
|
18704
|
+
"direction": "LR"
|
|
18705
|
+
},
|
|
18706
|
+
"colour_by": "status",
|
|
18707
|
+
"card_fields": [
|
|
18708
|
+
"title",
|
|
18709
|
+
"description",
|
|
18710
|
+
"status"
|
|
18711
|
+
]
|
|
18712
|
+
},
|
|
18713
|
+
"education": {
|
|
18714
|
+
"purpose": "Design the product itself as the primary growth driver (free tier, self-serve onboarding, in-product virality) to reduce dependence on sales-led acquisition.",
|
|
18715
|
+
"core_question": "Can our product acquire, activate, and expand users without human intervention, and where in the loop do we still need sales?",
|
|
18716
|
+
"when_to_use": [
|
|
18717
|
+
"You are launching a new product, feature, or entering a new market",
|
|
18718
|
+
"You need to coordinate cross-functional launch activities",
|
|
18719
|
+
"You want to define target customers, channels, and messaging"
|
|
18720
|
+
],
|
|
18721
|
+
"when_not_to_use": [
|
|
18722
|
+
"The product is mature with established distribution channels",
|
|
18723
|
+
"You are iterating on an existing product for existing customers"
|
|
18724
|
+
]
|
|
18725
|
+
}
|
|
18726
|
+
},
|
|
18727
|
+
{
|
|
18728
|
+
"id": "gtm-playbook",
|
|
18729
|
+
"approach_ids": [
|
|
18730
|
+
"plan"
|
|
18731
|
+
],
|
|
18732
|
+
"name": "GTM Playbook",
|
|
18733
|
+
"version": "1.0.0",
|
|
18734
|
+
"description": "End-to-end go-to-market plan. Covers market analysis, ICP definition, positioning, messaging, channel strategy, launch plan, and success metrics in a sequential flow.",
|
|
18735
|
+
"category": "go_to_market",
|
|
18736
|
+
"origin": {
|
|
18737
|
+
"type": "practitioner",
|
|
18738
|
+
"description": "Synthesised from go-to-market practice across product marketing, sales, and customer success. The playbook format coordinates all GTM functions into a single executable plan.",
|
|
18739
|
+
"license": "cc_by"
|
|
17357
18740
|
},
|
|
17358
18741
|
"tags": [
|
|
17359
18742
|
"go_to_market",
|
|
@@ -17361,34 +18744,34 @@ var UPG_FRAMEWORKS = [
|
|
|
17361
18744
|
],
|
|
17362
18745
|
"slots": [
|
|
17363
18746
|
{
|
|
17364
|
-
"label": "
|
|
17365
|
-
"role": "free_entry",
|
|
18747
|
+
"label": "Market Analysis",
|
|
17366
18748
|
"entityTypeId": "gtm_strategy",
|
|
17367
|
-
"description": "
|
|
18749
|
+
"description": "Market sizing and competitive landscape"
|
|
17368
18750
|
},
|
|
17369
18751
|
{
|
|
17370
|
-
"label": "
|
|
17371
|
-
"
|
|
17372
|
-
"
|
|
17373
|
-
"description": "First experience of core value"
|
|
18752
|
+
"label": "Ideal Customer Profile",
|
|
18753
|
+
"entityTypeId": "ideal_customer_profile",
|
|
18754
|
+
"description": "Ideal customer profile"
|
|
17374
18755
|
},
|
|
17375
18756
|
{
|
|
17376
|
-
"label": "
|
|
17377
|
-
"
|
|
17378
|
-
"
|
|
17379
|
-
"description": "How usage spreads within accounts"
|
|
18757
|
+
"label": "Positioning",
|
|
18758
|
+
"entityTypeId": "positioning",
|
|
18759
|
+
"description": "How you position against alternatives"
|
|
17380
18760
|
},
|
|
17381
18761
|
{
|
|
17382
|
-
"label": "
|
|
17383
|
-
"
|
|
17384
|
-
"
|
|
17385
|
-
"description": "When and how to convert to paid"
|
|
18762
|
+
"label": "Messaging",
|
|
18763
|
+
"entityTypeId": "messaging",
|
|
18764
|
+
"description": "Key messages by audience"
|
|
17386
18765
|
},
|
|
17387
18766
|
{
|
|
17388
|
-
"label": "
|
|
17389
|
-
"
|
|
17390
|
-
"
|
|
17391
|
-
|
|
18767
|
+
"label": "Launch",
|
|
18768
|
+
"entityTypeId": "launch",
|
|
18769
|
+
"description": "Launch activities and timeline"
|
|
18770
|
+
},
|
|
18771
|
+
{
|
|
18772
|
+
"label": "Sales Motion",
|
|
18773
|
+
"entityTypeId": "sales_motion",
|
|
18774
|
+
"description": "How you will sell"
|
|
17392
18775
|
}
|
|
17393
18776
|
],
|
|
17394
18777
|
"data": {
|
|
@@ -17398,11 +18781,23 @@ var UPG_FRAMEWORKS = [
|
|
|
17398
18781
|
"role": "item"
|
|
17399
18782
|
},
|
|
17400
18783
|
{
|
|
17401
|
-
"type": "
|
|
18784
|
+
"type": "ideal_customer_profile",
|
|
17402
18785
|
"role": "item"
|
|
17403
18786
|
},
|
|
17404
18787
|
{
|
|
17405
|
-
"type": "
|
|
18788
|
+
"type": "positioning",
|
|
18789
|
+
"role": "item"
|
|
18790
|
+
},
|
|
18791
|
+
{
|
|
18792
|
+
"type": "messaging",
|
|
18793
|
+
"role": "item"
|
|
18794
|
+
},
|
|
18795
|
+
{
|
|
18796
|
+
"type": "launch",
|
|
18797
|
+
"role": "item"
|
|
18798
|
+
},
|
|
18799
|
+
{
|
|
18800
|
+
"type": "sales_motion",
|
|
17406
18801
|
"role": "item"
|
|
17407
18802
|
}
|
|
17408
18803
|
],
|
|
@@ -17424,8 +18819,8 @@ var UPG_FRAMEWORKS = [
|
|
|
17424
18819
|
]
|
|
17425
18820
|
},
|
|
17426
18821
|
"education": {
|
|
17427
|
-
"purpose": "
|
|
17428
|
-
"core_question": "
|
|
18822
|
+
"purpose": "Orchestrate every go-to-market activity (messaging, channels, pricing, enablement, launch timeline) into a single executable plan.",
|
|
18823
|
+
"core_question": "Do all go-to-market functions (product, marketing, sales, CS) have a shared plan for how we'll bring this to market?",
|
|
17429
18824
|
"when_to_use": [
|
|
17430
18825
|
"You are launching a new product, feature, or entering a new market",
|
|
17431
18826
|
"You need to coordinate cross-functional launch activities",
|
|
@@ -19638,6 +21033,173 @@ var UPG_FRAMEWORKS = [
|
|
|
19638
21033
|
"You are in a delivery phase with clear requirements"
|
|
19639
21034
|
]
|
|
19640
21035
|
}
|
|
21036
|
+
},
|
|
21037
|
+
{
|
|
21038
|
+
"id": "assumption-map",
|
|
21039
|
+
"approach_ids": [
|
|
21040
|
+
"reflect"
|
|
21041
|
+
],
|
|
21042
|
+
"name": "Assumption Map",
|
|
21043
|
+
"version": "1.0.0",
|
|
21044
|
+
"description": "Plot assumptions on axes of uncertainty vs risk. Most risky + most uncertain = test first.",
|
|
21045
|
+
"category": "validation",
|
|
21046
|
+
"origin": {
|
|
21047
|
+
"type": "practitioner",
|
|
21048
|
+
"attribution": "David Bland",
|
|
21049
|
+
"description": "Published in Testing Business Ideas (Wiley). Prioritises which assumptions to validate first.",
|
|
21050
|
+
"url": "https://www.strategyzer.com/books/testing-business-ideas",
|
|
21051
|
+
"year": 2019,
|
|
21052
|
+
"license": "published_methodology"
|
|
21053
|
+
},
|
|
21054
|
+
"tags": [
|
|
21055
|
+
"validation",
|
|
21056
|
+
"quadrant"
|
|
21057
|
+
],
|
|
21058
|
+
"slots": [
|
|
21059
|
+
{
|
|
21060
|
+
"label": "Test First",
|
|
21061
|
+
"entityTypeId": "assumption",
|
|
21062
|
+
"role": "test_first",
|
|
21063
|
+
"description": "High risk, low confidence: validate immediately",
|
|
21064
|
+
"predicate": [
|
|
21065
|
+
{
|
|
21066
|
+
"scope": "entity",
|
|
21067
|
+
"property": "risk_level",
|
|
21068
|
+
"op": "band",
|
|
21069
|
+
"value": [
|
|
21070
|
+
4,
|
|
21071
|
+
6
|
|
21072
|
+
]
|
|
21073
|
+
},
|
|
21074
|
+
{
|
|
21075
|
+
"scope": "entity",
|
|
21076
|
+
"property": "confidence",
|
|
21077
|
+
"op": "band",
|
|
21078
|
+
"value": [
|
|
21079
|
+
1,
|
|
21080
|
+
4
|
|
21081
|
+
]
|
|
21082
|
+
}
|
|
21083
|
+
]
|
|
21084
|
+
},
|
|
21085
|
+
{
|
|
21086
|
+
"label": "Research",
|
|
21087
|
+
"entityTypeId": "assumption",
|
|
21088
|
+
"role": "research",
|
|
21089
|
+
"description": "Low risk, low confidence: learn more",
|
|
21090
|
+
"predicate": [
|
|
21091
|
+
{
|
|
21092
|
+
"scope": "entity",
|
|
21093
|
+
"property": "risk_level",
|
|
21094
|
+
"op": "band",
|
|
21095
|
+
"value": [
|
|
21096
|
+
1,
|
|
21097
|
+
4
|
|
21098
|
+
]
|
|
21099
|
+
},
|
|
21100
|
+
{
|
|
21101
|
+
"scope": "entity",
|
|
21102
|
+
"property": "confidence",
|
|
21103
|
+
"op": "band",
|
|
21104
|
+
"value": [
|
|
21105
|
+
1,
|
|
21106
|
+
4
|
|
21107
|
+
]
|
|
21108
|
+
}
|
|
21109
|
+
]
|
|
21110
|
+
},
|
|
21111
|
+
{
|
|
21112
|
+
"label": "Monitor",
|
|
21113
|
+
"entityTypeId": "assumption",
|
|
21114
|
+
"role": "monitor",
|
|
21115
|
+
"description": "High risk but well understood: watch closely",
|
|
21116
|
+
"predicate": [
|
|
21117
|
+
{
|
|
21118
|
+
"scope": "entity",
|
|
21119
|
+
"property": "risk_level",
|
|
21120
|
+
"op": "band",
|
|
21121
|
+
"value": [
|
|
21122
|
+
4,
|
|
21123
|
+
6
|
|
21124
|
+
]
|
|
21125
|
+
},
|
|
21126
|
+
{
|
|
21127
|
+
"scope": "entity",
|
|
21128
|
+
"property": "confidence",
|
|
21129
|
+
"op": "band",
|
|
21130
|
+
"value": [
|
|
21131
|
+
4,
|
|
21132
|
+
6
|
|
21133
|
+
]
|
|
21134
|
+
}
|
|
21135
|
+
]
|
|
21136
|
+
},
|
|
21137
|
+
{
|
|
21138
|
+
"label": "Accept",
|
|
21139
|
+
"entityTypeId": "assumption",
|
|
21140
|
+
"role": "accept",
|
|
21141
|
+
"description": "Low risk, high confidence: safe to proceed",
|
|
21142
|
+
"predicate": [
|
|
21143
|
+
{
|
|
21144
|
+
"scope": "entity",
|
|
21145
|
+
"property": "risk_level",
|
|
21146
|
+
"op": "band",
|
|
21147
|
+
"value": [
|
|
21148
|
+
1,
|
|
21149
|
+
4
|
|
21150
|
+
]
|
|
21151
|
+
},
|
|
21152
|
+
{
|
|
21153
|
+
"scope": "entity",
|
|
21154
|
+
"property": "confidence",
|
|
21155
|
+
"op": "band",
|
|
21156
|
+
"value": [
|
|
21157
|
+
4,
|
|
21158
|
+
6
|
|
21159
|
+
]
|
|
21160
|
+
}
|
|
21161
|
+
]
|
|
21162
|
+
}
|
|
21163
|
+
],
|
|
21164
|
+
"data": {
|
|
21165
|
+
"entity_types": [
|
|
21166
|
+
{
|
|
21167
|
+
"type": "assumption",
|
|
21168
|
+
"role": "item"
|
|
21169
|
+
}
|
|
21170
|
+
],
|
|
21171
|
+
"required_properties": {}
|
|
21172
|
+
},
|
|
21173
|
+
"structure": {
|
|
21174
|
+
"pattern": "quadrant"
|
|
21175
|
+
},
|
|
21176
|
+
"presentation": {
|
|
21177
|
+
"layout": {
|
|
21178
|
+
"type": "quadrant",
|
|
21179
|
+
"x_axis": "confidence",
|
|
21180
|
+
"y_axis": "risk_level",
|
|
21181
|
+
"x_label": "Confidence",
|
|
21182
|
+
"y_label": "Risk"
|
|
21183
|
+
},
|
|
21184
|
+
"colour_by": "group",
|
|
21185
|
+
"card_fields": [
|
|
21186
|
+
"title",
|
|
21187
|
+
"description"
|
|
21188
|
+
]
|
|
21189
|
+
},
|
|
21190
|
+
"education": {
|
|
21191
|
+
"purpose": "Plot assumptions on a risk/evidence matrix so teams test the most dangerous unknowns first instead of building on unvalidated beliefs.",
|
|
21192
|
+
"core_question": "Which assumptions carry the most risk and the least evidence? Where do we run experiments first?",
|
|
21193
|
+
"when_to_use": [
|
|
21194
|
+
"You have hypotheses about user needs or solutions that need testing",
|
|
21195
|
+
"You want to reduce risk before committing engineering resources",
|
|
21196
|
+
"The team is debating assumptions that can be tested empirically"
|
|
21197
|
+
],
|
|
21198
|
+
"when_not_to_use": [
|
|
21199
|
+
"The solution is already validated through real usage data",
|
|
21200
|
+
"Speed of shipping matters more than certainty about assumptions"
|
|
21201
|
+
]
|
|
21202
|
+
}
|
|
19641
21203
|
}
|
|
19642
21204
|
];
|
|
19643
21205
|
var UPG_FRAMEWORKS_BY_ID = Object.fromEntries(
|
|
@@ -20197,6 +21759,44 @@ function validateValue(key, def, val) {
|
|
|
20197
21759
|
}
|
|
20198
21760
|
break;
|
|
20199
21761
|
}
|
|
21762
|
+
case "string[]": {
|
|
21763
|
+
if (!Array.isArray(val)) {
|
|
21764
|
+
errors.push(`property "${key}" must be an array of strings`);
|
|
21765
|
+
break;
|
|
21766
|
+
}
|
|
21767
|
+
if (val.some((v) => typeof v !== "string")) {
|
|
21768
|
+
errors.push(`property "${key}" must contain only strings`);
|
|
21769
|
+
}
|
|
21770
|
+
if (val.length === 0) {
|
|
21771
|
+
errors.push(`property "${key}" must not be empty`);
|
|
21772
|
+
}
|
|
21773
|
+
break;
|
|
21774
|
+
}
|
|
21775
|
+
case "object": {
|
|
21776
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) {
|
|
21777
|
+
errors.push(`property "${key}" must be an object`);
|
|
21778
|
+
break;
|
|
21779
|
+
}
|
|
21780
|
+
const obj = val;
|
|
21781
|
+
for (const req2 of def.required ?? []) {
|
|
21782
|
+
if (obj[req2] === void 0 || obj[req2] === null) {
|
|
21783
|
+
errors.push(`property "${key}" is missing required key "${req2}"`);
|
|
21784
|
+
}
|
|
21785
|
+
}
|
|
21786
|
+
for (const [subKey, subDef] of Object.entries(def.properties ?? {})) {
|
|
21787
|
+
const subVal = obj[subKey];
|
|
21788
|
+
if (subVal === void 0 || subVal === null) continue;
|
|
21789
|
+
errors.push(...validateValue(`${key}.${subKey}`, subDef, subVal));
|
|
21790
|
+
}
|
|
21791
|
+
for (const presentKey of Object.keys(obj)) {
|
|
21792
|
+
if (def.properties && !(presentKey in def.properties)) {
|
|
21793
|
+
errors.push(
|
|
21794
|
+
`property "${key}" has unknown key "${presentKey}" (allowed: ${Object.keys(def.properties).join(", ")})`
|
|
21795
|
+
);
|
|
21796
|
+
}
|
|
21797
|
+
}
|
|
21798
|
+
break;
|
|
21799
|
+
}
|
|
20200
21800
|
}
|
|
20201
21801
|
return errors;
|
|
20202
21802
|
}
|
|
@@ -20270,6 +21870,7 @@ var ENTITY_EMOJI = {
|
|
|
20270
21870
|
competitor_signal: "\u{1F4E1}",
|
|
20271
21871
|
compliance_framework: "\u{1F3DF}\uFE0F",
|
|
20272
21872
|
compliance_requirement: "\u26F2",
|
|
21873
|
+
composition: "\u{1F5BC}\uFE0F",
|
|
20273
21874
|
constraint: "\u{1F512}",
|
|
20274
21875
|
contact: "\u{1F4C7}",
|
|
20275
21876
|
content_calendar: "\u{1F4C5}",
|
|
@@ -20415,6 +22016,7 @@ var ENTITY_EMOJI = {
|
|
|
20415
22016
|
pipeline_sales: "\u{1FA88}",
|
|
20416
22017
|
pipeline_stage: "\u{1F9FD}",
|
|
20417
22018
|
planning_cycle: "\u{1F5D3}\uFE0F",
|
|
22019
|
+
configuration_axis: "\u{1F39A}\uFE0F",
|
|
20418
22020
|
playbook: "\u{1F4D2}",
|
|
20419
22021
|
portfolio: "\u{1F9F8}",
|
|
20420
22022
|
positioning: "\u{1F3AA}",
|
|
@@ -21109,6 +22711,7 @@ var STANDARD_LABELS = {
|
|
|
21109
22711
|
roadmap_theme: { alt_labels: ["product theme", "roadmap theme"] },
|
|
21110
22712
|
// UPG-660: renamed from bare 'theme' (N6 lineage)
|
|
21111
22713
|
planning_cycle: { alt_labels: ["sprint", "iteration", "cycle", "program increment", "pi", "cadence", "time-box", "cooldown", "quarter"] },
|
|
22714
|
+
configuration_axis: { alt_labels: ["configuration", "config axis", "lever", "variant axis", "plan axis", "flag axis", "entitlement axis"] },
|
|
21112
22715
|
// Engineering layer
|
|
21113
22716
|
bounded_context: { alt_labels: ["context", "domain boundary", "module boundary"] },
|
|
21114
22717
|
service: { alt_labels: ["microservice", "backend service", "api service"] },
|
|
@@ -23630,7 +25233,7 @@ var PRODUCT_SPEC_GUIDE = {
|
|
|
23630
25233
|
// registered to product_spec but missing from the navigation order).
|
|
23631
25234
|
// `changelog` lives here because it is a structural product-shipping
|
|
23632
25235
|
// artefact; content domain references it only via cross-domain bridges.
|
|
23633
|
-
creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "roadmap_theme", "changelog", "planning_cycle"],
|
|
25236
|
+
creation_sequence: ["feature_area", "feature", "epic", "user_story", "acceptance_criterion", "task", "bug", "release", "roadmap", "roadmap_item", "roadmap_theme", "changelog", "planning_cycle", "configuration_axis"],
|
|
23634
25237
|
patterns: [
|
|
23635
25238
|
{
|
|
23636
25239
|
name: "Feature Decomposition",
|
|
@@ -24345,12 +25948,21 @@ var PORTFOLIO_GUIDE = {
|
|
|
24345
25948
|
var WORKSPACE_GUIDE = {
|
|
24346
25949
|
domain_id: "workspace",
|
|
24347
25950
|
anchor_entity: "workspace",
|
|
24348
|
-
creation_sequence: ["workspace", "framework_exercise"],
|
|
24349
|
-
patterns: [
|
|
25951
|
+
creation_sequence: ["workspace", "framework_exercise", "composition"],
|
|
25952
|
+
patterns: [
|
|
25953
|
+
{
|
|
25954
|
+
name: "Canvas to published view",
|
|
25955
|
+
description: "A workspace arranges the entities being thought about, produces the composition published from it, and the composition names what it focuses on so the published view stays answerable to the graph",
|
|
25956
|
+
entity_types: ["workspace", "composition"],
|
|
25957
|
+
edge_chain: ["workspace_arranges_node", "workspace_produced_node", "composition_focuses_node"]
|
|
25958
|
+
}
|
|
25959
|
+
],
|
|
24350
25960
|
required_bridges: [],
|
|
24351
25961
|
anti_patterns: [
|
|
24352
25962
|
{ description: "Workspaces are containers for exploration: do not over-structure them" },
|
|
24353
|
-
{ description: "Workspace content is transient by default: promote discoveries to the graph, do not leave them in the workspace" }
|
|
25963
|
+
{ description: "Workspace content is transient by default: promote discoveries to the graph, do not leave them in the workspace" },
|
|
25964
|
+
{ description: "Treating a canvas arrangement as structure: a workspace_arranges_node edge says where a card sits, never what contains what. The arranged entity keeps its real parent" },
|
|
25965
|
+
{ description: "Writing every scratch canvas into a shared graph: only a workspace someone deliberately kept (retention: durable) belongs in a file other people pull" }
|
|
24354
25966
|
]
|
|
24355
25967
|
};
|
|
24356
25968
|
var FOUNDATIONS_GUIDE = {
|
|
@@ -27319,7 +28931,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27319
28931
|
id: "contended-surface-without-arbitration",
|
|
27320
28932
|
since: "0.27.0",
|
|
27321
28933
|
name: "Contended surface without arbitration",
|
|
27322
|
-
description: 'At least one surface
|
|
28934
|
+
description: 'At least one surface 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) and carries no arbitration_rule, or it admits through arbitration_state that the answer is absent (`none`) or accidental (`safe_by_coincidence`). Unrecorded, that answer lives in whoever remembers the last argument.',
|
|
27323
28935
|
// 0.28.0 (feedback 852a9721, from a 30-surface field audit). Three changes,
|
|
27324
28936
|
// each earned by a reported false positive or a reported blind spot:
|
|
27325
28937
|
//
|
|
@@ -27413,12 +29025,26 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27413
29025
|
// the natural move if the ordering detector is ever minted; it is not worth a
|
|
27414
29026
|
// breaking property change before then.
|
|
27415
29027
|
//
|
|
27416
|
-
//
|
|
27417
|
-
// slot that honestly
|
|
27418
|
-
// branch (c), which carries no chained exemption.
|
|
27419
|
-
//
|
|
27420
|
-
//
|
|
27421
|
-
//
|
|
29028
|
+
// CLOSED (banked at 0.28.0, field-confirmed and resolved here): a `chained`
|
|
29029
|
+
// slot that honestly declared `arbitration_state: 'none'` used to re-fire
|
|
29030
|
+
// through branch (c), which carries no chained exemption. The field
|
|
29031
|
+
// confirmation arrived from a second reporter whose census left the state
|
|
29032
|
+
// unset on every one of its chained frames and named this branch as the
|
|
29033
|
+
// reason. THE FIX WAS THE VOCABULARY, NOT THE DETECTOR. Branch (c) matches
|
|
29034
|
+
// the literal string 'none', so the new fifth `SurfaceArbitrationState`
|
|
29035
|
+
// member `no_contention_by_design` never matches it and NOTHING in this
|
|
29036
|
+
// file, the evaluator, or the collector changed.
|
|
29037
|
+
//
|
|
29038
|
+
// Exempting `chained` from (c) was the alternative and it lost on both
|
|
29039
|
+
// correctness and cost. Correctness: `none` is defined as "nobody ever
|
|
29040
|
+
// decided", which is FALSE on a chained shell that has no decision to make,
|
|
29041
|
+
// and silencing a check on a false statement is worse than the empty field
|
|
29042
|
+
// it replaces. Cost: `evaluateEntityCount`'s except form covers the
|
|
29043
|
+
// PRESENCE filter only; the value-keyed path is a bare indexed lookup with
|
|
29044
|
+
// no exception arm, so giving it one means a parallel derived spec set, a
|
|
29045
|
+
// new collector aggregate keyed by (type, property, value, except_property,
|
|
29046
|
+
// except_value), the derivation walk, and its tests. That is the whole
|
|
29047
|
+
// 0.28.0 evaluator rider again, spent to make a false statement quiet.
|
|
27422
29048
|
structured_condition: {
|
|
27423
29049
|
operator: "and",
|
|
27424
29050
|
checks: [
|
|
@@ -27471,7 +29097,23 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27471
29097
|
]
|
|
27472
29098
|
},
|
|
27473
29099
|
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.",
|
|
27474
|
-
|
|
29100
|
+
// Remediation is deliberately the ACTIONABLE CORE only. It is read by
|
|
29101
|
+
// someone who has already been told there is a problem and now needs the
|
|
29102
|
+
// next move, so rationale in it is strictly in the way. The reasoning that
|
|
29103
|
+
// used to live here (why transcription is ten minutes rather than a
|
|
29104
|
+
// meeting, why safe_by_coincidence is the dangerous state, why declaring
|
|
29105
|
+
// capacity is the honest fix rather than inventing a rule, and why the
|
|
29106
|
+
// chained exemption is a factual claim rather than a mute button) is all
|
|
29107
|
+
// above in this comment block, which is where a reader who wants it looks.
|
|
29108
|
+
remediation: (
|
|
29109
|
+
// Remediation is read by someone already told there is a problem and now
|
|
29110
|
+
// wanting the action, so rationale is strictly in the way. The reasoning
|
|
29111
|
+
// trimmed out in 0.31.0: a surface at its designed capacity should state
|
|
29112
|
+
// that capacity rather than invent an arbitration rule it does not need,
|
|
29113
|
+
// and leaving the state empty on a chained surface reads as unassessed
|
|
29114
|
+
// rather than as "nothing to arbitrate".
|
|
29115
|
+
"Start from `target_node_ids`, then read `arbitration_state`: `enforced_undocumented` needs the rule transcribed into `arbitration_rule`, `none` needs a decision, `safe_by_coincidence` needs a guard in code. A surface holding all it was designed to should state its `capacity`. Occupants that wrap rather than compete declare `composition_mode: 'chained'` with `arbitration_state: 'no_contention_by_design'`."
|
|
29116
|
+
),
|
|
27475
29117
|
stages: ["build", "beta", "launch", "growth", "mature", "maintenance"],
|
|
27476
29118
|
severity: "medium",
|
|
27477
29119
|
source: { kind: "fundamental" }
|
|
@@ -29045,6 +30687,11 @@ var UPG_REGIONS = [
|
|
|
29045
30687
|
type: "planning_cycle",
|
|
29046
30688
|
role: "container",
|
|
29047
30689
|
notes: "self-nesting cadence axis"
|
|
30690
|
+
},
|
|
30691
|
+
{
|
|
30692
|
+
type: "configuration_axis",
|
|
30693
|
+
role: "leaf",
|
|
30694
|
+
notes: "the configuration lever whose values select which surface tree the product renders"
|
|
29048
30695
|
}
|
|
29049
30696
|
],
|
|
29050
30697
|
anchor: {
|
|
@@ -30438,6 +32085,63 @@ var UPG_STRUCTURE_PATTERNS = [
|
|
|
30438
32085
|
"flow"
|
|
30439
32086
|
];
|
|
30440
32087
|
|
|
32088
|
+
// src/frameworks/relational-paths.ts
|
|
32089
|
+
function resolveRelationalPath(spine, path, catalog = UPG_EDGE_CATALOG) {
|
|
32090
|
+
const steps = [];
|
|
32091
|
+
if (path.length === 0) {
|
|
32092
|
+
return { steps, endpoint: null, reason: "path is empty; a projection must declare at least one step", failedAt: 0 };
|
|
32093
|
+
}
|
|
32094
|
+
let current = spine;
|
|
32095
|
+
for (let i = 0; i < path.length; i++) {
|
|
32096
|
+
const step = path[i];
|
|
32097
|
+
const entry = catalog[step.edge];
|
|
32098
|
+
if (!entry) {
|
|
32099
|
+
return {
|
|
32100
|
+
steps,
|
|
32101
|
+
endpoint: null,
|
|
32102
|
+
failedAt: i,
|
|
32103
|
+
reason: `unknown edge type "${step.edge}" \u2014 not in the UPG edge catalog`
|
|
32104
|
+
};
|
|
32105
|
+
}
|
|
32106
|
+
if (step.direction !== "forward" && step.direction !== "reverse") {
|
|
32107
|
+
return {
|
|
32108
|
+
steps,
|
|
32109
|
+
endpoint: null,
|
|
32110
|
+
failedAt: i,
|
|
32111
|
+
reason: `step direction must be "forward" or "reverse", got "${String(step.direction)}"`
|
|
32112
|
+
};
|
|
32113
|
+
}
|
|
32114
|
+
const entersAt = step.direction === "forward" ? entry.source_type : entry.target_type;
|
|
32115
|
+
const landsOn = step.direction === "forward" ? entry.target_type : entry.source_type;
|
|
32116
|
+
if (entersAt !== current) {
|
|
32117
|
+
return {
|
|
32118
|
+
steps,
|
|
32119
|
+
endpoint: null,
|
|
32120
|
+
failedAt: i,
|
|
32121
|
+
reason: `step ${i} ("${step.edge}" ${step.direction}) enters at "${entersAt}" but the traversal is at "${current}". The edge is declared ${entry.source_type} -> ${entry.target_type}; traversing it ${step.direction} requires being at "${entersAt}".`
|
|
32122
|
+
};
|
|
32123
|
+
}
|
|
32124
|
+
steps.push({ from: current, to: landsOn });
|
|
32125
|
+
current = landsOn;
|
|
32126
|
+
}
|
|
32127
|
+
return { steps, endpoint: current, reason: null, failedAt: -1 };
|
|
32128
|
+
}
|
|
32129
|
+
function relationalCoveredEntityTypes(fw) {
|
|
32130
|
+
const covered = /* @__PURE__ */ new Set();
|
|
32131
|
+
const rel = fw.relational;
|
|
32132
|
+
if (!rel) return covered;
|
|
32133
|
+
covered.add(rel.spine);
|
|
32134
|
+
for (const column of rel.columns) {
|
|
32135
|
+
if (column.kind !== "projection") continue;
|
|
32136
|
+
const resolved = resolveRelationalPath(rel.spine, column.path);
|
|
32137
|
+
for (const step of resolved.steps) {
|
|
32138
|
+
covered.add(step.from);
|
|
32139
|
+
covered.add(step.to);
|
|
32140
|
+
}
|
|
32141
|
+
}
|
|
32142
|
+
return covered;
|
|
32143
|
+
}
|
|
32144
|
+
|
|
30441
32145
|
// src/frameworks/validate.ts
|
|
30442
32146
|
function validateUPGFramework(framework) {
|
|
30443
32147
|
const errors = [];
|
|
@@ -30542,6 +32246,25 @@ function validateUPGFramework(framework) {
|
|
|
30542
32246
|
}
|
|
30543
32247
|
}
|
|
30544
32248
|
}
|
|
32249
|
+
if (f.relational !== void 0) {
|
|
32250
|
+
if (typeof f.relational !== "object" || f.relational === null || Array.isArray(f.relational)) {
|
|
32251
|
+
errors.push('"relational" must be an object when present');
|
|
32252
|
+
} else {
|
|
32253
|
+
const rel = f.relational;
|
|
32254
|
+
errors.push(...validateRelationalSurface(rel));
|
|
32255
|
+
if (Array.isArray(f.slots) && f.slots.length > 0) {
|
|
32256
|
+
warnings.push(
|
|
32257
|
+
'"relational" and "slots" are both declared. `relational` is authoritative for a join surface; slots cannot express an edge path and will drift. Remove "slots" from this framework.'
|
|
32258
|
+
);
|
|
32259
|
+
}
|
|
32260
|
+
const presLayout = f.presentation?.layout;
|
|
32261
|
+
if (presLayout?.type === "table" && Array.isArray(presLayout.columns) && presLayout.columns.length > 0) {
|
|
32262
|
+
errors.push(
|
|
32263
|
+
`"presentation.layout.columns" declares ${presLayout.columns.length} columns, but "relational" is the authoritative roster for a join surface. Empty the presentation columns; do not maintain both.`
|
|
32264
|
+
);
|
|
32265
|
+
}
|
|
32266
|
+
}
|
|
32267
|
+
}
|
|
30545
32268
|
if (!f.education || typeof f.education !== "object") {
|
|
30546
32269
|
errors.push('"education" is required and must be an object');
|
|
30547
32270
|
} else {
|
|
@@ -30559,8 +32282,144 @@ function validateUPGFramework(framework) {
|
|
|
30559
32282
|
errors.push('"education.when_not_to_use" is required and must be an array');
|
|
30560
32283
|
}
|
|
30561
32284
|
}
|
|
32285
|
+
validateSlotPredicates(f.slots, errors);
|
|
30562
32286
|
return { valid: errors.length === 0, errors, warnings };
|
|
30563
32287
|
}
|
|
32288
|
+
var PREDICATE_SCOPES = ["entity", "framework"];
|
|
32289
|
+
var PREDICATE_OPS = ["eq", "in", "gte", "lt", "band"];
|
|
32290
|
+
function admits(atom) {
|
|
32291
|
+
switch (atom.op) {
|
|
32292
|
+
case "eq":
|
|
32293
|
+
return { kind: "discrete", values: [atom.value] };
|
|
32294
|
+
case "in":
|
|
32295
|
+
return { kind: "discrete", values: atom.value };
|
|
32296
|
+
case "gte":
|
|
32297
|
+
return { kind: "interval", lo: atom.value, hi: Number.POSITIVE_INFINITY };
|
|
32298
|
+
case "lt":
|
|
32299
|
+
return { kind: "interval", lo: Number.NEGATIVE_INFINITY, hi: atom.value };
|
|
32300
|
+
case "band":
|
|
32301
|
+
return { kind: "interval", lo: atom.value[0], hi: atom.value[1] };
|
|
32302
|
+
}
|
|
32303
|
+
}
|
|
32304
|
+
function atomsCannotBothHold(a, b) {
|
|
32305
|
+
const x = admits(a);
|
|
32306
|
+
const y = admits(b);
|
|
32307
|
+
if (x.kind === "discrete" && y.kind === "discrete") {
|
|
32308
|
+
return !x.values.some((v) => y.values.includes(v));
|
|
32309
|
+
}
|
|
32310
|
+
if (x.kind === "interval" && y.kind === "interval") {
|
|
32311
|
+
return !(x.lo < y.hi && y.lo < x.hi);
|
|
32312
|
+
}
|
|
32313
|
+
const set = x.kind === "discrete" ? x : y;
|
|
32314
|
+
const range = x.kind === "interval" ? x : y;
|
|
32315
|
+
return !set.values.some((v) => typeof v === "number" && v >= range.lo && v < range.hi);
|
|
32316
|
+
}
|
|
32317
|
+
function predicatesProvablyDisjoint(a, b) {
|
|
32318
|
+
for (const atomA of a) {
|
|
32319
|
+
for (const atomB of b) {
|
|
32320
|
+
if (atomA.scope === atomB.scope && atomA.property === atomB.property && atomsCannotBothHold(atomA, atomB)) {
|
|
32321
|
+
return true;
|
|
32322
|
+
}
|
|
32323
|
+
}
|
|
32324
|
+
}
|
|
32325
|
+
return false;
|
|
32326
|
+
}
|
|
32327
|
+
function parseAtom(atom, where, errors) {
|
|
32328
|
+
if (!atom || typeof atom !== "object" || Array.isArray(atom)) {
|
|
32329
|
+
errors.push(`${where} must be an object`);
|
|
32330
|
+
return null;
|
|
32331
|
+
}
|
|
32332
|
+
const a = atom;
|
|
32333
|
+
let ok = true;
|
|
32334
|
+
if (typeof a.scope !== "string" || !PREDICATE_SCOPES.includes(a.scope)) {
|
|
32335
|
+
errors.push(`${where}.scope must be one of: ${PREDICATE_SCOPES.join(", ")}`);
|
|
32336
|
+
ok = false;
|
|
32337
|
+
}
|
|
32338
|
+
if (typeof a.property !== "string" || a.property.length === 0) {
|
|
32339
|
+
errors.push(`${where}.property is required and must be a non-empty string`);
|
|
32340
|
+
ok = false;
|
|
32341
|
+
}
|
|
32342
|
+
if (typeof a.op !== "string" || !PREDICATE_OPS.includes(a.op)) {
|
|
32343
|
+
errors.push(`${where}.op must be one of: ${PREDICATE_OPS.join(", ")}`);
|
|
32344
|
+
return null;
|
|
32345
|
+
}
|
|
32346
|
+
switch (a.op) {
|
|
32347
|
+
case "eq":
|
|
32348
|
+
if (!["string", "number", "boolean"].includes(typeof a.value)) {
|
|
32349
|
+
errors.push(`${where}.value must be a string, number, or boolean for op "eq"`);
|
|
32350
|
+
ok = false;
|
|
32351
|
+
}
|
|
32352
|
+
break;
|
|
32353
|
+
case "in":
|
|
32354
|
+
if (!Array.isArray(a.value) || a.value.length === 0 || !a.value.every((v) => typeof v === "string" || typeof v === "number")) {
|
|
32355
|
+
errors.push(`${where}.value must be a non-empty array of strings or numbers for op "in"`);
|
|
32356
|
+
ok = false;
|
|
32357
|
+
}
|
|
32358
|
+
break;
|
|
32359
|
+
case "gte":
|
|
32360
|
+
case "lt":
|
|
32361
|
+
if (typeof a.value !== "number" || !Number.isFinite(a.value)) {
|
|
32362
|
+
errors.push(`${where}.value must be a finite number for op "${a.op}"`);
|
|
32363
|
+
ok = false;
|
|
32364
|
+
}
|
|
32365
|
+
break;
|
|
32366
|
+
case "band":
|
|
32367
|
+
if (!Array.isArray(a.value) || a.value.length !== 2 || !a.value.every((v) => typeof v === "number" && Number.isFinite(v))) {
|
|
32368
|
+
errors.push(`${where}.value must be a [min, max] pair of finite numbers for op "band"`);
|
|
32369
|
+
ok = false;
|
|
32370
|
+
} else if (a.value[0] >= a.value[1]) {
|
|
32371
|
+
errors.push(
|
|
32372
|
+
`${where}.value is an empty band: min (${a.value[0]}) must be less than max (${a.value[1]}). The interval is half-open [min, max)`
|
|
32373
|
+
);
|
|
32374
|
+
ok = false;
|
|
32375
|
+
}
|
|
32376
|
+
break;
|
|
32377
|
+
}
|
|
32378
|
+
return ok ? a : null;
|
|
32379
|
+
}
|
|
32380
|
+
function validateSlotPredicates(slots, errors) {
|
|
32381
|
+
if (!Array.isArray(slots)) return;
|
|
32382
|
+
const zones = [];
|
|
32383
|
+
slots.forEach((slot, i) => {
|
|
32384
|
+
if (!slot || typeof slot !== "object") return;
|
|
32385
|
+
const s = slot;
|
|
32386
|
+
if (s.predicate === void 0 || s.predicate === null) return;
|
|
32387
|
+
const label = typeof s.label === "string" && s.label.length > 0 ? ` ("${s.label}")` : "";
|
|
32388
|
+
const where = `"slots[${i}]${label}.predicate"`;
|
|
32389
|
+
if (!Array.isArray(s.predicate)) {
|
|
32390
|
+
errors.push(
|
|
32391
|
+
`${where} must be an array of atoms. A slot's membership rule is a conjunction, even when it has one atom`
|
|
32392
|
+
);
|
|
32393
|
+
return;
|
|
32394
|
+
}
|
|
32395
|
+
if (s.predicate.length === 0) {
|
|
32396
|
+
errors.push(
|
|
32397
|
+
`${where} must not be empty. A zero-atom predicate matches every entity, which is a catch-all cell \u2014 declare the property that decides membership`
|
|
32398
|
+
);
|
|
32399
|
+
return;
|
|
32400
|
+
}
|
|
32401
|
+
const atoms = s.predicate.map((atom, j) => parseAtom(atom, `${where}[${j}]`, errors));
|
|
32402
|
+
if (atoms.some((a) => a === null)) return;
|
|
32403
|
+
zones.push({
|
|
32404
|
+
where: `slots[${i}]${label}`,
|
|
32405
|
+
entityTypeId: typeof s.entityTypeId === "string" ? s.entityTypeId : null,
|
|
32406
|
+
predicate: atoms
|
|
32407
|
+
});
|
|
32408
|
+
});
|
|
32409
|
+
for (let i = 0; i < zones.length; i++) {
|
|
32410
|
+
for (let j = i + 1; j < zones.length; j++) {
|
|
32411
|
+
const a = zones[i];
|
|
32412
|
+
const b = zones[j];
|
|
32413
|
+
if (a.entityTypeId && b.entityTypeId && a.entityTypeId !== b.entityTypeId) continue;
|
|
32414
|
+
if (predicatesProvablyDisjoint(a.predicate, b.predicate)) continue;
|
|
32415
|
+
const shared = a.predicate.filter((x) => b.predicate.some((y) => y.scope === x.scope && y.property === x.property)).map((x) => `${x.scope}.${x.property}`);
|
|
32416
|
+
const hint = shared.length ? `They constrain the same properties (${[...new Set(shared)].join(", ")}) without separating on any of them` : `They constrain no property in common, so an entity can satisfy both`;
|
|
32417
|
+
errors.push(
|
|
32418
|
+
`"${a.where}" and "${b.where}" have predicates that are not provably disjoint, so an entity could land in both zones. ${hint}. Add an atom that separates them \u2014 for example, a value range on one property that the other excludes`
|
|
32419
|
+
);
|
|
32420
|
+
}
|
|
32421
|
+
}
|
|
32422
|
+
}
|
|
30564
32423
|
function validateExpression(expr) {
|
|
30565
32424
|
if (expr.trim().length === 0) {
|
|
30566
32425
|
return "Expression must not be empty";
|
|
@@ -30580,6 +32439,122 @@ function validateExpression(expr) {
|
|
|
30580
32439
|
}
|
|
30581
32440
|
return null;
|
|
30582
32441
|
}
|
|
32442
|
+
function validateRelationalSurface(rel) {
|
|
32443
|
+
const errors = [];
|
|
32444
|
+
const spine = rel.spine;
|
|
32445
|
+
if (!spine || typeof spine !== "string") {
|
|
32446
|
+
errors.push('"relational.spine" is required and must be a string (the entity type whose instances are the rows)');
|
|
32447
|
+
}
|
|
32448
|
+
if (!Array.isArray(rel.columns) || rel.columns.length === 0) {
|
|
32449
|
+
errors.push('"relational.columns" is required and must be a non-empty array');
|
|
32450
|
+
return errors;
|
|
32451
|
+
}
|
|
32452
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
32453
|
+
for (let i = 0; i < rel.columns.length; i++) {
|
|
32454
|
+
const col = rel.columns[i];
|
|
32455
|
+
const at = `"relational.columns[${i}]"`;
|
|
32456
|
+
if (!col || typeof col !== "object" || Array.isArray(col)) {
|
|
32457
|
+
errors.push(`${at} must be an object`);
|
|
32458
|
+
continue;
|
|
32459
|
+
}
|
|
32460
|
+
const c = col;
|
|
32461
|
+
if (!c.id || typeof c.id !== "string") {
|
|
32462
|
+
errors.push(`${at}.id is required and must be a string`);
|
|
32463
|
+
} else if (seenIds.has(c.id)) {
|
|
32464
|
+
errors.push(`${at}.id "${c.id}" is duplicated; column ids must be unique within a surface`);
|
|
32465
|
+
} else {
|
|
32466
|
+
seenIds.add(c.id);
|
|
32467
|
+
}
|
|
32468
|
+
if (!c.label || typeof c.label !== "string") {
|
|
32469
|
+
errors.push(`${at}.label is required and must be a string`);
|
|
32470
|
+
}
|
|
32471
|
+
switch (c.kind) {
|
|
32472
|
+
case "field": {
|
|
32473
|
+
if (!c.property || typeof c.property !== "string") {
|
|
32474
|
+
errors.push(`${at}.property is required on a field column and must be a string`);
|
|
32475
|
+
}
|
|
32476
|
+
if ("entityTypeId" in c) {
|
|
32477
|
+
errors.push(
|
|
32478
|
+
`${at} is a field column and must NOT declare "entityTypeId" \u2014 its entity is the spine by definition. A field column that names a type is the slot-forced shape this block replaces.`
|
|
32479
|
+
);
|
|
32480
|
+
}
|
|
32481
|
+
if ("path" in c) {
|
|
32482
|
+
errors.push(`${at} is a field column and must NOT declare "path"; use kind:"projection" to traverse edges`);
|
|
32483
|
+
}
|
|
32484
|
+
break;
|
|
32485
|
+
}
|
|
32486
|
+
case "computed": {
|
|
32487
|
+
if (!c.expression || typeof c.expression !== "string") {
|
|
32488
|
+
errors.push(`${at}.expression is required on a computed column and must be a string`);
|
|
32489
|
+
} else {
|
|
32490
|
+
const exprError = validateExpression(c.expression);
|
|
32491
|
+
if (exprError) errors.push(`${at}.expression: ${exprError}`);
|
|
32492
|
+
}
|
|
32493
|
+
break;
|
|
32494
|
+
}
|
|
32495
|
+
case "projection": {
|
|
32496
|
+
if (!Array.isArray(c.path) || c.path.length === 0) {
|
|
32497
|
+
errors.push(
|
|
32498
|
+
`${at}.path is required on a projection column and must be a non-empty array of { edge, direction } steps`
|
|
32499
|
+
);
|
|
32500
|
+
} else {
|
|
32501
|
+
let stepShapeOk = true;
|
|
32502
|
+
for (let s = 0; s < c.path.length; s++) {
|
|
32503
|
+
const step = c.path[s];
|
|
32504
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
32505
|
+
errors.push(`${at}.path[${s}] must be an object { edge, direction }`);
|
|
32506
|
+
stepShapeOk = false;
|
|
32507
|
+
continue;
|
|
32508
|
+
}
|
|
32509
|
+
const st = step;
|
|
32510
|
+
if (!st.edge || typeof st.edge !== "string") {
|
|
32511
|
+
errors.push(`${at}.path[${s}].edge is required and must be a string`);
|
|
32512
|
+
stepShapeOk = false;
|
|
32513
|
+
}
|
|
32514
|
+
if (st.direction !== "forward" && st.direction !== "reverse") {
|
|
32515
|
+
errors.push(
|
|
32516
|
+
`${at}.path[${s}].direction is required and must be "forward" or "reverse" (never inferred)`
|
|
32517
|
+
);
|
|
32518
|
+
stepShapeOk = false;
|
|
32519
|
+
}
|
|
32520
|
+
}
|
|
32521
|
+
if (stepShapeOk && typeof spine === "string") {
|
|
32522
|
+
const resolved = resolveRelationalPath(spine, c.path);
|
|
32523
|
+
if (resolved.reason) {
|
|
32524
|
+
errors.push(`${at}.path does not compose from spine "${spine}": ${resolved.reason}`);
|
|
32525
|
+
}
|
|
32526
|
+
}
|
|
32527
|
+
}
|
|
32528
|
+
if (!Array.isArray(c.fields) || c.fields.length === 0) {
|
|
32529
|
+
errors.push(`${at}.fields is required on a projection column and must be a non-empty array of property keys`);
|
|
32530
|
+
}
|
|
32531
|
+
if (c.limit !== void 0 && (typeof c.limit !== "number" || !Number.isInteger(c.limit) || c.limit < 1)) {
|
|
32532
|
+
errors.push(`${at}.limit must be a positive integer when present (it truncates rendering, never membership)`);
|
|
32533
|
+
}
|
|
32534
|
+
break;
|
|
32535
|
+
}
|
|
32536
|
+
default:
|
|
32537
|
+
errors.push(`${at}.kind must be one of: field, computed, projection. Got "${String(c.kind)}"`);
|
|
32538
|
+
}
|
|
32539
|
+
}
|
|
32540
|
+
const sort = rel.sort;
|
|
32541
|
+
if (!sort || typeof sort !== "object" || Array.isArray(sort)) {
|
|
32542
|
+
errors.push('"relational.sort" is required and must be an object { column, direction }');
|
|
32543
|
+
} else {
|
|
32544
|
+
const s = sort;
|
|
32545
|
+
if (!s.column || typeof s.column !== "string") {
|
|
32546
|
+
errors.push('"relational.sort.column" is required and must be a string (a column id)');
|
|
32547
|
+
} else if (seenIds.size > 0 && !seenIds.has(s.column)) {
|
|
32548
|
+
errors.push(
|
|
32549
|
+
`"relational.sort.column" is "${s.column}", which is not a declared column id. Declared ids: ${[...seenIds].join(", ")}`
|
|
32550
|
+
);
|
|
32551
|
+
}
|
|
32552
|
+
if (s.direction !== "asc" && s.direction !== "desc") {
|
|
32553
|
+
errors.push('"relational.sort.direction" is required and must be "asc" or "desc"');
|
|
32554
|
+
}
|
|
32555
|
+
}
|
|
32556
|
+
return errors;
|
|
32557
|
+
}
|
|
30583
32558
|
|
|
30584
32559
|
// src/format/canonical.ts
|
|
30585
32560
|
var nodeCrypto = __toESM(require("crypto"), 1);
|
|
@@ -30710,12 +32685,13 @@ function canonicalProduct(product) {
|
|
|
30710
32685
|
function effectiveRootProduct(doc) {
|
|
30711
32686
|
const node = doc.nodes?.find((n) => n.type === "product" && n.id === doc.product.id);
|
|
30712
32687
|
if (!node) return doc.product;
|
|
32688
|
+
const propStage = node.properties?.stage;
|
|
32689
|
+
const nodeStage = (typeof propStage === "string" ? propStage : void 0) ?? node.status;
|
|
30713
32690
|
return {
|
|
30714
32691
|
...doc.product,
|
|
30715
32692
|
title: node.title ?? doc.product.title,
|
|
30716
32693
|
description: node.description ?? doc.product.description,
|
|
30717
|
-
|
|
30718
|
-
stage: node.status ?? doc.product.stage
|
|
32694
|
+
stage: nodeStage ?? doc.product.stage
|
|
30719
32695
|
};
|
|
30720
32696
|
}
|
|
30721
32697
|
function singleBody(doc) {
|
|
@@ -30961,7 +32937,7 @@ function checkHeaderSealText(text) {
|
|
|
30961
32937
|
}
|
|
30962
32938
|
|
|
30963
32939
|
// src/index.ts
|
|
30964
|
-
var UPG_VERSION = "0.
|
|
32940
|
+
var UPG_VERSION = "0.31.0";
|
|
30965
32941
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
30966
32942
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
30967
32943
|
var UPG_TYPES = getTypes();
|
|
@@ -31026,11 +33002,14 @@ var UPG_EDGE_COUNT = UPG_EDGE_TYPES.length;
|
|
|
31026
33002
|
var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
31027
33003
|
// Annotate the CommonJS export names for ESM import in node:
|
|
31028
33004
|
0 && (module.exports = {
|
|
33005
|
+
ACTIVE_WHEN_PROPERTY,
|
|
31029
33006
|
ANALYTICS_DATA_PLAYBOOK,
|
|
31030
33007
|
BUSINESS_GROWTH_METRIC_DRIVEN_PLAYBOOK,
|
|
31031
33008
|
BUSINESS_GTM_GROWTH_PLAYBOOK,
|
|
31032
33009
|
BUSINESS_MARKETING_AUDIENCE_FIRST_PLAYBOOK,
|
|
31033
33010
|
CLASSIFICATION_EDGE_PROPERTY_SCHEMA,
|
|
33011
|
+
CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA,
|
|
33012
|
+
CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA,
|
|
31034
33013
|
CONTENT_DEPTH_WARNING_RULES,
|
|
31035
33014
|
COVERAGE_ANTI_PATTERNS,
|
|
31036
33015
|
DEFAULT_ENTITY_EMOJI,
|
|
@@ -31045,10 +33024,12 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
31045
33024
|
MARKDOWN_FORMAT_VERSION,
|
|
31046
33025
|
MARKET_COMPETITIVE_PLAYBOOK,
|
|
31047
33026
|
OPERATIONS_QUALITY_PLAYBOOK,
|
|
33027
|
+
PRESENT_UNDER_PROPERTY,
|
|
31048
33028
|
PRODUCT_DELIVERY_PLAYBOOK,
|
|
31049
33029
|
PROPERTY_MODIFIERS,
|
|
31050
33030
|
PROPERTY_MODIFIER_SEMANTICS,
|
|
31051
33031
|
PROPERTY_SCALE_MAP,
|
|
33032
|
+
QUALIFIABLE_EDGE_TYPES,
|
|
31052
33033
|
RECORD_ENTITY_TYPES,
|
|
31053
33034
|
REFLECT_MODES,
|
|
31054
33035
|
REGISTRY_PRODUCT_ID,
|
|
@@ -31131,9 +33112,12 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
31131
33112
|
UPG_WILDCARD_ENDPOINT,
|
|
31132
33113
|
USERS_NEEDS_PLAYBOOK,
|
|
31133
33114
|
UnknownEntityTypeError,
|
|
33115
|
+
VARIES_BY_EDGE,
|
|
33116
|
+
WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA,
|
|
31134
33117
|
backfillSlug,
|
|
31135
33118
|
buildTypeAliases,
|
|
31136
33119
|
canBeChildOf,
|
|
33120
|
+
checkConfigurationDrift,
|
|
31137
33121
|
checkHeaderSeal,
|
|
31138
33122
|
checkHeaderSealText,
|
|
31139
33123
|
checkToEdgeCountSpec,
|
|
@@ -31154,6 +33138,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
31154
33138
|
edgeCarriesProperties,
|
|
31155
33139
|
edgeCountSpecKey,
|
|
31156
33140
|
entityFilterKey,
|
|
33141
|
+
enumerateProjections,
|
|
31157
33142
|
evaluateAntiPatterns,
|
|
31158
33143
|
findInvalidStatusMigrationTargets,
|
|
31159
33144
|
formatUpgText,
|
|
@@ -31244,11 +33229,17 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
31244
33229
|
normalizeDocument,
|
|
31245
33230
|
parseUpg,
|
|
31246
33231
|
pickCanonicalEdge,
|
|
33232
|
+
predicatesProvablyDisjoint,
|
|
31247
33233
|
presenceExceptKey,
|
|
33234
|
+
projectGraph,
|
|
33235
|
+
readActiveWhen,
|
|
33236
|
+
readStringArray,
|
|
33237
|
+
relationalCoveredEntityTypes,
|
|
31248
33238
|
resolveAllEdges,
|
|
31249
33239
|
resolveContainmentEdge,
|
|
31250
33240
|
resolveEntityType,
|
|
31251
33241
|
resolveLabel,
|
|
33242
|
+
resolveRelationalPath,
|
|
31252
33243
|
resolveSlugCollision,
|
|
31253
33244
|
resolveTreePatternEdges,
|
|
31254
33245
|
ringOrderedDomainIds,
|