@unified-product-graph/core 0.30.0 → 0.32.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 +64 -0
- package/README.md +1 -1
- package/dist/index.cjs +2013 -266
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12139 -10650
- package/dist/index.d.ts +12139 -10650
- package/dist/index.js +2009 -266
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -394,7 +394,7 @@ var UPG_DOMAINS = [
|
|
|
394
394
|
{
|
|
395
395
|
id: "program_mgmt",
|
|
396
396
|
label: "Program Management",
|
|
397
|
-
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).",
|
|
397
|
+
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).",
|
|
398
398
|
types: [
|
|
399
399
|
"program",
|
|
400
400
|
"project",
|
|
@@ -490,8 +490,8 @@ var UPG_DOMAINS = [
|
|
|
490
490
|
{
|
|
491
491
|
id: "workspace",
|
|
492
492
|
label: "Workspace",
|
|
493
|
-
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.",
|
|
494
|
-
types: ["workspace", "framework_exercise"]
|
|
493
|
+
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.",
|
|
494
|
+
types: ["workspace", "framework_exercise", "composition", "capture"]
|
|
495
495
|
},
|
|
496
496
|
{
|
|
497
497
|
id: "foundations",
|
|
@@ -978,7 +978,11 @@ var UPG_ENTITY_META = [
|
|
|
978
978
|
{ name: "partner_revenue_share", type_id: "ent_312", maturity: "stable", since: "0.1.0" },
|
|
979
979
|
// ── Workspace ──
|
|
980
980
|
{ name: "workspace", type_id: "ent_328", maturity: "proposed", since: "0.2.0" },
|
|
981
|
-
{ name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" }
|
|
981
|
+
{ name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" },
|
|
982
|
+
{ name: "composition", type_id: "ent_361", maturity: "proposed", since: "0.31.0" },
|
|
983
|
+
// A dated, hashed rendition of a node that is already in the graph. Enters
|
|
984
|
+
// `proposed` per the maturity rubric (precedent: composition 0.31.0).
|
|
985
|
+
{ name: "capture", type_id: "ent_362", maturity: "proposed", since: "0.32.0" }
|
|
982
986
|
];
|
|
983
987
|
var DEFERRED_PROPOSED_BY_CONTRACT = {
|
|
984
988
|
// user_story (UPG-571) and experiment_run (UPG-664) graduated to stable in
|
|
@@ -1072,6 +1076,24 @@ var CONFIGURATION_QUALIFIER_EDGE_PROPERTY_SCHEMA = {
|
|
|
1072
1076
|
required: ["axis", "values"]
|
|
1073
1077
|
}
|
|
1074
1078
|
};
|
|
1079
|
+
var WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA = {
|
|
1080
|
+
x: {
|
|
1081
|
+
type: "number",
|
|
1082
|
+
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."
|
|
1083
|
+
},
|
|
1084
|
+
y: {
|
|
1085
|
+
type: "number",
|
|
1086
|
+
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."
|
|
1087
|
+
},
|
|
1088
|
+
expanded: {
|
|
1089
|
+
type: "boolean",
|
|
1090
|
+
description: "Whether the card is drawn expanded rather than collapsed. Presentation state that survives a reload, unlike selection."
|
|
1091
|
+
},
|
|
1092
|
+
frame_id: {
|
|
1093
|
+
type: "string",
|
|
1094
|
+
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."
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1075
1097
|
var UPG_EDGE_CATALOG = {
|
|
1076
1098
|
// ── Part 1: Core Narrative Spine (Ring 1) ──────────────────────────────────
|
|
1077
1099
|
// 1.1 User Domain
|
|
@@ -1248,6 +1270,24 @@ var UPG_EDGE_CATALOG = {
|
|
|
1248
1270
|
// the catalog's singular-source/target grammar (polymorphic targets
|
|
1249
1271
|
// would be a separate catalog-mechanic change).
|
|
1250
1272
|
competitive_analysis_dimensioned_by_classification_axis: { forward_verb: "dimensioned_by", reverse_verb: "dimensions", classification: "hierarchy", source_type: "competitive_analysis", target_type: "classification_axis" },
|
|
1273
|
+
// A product's own classification axes (0.32.0). Before this the only parent a
|
|
1274
|
+
// classification_axis could have was a competitive_analysis, so a taxonomy the
|
|
1275
|
+
// product uses on its OWN entities — the grouped-label case, one named group
|
|
1276
|
+
// over a set of values — had to hang off a competitive analysis it has nothing
|
|
1277
|
+
// to do with, or float parentless.
|
|
1278
|
+
//
|
|
1279
|
+
// Hierarchy, so the axis has a home in get_tree, on the same honest verb the
|
|
1280
|
+
// competitive_analysis edge already uses; and it earns its
|
|
1281
|
+
// UPG_VALID_CHILDREN.product pair the same way product_defines_configuration_axis
|
|
1282
|
+
// did at 0.30.0 — axes are few per product, and visibility of a mechanism
|
|
1283
|
+
// matters more than the tree cost.
|
|
1284
|
+
//
|
|
1285
|
+
// This is what makes one level of label grouping expressible with NO new label
|
|
1286
|
+
// surface: the group is the axis, the labels are its values, and carrying one
|
|
1287
|
+
// is node_classified_as_classification_value, all of it stable since 0.4.0.
|
|
1288
|
+
// Ungrouped freeform labels stay in `tags`. The line between them: if the
|
|
1289
|
+
// group has a name a person would filter by, it is an axis; otherwise a tag.
|
|
1290
|
+
product_dimensioned_by_classification_axis: { forward_verb: "dimensioned_by", reverse_verb: "dimensions", classification: "hierarchy", source_type: "product", target_type: "classification_axis" },
|
|
1251
1291
|
classification_axis_includes_classification_value: { forward_verb: "includes", reverse_verb: "value_of", classification: "hierarchy", source_type: "classification_axis", target_type: "classification_value" },
|
|
1252
1292
|
// 0.10.2: also dual-registered as a cross-edge (UPG_CROSS_EDGE_TYPES) so a
|
|
1253
1293
|
// competitor can be classified directly against a `registry/{classification_value}`
|
|
@@ -1487,10 +1527,20 @@ var UPG_EDGE_CATALOG = {
|
|
|
1487
1527
|
// iterations; a cycle contains its cooldown), mirroring team_contains_team.
|
|
1488
1528
|
planning_cycle_contains_planning_cycle: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "planning_cycle", target_type: "planning_cycle" },
|
|
1489
1529
|
// E3: scheduling work into a cycle is a deliberate authoring act, not a
|
|
1490
|
-
// containment nesting —
|
|
1491
|
-
// referenced by the cycle. deliberate_only so the generic-inference
|
|
1492
|
-
// never auto-materialise it.
|
|
1493
|
-
|
|
1530
|
+
// containment nesting — the work item keeps its feature/epic parent and is
|
|
1531
|
+
// merely referenced by the cycle. deliberate_only so the generic-inference
|
|
1532
|
+
// chokepoints never auto-materialise it.
|
|
1533
|
+
//
|
|
1534
|
+
// WIDENED AT 0.32.0, renamed from planning_cycle_schedules_user_story. The
|
|
1535
|
+
// story-only endpoint could not hold a real tracker import: the adapters
|
|
1536
|
+
// default an unrecognised issue to `task`, so the type a cycle most needed to
|
|
1537
|
+
// schedule was the one type it could not reach. Endpoint-polymorphic over the
|
|
1538
|
+
// work-item set {feature, epic, user_story, task, bug} by the same
|
|
1539
|
+
// construction as work_item_blocks_work_item in the same 0.20.0 batch — the
|
|
1540
|
+
// `work_item` token names the intended semantic domain, the endpoint is the
|
|
1541
|
+
// `node` wildcard. Three typed edges were the alternative and would have
|
|
1542
|
+
// re-opened a decision this vocabulary already took for the same entity set.
|
|
1543
|
+
planning_cycle_schedules_work_item: { forward_verb: "schedules", reverse_verb: "scheduled_in", classification: "semantic", source_type: "planning_cycle", target_type: "node", deliberate_only: true },
|
|
1494
1544
|
// E2: the OKR cycle-scoping anchor. objective -> planning_cycle. planning_cycle
|
|
1495
1545
|
// is portfolio_shared, so this passes the cross-scope gate and ships as
|
|
1496
1546
|
// PROVISIONAL (allowed cross-product with a write-time warning, self-maintaining,
|
|
@@ -1968,7 +2018,40 @@ var UPG_EDGE_CATALOG = {
|
|
|
1968
2018
|
// real edge, not just a string property — the domain's own anti-pattern is
|
|
1969
2019
|
// "version prompts like code".
|
|
1970
2020
|
prompt_version_supersedes_prompt_version: { forward_verb: "supersedes", reverse_verb: "superseded_by", classification: "semantic", source_type: "prompt_version", target_type: "prompt_version" },
|
|
2021
|
+
// The TYPED subject edge. Kept alongside the polymorphic `eval_benchmark_measures_node`
|
|
2022
|
+
// added in 0.31.0, on the same footing as `constraint_constrains_feature` beside
|
|
2023
|
+
// `node_constrains_node`: reach for THIS one when the subject is a feature, and for the
|
|
2024
|
+
// wildcard only when it is not. Two ways to say the same thing about a feature is the
|
|
2025
|
+
// cost of the pair, and it is paid deliberately rather than by accident.
|
|
1971
2026
|
eval_benchmark_measures_feature: { forward_verb: "measures", reverse_verb: "measured_by", classification: "cross-domain", source_type: "eval_benchmark", target_type: "feature" },
|
|
2027
|
+
/*
|
|
2028
|
+
* 0.31.0 — the benchmark SUBJECT, polymorphic.
|
|
2029
|
+
*
|
|
2030
|
+
* An eval measures a tool, a document, a check, an importer, a feature. Those
|
|
2031
|
+
* are unrelated types, so the subject relation is genuinely wildcard-shaped and
|
|
2032
|
+
* follows the decision-to-anything family (`decision_influences_node`).
|
|
2033
|
+
*
|
|
2034
|
+
* THE WIDTH IS OVER-WIDE, AND THAT IS ON THE LABEL RATHER THAN LAUNDERED.
|
|
2035
|
+
* The decision family's warrant is that a decision really is about anything. A
|
|
2036
|
+
* benchmark is not: it measures a MEASURABLE thing, and this edge will happily
|
|
2037
|
+
* claim a benchmark can measure a persona, which is meaningless. It ships as the
|
|
2038
|
+
* honest deferral it is — the alternative was minting typed edges for subject
|
|
2039
|
+
* types the spec does not yet have, which would answer "what is a tool in the
|
|
2040
|
+
* graph" as a side effect of building an eval harness. That question is a stated
|
|
2041
|
+
* non-goal, and deferring it is the point of choosing the wildcard.
|
|
2042
|
+
*
|
|
2043
|
+
* Registered in UPG_POLYMORPHIC_EDGE_KEYS; Audit 05 errors on any wildcard that
|
|
2044
|
+
* is not.
|
|
2045
|
+
*/
|
|
2046
|
+
eval_benchmark_measures_node: { forward_verb: "measures", reverse_verb: "measured_by", classification: "semantic", source_type: "eval_benchmark", target_type: "node" },
|
|
2047
|
+
/*
|
|
2048
|
+
* 0.31.0 — the labeled corpus a benchmark draws its cases from.
|
|
2049
|
+
*
|
|
2050
|
+
* `ai_dataset` reused rather than a new `corpus` type minted (Q5, ratified): the
|
|
2051
|
+
* existing type plus one join edge carries the need, and a corpus type would be
|
|
2052
|
+
* minted only if that demonstrably failed. It has not.
|
|
2053
|
+
*/
|
|
2054
|
+
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" },
|
|
1972
2055
|
ai_guardrail_enforces_security_policy: { forward_verb: "enforces", reverse_verb: "enforced_by", classification: "cross-domain", source_type: "ai_guardrail", target_type: "security_policy" },
|
|
1973
2056
|
model_comparison_informs_decision: { forward_verb: "informs", reverse_verb: "informed_by", classification: "cross-domain", source_type: "model_comparison", target_type: "decision" },
|
|
1974
2057
|
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" },
|
|
@@ -2006,6 +2089,23 @@ var UPG_EDGE_CATALOG = {
|
|
|
2006
2089
|
agent_hook_triggers_ci_pipeline: { forward_verb: "triggers", reverse_verb: "triggered_by", classification: "cross-domain", source_type: "agent_hook", target_type: "ci_pipeline" },
|
|
2007
2090
|
workflow_template_defines_agent_task: { forward_verb: "defines", reverse_verb: "defined_by", classification: "hierarchy", source_type: "workflow_template", target_type: "agent_task" },
|
|
2008
2091
|
agent_definition_spawns_agent_task: { forward_verb: "spawns", reverse_verb: "spawned_by", classification: "hierarchy", source_type: "agent_definition", target_type: "agent_task" },
|
|
2092
|
+
// The delegation bridge (tasks-workflows-2026-08 §5). Before this, the whole
|
|
2093
|
+
// automation domain could reach product work only at coarse grain —
|
|
2094
|
+
// `workflow_run_implements_initiative` (initiative), `agent_skill_extends_feature`
|
|
2095
|
+
// (feature), `agent_session_creates_decision` (decision),
|
|
2096
|
+
// `workflow_artifact_references_deliverable` (deliverable) — and NOTHING reached
|
|
2097
|
+
// `task`, the atomic unit of product work that `feature_decomposes_into_task` /
|
|
2098
|
+
// `epic_decomposes_into_task` make the delivery hierarchy's leaf. So "this agent
|
|
2099
|
+
// work item IS the execution of that product task" was inexpressible, and a
|
|
2100
|
+
// human-vs-agent assignee could not be read off the graph.
|
|
2101
|
+
// Cross-domain: agent_task lives in `automation`, task in product delivery.
|
|
2102
|
+
// NOT hierarchy — the agent_task does not CONTAIN the task; both are
|
|
2103
|
+
// independently-parented work items (`workflow_template_defines_agent_task` /
|
|
2104
|
+
// `agent_definition_spawns_agent_task` own the agent side, feature/epic the
|
|
2105
|
+
// product side), and the delegation is a lateral reference between them.
|
|
2106
|
+
// Verb `executes` mirrors the domain's own vocabulary (`workflow_template`
|
|
2107
|
+
// is `executed_as` a run); reverse reads "this task is executed_by that agent task".
|
|
2108
|
+
agent_task_executes_task: { forward_verb: "executes", reverse_verb: "executed_by", classification: "cross-domain", source_type: "agent_task", target_type: "task" },
|
|
2009
2109
|
// ── Part 4: Scale Ring (Ring 4) ────────────────────────────────────────────
|
|
2010
2110
|
// 4.1 Growth Domain
|
|
2011
2111
|
product_measures_funnel: { forward_verb: "measures", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "product", target_type: "funnel" },
|
|
@@ -2356,6 +2456,25 @@ var UPG_EDGE_CATALOG = {
|
|
|
2356
2456
|
product_managed_via_program: { forward_verb: "managed_via", reverse_verb: "manages", classification: "hierarchy", source_type: "product", target_type: "program" },
|
|
2357
2457
|
program_contains_project: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "program", target_type: "project" },
|
|
2358
2458
|
project_targets_milestone: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "project", target_type: "milestone" },
|
|
2459
|
+
// Portfolio Phase 2 (ratified 2026-08-14): a milestone that a PRODUCT owns
|
|
2460
|
+
// outright, with no program/project above it. `milestone` had exactly one
|
|
2461
|
+
// inbound edge spec-wide (`project_targets_milestone`) and `project` was its
|
|
2462
|
+
// only declared parent, so a portfolio-grain milestone — which names a
|
|
2463
|
+
// product, not a project — was inexpressible. This is the 0.23.0 epic-twin
|
|
2464
|
+
// precedent: an existing leaf type gains a second, SHALLOWER parent, and the
|
|
2465
|
+
// name, verbs and classification are the deeper edge's, inherited unchanged
|
|
2466
|
+
// rather than invented.
|
|
2467
|
+
//
|
|
2468
|
+
// The mediated path was checked and rejected, not overlooked. The relation IS
|
|
2469
|
+
// reachable as product —managed_via→ program —contains→ project —targets→
|
|
2470
|
+
// milestone, but at three hops (past the 1-2-hop threshold), and the hop count
|
|
2471
|
+
// is not the real objection: mediation forces `program` and `project` nodes
|
|
2472
|
+
// into existence that the source data does not have.
|
|
2473
|
+
//
|
|
2474
|
+
// NOT cross_product_eligible: a milestone belongs to the graph of the product
|
|
2475
|
+
// that owns it. `UPG_VALID_CHILDREN.product` gains 'milestone' in step, which
|
|
2476
|
+
// the hierarchy-orphan guard requires.
|
|
2477
|
+
product_targets_milestone: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "product", target_type: "milestone" },
|
|
2359
2478
|
project_produces_deliverable: { forward_verb: "produces", reverse_verb: "produced_by", classification: "hierarchy", source_type: "project", target_type: "deliverable" },
|
|
2360
2479
|
program_tracked_via_risk_register: { forward_verb: "tracked_via", reverse_verb: "tracks", classification: "hierarchy", source_type: "program", target_type: "risk_register" },
|
|
2361
2480
|
risk_register_contains_risk: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "risk_register", target_type: "risk" },
|
|
@@ -2502,6 +2621,60 @@ var UPG_EDGE_CATALOG = {
|
|
|
2502
2621
|
// node) has no schema mechanism yet — deliberately deferred, see the WS3
|
|
2503
2622
|
// proposal doc, ripple map "surprises" #3.
|
|
2504
2623
|
workspace_produced_node: { forward_verb: "produced", reverse_verb: "produced_in", classification: "causal", source_type: "workspace", target_type: "node" },
|
|
2624
|
+
// Canvas arrangement: which entities are ON a workspace, and where. This is
|
|
2625
|
+
// the edge `WorkspaceProperties`' docstring promised for months without the
|
|
2626
|
+
// catalog having it; the promise is now true.
|
|
2627
|
+
//
|
|
2628
|
+
// Polymorphic per the enum-vs-polymorphism ADR (2026-06-16), on all three
|
|
2629
|
+
// arms: universality holds (any entity can be dragged onto a canvas, with no
|
|
2630
|
+
// principled stopping point), constraint value is nil (nobody could defend
|
|
2631
|
+
// "you may arrange features but not personas"), and there is NO STRUCTURAL
|
|
2632
|
+
// ROLE. That third arm is load-bearing rather than decorative.
|
|
2633
|
+
// `classification: 'cross-domain'` is therefore mandatory, not cosmetic: it
|
|
2634
|
+
// keeps the edge out of `UPG_VALID_CHILDREN` and `resolveContainmentEdge`, so
|
|
2635
|
+
// `get_tree` never walks it and a canvas placement can never masquerade as
|
|
2636
|
+
// containment. An arranged node keeps its real containment parent. Same
|
|
2637
|
+
// posture as `framework_exercise_includes_node`.
|
|
2638
|
+
//
|
|
2639
|
+
// The alternative was putting the whole canvas snapshot in one opaque node
|
|
2640
|
+
// property, which is cheaper and loses. A node id inside a blob is a foreign
|
|
2641
|
+
// key held as a scalar (the exact thing P14 forbids), the placed entities
|
|
2642
|
+
// become invisible to `get_node` and `query`, and a deleted node leaves a
|
|
2643
|
+
// stale reference that `repair_dangling_edges` cannot see. As an edge, that
|
|
2644
|
+
// guard is inherited for free. Only the furniture with no graph referent
|
|
2645
|
+
// stays opaque, in `workspace.properties.canvas`.
|
|
2646
|
+
//
|
|
2647
|
+
// NOT cross_product_eligible: deliberate scope limit mirroring the WS3
|
|
2648
|
+
// anchors, not an oversight.
|
|
2649
|
+
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 },
|
|
2650
|
+
// What a published composition SHOWS. Passes the same three-arm ADR test the
|
|
2651
|
+
// same way, and carries no properties: a focus is a bare reference.
|
|
2652
|
+
//
|
|
2653
|
+
// It earns its place by making a composition legible to a tool that cannot
|
|
2654
|
+
// parse the publishing tool's URLs. "Which published views show this
|
|
2655
|
+
// persona?" is a LEARN question the graph exists to answer, and the edge is
|
|
2656
|
+
// what makes a composition's graph references repairable when a focused node
|
|
2657
|
+
// is deleted. Adding it in a later release would be a second blast-radius
|
|
2658
|
+
// event on the same track.
|
|
2659
|
+
//
|
|
2660
|
+
// POPULATION IS BEST-EFFORT and an empty focus set is VALID: a composition
|
|
2661
|
+
// whose member hrefs the writing tool cannot resolve to node ids is still a
|
|
2662
|
+
// well-formed composition. That keeps the spec complete without blocking on
|
|
2663
|
+
// app work, and it is why no check fires on an unfocused composition.
|
|
2664
|
+
//
|
|
2665
|
+
// NOT cross_product_eligible: a portfolio-level composition spanning several
|
|
2666
|
+
// product graphs is conceivable but unevidenced. Same YAGNI call as above.
|
|
2667
|
+
composition_focuses_node: { forward_verb: "focuses", reverse_verb: "focused_in", classification: "cross-domain", source_type: "composition", target_type: "node" },
|
|
2668
|
+
// What a capture is a picture OF (0.32.0). Polymorphic because anything in the
|
|
2669
|
+
// graph can be rendered — a surface, a screen, a report, a canvas — and being
|
|
2670
|
+
// captured carries no structural role, so the endpoint collapses to the
|
|
2671
|
+
// wildcard rather than enumerating a list that would never finish.
|
|
2672
|
+
//
|
|
2673
|
+
// This edge is also where the preservation-is-not-permission rule lands for
|
|
2674
|
+
// renditions: a consumer renders the captures the GRAPH says exist, never the
|
|
2675
|
+
// ones some tool's opaque bag still remembers. A capture that was deleted is a
|
|
2676
|
+
// node that was deleted, and the spec needs no second deletion vocabulary.
|
|
2677
|
+
capture_renders_node: { forward_verb: "renders", reverse_verb: "rendered_by", classification: "cross-domain", source_type: "capture", target_type: "node" },
|
|
2505
2678
|
// ── P14 edges (replacing foreign-key properties) ─────────────────────────
|
|
2506
2679
|
feature_addresses_job: { forward_verb: "addresses", reverse_verb: "addressed_by", classification: "cross-domain", source_type: "feature", target_type: "job" },
|
|
2507
2680
|
feature_drives_key_result: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "feature", target_type: "key_result" },
|
|
@@ -2618,6 +2791,23 @@ var UPG_EDGE_CATALOG = {
|
|
|
2618
2791
|
node_owned_by_stakeholder: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "stakeholder" },
|
|
2619
2792
|
node_owned_by_department: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "department", cross_product_eligible: true },
|
|
2620
2793
|
node_owned_by_person: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "person" },
|
|
2794
|
+
// ── Cross-domain: Assignment (0.32.0) ───────────────────────────────────────
|
|
2795
|
+
// Assignment is NOT ownership, and 0.12.0 ruled otherwise. That ruling routed
|
|
2796
|
+
// task.assignee and bug.assignee into node_owned_by_person, and it is
|
|
2797
|
+
// superseded here rather than quietly widened, because the two differ in a way
|
|
2798
|
+
// the field measures.
|
|
2799
|
+
//
|
|
2800
|
+
// ASSIGNMENT HAS A TIME INTERVAL AND OWNERSHIP DOES NOT. A shipped ops plane
|
|
2801
|
+
// models assignment as a claim carrying claimed-at, released-at and a partial
|
|
2802
|
+
// unique index enforcing one live claim; nothing in the spec gives ownership
|
|
2803
|
+
// that shape. And a real 184-item board ran with 157 items (85%) unassigned
|
|
2804
|
+
// while every one of them was owned in the ordinary sense. One edge meaning
|
|
2805
|
+
// both cannot express "owned by the team, assigned to nobody", which is the
|
|
2806
|
+
// modal state of a working board.
|
|
2807
|
+
//
|
|
2808
|
+
// Not cross_product_eligible, matching node_owned_by_person: an assignee is a
|
|
2809
|
+
// person in this graph's own roster.
|
|
2810
|
+
node_assigned_to_person: { forward_verb: "assigned_to", reverse_verb: "assigned", classification: "cross-domain", source_type: "node", target_type: "person" },
|
|
2621
2811
|
// ── Cross-domain: Architecture (DDD) ─────────────────────────────────────────
|
|
2622
2812
|
// Service, domain_event, domain_entity, aggregate, read_model, api_contract,
|
|
2623
2813
|
// value_object, command, and data_model are DDD building blocks that all
|
|
@@ -3546,6 +3736,9 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
|
|
|
3546
3736
|
"framework_exercise_includes_node",
|
|
3547
3737
|
// Universal classification (any node classified against a classification_value)
|
|
3548
3738
|
"node_classified_as_classification_value",
|
|
3739
|
+
// Benchmark subject (0.31.0): an eval measures a tool, a doc, a check, an
|
|
3740
|
+
// importer, a feature. Over-wide by admission — see the definition's comment.
|
|
3741
|
+
"eval_benchmark_measures_node",
|
|
3549
3742
|
// Work-item issue links (0.20.0): endpoint-polymorphic over the work-item set
|
|
3550
3743
|
// {feature, epic, user_story, task, bug}. The `work_item_` key names the
|
|
3551
3744
|
// intended semantic domain; the endpoints are the `node` wildcard.
|
|
@@ -3554,7 +3747,23 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
|
|
|
3554
3747
|
"work_item_duplicates_work_item",
|
|
3555
3748
|
// Workspace provenance (WS3, 2026-07-05): a workspace can produce any
|
|
3556
3749
|
// entity type arranged in it. Widened from workspace_produced_decision.
|
|
3557
|
-
"workspace_produced_node"
|
|
3750
|
+
"workspace_produced_node",
|
|
3751
|
+
// Canvas arrangement and published-view focus: any entity can be dragged
|
|
3752
|
+
// onto a canvas or shown in a published view, and neither carries a
|
|
3753
|
+
// structural role, so both collapse polymorphic rather than enumerating.
|
|
3754
|
+
"workspace_arranges_node",
|
|
3755
|
+
"composition_focuses_node",
|
|
3756
|
+
// Universal assignment (0.32.0): who is working on this, as distinct from who
|
|
3757
|
+
// owns it. Wildcard source for the same reason node_owned_by_person has one.
|
|
3758
|
+
"node_assigned_to_person",
|
|
3759
|
+
// Capture subject (0.32.0): anything in the graph can be rendered, and being
|
|
3760
|
+
// rendered carries no structural role.
|
|
3761
|
+
"capture_renders_node",
|
|
3762
|
+
// Cadence scheduling (0.32.0, widened from planning_cycle_schedules_user_story):
|
|
3763
|
+
// endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
|
|
3764
|
+
// bug}. The `work_item` key token names the intended semantic domain; the
|
|
3765
|
+
// endpoint is the `node` wildcard.
|
|
3766
|
+
"planning_cycle_schedules_work_item"
|
|
3558
3767
|
];
|
|
3559
3768
|
var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
|
|
3560
3769
|
function isPolymorphicEdge(key) {
|
|
@@ -3749,6 +3958,11 @@ var UPG_VALID_CHILDREN = {
|
|
|
3749
3958
|
// the product renders. Few per product, and top-level because an axis is a
|
|
3750
3959
|
// product-wide fact rather than a child of any one feature or surface.
|
|
3751
3960
|
"configuration_axis",
|
|
3961
|
+
// Classification (0.32.0): the product's own taxonomies — the grouped-label
|
|
3962
|
+
// case, one named group over a set of values. Same reasoning as the
|
|
3963
|
+
// configuration axis above, and until now the only parent a
|
|
3964
|
+
// classification_axis could have was a competitive_analysis.
|
|
3965
|
+
"classification_axis",
|
|
3752
3966
|
// Engineering
|
|
3753
3967
|
"bounded_context",
|
|
3754
3968
|
"code_repository",
|
|
@@ -3839,7 +4053,12 @@ var UPG_VALID_CHILDREN = {
|
|
|
3839
4053
|
"subscription",
|
|
3840
4054
|
"forecast",
|
|
3841
4055
|
// Program Management
|
|
4056
|
+
// `milestone` is a SECOND, shallower parent here (Portfolio Phase 2,
|
|
4057
|
+
// 2026-08-14), alongside its existing `project` parent: a product can own a
|
|
4058
|
+
// milestone outright with no program/project above it. Required by the
|
|
4059
|
+
// hierarchy-orphan guard now that `product_targets_milestone` exists.
|
|
3842
4060
|
"program",
|
|
4061
|
+
"milestone",
|
|
3843
4062
|
// Marketing
|
|
3844
4063
|
"marketing_strategy",
|
|
3845
4064
|
"press_release",
|
|
@@ -4028,7 +4247,7 @@ var UPG_VALID_CHILDREN = {
|
|
|
4028
4247
|
// contains iterations; a cycle contains its cooldown) via
|
|
4029
4248
|
// planning_cycle_contains_planning_cycle. It does NOT contain user_story as a
|
|
4030
4249
|
// child: scheduling work into a cycle is the semantic, deliberate-only
|
|
4031
|
-
//
|
|
4250
|
+
// planning_cycle_schedules_work_item edge, so the item keeps its feature/epic
|
|
4032
4251
|
// containment parent and is merely referenced by the cycle.
|
|
4033
4252
|
planning_cycle: ["planning_cycle"],
|
|
4034
4253
|
// ── Engineering hierarchy ───────────────────────────────────────────────────
|
|
@@ -4366,7 +4585,20 @@ var UPG_CONTAINMENT_FREE_TYPES = /* @__PURE__ */ new Set([
|
|
|
4366
4585
|
// containment. Modelling it as a product child would force a containment edge
|
|
4367
4586
|
// and read against the whole point of the exercise model (relational, not
|
|
4368
4587
|
// hierarchical). Same posture as `person`. See ADR 2026-06-02-framework-exercises.
|
|
4369
|
-
"framework_exercise"
|
|
4588
|
+
"framework_exercise",
|
|
4589
|
+
// A composition has no structural parent for the same reason a
|
|
4590
|
+
// framework_exercise does not: it anchors to what it SHOWS, relationally, via
|
|
4591
|
+
// `composition_focuses_node`. Modelling it as a product child would force a
|
|
4592
|
+
// containment edge that reads against the point — a published view is not a
|
|
4593
|
+
// part of the product, it is a lens onto one. This is also what keeps the
|
|
4594
|
+
// type to two edges rather than three: no provenance edge is minted (that is
|
|
4595
|
+
// `workspace_produced_node`, reused) and no parent edge is needed.
|
|
4596
|
+
"composition",
|
|
4597
|
+
// A capture has no structural parent for the same reason: it anchors to what
|
|
4598
|
+
// it RENDERS, via `capture_renders_node`. Giving it a product parent would
|
|
4599
|
+
// claim a capture is part of the product rather than a picture of one, and
|
|
4600
|
+
// would mint a containment edge whose only job is to satisfy the hierarchy.
|
|
4601
|
+
"capture"
|
|
4370
4602
|
]);
|
|
4371
4603
|
function isContainmentFreeType(entityType) {
|
|
4372
4604
|
return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
|
|
@@ -6472,7 +6704,8 @@ var WORK_ITEM_TEMPLATE = {
|
|
|
6472
6704
|
initial_phase: "todo",
|
|
6473
6705
|
terminal_phases: ["done", "cancelled"],
|
|
6474
6706
|
phases: [
|
|
6475
|
-
{ id: "
|
|
6707
|
+
{ id: "backlog", status_category: "backlog", label: "Backlog", description: "Accepted or set aside deliberately: real work, not scheduled. Distinct from `todo`, which is committed and merely not started.", transitions_to: ["todo", "in_progress", "cancelled"] },
|
|
6708
|
+
{ id: "todo", status_category: "unstarted", label: "To Do", description: "Identified but not yet started.", transitions_to: ["in_progress", "backlog", "cancelled"] },
|
|
6476
6709
|
{ id: "in_progress", status_category: "started", label: "In Progress", description: "Actively being worked on.", transitions_to: ["in_review", "todo", "cancelled"] },
|
|
6477
6710
|
{ id: "in_review", status_category: "started", label: "In Review", description: "Work completed, awaiting review or acceptance.", transitions_to: ["done", "in_progress", "cancelled"] },
|
|
6478
6711
|
{ id: "done", status_category: "completed", label: "Done", description: "Completed and accepted.", transitions_to: [] },
|
|
@@ -6742,6 +6975,34 @@ var FRAMEWORK_EXERCISE_LIFECYCLE = {
|
|
|
6742
6975
|
}
|
|
6743
6976
|
]
|
|
6744
6977
|
};
|
|
6978
|
+
var COMPOSITION_LIFECYCLE = {
|
|
6979
|
+
entity_type: "composition",
|
|
6980
|
+
initial_phase: "draft",
|
|
6981
|
+
terminal_phases: ["archived"],
|
|
6982
|
+
phases: [
|
|
6983
|
+
{
|
|
6984
|
+
id: "draft",
|
|
6985
|
+
status_category: "started",
|
|
6986
|
+
label: "Draft",
|
|
6987
|
+
description: "Being arranged. It has a slug reserved and members placed, but has never been published, so nothing resolves at its address yet.",
|
|
6988
|
+
transitions_to: ["published", "archived"]
|
|
6989
|
+
},
|
|
6990
|
+
{
|
|
6991
|
+
id: "published",
|
|
6992
|
+
status_category: "completed",
|
|
6993
|
+
label: "Published",
|
|
6994
|
+
description: "Live at its slug and linkable. Republishing the same slug stays in this phase and bumps the revision rather than moving the composition.",
|
|
6995
|
+
transitions_to: ["archived", "draft"]
|
|
6996
|
+
},
|
|
6997
|
+
{
|
|
6998
|
+
id: "archived",
|
|
6999
|
+
status_category: "completed",
|
|
7000
|
+
label: "Archived",
|
|
7001
|
+
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.",
|
|
7002
|
+
transitions_to: ["published", "draft"]
|
|
7003
|
+
}
|
|
7004
|
+
]
|
|
7005
|
+
};
|
|
6745
7006
|
var SPECIFICATION_LIFECYCLE = {
|
|
6746
7007
|
entity_type: "specification",
|
|
6747
7008
|
initial_phase: "draft",
|
|
@@ -6873,6 +7134,7 @@ var UPG_LIFECYCLES = [
|
|
|
6873
7134
|
GROWTH_CAMPAIGN_LIFECYCLE,
|
|
6874
7135
|
// Workspace
|
|
6875
7136
|
FRAMEWORK_EXERCISE_LIFECYCLE,
|
|
7137
|
+
COMPOSITION_LIFECYCLE,
|
|
6876
7138
|
// ── Template-generated lifecycles ─────────────────────────────────
|
|
6877
7139
|
...TEMPLATE_LIFECYCLES
|
|
6878
7140
|
];
|
|
@@ -7074,7 +7336,11 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
|
|
|
7074
7336
|
// from experiment_runs (which have lifecycles); hypothesis_evidence was
|
|
7075
7337
|
// lifecycle-free here but is deprecated at v0.4.0 ──────────────────────────────────────
|
|
7076
7338
|
"evidence",
|
|
7077
|
-
"learning"
|
|
7339
|
+
"learning",
|
|
7340
|
+
// ── Workspace (1 of 4): a capture is a fact about a moment, not a workflow.
|
|
7341
|
+
// It is taken, and from then on it either still matches its subject or it
|
|
7342
|
+
// does not — which is what `content_hash` answers, not a phase. ─────────
|
|
7343
|
+
"capture"
|
|
7078
7344
|
]);
|
|
7079
7345
|
function isLifecycleFreeType(entityType) {
|
|
7080
7346
|
return UPG_LIFECYCLE_FREE_TYPES.has(entityType);
|
|
@@ -7764,8 +8030,7 @@ var UPG_MIGRATIONS = {
|
|
|
7764
8030
|
{
|
|
7765
8031
|
from: "metric_definition",
|
|
7766
8032
|
to: "metric",
|
|
7767
|
-
|
|
7768
|
-
reason: "A metric definition is a metric without implementation. Lifecycle property, not a separate type."
|
|
8033
|
+
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.)"
|
|
7769
8034
|
},
|
|
7770
8035
|
// ── Experiment consolidation ────────────────────────────────────────────
|
|
7771
8036
|
{
|
|
@@ -7790,8 +8055,8 @@ var UPG_MIGRATIONS = {
|
|
|
7790
8055
|
{
|
|
7791
8056
|
from: "risk_item",
|
|
7792
8057
|
to: "risk",
|
|
7793
|
-
defaults: {
|
|
7794
|
-
reason: "risk_item (Program Mgmt) is identical to risk (Legal). Consolidated
|
|
8058
|
+
defaults: { risk_type: "program" },
|
|
8059
|
+
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.)"
|
|
7795
8060
|
},
|
|
7796
8061
|
{
|
|
7797
8062
|
from: "security_incident",
|
|
@@ -7802,8 +8067,8 @@ var UPG_MIGRATIONS = {
|
|
|
7802
8067
|
{
|
|
7803
8068
|
from: "defect_report",
|
|
7804
8069
|
to: "support_ticket",
|
|
7805
|
-
defaults: {
|
|
7806
|
-
reason: "Defect report is a support ticket with
|
|
8070
|
+
defaults: { ticket_type: "bug" },
|
|
8071
|
+
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.)"
|
|
7807
8072
|
},
|
|
7808
8073
|
{
|
|
7809
8074
|
from: "onboarding_flow",
|
|
@@ -7979,6 +8244,46 @@ function getDeprecatedTypes() {
|
|
|
7979
8244
|
return deprecated;
|
|
7980
8245
|
}
|
|
7981
8246
|
var UPG_PROPERTY_MIGRATIONS = {
|
|
8247
|
+
"0.32.0": [
|
|
8248
|
+
// C3 — archive becomes a base-node axis. WorkspaceProperties.archived /
|
|
8249
|
+
// archived_at shipped the orthogonal-archive idea for exactly one type;
|
|
8250
|
+
// field data showed it is general (a tracker held 559 archived-completed
|
|
8251
|
+
// items beside 18 live-completed ones), so the pair moves to UPGBaseNode
|
|
8252
|
+
// and the workspace properties are @deprecated. Lift, not drop: the value
|
|
8253
|
+
// is real and its new home means the same thing.
|
|
8254
|
+
{
|
|
8255
|
+
kind: "lift_property_to_top_level",
|
|
8256
|
+
type: "workspace",
|
|
8257
|
+
from_property: "archived",
|
|
8258
|
+
to: "archived",
|
|
8259
|
+
reason: "0.32.0: archive generalised from WorkspaceProperties to UPGBaseNode.archived. Same fact, wider home; the workspace property is @deprecated."
|
|
8260
|
+
},
|
|
8261
|
+
{
|
|
8262
|
+
kind: "lift_property_to_top_level",
|
|
8263
|
+
type: "workspace",
|
|
8264
|
+
from_property: "archived_at",
|
|
8265
|
+
to: "archived_at",
|
|
8266
|
+
reason: "0.32.0: pairs with the archived lift above. UPGBaseNode.archived_at is the canonical home."
|
|
8267
|
+
},
|
|
8268
|
+
// C4 — the duplicate label surfaces. TaskProperties.labels and
|
|
8269
|
+
// BugProperties.labels were declared "applied uniformly across work item
|
|
8270
|
+
// types" and duplicated base-node `tags` with no consumer anywhere. Three
|
|
8271
|
+
// label surfaces (base tags, per-type tags, per-type labels) is two too
|
|
8272
|
+
// many; grouped labels are classification_axis + classification_value, and
|
|
8273
|
+
// ungrouped ones are `tags`.
|
|
8274
|
+
{
|
|
8275
|
+
kind: "drop_props",
|
|
8276
|
+
type: "task",
|
|
8277
|
+
drop_props: ["labels"],
|
|
8278
|
+
reason: "0.32.0: TaskProperties.labels duplicated base-node `tags` and had no consumers. Freeform labels live in `tags`; grouped labels are classification_axis + node_classified_as_classification_value."
|
|
8279
|
+
},
|
|
8280
|
+
{
|
|
8281
|
+
kind: "drop_props",
|
|
8282
|
+
type: "bug",
|
|
8283
|
+
drop_props: ["labels"],
|
|
8284
|
+
reason: "0.32.0: BugProperties.labels duplicated base-node `tags` and had no consumers. Same resolution as task."
|
|
8285
|
+
}
|
|
8286
|
+
],
|
|
7982
8287
|
// ── v0.21.0: UPG-690 D.1 — collapse residual *_status shadows into status ────
|
|
7983
8288
|
//
|
|
7984
8289
|
// Six `*_status` enum properties that re-encode their entity's own lifecycle
|
|
@@ -9114,6 +9419,48 @@ function getSplitMigrations(fromVersion, toVersion) {
|
|
|
9114
9419
|
return result;
|
|
9115
9420
|
}
|
|
9116
9421
|
var UPG_SCALAR_TO_EDGE_MIGRATIONS = {
|
|
9422
|
+
"0.32.0": [
|
|
9423
|
+
// C2 — assignment gets its own edge, superseding the 0.12.0 rules that
|
|
9424
|
+
// routed task/bug `assignee` into node_owned_by_person.
|
|
9425
|
+
//
|
|
9426
|
+
// WHY THIS IS A REVERSAL AND NOT A GAP-FILL. 0.12.0 ruled that an assignee
|
|
9427
|
+
// is an owner. Assignment has a time interval and an exclusivity that
|
|
9428
|
+
// ownership does not, and a real board ran 85% unassigned while every item
|
|
9429
|
+
// was owned — a state one edge cannot express.
|
|
9430
|
+
//
|
|
9431
|
+
// WHY EXISTING EDGES ARE NOT REWRITTEN. A node_owned_by_person edge on a
|
|
9432
|
+
// task is indistinguishable by inspection from a genuine ownership edge, so
|
|
9433
|
+
// a blanket rename would silently reclassify real facts. Measured population
|
|
9434
|
+
// across every graph in the originating workspace was ~zero, which makes the
|
|
9435
|
+
// cost of leaving them nil and the cost of guessing a wrong fact.
|
|
9436
|
+
//
|
|
9437
|
+
// user_story is included here and was MISSING from 0.12.0 entirely, though
|
|
9438
|
+
// it has carried `assignee` since 0.20.0.
|
|
9439
|
+
{
|
|
9440
|
+
from_type: "task",
|
|
9441
|
+
scalar_property: "assignee",
|
|
9442
|
+
target_type: "person",
|
|
9443
|
+
edge_type: "node_assigned_to_person",
|
|
9444
|
+
drop_scalar: false,
|
|
9445
|
+
reason: 'P14 actor: the assignee is a first-class person ("every task assigned to this person"). Assignment is not ownership: it has an interval and an exclusivity that ownership lacks, so it links node_assigned_to_person, superseding the 0.12.0 node_owned_by_person rule. Display string kept.'
|
|
9446
|
+
},
|
|
9447
|
+
{
|
|
9448
|
+
from_type: "bug",
|
|
9449
|
+
scalar_property: "assignee",
|
|
9450
|
+
target_type: "person",
|
|
9451
|
+
edge_type: "node_assigned_to_person",
|
|
9452
|
+
drop_scalar: false,
|
|
9453
|
+
reason: "P14 actor: same as task. Supersedes the 0.12.0 node_owned_by_person rule. Display string kept."
|
|
9454
|
+
},
|
|
9455
|
+
{
|
|
9456
|
+
from_type: "user_story",
|
|
9457
|
+
scalar_property: "assignee",
|
|
9458
|
+
target_type: "person",
|
|
9459
|
+
edge_type: "node_assigned_to_person",
|
|
9460
|
+
drop_scalar: false,
|
|
9461
|
+
reason: "P14 actor: user_story has carried `assignee` since 0.20.0 and had no promotion rule at all; an asymmetry with task and bug that this closes. Display string kept."
|
|
9462
|
+
}
|
|
9463
|
+
],
|
|
9117
9464
|
"0.12.0": [
|
|
9118
9465
|
// ── Bucket A1 — orphan scalars (no edge existed): add edge + migrate + drop ──
|
|
9119
9466
|
{
|
|
@@ -9524,22 +9871,12 @@ var UPG_SCALAR_TO_EDGE_MIGRATIONS = {
|
|
|
9524
9871
|
drop_scalar: false,
|
|
9525
9872
|
reason: "P14 actor: the release owner is a first-class person; link node_owned_by_person. Display string kept."
|
|
9526
9873
|
},
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
reason: 'P14 actor: the assignee is a first-class person ("every task assigned to this person"); link node_owned_by_person. Display string kept.'
|
|
9534
|
-
},
|
|
9535
|
-
{
|
|
9536
|
-
from_type: "bug",
|
|
9537
|
-
scalar_property: "assignee",
|
|
9538
|
-
target_type: "person",
|
|
9539
|
-
edge_type: "node_owned_by_person",
|
|
9540
|
-
drop_scalar: false,
|
|
9541
|
-
reason: "P14 actor: the assignee is a first-class person; link node_owned_by_person. Display string kept."
|
|
9542
|
-
},
|
|
9874
|
+
// NOTE (0.32.0): the task/bug `assignee` rules that lived here are SUPERSEDED
|
|
9875
|
+
// by the '0.32.0' block below, which routes them to node_assigned_to_person.
|
|
9876
|
+
// They are removed rather than left to double-run: two rules over one scalar
|
|
9877
|
+
// would mint the person once and link it twice, under two verbs that now mean
|
|
9878
|
+
// different things. Graphs promoted before 0.32.0 keep the ownership edges
|
|
9879
|
+
// they were given — see the 0.32.0 block for why those are not rewritten.
|
|
9543
9880
|
{
|
|
9544
9881
|
from_type: "roadmap",
|
|
9545
9882
|
scalar_property: "owner",
|
|
@@ -9635,6 +9972,14 @@ function getScalarToEdgeMigrations(fromVersion, toVersion) {
|
|
|
9635
9972
|
return result;
|
|
9636
9973
|
}
|
|
9637
9974
|
var UPG_EDGE_MIGRATIONS = {
|
|
9975
|
+
"0.32.0": [
|
|
9976
|
+
// C5 — the cadence scheduling edge widens from user_story to the work-item
|
|
9977
|
+
// set. No requires_target_type: the destination's target_type is the `node`
|
|
9978
|
+
// wildcard, and endpoint guards are checked against the catalog's declared
|
|
9979
|
+
// type rather than the concrete instance (same reasoning as the 0.22.0
|
|
9980
|
+
// workspace_produced_node rename below). Gate only on the unwidened source.
|
|
9981
|
+
{ kind: "rename", from: "planning_cycle_schedules_user_story", to: "planning_cycle_schedules_work_item", requires_source_type: "planning_cycle", reason: "0.32.0: a cycle could only schedule a user_story, while tracker imports produce `task` by default; the type a cadence most needed to hold was the one it could not reach. Widened to the polymorphic work-item endpoint; no flip, source unchanged. Measured live population of the old type at release: zero (no .upg in the repo or the dogfood graph carried it), so this rename is a vocabulary change, not a data migration." }
|
|
9982
|
+
],
|
|
9638
9983
|
// (target version TBD at release ceremony — pencilled at 0.22.0, the next
|
|
9639
9984
|
// breaking slot after 0.21.0 per the docket; adjust the key if Wave 4
|
|
9640
9985
|
// claims it first.) WS3 commit-provenance collapse (2026-07-05, ratified):
|
|
@@ -10825,7 +11170,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
10825
11170
|
acceptance_criterion: {
|
|
10826
11171
|
condition: { type: "string", description: "Required condition (Given/When/Then or plain text)" },
|
|
10827
11172
|
test_type: { type: "string", enum: ["manual", "automated"], description: "Test mode" },
|
|
10828
|
-
pass_status: { type: "string", enum: ["untested", "pass", "fail"], description: "Current
|
|
11173
|
+
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".' }
|
|
10829
11174
|
},
|
|
10830
11175
|
// AccessPolicyProperties: Access policy.
|
|
10831
11176
|
access_policy: {
|
|
@@ -11188,9 +11533,9 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11188
11533
|
priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Urgency relative to other work. Independent of `bug_severity` (a critical bug can have low priority if rare)." },
|
|
11189
11534
|
assignee: { type: "string", description: "Assigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
|
|
11190
11535
|
due_date: { type: "string", description: "ISO date due. Often tied to a release gate or SLA." },
|
|
11191
|
-
labels: { type: "string[]", description: "Free-form classification tags.
|
|
11536
|
+
labels: { type: "string[]", description: "Free-form classification tags. @deprecated since 0.32.0. Use base-node `tags` for ungrouped labels, and `classification_axis` + `classification_value` + `node_classified_as_classification_value` when the labels belong to a named group. This field duplicated `tags` and had no consumers; three parallel label surfaces (base `tags`, per-type `tags`, per-type `labels`) were two too many. `UPG_PROPERTY_MIGRATIONS['0.32.0']` drops it." },
|
|
11192
11537
|
workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
|
|
11193
|
-
workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning
|
|
11538
|
+
workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
|
|
11194
11539
|
},
|
|
11195
11540
|
// BuildArtifactProperties: Build artifact.
|
|
11196
11541
|
build_artifact: {
|
|
@@ -11221,6 +11566,16 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11221
11566
|
allocated: { type: "number", description: "Capacity already allocated to work" },
|
|
11222
11567
|
available: { type: "number", description: "Remaining unallocated capacity" }
|
|
11223
11568
|
},
|
|
11569
|
+
// CaptureProperties: Capture: a dated, hashed rendition of something already in the graph.
|
|
11570
|
+
capture: {
|
|
11571
|
+
capture_uri: { type: "string", description: "Where the bytes are. `https://` for hosted, `file://` or a relative path for local. Required: a capture with no location renders nothing." },
|
|
11572
|
+
content_hash: { type: "string", description: "Content hash of the bytes. The regeneration signal; see the type's `@remarks` for why mtime and size are not." },
|
|
11573
|
+
hash_algorithm: { type: "string", enum: ["sha256", "sha1", "md5"], description: "Hash algorithm. Absent means `sha256`." },
|
|
11574
|
+
captured_at: { type: "string", description: "ISO timestamp the capture was taken." },
|
|
11575
|
+
media_type: { type: "string", description: 'IANA media type of the bytes. @example "image/png"' },
|
|
11576
|
+
fidelity: { type: "string", enum: ["exact", "approximate", "not_applicable"], description: "How faithfully this rendition represents its subject. `approximate` covers a capture taken in a stand-in state or at the wrong viewport." },
|
|
11577
|
+
capture_status: { type: "string", enum: ["captured", "blocked", "skipped"], description: "Whether the capture succeeded.", notes: "A `blocked` capture is a real record rather than a missing one: it says the subject exists and could not be rendered, which is what stops the next run rediscovering the same obstacle. `skipped` is the deliberate exclusion." }
|
|
11578
|
+
},
|
|
11224
11579
|
// CeremonyProperties: Ceremony entity.
|
|
11225
11580
|
ceremony: {
|
|
11226
11581
|
ceremony_type: { type: "string", enum: ["standup", "planning", "review", "retro", "sync", "demo", "other"], description: "Kind of recurring meeting" },
|
|
@@ -11272,14 +11627,14 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11272
11627
|
// ClassificationAxisProperties: ClassificationAxis: a dimension along which subjects are classified.
|
|
11273
11628
|
classification_axis: {
|
|
11274
11629
|
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)." },
|
|
11275
|
-
cardinality: { type: "string", enum: ["single", "multi"], description:
|
|
11630
|
+
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.' }
|
|
11276
11631
|
},
|
|
11277
11632
|
// ClassificationValueProperties: ClassificationValue: a value on a classification axis.
|
|
11278
11633
|
classification_value: {
|
|
11279
11634
|
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." },
|
|
11280
11635
|
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']" },
|
|
11281
|
-
commitments: { type: "object[]", description: "The
|
|
11282
|
-
capabilities: { type: "object[]", description: "Structured capability bullets across the six canonical surfaces. Each surface appears at most once per `classification_value`.
|
|
11636
|
+
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.' }, ]" },
|
|
11637
|
+
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." }
|
|
11283
11638
|
},
|
|
11284
11639
|
// CodeRepositoryProperties: Code repository.
|
|
11285
11640
|
code_repository: {
|
|
@@ -11314,7 +11669,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11314
11669
|
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." },
|
|
11315
11670
|
analysis_date: { type: "string", description: 'ISO date conducted. Competitive intelligence decays quickly; track snapshot age. @example "2026-03-15"' },
|
|
11316
11671
|
framework_id: { type: "string", description: 'Framework ID (references `UPGFramework.id`). @example "porter-five-forces", "swot-analysis", "competitive-matrix"' },
|
|
11317
|
-
empty_cells: { type: "object[]", description: "Empty cells in a
|
|
11672
|
+
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.' }, ]" },
|
|
11318
11673
|
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"' },
|
|
11319
11674
|
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/"' },
|
|
11320
11675
|
confidence: {
|
|
@@ -11384,7 +11739,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11384
11739
|
// CompetitorSignalProperties: Competitor signal: a dated competitor move mapped onto our portfolio.
|
|
11385
11740
|
competitor_signal: {
|
|
11386
11741
|
observed_at: { type: "string", description: 'ISO date-time the move was observed. @example "2026-06-10"' },
|
|
11387
|
-
signal_type: { type: "string", enum: ["feature_launch", "pricing_change", "acquisition", "partnership", "market_entry", "reclassification"], description: "Kind of move
|
|
11742
|
+
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." },
|
|
11388
11743
|
summary: { type: "string", description: "One-line factual summary of the move (what shipped, not marketing copy)." },
|
|
11389
11744
|
impact: { type: "string", enum: ["high", "medium", "low"], description: "Expected impact on our position." },
|
|
11390
11745
|
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." },
|
|
@@ -11416,12 +11771,22 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11416
11771
|
// ComplianceRequirementProperties: Compliance requirement.
|
|
11417
11772
|
compliance_requirement: {
|
|
11418
11773
|
regulation: { type: "string", enum: ["gdpr", "ccpa", "hipaa", "soc2", "iso27001", "pci_dss", "other"], description: "Regulation or standard this requirement derives from" },
|
|
11419
|
-
compliance_status: { type: "string", enum: ["compliant", "non_compliant", "in_progress", "not_applicable"], description: "Current compliance posture" }
|
|
11774
|
+
compliance_status: { type: "string", enum: ["compliant", "non_compliant", "in_progress", "not_applicable"], description: "Current compliance posture" },
|
|
11775
|
+
owner: { type: "string", description: "Accountable person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." }
|
|
11776
|
+
},
|
|
11777
|
+
// CompositionProperties
|
|
11778
|
+
composition: {
|
|
11779
|
+
members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
|
|
11780
|
+
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." },
|
|
11781
|
+
published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
|
|
11782
|
+
published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." },
|
|
11783
|
+
member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement.", notes: "This is the portable statement of what the composition shows. `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A composition with no `member_query` is authored, which is what every composition written before 0.32.0 is." },
|
|
11784
|
+
presentation: { type: "object", description: "Advisory rendering intent for the composition as a whole. A consumer may ignore it entirely and still be conformant." }
|
|
11420
11785
|
},
|
|
11421
11786
|
// ConfigurationAxisProperties: A named dimension along which the product's composition differs.
|
|
11422
11787
|
configuration_axis: {
|
|
11423
|
-
values: { type: "string[]", description: "The closed set of values this axis can take. Required: an axis with no values selects nothing and cannot be projected along. Two values is the common case and three is not unusual (a plan ladder). Order is not significant
|
|
11424
|
-
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
|
|
11788
|
+
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." },
|
|
11789
|
+
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." },
|
|
11425
11790
|
kind: { type: "string", enum: ["feature_flag", "plan_tier", "permission_level", "beta_program", "other"], description: "What kind of lever this is. Names the mechanism family." }
|
|
11426
11791
|
},
|
|
11427
11792
|
// ConstraintProperties: Constraint: a named limitation or boundary on product creation.
|
|
@@ -11437,7 +11802,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11437
11802
|
contact: {
|
|
11438
11803
|
contact_role: { type: "string", description: "Job title or role within the account" },
|
|
11439
11804
|
is_decision_maker: { type: "boolean", description: "Whether this person has purchasing authority" },
|
|
11440
|
-
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
|
|
11805
|
+
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." }
|
|
11441
11806
|
},
|
|
11442
11807
|
// ContentCalendarProperties: Content calendar.
|
|
11443
11808
|
content_calendar: {
|
|
@@ -11558,7 +11923,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11558
11923
|
data_contract: {
|
|
11559
11924
|
retention_period: { type: "string", description: "How long data is retained before deletion" },
|
|
11560
11925
|
deletion_policy: { type: "string", description: "Policy governing data deletion" },
|
|
11561
|
-
third_party_sharing: { type: "boolean", description: "Whether data is shared with third parties" }
|
|
11926
|
+
third_party_sharing: { type: "boolean", description: "Whether data is shared with third parties" },
|
|
11927
|
+
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." }
|
|
11562
11928
|
},
|
|
11563
11929
|
// DataDomainProperties: DataDomain. A coarse-grained grouping of related data assets (sources,
|
|
11564
11930
|
data_domain: {
|
|
@@ -11604,7 +11970,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11604
11970
|
// DataProductProperties: DataProduct entity.
|
|
11605
11971
|
data_product: {
|
|
11606
11972
|
data_product_type: { type: "string", enum: ["report", "dataset", "stream", "api", "ml_feature", "other"], description: "Classification of the data product (UPG-579 Option B)." },
|
|
11607
|
-
sla_freshness: { type: "string", description: 'Freshness SLA commitment (e.g. "< 1 hour")' }
|
|
11973
|
+
sla_freshness: { type: "string", description: 'Freshness SLA commitment (e.g. "< 1 hour")' },
|
|
11974
|
+
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." }
|
|
11608
11975
|
},
|
|
11609
11976
|
// DataQualityRuleProperties: DataQualityRule entity.
|
|
11610
11977
|
data_quality_rule: {
|
|
@@ -11634,7 +12001,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11634
12001
|
deal_value: { type: "number", description: "Monetary value of the deal" },
|
|
11635
12002
|
close_date: { type: "string", description: "Expected close date (ISO format)" },
|
|
11636
12003
|
probability: { type: "number", description: "Likelihood of closing (0-100%)" },
|
|
11637
|
-
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)
|
|
12004
|
+
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." },
|
|
11638
12005
|
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'" },
|
|
11639
12006
|
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'" },
|
|
11640
12007
|
next_step_date: { type: "string", description: "When the `next_step` is due (ISO format)." },
|
|
@@ -11650,7 +12017,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11650
12017
|
normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
|
|
11651
12018
|
},
|
|
11652
12019
|
required: ["value", "label"]
|
|
11653
|
-
}
|
|
12020
|
+
},
|
|
12021
|
+
owner: { type: "string", description: "Accountable person or team carrying the deal. Promote to a `node_owned_by_person` edge if ownership must be queryable." }
|
|
11654
12022
|
},
|
|
11655
12023
|
// DecisionProperties: Decision record. Strategic, product, engineering, or design.
|
|
11656
12024
|
decision: {
|
|
@@ -11863,13 +12231,14 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11863
12231
|
},
|
|
11864
12232
|
// EpicProperties: A collection of related user stories that delivers a feature or capability.
|
|
11865
12233
|
epic: {
|
|
11866
|
-
|
|
12234
|
+
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`).' },
|
|
12235
|
+
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`.' },
|
|
11867
12236
|
priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Task-level priority" },
|
|
11868
12237
|
owner: { type: "string", description: "Responsible person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
|
|
11869
12238
|
start_date: { type: "string", description: "ISO date work begins" },
|
|
11870
12239
|
target_date: { type: "string", description: "ISO date work completes" },
|
|
11871
12240
|
workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
|
|
11872
|
-
workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning
|
|
12241
|
+
workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
|
|
11873
12242
|
},
|
|
11874
12243
|
// ErrorBudgetProperties: Error budget.
|
|
11875
12244
|
error_budget: {
|
|
@@ -11880,7 +12249,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11880
12249
|
},
|
|
11881
12250
|
// EvalBenchmarkProperties: Evaluation benchmark.
|
|
11882
12251
|
eval_benchmark: {
|
|
11883
|
-
benchmark_type: { type: "string", enum: ["accuracy", "latency", "cost", "safety", "custom"], description: "Measured dimension" },
|
|
12252
|
+
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.' },
|
|
11884
12253
|
test_case_count: { type: "number", description: "Test cases in the suite", modifier: "derived" },
|
|
11885
12254
|
passing_threshold: { type: "number", description: "Minimum passing score" },
|
|
11886
12255
|
last_run: { type: "string", description: "ISO date of the most recent run" }
|
|
@@ -11896,7 +12265,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11896
12265
|
output_token_count: { type: "number", description: "Output tokens" },
|
|
11897
12266
|
cost: { type: "number", description: "Total run cost" },
|
|
11898
12267
|
error_rate: { type: "number", description: "Percentage of test cases that errored" },
|
|
11899
|
-
feedback_scores: { type: "string", description: "Feedback score summary (human or automated)" }
|
|
12268
|
+
feedback_scores: { type: "string", description: "Feedback score summary (human or automated)" },
|
|
12269
|
+
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." }
|
|
11900
12270
|
},
|
|
11901
12271
|
// EventProperties: Event.
|
|
11902
12272
|
event: {
|
|
@@ -12000,6 +12370,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12000
12370
|
},
|
|
12001
12371
|
// ExperimentRunProperties: Execution evidence for a structured test of a hypothesis (UCS pattern P6: event-occurrence).
|
|
12002
12372
|
experiment_run: {
|
|
12373
|
+
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." },
|
|
12003
12374
|
actual_start_date: { type: "string", description: "ISO actual start date (may differ from the plan's `planned_start_date`)" },
|
|
12004
12375
|
actual_end_date: { type: "string", description: "ISO actual end date" },
|
|
12005
12376
|
actual_reach: { type: "number", description: "Observed reach: how many people the run actually touched" },
|
|
@@ -12051,7 +12422,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12051
12422
|
target_date: { type: "string", description: "ISO date work completes" },
|
|
12052
12423
|
health: { type: "string", enum: ["on_track", "at_risk", "off_track"], description: "Delivery health" },
|
|
12053
12424
|
workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
|
|
12054
|
-
workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning
|
|
12425
|
+
workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
|
|
12055
12426
|
},
|
|
12056
12427
|
// FeatureAreaProperties: A structural grouping of related features within a product.
|
|
12057
12428
|
feature_area: {
|
|
@@ -12167,7 +12538,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12167
12538
|
// FrameworkExerciseProperties: Framework exercise: one run of a framework over a set of entities.
|
|
12168
12539
|
framework_exercise: {
|
|
12169
12540
|
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." },
|
|
12170
|
-
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)." }
|
|
12541
|
+
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)." },
|
|
12542
|
+
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." }
|
|
12171
12543
|
},
|
|
12172
12544
|
// FunnelProperties: Funnel entity.
|
|
12173
12545
|
funnel: {
|
|
@@ -12184,7 +12556,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12184
12556
|
// GlossaryTermProperties: GlossaryTerm entity.
|
|
12185
12557
|
glossary_term: {
|
|
12186
12558
|
term_definition: { type: "string", description: "Plain-language definition of the term" },
|
|
12187
|
-
synonyms: { type: "string[]", description: "Alternative names or abbreviations for this term" }
|
|
12559
|
+
synonyms: { type: "string[]", description: "Alternative names or abbreviations for this term" },
|
|
12560
|
+
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." }
|
|
12188
12561
|
},
|
|
12189
12562
|
// GrowthCampaignProperties: GrowthCampaign entity.
|
|
12190
12563
|
growth_campaign: {
|
|
@@ -12257,7 +12630,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12257
12630
|
// HypothesisEvidenceProperties
|
|
12258
12631
|
hypothesis_evidence: {
|
|
12259
12632
|
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." },
|
|
12260
|
-
direction: { type: "string", enum: ["supports", "refutes", "neutral"], description: "Direction relative to the parent claim
|
|
12633
|
+
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." },
|
|
12261
12634
|
weight: {
|
|
12262
12635
|
type: "assessment",
|
|
12263
12636
|
scale_id: "importance_5",
|
|
@@ -12327,6 +12700,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12327
12700
|
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." },
|
|
12328
12701
|
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." },
|
|
12329
12702
|
source_method: { type: "string", description: 'Producing research method. @example "usability_study", "interview_series", "survey"' },
|
|
12703
|
+
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." },
|
|
12330
12704
|
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."' },
|
|
12331
12705
|
implications: { type: "string", description: "Product implications. The so-what." }
|
|
12332
12706
|
},
|
|
@@ -12348,8 +12722,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12348
12722
|
},
|
|
12349
12723
|
// InternalDocProperties: Internal document.
|
|
12350
12724
|
internal_doc: {
|
|
12351
|
-
doc_type: { type: "string", enum: ["rfc", "runbook", "guide", "spec", "onboarding", "other"], description: "Classification of the document" },
|
|
12352
|
-
url: { type: "string", description: "URL or path to the document" }
|
|
12725
|
+
doc_type: { type: "string", enum: ["rfc", "runbook", "guide", "spec", "onboarding", "other"], description: "Classification of the document. @deprecated use `document.document_type`" },
|
|
12726
|
+
url: { type: "string", description: "URL or path to the document. @deprecated use `document.source_url`" }
|
|
12353
12727
|
},
|
|
12354
12728
|
// InterviewGuideProperties: Interview guide document.
|
|
12355
12729
|
interview_guide: {
|
|
@@ -12707,7 +13081,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12707
13081
|
range_max: { type: "number", description: "Maximum expected or ceiling value" },
|
|
12708
13082
|
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." },
|
|
12709
13083
|
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." },
|
|
12710
|
-
metric_health: { type: "string", enum: ["healthy", "at_risk", "unhealthy", "unknown"], description: "Universal health rollup
|
|
13084
|
+
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." },
|
|
12711
13085
|
guardrail_threshold_min: { type: "number", description: "Lower bound for guardrail safety (below this = breach)" },
|
|
12712
13086
|
guardrail_threshold_max: { type: "number", description: "Upper bound for guardrail safety (above this = breach)" },
|
|
12713
13087
|
guardrail_status: { type: "string", enum: ["safe", "warning", "breached"], description: "Current guardrail health state" }
|
|
@@ -12727,7 +13101,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12727
13101
|
},
|
|
12728
13102
|
// MilestoneProperties: Milestone.
|
|
12729
13103
|
milestone: {
|
|
12730
|
-
milestone_order: { type: "number", description: "Display order of this milestone within its parent
|
|
13104
|
+
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." },
|
|
12731
13105
|
due_date: { type: "string", description: "Target due date (ISO format)" },
|
|
12732
13106
|
met_on_time: { type: "boolean", description: "Whether the milestone was met on time" }
|
|
12733
13107
|
},
|
|
@@ -12957,8 +13331,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12957
13331
|
motivation: { type: "string", description: 'Primary motivation or driving need @example "Making confident, evidence-based decisions"' },
|
|
12958
13332
|
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`." },
|
|
12959
13333
|
domain_expertise: { type: "string", description: 'Industry or domain knowledge this persona brings @example "10+ years SaaS experience", "New to healthcare IT"' },
|
|
12960
|
-
audience_role: { type: "string", enum: ["buyer", "user", "champion", "influencer", "partner"], description: 'Role in the buying
|
|
12961
|
-
actor_kind: { type: "string", enum: ["human", "agent", "system"], description: 'Who performs the work: a human archetype
|
|
13334
|
+
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." },
|
|
13335
|
+
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." }
|
|
12962
13336
|
},
|
|
12963
13337
|
// PipelineSalesProperties: Sales pipeline.
|
|
12964
13338
|
pipeline_sales: {
|
|
@@ -13044,7 +13418,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13044
13418
|
url: { type: "string", description: "Where the product lives. Marketing site, app store URL, etc.", modifier: "volatile" },
|
|
13045
13419
|
logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
|
|
13046
13420
|
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
|
|
13047
|
-
described_configuration: { type: "string", description:
|
|
13421
|
+
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." },
|
|
13422
|
+
key_prefix: { type: "string", description: 'Prefix for keys minted onto this product\'s nodes (e.g. `"LTN"`, giving `LTN-1`, `LTN-2`, \u2026). A product with no prefix mints no keys. @example "LTN"', notes: 'Pairs with `UPGBaseNode.key`, which holds the minted value. The prefix sits on the product because the key\'s uniqueness scope is the product, across entity types; the namespace a tracker calls a "team". Only the prefix is serialised: the next number is `max(existing) + 1`, derived from the graph, because a counter is a fact about a store rather than about the product.' }
|
|
13048
13423
|
},
|
|
13049
13424
|
// ProductAreaProperties: ProductArea entity.
|
|
13050
13425
|
product_area: {
|
|
@@ -13096,7 +13471,10 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13096
13471
|
qa_session: {
|
|
13097
13472
|
session_type: { type: "string", enum: ["exploratory", "regression", "smoke", "uat"], description: "Type of QA session" },
|
|
13098
13473
|
duration_minutes: { type: "number", description: "Duration of the session in minutes" },
|
|
13099
|
-
bugs_found: { type: "number", description: "Number of bugs found during the session" }
|
|
13474
|
+
bugs_found: { type: "number", description: "Number of bugs found during the session" },
|
|
13475
|
+
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`." },
|
|
13476
|
+
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." },
|
|
13477
|
+
executed_at: { type: "string", description: "ISO timestamp of when the session was run. Mirrors `TestResultProperties.executed_at`." }
|
|
13100
13478
|
},
|
|
13101
13479
|
// QueueTopicProperties: Message queue or topic.
|
|
13102
13480
|
queue_topic: {
|
|
@@ -13163,7 +13541,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13163
13541
|
},
|
|
13164
13542
|
// ReleaseStrategyProperties: Release strategy.
|
|
13165
13543
|
release_strategy: {
|
|
13166
|
-
strategy_type: { type: "string", enum: ["blue_green", "canary", "rolling", "recreate", "feature_flag"], description: 'How a new version reaches production.
|
|
13544
|
+
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." },
|
|
13167
13545
|
canary_percentage: { type: "number", description: "Traffic routed to canary. Applies when `strategy_type === 'canary'`. @example 5 (5% initial canary before full rollout)" },
|
|
13168
13546
|
rollback_criteria: { type: "string", description: 'Rollback triggers, automatic or manual. @example "Error rate > 1% over 5 minutes", "Latency p99 > 2x baseline"' },
|
|
13169
13547
|
bake_time: { type: "string", description: 'Soak time before promoting to full production. @example "30m", "2h", "24h"' },
|
|
@@ -13224,7 +13602,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13224
13602
|
},
|
|
13225
13603
|
// RiskProperties: Risk.
|
|
13226
13604
|
risk: {
|
|
13227
|
-
risk_type: { type: "string", enum: ["technical", "business", "legal", "security", "operational"], description: "Domain the risk belongs to" },
|
|
13605
|
+
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." },
|
|
13228
13606
|
probability: {
|
|
13229
13607
|
type: "assessment",
|
|
13230
13608
|
scale_id: "confidence_5",
|
|
@@ -13372,7 +13750,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13372
13750
|
effective_date: { type: "string", description: "ISO effective date" },
|
|
13373
13751
|
url: { type: "string", description: "Policy document URL" },
|
|
13374
13752
|
policy_status: { type: "string", enum: ["draft", "active", "under_review", "retired"], description: "Lifecycle status" },
|
|
13375
|
-
rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Imperative force" }
|
|
13753
|
+
rule_strength: { type: "string", enum: ["must", "must_not", "exception", "warning", "guideline"], description: "Imperative force" },
|
|
13754
|
+
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." }
|
|
13376
13755
|
},
|
|
13377
13756
|
// SecurityReviewProperties: Security review.
|
|
13378
13757
|
security_review: {
|
|
@@ -13572,12 +13951,12 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13572
13951
|
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." },
|
|
13573
13952
|
persistence: { type: "string", enum: ["always", "conditional", "on_demand", "transient"], description: "How reliably the surface is present." },
|
|
13574
13953
|
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"' },
|
|
13575
|
-
capacity: { type: "number", description:
|
|
13954
|
+
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." },
|
|
13576
13955
|
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." },
|
|
13577
13956
|
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.' },
|
|
13578
13957
|
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." },
|
|
13579
|
-
arbitration_rule: { type: "string", description: 'Who wins when more occupants want the surface than `capacity` allows, and why.
|
|
13580
|
-
arbitration_state: { type: "string", enum: ["enforced_documented", "enforced_undocumented", "safe_by_coincidence", "none"], description:
|
|
13958
|
+
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." },
|
|
13959
|
+
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.' },
|
|
13581
13960
|
extensibility: { type: "string", enum: ["closed", "plugin_registerable", "user_configurable"], description: "Who may add occupants to the surface." },
|
|
13582
13961
|
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." },
|
|
13583
13962
|
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"' }
|
|
@@ -13640,9 +14019,9 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13640
14019
|
effort: { type: "string", description: 'Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team.' },
|
|
13641
14020
|
priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Relative importance against other tasks" },
|
|
13642
14021
|
due_date: { type: "string", description: "ISO date due. Typically bounded by the containing story's due date." },
|
|
13643
|
-
labels: { type: "string[]", description: "Free-form classification tags.
|
|
14022
|
+
labels: { type: "string[]", description: "Free-form classification tags. @deprecated since 0.32.0. Use base-node `tags` for ungrouped labels, and `classification_axis` + `classification_value` + `node_classified_as_classification_value` when the labels belong to a named group. This field duplicated `tags` and had no consumers; three parallel label surfaces (base `tags`, per-type `tags`, per-type `labels`) were two too many. `UPG_PROPERTY_MIGRATIONS['0.32.0']` drops it." },
|
|
13644
14023
|
workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
|
|
13645
|
-
workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning
|
|
14024
|
+
workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
|
|
13646
14025
|
},
|
|
13647
14026
|
// TeamProperties: Team entity.
|
|
13648
14027
|
team: {
|
|
@@ -13745,7 +14124,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13745
14124
|
},
|
|
13746
14125
|
// TestSuiteProperties: Test suite.
|
|
13747
14126
|
test_suite: {
|
|
13748
|
-
suite_type: { type: "string", enum: ["unit", "integration", "e2e", "performance", "security", "accessibility"], description: "Category of test suite" },
|
|
14127
|
+
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." },
|
|
13749
14128
|
test_count: { type: "number", description: "Number of tests in the suite", modifier: "derived" },
|
|
13750
14129
|
pass_rate: { type: "number", description: "Percentage of tests passing (0-100)", modifier: "snapshot" },
|
|
13751
14130
|
last_run: { type: "string", description: "ISO date of last execution" },
|
|
@@ -13788,7 +14167,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13788
14167
|
},
|
|
13789
14168
|
// ThreatModelProperties: Threat model.
|
|
13790
14169
|
threat_model: {
|
|
13791
|
-
methodology: { type: "string", enum: ["stride", "dread", "pasta", "attack_tree", "other"], description: '
|
|
14170
|
+
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." },
|
|
13792
14171
|
scope: { type: "string", description: 'What system, feature, or data flow is being analysed. @example "user authentication", "payment processing", "admin API"' },
|
|
13793
14172
|
last_reviewed: { type: "string", description: 'ISO date last reviewed. Models become stale as systems evolve. @example "2026-03-01"' },
|
|
13794
14173
|
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)"' },
|
|
@@ -13840,6 +14219,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13840
14219
|
},
|
|
13841
14220
|
// UserFlowProperties: User flow.
|
|
13842
14221
|
user_flow: {
|
|
14222
|
+
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." },
|
|
13843
14223
|
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." },
|
|
13844
14224
|
trigger: { type: "string", description: "Initiating event" },
|
|
13845
14225
|
steps: { type: "string[]", description: "Ordered steps" },
|
|
@@ -13863,7 +14243,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13863
14243
|
priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Relative importance against other stories. Lifted onto user_story (0.20.0) so the story is a first-class plannable unit alongside task, matching Jira/Linear where the story/issue is the estimated-and-assigned atom." },
|
|
13864
14244
|
due_date: { type: "string", description: "ISO date due. Typically bounded by the release or planning_cycle the story is scheduled into." },
|
|
13865
14245
|
workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
|
|
13866
|
-
workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning
|
|
14246
|
+
workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
|
|
13867
14247
|
},
|
|
13868
14248
|
// ValueObjectProperties: DDD value object.
|
|
13869
14249
|
value_object: {
|
|
@@ -13902,7 +14282,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13902
14282
|
severity: {
|
|
13903
14283
|
type: "assessment",
|
|
13904
14284
|
scale_id: "severity_5",
|
|
13905
|
-
description: "Categorical severity derived from CVSS
|
|
14285
|
+
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)",
|
|
14286
|
+
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.",
|
|
13906
14287
|
properties: {
|
|
13907
14288
|
value: { type: "number", description: "The numeric value, used for computation." },
|
|
13908
14289
|
label: { type: "string", description: "The qualitative label (what the assessor meant)." },
|
|
@@ -13974,12 +14355,14 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13974
14355
|
workspace: {
|
|
13975
14356
|
visibility: { type: "string", enum: ["private", "shared", "public"], description: "Who can see this workspace" },
|
|
13976
14357
|
purpose: { type: "string", description: "Free-text description. Pairs with the closed-enum `workspace_purpose`." },
|
|
13977
|
-
workspace_purpose: { type: "string", enum: ["discovery", "planning", "retrospective", "design", "research", "strategy", "general"], description: "
|
|
14358
|
+
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." },
|
|
13978
14359
|
owner: { type: "string", description: "Workspace owner (handle or email). Display label; canonical owner is `team_owns_workspace` or `persona_owns_workspace`." },
|
|
13979
14360
|
member_count: { type: "number", description: "Snapshot count. `team_works_in_workspace` edges are the source of truth.", modifier: "derived" },
|
|
13980
|
-
archived: { type: "boolean", description: "Archived. Archived workspaces remain queryable but hidden from default views." },
|
|
13981
|
-
archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`." },
|
|
13982
|
-
icon: { type: "string", description: "Display icon (emoji or icon name)" }
|
|
14361
|
+
archived: { type: "boolean", description: "Archived. Archived workspaces remain queryable but hidden from default views. @deprecated since 0.32.0. Use `UPGBaseNode.archived`, which generalises this pair to every entity type. Field data showed the archived/status split is not a workspace peculiarity: a tracker held 559 archived-completed items beside 18 live-completed ones, and one status field has nowhere to put the difference. `UPG_PROPERTY_MIGRATIONS['0.32.0']` lifts this value to the top level." },
|
|
14362
|
+
archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`. @deprecated since 0.32.0. Use `UPGBaseNode.archived_at`. Lifted by `UPG_PROPERTY_MIGRATIONS['0.32.0']`." },
|
|
14363
|
+
icon: { type: "string", description: "Display icon (emoji or icon name)" },
|
|
14364
|
+
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." },
|
|
14365
|
+
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." }
|
|
13983
14366
|
}
|
|
13984
14367
|
};
|
|
13985
14368
|
function getPropertySchema(entityType) {
|
|
@@ -15023,159 +15406,220 @@ var UPG_FRAMEWORKS = [
|
|
|
15023
15406
|
}
|
|
15024
15407
|
},
|
|
15025
15408
|
{
|
|
15026
|
-
"id": "
|
|
15027
|
-
"
|
|
15028
|
-
"inspect"
|
|
15029
|
-
],
|
|
15030
|
-
"name": "Porter Five Forces",
|
|
15409
|
+
"id": "lean-canvas",
|
|
15410
|
+
"name": "Lean Canvas",
|
|
15031
15411
|
"version": "1.0.0",
|
|
15032
|
-
"description": "
|
|
15033
|
-
"category": "
|
|
15412
|
+
"description": "Startup-focused adaptation of the BMC. Replaces partners/resources with problem/solution/unfair advantage.",
|
|
15413
|
+
"category": "business_model",
|
|
15034
15414
|
"origin": {
|
|
15035
15415
|
"type": "practitioner",
|
|
15036
|
-
"attribution": "
|
|
15037
|
-
"description": "Published in
|
|
15038
|
-
"
|
|
15039
|
-
"
|
|
15416
|
+
"attribution": "Ash Maurya",
|
|
15417
|
+
"description": "Adapted from BMC for startups. Published in Running Lean (O'Reilly). Replaces partners/resources with problem/solution.",
|
|
15418
|
+
"url": "https://leanstack.com/lean-canvas",
|
|
15419
|
+
"year": 2012,
|
|
15420
|
+
"license": "published_methodology"
|
|
15040
15421
|
},
|
|
15041
15422
|
"tags": [
|
|
15042
|
-
"
|
|
15043
|
-
"
|
|
15423
|
+
"business_model",
|
|
15424
|
+
"matrix"
|
|
15044
15425
|
],
|
|
15045
15426
|
"slots": [
|
|
15046
15427
|
{
|
|
15047
|
-
"label": "
|
|
15048
|
-
"
|
|
15049
|
-
"
|
|
15050
|
-
"description": "Intensity of competition among existing players"
|
|
15428
|
+
"label": "Problem",
|
|
15429
|
+
"entityTypeId": "need",
|
|
15430
|
+
"description": "Top 3 problems your customers face"
|
|
15051
15431
|
},
|
|
15052
15432
|
{
|
|
15053
|
-
"label": "
|
|
15054
|
-
"
|
|
15055
|
-
"
|
|
15056
|
-
"description": "How easy is it for new competitors to enter?"
|
|
15433
|
+
"label": "Solution",
|
|
15434
|
+
"entityTypeId": "solution",
|
|
15435
|
+
"description": "Top 3 features addressing each problem"
|
|
15057
15436
|
},
|
|
15058
15437
|
{
|
|
15059
|
-
"label": "
|
|
15060
|
-
"
|
|
15061
|
-
"
|
|
15062
|
-
"description": "Can customers switch to alternatives?"
|
|
15438
|
+
"label": "Key Metrics",
|
|
15439
|
+
"entityTypeId": "metric",
|
|
15440
|
+
"description": "Key numbers that tell you how your business is doing"
|
|
15063
15441
|
},
|
|
15064
15442
|
{
|
|
15065
|
-
"label": "
|
|
15066
|
-
"
|
|
15067
|
-
"
|
|
15068
|
-
"description": "How much leverage do buyers have?"
|
|
15443
|
+
"label": "Unique Value Prop",
|
|
15444
|
+
"entityTypeId": "value_proposition",
|
|
15445
|
+
"description": "Single, clear, compelling message"
|
|
15069
15446
|
},
|
|
15070
15447
|
{
|
|
15071
|
-
"label": "
|
|
15072
|
-
"
|
|
15448
|
+
"label": "Unfair Advantage",
|
|
15449
|
+
"entityTypeId": "capability",
|
|
15450
|
+
"description": "Something that cannot be easily copied"
|
|
15451
|
+
},
|
|
15452
|
+
{
|
|
15453
|
+
"label": "Channels",
|
|
15454
|
+
"entityTypeId": "acquisition_channel",
|
|
15455
|
+
"description": "Path to customers"
|
|
15456
|
+
},
|
|
15457
|
+
{
|
|
15458
|
+
"label": "Customer Segments",
|
|
15073
15459
|
"entityTypeId": "persona",
|
|
15074
|
-
"description": "
|
|
15460
|
+
"description": "Target customers"
|
|
15461
|
+
},
|
|
15462
|
+
{
|
|
15463
|
+
"label": "Existing Alternatives",
|
|
15464
|
+
"entityTypeId": "competitor",
|
|
15465
|
+
"description": "What do customers use today?"
|
|
15466
|
+
},
|
|
15467
|
+
{
|
|
15468
|
+
"label": "Early Adopters",
|
|
15469
|
+
"entityTypeId": "behavioral_segment",
|
|
15470
|
+
"description": "Your first target customers"
|
|
15471
|
+
},
|
|
15472
|
+
{
|
|
15473
|
+
"label": "Cost Structure",
|
|
15474
|
+
"entityTypeId": "cost_structure",
|
|
15475
|
+
"description": "Customer acquisition costs, hosting, etc."
|
|
15476
|
+
},
|
|
15477
|
+
{
|
|
15478
|
+
"label": "Revenue Streams",
|
|
15479
|
+
"entityTypeId": "revenue_stream",
|
|
15480
|
+
"description": "Revenue model, lifetime value, margins"
|
|
15075
15481
|
}
|
|
15076
15482
|
],
|
|
15077
15483
|
"data": {
|
|
15078
15484
|
"entity_types": [
|
|
15485
|
+
{
|
|
15486
|
+
"type": "need",
|
|
15487
|
+
"role": "bucket"
|
|
15488
|
+
},
|
|
15489
|
+
{
|
|
15490
|
+
"type": "solution",
|
|
15491
|
+
"role": "bucket"
|
|
15492
|
+
},
|
|
15493
|
+
{
|
|
15494
|
+
"type": "value_proposition",
|
|
15495
|
+
"role": "bucket"
|
|
15496
|
+
},
|
|
15079
15497
|
{
|
|
15080
15498
|
"type": "competitor",
|
|
15081
|
-
"role": "
|
|
15499
|
+
"role": "bucket"
|
|
15082
15500
|
},
|
|
15083
15501
|
{
|
|
15084
15502
|
"type": "persona",
|
|
15503
|
+
"role": "bucket"
|
|
15504
|
+
},
|
|
15505
|
+
{
|
|
15506
|
+
"type": "behavioral_segment",
|
|
15507
|
+
"role": "bucket"
|
|
15508
|
+
},
|
|
15509
|
+
{
|
|
15510
|
+
"type": "metric",
|
|
15511
|
+
"role": "bucket"
|
|
15512
|
+
},
|
|
15513
|
+
{
|
|
15514
|
+
"type": "acquisition_channel",
|
|
15515
|
+
"role": "bucket"
|
|
15516
|
+
},
|
|
15517
|
+
{
|
|
15518
|
+
"type": "cost_structure",
|
|
15519
|
+
"role": "bucket"
|
|
15520
|
+
},
|
|
15521
|
+
{
|
|
15522
|
+
"type": "revenue_stream",
|
|
15523
|
+
"role": "bucket"
|
|
15524
|
+
},
|
|
15525
|
+
{
|
|
15526
|
+
"type": "capability",
|
|
15085
15527
|
"role": "item"
|
|
15086
15528
|
}
|
|
15087
15529
|
],
|
|
15088
15530
|
"required_properties": {}
|
|
15089
15531
|
},
|
|
15090
15532
|
"structure": {
|
|
15091
|
-
"pattern": "
|
|
15533
|
+
"pattern": "matrix"
|
|
15092
15534
|
},
|
|
15093
15535
|
"presentation": {
|
|
15094
15536
|
"layout": {
|
|
15095
|
-
"type": "
|
|
15096
|
-
"
|
|
15537
|
+
"type": "matrix",
|
|
15538
|
+
"rows": 3,
|
|
15539
|
+
"cols": 4
|
|
15097
15540
|
},
|
|
15098
15541
|
"sort_by": {
|
|
15099
15542
|
"property": "title",
|
|
15100
15543
|
"direction": "asc"
|
|
15101
15544
|
},
|
|
15102
|
-
"colour_by": "
|
|
15545
|
+
"colour_by": "group",
|
|
15103
15546
|
"card_fields": [
|
|
15104
15547
|
"title",
|
|
15105
15548
|
"description"
|
|
15106
15549
|
]
|
|
15107
15550
|
},
|
|
15108
15551
|
"education": {
|
|
15109
|
-
"purpose": "
|
|
15110
|
-
"core_question": "What
|
|
15552
|
+
"purpose": "Capture an entire business model hypothesis on a single page so founders can test assumptions rapidly without writing a full business plan.",
|
|
15553
|
+
"core_question": "What are the riskiest assumptions in our business model, and how can we test them quickly?",
|
|
15111
15554
|
"when_to_use": [
|
|
15112
|
-
"You
|
|
15113
|
-
"
|
|
15114
|
-
"
|
|
15555
|
+
"You are designing or redesigning how the business creates and captures value",
|
|
15556
|
+
"You need to communicate the business model to stakeholders or investors",
|
|
15557
|
+
"You want to identify risks and assumptions in your business model"
|
|
15115
15558
|
],
|
|
15116
15559
|
"when_not_to_use": [
|
|
15117
|
-
"
|
|
15118
|
-
"
|
|
15560
|
+
"The business model is mature and well-understood by all stakeholders",
|
|
15561
|
+
"You are focused on tactical execution rather than model design"
|
|
15119
15562
|
]
|
|
15120
15563
|
}
|
|
15121
15564
|
},
|
|
15122
15565
|
{
|
|
15123
|
-
"id": "
|
|
15566
|
+
"id": "competitive-landscape",
|
|
15124
15567
|
"approach_ids": [
|
|
15125
15568
|
"inspect"
|
|
15126
15569
|
],
|
|
15127
|
-
"name": "
|
|
15570
|
+
"name": "Competitive Landscape",
|
|
15128
15571
|
"version": "1.0.0",
|
|
15129
|
-
"description": "
|
|
15130
|
-
"category": "
|
|
15572
|
+
"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.",
|
|
15573
|
+
"category": "competitive",
|
|
15131
15574
|
"origin": {
|
|
15132
|
-
"type": "
|
|
15133
|
-
"
|
|
15134
|
-
"
|
|
15575
|
+
"type": "custom",
|
|
15576
|
+
"attribution": "The Product Creator",
|
|
15577
|
+
"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.",
|
|
15578
|
+
"license": "open_attribution"
|
|
15135
15579
|
},
|
|
15136
15580
|
"tags": [
|
|
15137
|
-
"
|
|
15581
|
+
"competitive",
|
|
15138
15582
|
"matrix"
|
|
15139
15583
|
],
|
|
15140
15584
|
"slots": [
|
|
15141
15585
|
{
|
|
15142
|
-
"label": "
|
|
15143
|
-
"entityTypeId": "
|
|
15144
|
-
"description": "
|
|
15586
|
+
"label": "Competitors",
|
|
15587
|
+
"entityTypeId": "competitor",
|
|
15588
|
+
"description": "The rivals in the market you are surveying"
|
|
15145
15589
|
},
|
|
15146
15590
|
{
|
|
15147
|
-
"label": "
|
|
15148
|
-
"entityTypeId": "
|
|
15149
|
-
"description": "
|
|
15591
|
+
"label": "Competitor Features",
|
|
15592
|
+
"entityTypeId": "competitor_feature",
|
|
15593
|
+
"description": "What those competitors actually ship"
|
|
15150
15594
|
},
|
|
15151
15595
|
{
|
|
15152
|
-
"label": "
|
|
15153
|
-
"entityTypeId": "
|
|
15154
|
-
"description": "
|
|
15596
|
+
"label": "Market Trends",
|
|
15597
|
+
"entityTypeId": "market_trend",
|
|
15598
|
+
"description": "Forces acting on the whole market, not on one rival"
|
|
15155
15599
|
},
|
|
15156
15600
|
{
|
|
15157
|
-
"label": "
|
|
15158
|
-
"entityTypeId": "
|
|
15159
|
-
"description": "
|
|
15601
|
+
"label": "Positioning",
|
|
15602
|
+
"entityTypeId": "positioning",
|
|
15603
|
+
"description": "Where you stand relative to the field"
|
|
15160
15604
|
}
|
|
15161
15605
|
],
|
|
15162
15606
|
"data": {
|
|
15163
15607
|
"entity_types": [
|
|
15164
15608
|
{
|
|
15165
|
-
"type": "
|
|
15166
|
-
"role": "
|
|
15609
|
+
"type": "competitor",
|
|
15610
|
+
"role": "item"
|
|
15167
15611
|
},
|
|
15168
15612
|
{
|
|
15169
|
-
"type": "
|
|
15170
|
-
"role": "
|
|
15613
|
+
"type": "competitor_feature",
|
|
15614
|
+
"role": "item"
|
|
15171
15615
|
},
|
|
15172
15616
|
{
|
|
15173
|
-
"type": "
|
|
15174
|
-
"role": "
|
|
15617
|
+
"type": "market_trend",
|
|
15618
|
+
"role": "item"
|
|
15175
15619
|
},
|
|
15176
15620
|
{
|
|
15177
|
-
"type": "
|
|
15178
|
-
"role": "
|
|
15621
|
+
"type": "positioning",
|
|
15622
|
+
"role": "item"
|
|
15179
15623
|
}
|
|
15180
15624
|
],
|
|
15181
15625
|
"required_properties": {}
|
|
@@ -15189,15 +15633,204 @@ var UPG_FRAMEWORKS = [
|
|
|
15189
15633
|
"rows": 2,
|
|
15190
15634
|
"cols": 2
|
|
15191
15635
|
},
|
|
15192
|
-
"
|
|
15193
|
-
"property": "title",
|
|
15194
|
-
"direction": "asc"
|
|
15195
|
-
},
|
|
15196
|
-
"colour_by": "group",
|
|
15636
|
+
"colour_by": "type",
|
|
15197
15637
|
"card_fields": [
|
|
15198
15638
|
"title",
|
|
15199
|
-
"description"
|
|
15200
|
-
|
|
15639
|
+
"description",
|
|
15640
|
+
"status"
|
|
15641
|
+
]
|
|
15642
|
+
},
|
|
15643
|
+
"education": {
|
|
15644
|
+
"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.",
|
|
15645
|
+
"core_question": "What does the field look like from above, and where do we sit in it?",
|
|
15646
|
+
"when_to_use": [
|
|
15647
|
+
"You need the shape of a whole market before choosing which rival to study in depth",
|
|
15648
|
+
"Trends are moving the category and you need them beside the competitor set, not in a separate document",
|
|
15649
|
+
"A positioning statement needs the competitive evidence sitting next to it"
|
|
15650
|
+
],
|
|
15651
|
+
"when_not_to_use": [
|
|
15652
|
+
"You already know which single rival matters and want depth on them: use competitor-profile",
|
|
15653
|
+
"You are comparing capabilities feature-by-feature against named rivals: use competitive-matrix",
|
|
15654
|
+
"You need to place rivals positionally on two scored axes: use positioning-map"
|
|
15655
|
+
]
|
|
15656
|
+
}
|
|
15657
|
+
},
|
|
15658
|
+
{
|
|
15659
|
+
"id": "porter-five-forces",
|
|
15660
|
+
"approach_ids": [
|
|
15661
|
+
"inspect"
|
|
15662
|
+
],
|
|
15663
|
+
"name": "Porter Five Forces",
|
|
15664
|
+
"version": "1.0.0",
|
|
15665
|
+
"description": "Analyse industry competitiveness through five forces: rivalry, new entrants, substitutes, buyer power, and supplier power.",
|
|
15666
|
+
"category": "strategy",
|
|
15667
|
+
"origin": {
|
|
15668
|
+
"type": "practitioner",
|
|
15669
|
+
"attribution": "Michael Porter",
|
|
15670
|
+
"description": "Published in Competitive Strategy (Free Press). The foundational framework for industry analysis.",
|
|
15671
|
+
"year": 1979,
|
|
15672
|
+
"license": "public_domain"
|
|
15673
|
+
},
|
|
15674
|
+
"tags": [
|
|
15675
|
+
"strategy",
|
|
15676
|
+
"collection"
|
|
15677
|
+
],
|
|
15678
|
+
"slots": [
|
|
15679
|
+
{
|
|
15680
|
+
"label": "Competitive Rivalry",
|
|
15681
|
+
"role": "competitive_rivalry",
|
|
15682
|
+
"entityTypeId": "competitor",
|
|
15683
|
+
"description": "Intensity of competition among existing players"
|
|
15684
|
+
},
|
|
15685
|
+
{
|
|
15686
|
+
"label": "Threat of New Entrants",
|
|
15687
|
+
"role": "new_entrants",
|
|
15688
|
+
"entityTypeId": "competitor",
|
|
15689
|
+
"description": "How easy is it for new competitors to enter?"
|
|
15690
|
+
},
|
|
15691
|
+
{
|
|
15692
|
+
"label": "Threat of Substitutes",
|
|
15693
|
+
"role": "substitutes",
|
|
15694
|
+
"entityTypeId": "competitor",
|
|
15695
|
+
"description": "Can customers switch to alternatives?"
|
|
15696
|
+
},
|
|
15697
|
+
{
|
|
15698
|
+
"label": "Buyer Power",
|
|
15699
|
+
"role": "buyer_power",
|
|
15700
|
+
"entityTypeId": "persona",
|
|
15701
|
+
"description": "How much leverage do buyers have?"
|
|
15702
|
+
},
|
|
15703
|
+
{
|
|
15704
|
+
"label": "Supplier Power",
|
|
15705
|
+
"role": "supplier_power",
|
|
15706
|
+
"entityTypeId": "persona",
|
|
15707
|
+
"description": "How much leverage do suppliers have?"
|
|
15708
|
+
}
|
|
15709
|
+
],
|
|
15710
|
+
"data": {
|
|
15711
|
+
"entity_types": [
|
|
15712
|
+
{
|
|
15713
|
+
"type": "competitor",
|
|
15714
|
+
"role": "item"
|
|
15715
|
+
},
|
|
15716
|
+
{
|
|
15717
|
+
"type": "persona",
|
|
15718
|
+
"role": "item"
|
|
15719
|
+
}
|
|
15720
|
+
],
|
|
15721
|
+
"required_properties": {}
|
|
15722
|
+
},
|
|
15723
|
+
"structure": {
|
|
15724
|
+
"pattern": "collection"
|
|
15725
|
+
},
|
|
15726
|
+
"presentation": {
|
|
15727
|
+
"layout": {
|
|
15728
|
+
"type": "grid",
|
|
15729
|
+
"groupBy": "type"
|
|
15730
|
+
},
|
|
15731
|
+
"sort_by": {
|
|
15732
|
+
"property": "title",
|
|
15733
|
+
"direction": "asc"
|
|
15734
|
+
},
|
|
15735
|
+
"colour_by": "type",
|
|
15736
|
+
"card_fields": [
|
|
15737
|
+
"title",
|
|
15738
|
+
"description"
|
|
15739
|
+
]
|
|
15740
|
+
},
|
|
15741
|
+
"education": {
|
|
15742
|
+
"purpose": "Analyse the competitive dynamics of an industry through five structural forces that determine profitability, revealing where power lies and where threats emerge.",
|
|
15743
|
+
"core_question": "What structural forces shape competition in our industry, and how do they affect our ability to capture value?",
|
|
15744
|
+
"when_to_use": [
|
|
15745
|
+
"You need to align the team on long-term direction",
|
|
15746
|
+
"Market conditions are shifting and you need to reassess positioning",
|
|
15747
|
+
"Leadership needs a structured view of strategic options"
|
|
15748
|
+
],
|
|
15749
|
+
"when_not_to_use": [
|
|
15750
|
+
"You are in pure execution mode with a clear strategy already set",
|
|
15751
|
+
"The team is too early-stage to commit to strategic constraints"
|
|
15752
|
+
]
|
|
15753
|
+
}
|
|
15754
|
+
},
|
|
15755
|
+
{
|
|
15756
|
+
"id": "swot-analysis",
|
|
15757
|
+
"approach_ids": [
|
|
15758
|
+
"inspect"
|
|
15759
|
+
],
|
|
15760
|
+
"name": "SWOT Analysis",
|
|
15761
|
+
"version": "1.0.0",
|
|
15762
|
+
"description": "Map Strengths, Weaknesses, Opportunities, and Threats in a 2x2 grid. Internal vs external, helpful vs harmful.",
|
|
15763
|
+
"category": "strategy",
|
|
15764
|
+
"origin": {
|
|
15765
|
+
"type": "practitioner",
|
|
15766
|
+
"description": "Widely attributed to Albert Humphrey at Stanford Research Institute. One of the most commonly used strategy frameworks.",
|
|
15767
|
+
"license": "public_domain"
|
|
15768
|
+
},
|
|
15769
|
+
"tags": [
|
|
15770
|
+
"strategy",
|
|
15771
|
+
"matrix"
|
|
15772
|
+
],
|
|
15773
|
+
"slots": [
|
|
15774
|
+
{
|
|
15775
|
+
"label": "Strengths",
|
|
15776
|
+
"entityTypeId": "capability",
|
|
15777
|
+
"description": "Internal advantages and core competencies"
|
|
15778
|
+
},
|
|
15779
|
+
{
|
|
15780
|
+
"label": "Weaknesses",
|
|
15781
|
+
"entityTypeId": "need",
|
|
15782
|
+
"description": "Internal limitations and gaps"
|
|
15783
|
+
},
|
|
15784
|
+
{
|
|
15785
|
+
"label": "Opportunities",
|
|
15786
|
+
"entityTypeId": "opportunity",
|
|
15787
|
+
"description": "External trends and openings"
|
|
15788
|
+
},
|
|
15789
|
+
{
|
|
15790
|
+
"label": "Threats",
|
|
15791
|
+
"entityTypeId": "competitor",
|
|
15792
|
+
"description": "External risks and competitive pressures"
|
|
15793
|
+
}
|
|
15794
|
+
],
|
|
15795
|
+
"data": {
|
|
15796
|
+
"entity_types": [
|
|
15797
|
+
{
|
|
15798
|
+
"type": "capability",
|
|
15799
|
+
"role": "bucket"
|
|
15800
|
+
},
|
|
15801
|
+
{
|
|
15802
|
+
"type": "need",
|
|
15803
|
+
"role": "bucket"
|
|
15804
|
+
},
|
|
15805
|
+
{
|
|
15806
|
+
"type": "opportunity",
|
|
15807
|
+
"role": "bucket"
|
|
15808
|
+
},
|
|
15809
|
+
{
|
|
15810
|
+
"type": "competitor",
|
|
15811
|
+
"role": "bucket"
|
|
15812
|
+
}
|
|
15813
|
+
],
|
|
15814
|
+
"required_properties": {}
|
|
15815
|
+
},
|
|
15816
|
+
"structure": {
|
|
15817
|
+
"pattern": "matrix"
|
|
15818
|
+
},
|
|
15819
|
+
"presentation": {
|
|
15820
|
+
"layout": {
|
|
15821
|
+
"type": "matrix",
|
|
15822
|
+
"rows": 2,
|
|
15823
|
+
"cols": 2
|
|
15824
|
+
},
|
|
15825
|
+
"sort_by": {
|
|
15826
|
+
"property": "title",
|
|
15827
|
+
"direction": "asc"
|
|
15828
|
+
},
|
|
15829
|
+
"colour_by": "group",
|
|
15830
|
+
"card_fields": [
|
|
15831
|
+
"title",
|
|
15832
|
+
"description"
|
|
15833
|
+
]
|
|
15201
15834
|
},
|
|
15202
15835
|
"education": {
|
|
15203
15836
|
"purpose": "Map internal strengths and weaknesses against external opportunities and threats so teams can align strategy with reality.",
|
|
@@ -15314,6 +15947,108 @@ var UPG_FRAMEWORKS = [
|
|
|
15314
15947
|
]
|
|
15315
15948
|
}
|
|
15316
15949
|
},
|
|
15950
|
+
{
|
|
15951
|
+
"id": "product-vision-board",
|
|
15952
|
+
"approach_ids": [
|
|
15953
|
+
"plan"
|
|
15954
|
+
],
|
|
15955
|
+
"name": "Product Vision Board",
|
|
15956
|
+
"version": "1.0.0",
|
|
15957
|
+
"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.",
|
|
15958
|
+
"category": "strategy",
|
|
15959
|
+
"origin": {
|
|
15960
|
+
"type": "practitioner",
|
|
15961
|
+
"attribution": "Roman Pichler",
|
|
15962
|
+
"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.',
|
|
15963
|
+
"license": "published_methodology"
|
|
15964
|
+
},
|
|
15965
|
+
"tags": [
|
|
15966
|
+
"strategy",
|
|
15967
|
+
"matrix"
|
|
15968
|
+
],
|
|
15969
|
+
"slots": [
|
|
15970
|
+
{
|
|
15971
|
+
"label": "Vision",
|
|
15972
|
+
"entityTypeId": "vision",
|
|
15973
|
+
"description": "The long-term change the product exists to bring about"
|
|
15974
|
+
},
|
|
15975
|
+
{
|
|
15976
|
+
"label": "Target Group",
|
|
15977
|
+
"entityTypeId": "persona",
|
|
15978
|
+
"description": "The customers and users the vision is for"
|
|
15979
|
+
},
|
|
15980
|
+
{
|
|
15981
|
+
"label": "Needs",
|
|
15982
|
+
"entityTypeId": "need",
|
|
15983
|
+
"description": "The problem the product solves or the benefit it provides for that group"
|
|
15984
|
+
},
|
|
15985
|
+
{
|
|
15986
|
+
"label": "Product",
|
|
15987
|
+
"entityTypeId": "solution",
|
|
15988
|
+
"description": "What the product is, and what makes it stand out"
|
|
15989
|
+
},
|
|
15990
|
+
{
|
|
15991
|
+
"label": "Business Goals",
|
|
15992
|
+
"entityTypeId": "outcome",
|
|
15993
|
+
"description": "How the product benefits the company that builds it"
|
|
15994
|
+
}
|
|
15995
|
+
],
|
|
15996
|
+
"data": {
|
|
15997
|
+
"entity_types": [
|
|
15998
|
+
{
|
|
15999
|
+
"type": "vision",
|
|
16000
|
+
"role": "item"
|
|
16001
|
+
},
|
|
16002
|
+
{
|
|
16003
|
+
"type": "persona",
|
|
16004
|
+
"role": "item"
|
|
16005
|
+
},
|
|
16006
|
+
{
|
|
16007
|
+
"type": "need",
|
|
16008
|
+
"role": "item"
|
|
16009
|
+
},
|
|
16010
|
+
{
|
|
16011
|
+
"type": "solution",
|
|
16012
|
+
"role": "item"
|
|
16013
|
+
},
|
|
16014
|
+
{
|
|
16015
|
+
"type": "outcome",
|
|
16016
|
+
"role": "item"
|
|
16017
|
+
}
|
|
16018
|
+
],
|
|
16019
|
+
"required_properties": {}
|
|
16020
|
+
},
|
|
16021
|
+
"structure": {
|
|
16022
|
+
"pattern": "matrix"
|
|
16023
|
+
},
|
|
16024
|
+
"presentation": {
|
|
16025
|
+
"layout": {
|
|
16026
|
+
"type": "matrix",
|
|
16027
|
+
"rows": 5,
|
|
16028
|
+
"cols": 1
|
|
16029
|
+
},
|
|
16030
|
+
"colour_by": "type",
|
|
16031
|
+
"card_fields": [
|
|
16032
|
+
"title",
|
|
16033
|
+
"description",
|
|
16034
|
+
"status"
|
|
16035
|
+
]
|
|
16036
|
+
},
|
|
16037
|
+
"education": {
|
|
16038
|
+
"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.",
|
|
16039
|
+
"core_question": "What change are we trying to bring about, for whom, and how does building it pay off?",
|
|
16040
|
+
"when_to_use": [
|
|
16041
|
+
"A new product or a major new direction needs a shared vision before any roadmap exists",
|
|
16042
|
+
"The team can describe what it is building but not why anyone should want it",
|
|
16043
|
+
"Vision, target group and business goals are living in three different documents and drifting apart"
|
|
16044
|
+
],
|
|
16045
|
+
"when_not_to_use": [
|
|
16046
|
+
"The vision is settled and the open question is sequencing: use goal-oriented-roadmap",
|
|
16047
|
+
"You need the causal chain from vision down to daily work: use strategic-cascade",
|
|
16048
|
+
"The question is which business model captures the value: use business-model-canvas"
|
|
16049
|
+
]
|
|
16050
|
+
}
|
|
16051
|
+
},
|
|
15317
16052
|
{
|
|
15318
16053
|
"id": "three-horizons",
|
|
15319
16054
|
"approach_ids": [
|
|
@@ -15899,25 +16634,95 @@ var UPG_FRAMEWORKS = [
|
|
|
15899
16634
|
"label": "Must-haves",
|
|
15900
16635
|
"role": "must_have",
|
|
15901
16636
|
"entityTypeId": "feature",
|
|
15902
|
-
"description": "Expected features: absence causes dissatisfaction"
|
|
16637
|
+
"description": "Expected features: absence causes dissatisfaction",
|
|
16638
|
+
"predicate": [
|
|
16639
|
+
{
|
|
16640
|
+
"scope": "framework",
|
|
16641
|
+
"property": "functional_response",
|
|
16642
|
+
"op": "in",
|
|
16643
|
+
"value": [
|
|
16644
|
+
"i_expect_it",
|
|
16645
|
+
"i_am_neutral",
|
|
16646
|
+
"i_can_tolerate_it"
|
|
16647
|
+
]
|
|
16648
|
+
},
|
|
16649
|
+
{
|
|
16650
|
+
"scope": "framework",
|
|
16651
|
+
"property": "dysfunctional_response",
|
|
16652
|
+
"op": "eq",
|
|
16653
|
+
"value": "i_dislike_it"
|
|
16654
|
+
}
|
|
16655
|
+
]
|
|
15903
16656
|
},
|
|
15904
16657
|
{
|
|
15905
16658
|
"label": "Performance",
|
|
15906
16659
|
"role": "performance",
|
|
15907
16660
|
"entityTypeId": "feature",
|
|
15908
|
-
"description": "More is better: linear satisfaction increase"
|
|
16661
|
+
"description": "More is better: linear satisfaction increase",
|
|
16662
|
+
"predicate": [
|
|
16663
|
+
{
|
|
16664
|
+
"scope": "framework",
|
|
16665
|
+
"property": "functional_response",
|
|
16666
|
+
"op": "eq",
|
|
16667
|
+
"value": "i_like_it"
|
|
16668
|
+
},
|
|
16669
|
+
{
|
|
16670
|
+
"scope": "framework",
|
|
16671
|
+
"property": "dysfunctional_response",
|
|
16672
|
+
"op": "eq",
|
|
16673
|
+
"value": "i_dislike_it"
|
|
16674
|
+
}
|
|
16675
|
+
]
|
|
15909
16676
|
},
|
|
15910
16677
|
{
|
|
15911
16678
|
"label": "Delighters",
|
|
15912
16679
|
"role": "delighter",
|
|
15913
16680
|
"entityTypeId": "feature",
|
|
15914
|
-
"description": "Unexpected features: presence creates delight"
|
|
16681
|
+
"description": "Unexpected features: presence creates delight",
|
|
16682
|
+
"predicate": [
|
|
16683
|
+
{
|
|
16684
|
+
"scope": "framework",
|
|
16685
|
+
"property": "functional_response",
|
|
16686
|
+
"op": "eq",
|
|
16687
|
+
"value": "i_like_it"
|
|
16688
|
+
},
|
|
16689
|
+
{
|
|
16690
|
+
"scope": "framework",
|
|
16691
|
+
"property": "dysfunctional_response",
|
|
16692
|
+
"op": "in",
|
|
16693
|
+
"value": [
|
|
16694
|
+
"i_expect_it",
|
|
16695
|
+
"i_am_neutral",
|
|
16696
|
+
"i_can_tolerate_it"
|
|
16697
|
+
]
|
|
16698
|
+
}
|
|
16699
|
+
]
|
|
15915
16700
|
},
|
|
15916
16701
|
{
|
|
15917
16702
|
"label": "Indifferent",
|
|
15918
16703
|
"role": "indifferent",
|
|
15919
16704
|
"entityTypeId": "feature",
|
|
15920
|
-
"description": "Features users don't care about either way"
|
|
16705
|
+
"description": "Features users don't care about either way",
|
|
16706
|
+
"predicate": [
|
|
16707
|
+
{
|
|
16708
|
+
"scope": "framework",
|
|
16709
|
+
"property": "functional_response",
|
|
16710
|
+
"op": "in",
|
|
16711
|
+
"value": [
|
|
16712
|
+
"i_am_neutral",
|
|
16713
|
+
"i_can_tolerate_it"
|
|
16714
|
+
]
|
|
16715
|
+
},
|
|
16716
|
+
{
|
|
16717
|
+
"scope": "framework",
|
|
16718
|
+
"property": "dysfunctional_response",
|
|
16719
|
+
"op": "in",
|
|
16720
|
+
"value": [
|
|
16721
|
+
"i_am_neutral",
|
|
16722
|
+
"i_can_tolerate_it"
|
|
16723
|
+
]
|
|
16724
|
+
}
|
|
16725
|
+
]
|
|
15921
16726
|
}
|
|
15922
16727
|
],
|
|
15923
16728
|
"data": {
|
|
@@ -16252,32 +17057,213 @@ var UPG_FRAMEWORKS = [
|
|
|
16252
17057
|
"entityTypeId": "feature",
|
|
16253
17058
|
"description": "Features or requirements sorted into the four MoSCoW buckets"
|
|
16254
17059
|
}
|
|
16255
|
-
],
|
|
17060
|
+
],
|
|
17061
|
+
"data": {
|
|
17062
|
+
"entity_types": [
|
|
17063
|
+
{
|
|
17064
|
+
"type": "feature",
|
|
17065
|
+
"role": "scored_item"
|
|
17066
|
+
}
|
|
17067
|
+
],
|
|
17068
|
+
"required_properties": {
|
|
17069
|
+
"feature": [
|
|
17070
|
+
{
|
|
17071
|
+
"property": "moscow",
|
|
17072
|
+
"type": "enum",
|
|
17073
|
+
"required": true,
|
|
17074
|
+
"scope": "framework",
|
|
17075
|
+
"label": "MoSCoW priority",
|
|
17076
|
+
"description": "Which scope bucket this requirement falls into for the current release",
|
|
17077
|
+
"enum_values": [
|
|
17078
|
+
"must",
|
|
17079
|
+
"should",
|
|
17080
|
+
"could",
|
|
17081
|
+
"wont"
|
|
17082
|
+
]
|
|
17083
|
+
}
|
|
17084
|
+
]
|
|
17085
|
+
}
|
|
17086
|
+
},
|
|
17087
|
+
"structure": {
|
|
17088
|
+
"pattern": "table"
|
|
17089
|
+
},
|
|
17090
|
+
"presentation": {
|
|
17091
|
+
"layout": {
|
|
17092
|
+
"type": "table",
|
|
17093
|
+
"columns": [
|
|
17094
|
+
{
|
|
17095
|
+
"property": "title",
|
|
17096
|
+
"label": "Requirement",
|
|
17097
|
+
"sortable": true
|
|
17098
|
+
},
|
|
17099
|
+
{
|
|
17100
|
+
"property": "moscow",
|
|
17101
|
+
"label": "Priority",
|
|
17102
|
+
"sortable": true,
|
|
17103
|
+
"format": "badge"
|
|
17104
|
+
}
|
|
17105
|
+
]
|
|
17106
|
+
},
|
|
17107
|
+
"sort_by": {
|
|
17108
|
+
"property": "moscow",
|
|
17109
|
+
"direction": "asc"
|
|
17110
|
+
},
|
|
17111
|
+
"colour_by": "type",
|
|
17112
|
+
"card_fields": [
|
|
17113
|
+
"title",
|
|
17114
|
+
"description",
|
|
17115
|
+
"status"
|
|
17116
|
+
]
|
|
17117
|
+
},
|
|
17118
|
+
"education": {
|
|
17119
|
+
"purpose": "Categorise requirements into Must, Should, Could, and Won't buckets so stakeholders agree on scope boundaries before development begins.",
|
|
17120
|
+
"core_question": "Which requirements are truly non-negotiable for this release, and which can be deferred without blocking the goal?",
|
|
17121
|
+
"when_to_use": [
|
|
17122
|
+
"You have more ideas or features than capacity to build them",
|
|
17123
|
+
"Stakeholders disagree on what to build next",
|
|
17124
|
+
"You need a transparent, defensible prioritisation process"
|
|
17125
|
+
],
|
|
17126
|
+
"when_not_to_use": [
|
|
17127
|
+
"You have a single obvious next step with no contention",
|
|
17128
|
+
"The backlog is small enough to sequence intuitively"
|
|
17129
|
+
]
|
|
17130
|
+
}
|
|
17131
|
+
},
|
|
17132
|
+
{
|
|
17133
|
+
"id": "experiment-tracker",
|
|
17134
|
+
"name": "Experiment Tracker",
|
|
17135
|
+
"version": "1.0.0",
|
|
17136
|
+
"description": "Track experiments from design through execution to results. Velocity-focused.",
|
|
17137
|
+
"category": "validation",
|
|
17138
|
+
"origin": {
|
|
17139
|
+
"type": "practitioner",
|
|
17140
|
+
"description": "Common in growth and product teams running continuous experiments.",
|
|
17141
|
+
"license": "cc_by"
|
|
17142
|
+
},
|
|
17143
|
+
"tags": [
|
|
17144
|
+
"validation",
|
|
17145
|
+
"table"
|
|
17146
|
+
],
|
|
17147
|
+
"relational": {
|
|
17148
|
+
"spine": "experiment_run",
|
|
17149
|
+
"columns": [
|
|
17150
|
+
{
|
|
17151
|
+
"kind": "field",
|
|
17152
|
+
"id": "experiment",
|
|
17153
|
+
"label": "Experiment",
|
|
17154
|
+
"property": "title",
|
|
17155
|
+
"sortable": true
|
|
17156
|
+
},
|
|
17157
|
+
{
|
|
17158
|
+
"kind": "projection",
|
|
17159
|
+
"id": "hypothesis",
|
|
17160
|
+
"label": "Hypothesis",
|
|
17161
|
+
"path": [
|
|
17162
|
+
{
|
|
17163
|
+
"edge": "experiment_plan_ran_as_experiment_run",
|
|
17164
|
+
"direction": "reverse"
|
|
17165
|
+
},
|
|
17166
|
+
{
|
|
17167
|
+
"edge": "hypothesis_requires_experiment_plan",
|
|
17168
|
+
"direction": "reverse"
|
|
17169
|
+
}
|
|
17170
|
+
],
|
|
17171
|
+
"fields": [
|
|
17172
|
+
"title"
|
|
17173
|
+
],
|
|
17174
|
+
"sortable": true
|
|
17175
|
+
},
|
|
17176
|
+
{
|
|
17177
|
+
"kind": "projection",
|
|
17178
|
+
"id": "success_metric",
|
|
17179
|
+
"label": "Success Metric",
|
|
17180
|
+
"path": [
|
|
17181
|
+
{
|
|
17182
|
+
"edge": "experiment_run_measures_metric",
|
|
17183
|
+
"direction": "forward"
|
|
17184
|
+
}
|
|
17185
|
+
],
|
|
17186
|
+
"fields": [
|
|
17187
|
+
"title",
|
|
17188
|
+
"current_value",
|
|
17189
|
+
"target_value"
|
|
17190
|
+
]
|
|
17191
|
+
},
|
|
17192
|
+
{
|
|
17193
|
+
"kind": "field",
|
|
17194
|
+
"id": "stage",
|
|
17195
|
+
"label": "Stage",
|
|
17196
|
+
"property": "status"
|
|
17197
|
+
},
|
|
17198
|
+
{
|
|
17199
|
+
"kind": "field",
|
|
17200
|
+
"id": "verdict",
|
|
17201
|
+
"label": "Verdict",
|
|
17202
|
+
"property": "disposition"
|
|
17203
|
+
},
|
|
17204
|
+
{
|
|
17205
|
+
"kind": "projection",
|
|
17206
|
+
"id": "result",
|
|
17207
|
+
"label": "Result",
|
|
17208
|
+
"path": [
|
|
17209
|
+
{
|
|
17210
|
+
"edge": "experiment_run_produces_learning",
|
|
17211
|
+
"direction": "forward"
|
|
17212
|
+
}
|
|
17213
|
+
],
|
|
17214
|
+
"fields": [
|
|
17215
|
+
"title",
|
|
17216
|
+
"result_direction"
|
|
17217
|
+
]
|
|
17218
|
+
},
|
|
17219
|
+
{
|
|
17220
|
+
"kind": "projection",
|
|
17221
|
+
"id": "evidence",
|
|
17222
|
+
"label": "Evidence",
|
|
17223
|
+
"path": [
|
|
17224
|
+
{
|
|
17225
|
+
"edge": "experiment_run_yields_evidence",
|
|
17226
|
+
"direction": "forward"
|
|
17227
|
+
}
|
|
17228
|
+
],
|
|
17229
|
+
"fields": [
|
|
17230
|
+
"title"
|
|
17231
|
+
]
|
|
17232
|
+
}
|
|
17233
|
+
],
|
|
17234
|
+
"sort": {
|
|
17235
|
+
"column": "experiment",
|
|
17236
|
+
"direction": "asc"
|
|
17237
|
+
}
|
|
17238
|
+
},
|
|
16256
17239
|
"data": {
|
|
16257
17240
|
"entity_types": [
|
|
16258
17241
|
{
|
|
16259
|
-
"type": "
|
|
16260
|
-
"role": "
|
|
17242
|
+
"type": "experiment_run",
|
|
17243
|
+
"role": "item"
|
|
17244
|
+
},
|
|
17245
|
+
{
|
|
17246
|
+
"type": "experiment_plan",
|
|
17247
|
+
"role": "item"
|
|
17248
|
+
},
|
|
17249
|
+
{
|
|
17250
|
+
"type": "hypothesis",
|
|
17251
|
+
"role": "item"
|
|
17252
|
+
},
|
|
17253
|
+
{
|
|
17254
|
+
"type": "metric",
|
|
17255
|
+
"role": "item"
|
|
17256
|
+
},
|
|
17257
|
+
{
|
|
17258
|
+
"type": "learning",
|
|
17259
|
+
"role": "item"
|
|
17260
|
+
},
|
|
17261
|
+
{
|
|
17262
|
+
"type": "evidence",
|
|
17263
|
+
"role": "item"
|
|
16261
17264
|
}
|
|
16262
17265
|
],
|
|
16263
|
-
"required_properties": {
|
|
16264
|
-
"feature": [
|
|
16265
|
-
{
|
|
16266
|
-
"property": "moscow",
|
|
16267
|
-
"type": "enum",
|
|
16268
|
-
"required": true,
|
|
16269
|
-
"scope": "framework",
|
|
16270
|
-
"label": "MoSCoW priority",
|
|
16271
|
-
"description": "Which scope bucket this requirement falls into for the current release",
|
|
16272
|
-
"enum_values": [
|
|
16273
|
-
"must",
|
|
16274
|
-
"should",
|
|
16275
|
-
"could",
|
|
16276
|
-
"wont"
|
|
16277
|
-
]
|
|
16278
|
-
}
|
|
16279
|
-
]
|
|
16280
|
-
}
|
|
17266
|
+
"required_properties": {}
|
|
16281
17267
|
},
|
|
16282
17268
|
"structure": {
|
|
16283
17269
|
"pattern": "table"
|
|
@@ -16285,22 +17271,10 @@ var UPG_FRAMEWORKS = [
|
|
|
16285
17271
|
"presentation": {
|
|
16286
17272
|
"layout": {
|
|
16287
17273
|
"type": "table",
|
|
16288
|
-
"columns": [
|
|
16289
|
-
{
|
|
16290
|
-
"property": "title",
|
|
16291
|
-
"label": "Requirement",
|
|
16292
|
-
"sortable": true
|
|
16293
|
-
},
|
|
16294
|
-
{
|
|
16295
|
-
"property": "moscow",
|
|
16296
|
-
"label": "Priority",
|
|
16297
|
-
"sortable": true,
|
|
16298
|
-
"format": "badge"
|
|
16299
|
-
}
|
|
16300
|
-
]
|
|
17274
|
+
"columns": []
|
|
16301
17275
|
},
|
|
16302
17276
|
"sort_by": {
|
|
16303
|
-
"property": "
|
|
17277
|
+
"property": "title",
|
|
16304
17278
|
"direction": "asc"
|
|
16305
17279
|
},
|
|
16306
17280
|
"colour_by": "type",
|
|
@@ -16311,16 +17285,16 @@ var UPG_FRAMEWORKS = [
|
|
|
16311
17285
|
]
|
|
16312
17286
|
},
|
|
16313
17287
|
"education": {
|
|
16314
|
-
"purpose": "
|
|
16315
|
-
"core_question": "
|
|
17288
|
+
"purpose": "Track all running and completed experiments in one place: hypothesis, method, status, result, learnings. The team builds a cumulative record of validated knowledge.",
|
|
17289
|
+
"core_question": "What experiments are running, what have we learned from completed experiments, and are we applying those learnings to new hypotheses?",
|
|
16316
17290
|
"when_to_use": [
|
|
16317
|
-
"You have
|
|
16318
|
-
"
|
|
16319
|
-
"
|
|
17291
|
+
"You have hypotheses about user needs or solutions that need testing",
|
|
17292
|
+
"You want to reduce risk before committing engineering resources",
|
|
17293
|
+
"The team is debating assumptions that can be tested empirically"
|
|
16320
17294
|
],
|
|
16321
17295
|
"when_not_to_use": [
|
|
16322
|
-
"
|
|
16323
|
-
"
|
|
17296
|
+
"The solution is already validated through real usage data",
|
|
17297
|
+
"Speed of shipping matters more than certainty about assumptions"
|
|
16324
17298
|
]
|
|
16325
17299
|
}
|
|
16326
17300
|
},
|
|
@@ -16650,6 +17624,117 @@ var UPG_FRAMEWORKS = [
|
|
|
16650
17624
|
]
|
|
16651
17625
|
}
|
|
16652
17626
|
},
|
|
17627
|
+
{
|
|
17628
|
+
"id": "threat-model-canvas",
|
|
17629
|
+
"approach_ids": [
|
|
17630
|
+
"inspect"
|
|
17631
|
+
],
|
|
17632
|
+
"name": "Threat Model Canvas",
|
|
17633
|
+
"version": "1.0.0",
|
|
17634
|
+
"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.",
|
|
17635
|
+
"category": "security",
|
|
17636
|
+
"origin": {
|
|
17637
|
+
"type": "custom",
|
|
17638
|
+
"attribution": "The Product Creator",
|
|
17639
|
+
"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.",
|
|
17640
|
+
"license": "open_attribution"
|
|
17641
|
+
},
|
|
17642
|
+
"tags": [
|
|
17643
|
+
"security",
|
|
17644
|
+
"matrix"
|
|
17645
|
+
],
|
|
17646
|
+
"slots": [
|
|
17647
|
+
{
|
|
17648
|
+
"label": "Threat Models",
|
|
17649
|
+
"entityTypeId": "threat_model",
|
|
17650
|
+
"description": "The modelling exercises themselves: scope, system, assumptions"
|
|
17651
|
+
},
|
|
17652
|
+
{
|
|
17653
|
+
"label": "Threats",
|
|
17654
|
+
"entityTypeId": "threat",
|
|
17655
|
+
"description": "What an adversary could attempt against the system"
|
|
17656
|
+
},
|
|
17657
|
+
{
|
|
17658
|
+
"label": "Vulnerabilities",
|
|
17659
|
+
"entityTypeId": "vulnerability",
|
|
17660
|
+
"description": "Weaknesses that make a threat realisable"
|
|
17661
|
+
},
|
|
17662
|
+
{
|
|
17663
|
+
"label": "Controls",
|
|
17664
|
+
"entityTypeId": "security_control",
|
|
17665
|
+
"description": "Technical and procedural measures that reduce risk"
|
|
17666
|
+
},
|
|
17667
|
+
{
|
|
17668
|
+
"label": "Policies",
|
|
17669
|
+
"entityTypeId": "security_policy",
|
|
17670
|
+
"description": "The standing rules the controls implement"
|
|
17671
|
+
},
|
|
17672
|
+
{
|
|
17673
|
+
"label": "Reviews",
|
|
17674
|
+
"entityTypeId": "security_review",
|
|
17675
|
+
"description": "Assessments that check the posture actually holds"
|
|
17676
|
+
}
|
|
17677
|
+
],
|
|
17678
|
+
"data": {
|
|
17679
|
+
"entity_types": [
|
|
17680
|
+
{
|
|
17681
|
+
"type": "threat_model",
|
|
17682
|
+
"role": "item"
|
|
17683
|
+
},
|
|
17684
|
+
{
|
|
17685
|
+
"type": "threat",
|
|
17686
|
+
"role": "item"
|
|
17687
|
+
},
|
|
17688
|
+
{
|
|
17689
|
+
"type": "vulnerability",
|
|
17690
|
+
"role": "item"
|
|
17691
|
+
},
|
|
17692
|
+
{
|
|
17693
|
+
"type": "security_control",
|
|
17694
|
+
"role": "item"
|
|
17695
|
+
},
|
|
17696
|
+
{
|
|
17697
|
+
"type": "security_policy",
|
|
17698
|
+
"role": "item"
|
|
17699
|
+
},
|
|
17700
|
+
{
|
|
17701
|
+
"type": "security_review",
|
|
17702
|
+
"role": "item"
|
|
17703
|
+
}
|
|
17704
|
+
],
|
|
17705
|
+
"required_properties": {}
|
|
17706
|
+
},
|
|
17707
|
+
"structure": {
|
|
17708
|
+
"pattern": "matrix"
|
|
17709
|
+
},
|
|
17710
|
+
"presentation": {
|
|
17711
|
+
"layout": {
|
|
17712
|
+
"type": "matrix",
|
|
17713
|
+
"rows": 2,
|
|
17714
|
+
"cols": 3
|
|
17715
|
+
},
|
|
17716
|
+
"colour_by": "type",
|
|
17717
|
+
"card_fields": [
|
|
17718
|
+
"title",
|
|
17719
|
+
"description",
|
|
17720
|
+
"status"
|
|
17721
|
+
]
|
|
17722
|
+
},
|
|
17723
|
+
"education": {
|
|
17724
|
+
"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.",
|
|
17725
|
+
"core_question": "What could go wrong, what makes it possible, and what have we actually put in place against it?",
|
|
17726
|
+
"when_to_use": [
|
|
17727
|
+
"A threat modelling session needs somewhere to land that is not a document nobody reopens",
|
|
17728
|
+
"Controls and policies exist but nobody can point to the threats they answer",
|
|
17729
|
+
"An audit or review asks you to show posture, not just a list of findings"
|
|
17730
|
+
],
|
|
17731
|
+
"when_not_to_use": [
|
|
17732
|
+
"You want STRIDE's six threat categories specifically. This canvas is a lifecycle, not that taxonomy",
|
|
17733
|
+
"You are triaging a live incident rather than modelling posture",
|
|
17734
|
+
"The work is scoring and ranking risks against each other rather than laying out what exists"
|
|
17735
|
+
]
|
|
17736
|
+
}
|
|
17737
|
+
},
|
|
16653
17738
|
{
|
|
16654
17739
|
"id": "atomic-design",
|
|
16655
17740
|
"approach_ids": [
|
|
@@ -17423,32 +18508,128 @@ var UPG_FRAMEWORKS = [
|
|
|
17423
18508
|
"purpose": "Systematically test all 19 traction channels to find the one or two that drive scalable growth, avoiding premature commitment to a favourite channel.",
|
|
17424
18509
|
"core_question": "Which of the 19 traction channels is our bullseye, the one that works at our current stage and scale?",
|
|
17425
18510
|
"when_to_use": [
|
|
17426
|
-
"You need to systematically identify and optimise growth levers",
|
|
17427
|
-
"User acquisition, activation, or retention metrics need improvement",
|
|
17428
|
-
"You want to build a structured growth experimentation practice"
|
|
18511
|
+
"You need to systematically identify and optimise growth levers",
|
|
18512
|
+
"User acquisition, activation, or retention metrics need improvement",
|
|
18513
|
+
"You want to build a structured growth experimentation practice"
|
|
18514
|
+
],
|
|
18515
|
+
"when_not_to_use": [
|
|
18516
|
+
"The product has not yet achieved product-market fit",
|
|
18517
|
+
"Growth would scale problems rather than value"
|
|
18518
|
+
]
|
|
18519
|
+
}
|
|
18520
|
+
},
|
|
18521
|
+
{
|
|
18522
|
+
"id": "product-led-growth-framework",
|
|
18523
|
+
"approach_ids": [
|
|
18524
|
+
"plan"
|
|
18525
|
+
],
|
|
18526
|
+
"name": "PLG Framework",
|
|
18527
|
+
"version": "1.0.0",
|
|
18528
|
+
"description": "Product-led go-to-market motion. Free entry gives users access, the aha moment hooks them, they expand usage within their team, and monetisation captures value from power users.",
|
|
18529
|
+
"category": "go_to_market",
|
|
18530
|
+
"origin": {
|
|
18531
|
+
"type": "practitioner",
|
|
18532
|
+
"attribution": "Wes Bush",
|
|
18533
|
+
"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.',
|
|
18534
|
+
"url": "https://www.productled.com/",
|
|
18535
|
+
"year": 2019,
|
|
18536
|
+
"license": "published_methodology"
|
|
18537
|
+
},
|
|
18538
|
+
"tags": [
|
|
18539
|
+
"go_to_market",
|
|
18540
|
+
"flow"
|
|
18541
|
+
],
|
|
18542
|
+
"slots": [
|
|
18543
|
+
{
|
|
18544
|
+
"label": "Free Entry",
|
|
18545
|
+
"role": "free_entry",
|
|
18546
|
+
"entityTypeId": "gtm_strategy",
|
|
18547
|
+
"description": "How users access the product for free"
|
|
18548
|
+
},
|
|
18549
|
+
{
|
|
18550
|
+
"label": "Aha Moment",
|
|
18551
|
+
"role": "aha_moment",
|
|
18552
|
+
"entityTypeId": "gtm_strategy",
|
|
18553
|
+
"description": "First experience of core value"
|
|
18554
|
+
},
|
|
18555
|
+
{
|
|
18556
|
+
"label": "Expansion Motion",
|
|
18557
|
+
"role": "expansion_motion",
|
|
18558
|
+
"entityTypeId": "sales_motion",
|
|
18559
|
+
"description": "How usage spreads within accounts"
|
|
18560
|
+
},
|
|
18561
|
+
{
|
|
18562
|
+
"label": "Monetisation Trigger",
|
|
18563
|
+
"role": "monetisation_trigger",
|
|
18564
|
+
"entityTypeId": "gtm_strategy",
|
|
18565
|
+
"description": "When and how to convert to paid"
|
|
18566
|
+
},
|
|
18567
|
+
{
|
|
18568
|
+
"label": "Metric",
|
|
18569
|
+
"role": "metric",
|
|
18570
|
+
"entityTypeId": "metric",
|
|
18571
|
+
"description": "Key metric driving the PLG motion"
|
|
18572
|
+
}
|
|
18573
|
+
],
|
|
18574
|
+
"data": {
|
|
18575
|
+
"entity_types": [
|
|
18576
|
+
{
|
|
18577
|
+
"type": "gtm_strategy",
|
|
18578
|
+
"role": "item"
|
|
18579
|
+
},
|
|
18580
|
+
{
|
|
18581
|
+
"type": "sales_motion",
|
|
18582
|
+
"role": "item"
|
|
18583
|
+
},
|
|
18584
|
+
{
|
|
18585
|
+
"type": "metric",
|
|
18586
|
+
"role": "item"
|
|
18587
|
+
}
|
|
18588
|
+
],
|
|
18589
|
+
"required_properties": {}
|
|
18590
|
+
},
|
|
18591
|
+
"structure": {
|
|
18592
|
+
"pattern": "flow"
|
|
18593
|
+
},
|
|
18594
|
+
"presentation": {
|
|
18595
|
+
"layout": {
|
|
18596
|
+
"type": "flow",
|
|
18597
|
+
"direction": "LR"
|
|
18598
|
+
},
|
|
18599
|
+
"colour_by": "status",
|
|
18600
|
+
"card_fields": [
|
|
18601
|
+
"title",
|
|
18602
|
+
"description",
|
|
18603
|
+
"status"
|
|
18604
|
+
]
|
|
18605
|
+
},
|
|
18606
|
+
"education": {
|
|
18607
|
+
"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.",
|
|
18608
|
+
"core_question": "Can our product acquire, activate, and expand users without human intervention, and where in the loop do we still need sales?",
|
|
18609
|
+
"when_to_use": [
|
|
18610
|
+
"You are launching a new product, feature, or entering a new market",
|
|
18611
|
+
"You need to coordinate cross-functional launch activities",
|
|
18612
|
+
"You want to define target customers, channels, and messaging"
|
|
17429
18613
|
],
|
|
17430
18614
|
"when_not_to_use": [
|
|
17431
|
-
"The product
|
|
17432
|
-
"
|
|
18615
|
+
"The product is mature with established distribution channels",
|
|
18616
|
+
"You are iterating on an existing product for existing customers"
|
|
17433
18617
|
]
|
|
17434
18618
|
}
|
|
17435
18619
|
},
|
|
17436
18620
|
{
|
|
17437
|
-
"id": "
|
|
18621
|
+
"id": "gtm-playbook",
|
|
17438
18622
|
"approach_ids": [
|
|
17439
18623
|
"plan"
|
|
17440
18624
|
],
|
|
17441
|
-
"name": "
|
|
18625
|
+
"name": "GTM Playbook",
|
|
17442
18626
|
"version": "1.0.0",
|
|
17443
|
-
"description": "
|
|
18627
|
+
"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.",
|
|
17444
18628
|
"category": "go_to_market",
|
|
17445
18629
|
"origin": {
|
|
17446
18630
|
"type": "practitioner",
|
|
17447
|
-
"
|
|
17448
|
-
"
|
|
17449
|
-
"url": "https://www.productled.com/",
|
|
17450
|
-
"year": 2019,
|
|
17451
|
-
"license": "published_methodology"
|
|
18631
|
+
"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.",
|
|
18632
|
+
"license": "cc_by"
|
|
17452
18633
|
},
|
|
17453
18634
|
"tags": [
|
|
17454
18635
|
"go_to_market",
|
|
@@ -17456,34 +18637,34 @@ var UPG_FRAMEWORKS = [
|
|
|
17456
18637
|
],
|
|
17457
18638
|
"slots": [
|
|
17458
18639
|
{
|
|
17459
|
-
"label": "
|
|
17460
|
-
"role": "free_entry",
|
|
18640
|
+
"label": "Market Analysis",
|
|
17461
18641
|
"entityTypeId": "gtm_strategy",
|
|
17462
|
-
"description": "
|
|
18642
|
+
"description": "Market sizing and competitive landscape"
|
|
17463
18643
|
},
|
|
17464
18644
|
{
|
|
17465
|
-
"label": "
|
|
17466
|
-
"
|
|
17467
|
-
"
|
|
17468
|
-
"description": "First experience of core value"
|
|
18645
|
+
"label": "Ideal Customer Profile",
|
|
18646
|
+
"entityTypeId": "ideal_customer_profile",
|
|
18647
|
+
"description": "Ideal customer profile"
|
|
17469
18648
|
},
|
|
17470
18649
|
{
|
|
17471
|
-
"label": "
|
|
17472
|
-
"
|
|
17473
|
-
"
|
|
17474
|
-
"description": "How usage spreads within accounts"
|
|
18650
|
+
"label": "Positioning",
|
|
18651
|
+
"entityTypeId": "positioning",
|
|
18652
|
+
"description": "How you position against alternatives"
|
|
17475
18653
|
},
|
|
17476
18654
|
{
|
|
17477
|
-
"label": "
|
|
17478
|
-
"
|
|
17479
|
-
"
|
|
17480
|
-
"description": "When and how to convert to paid"
|
|
18655
|
+
"label": "Messaging",
|
|
18656
|
+
"entityTypeId": "messaging",
|
|
18657
|
+
"description": "Key messages by audience"
|
|
17481
18658
|
},
|
|
17482
18659
|
{
|
|
17483
|
-
"label": "
|
|
17484
|
-
"
|
|
17485
|
-
"
|
|
17486
|
-
|
|
18660
|
+
"label": "Launch",
|
|
18661
|
+
"entityTypeId": "launch",
|
|
18662
|
+
"description": "Launch activities and timeline"
|
|
18663
|
+
},
|
|
18664
|
+
{
|
|
18665
|
+
"label": "Sales Motion",
|
|
18666
|
+
"entityTypeId": "sales_motion",
|
|
18667
|
+
"description": "How you will sell"
|
|
17487
18668
|
}
|
|
17488
18669
|
],
|
|
17489
18670
|
"data": {
|
|
@@ -17493,11 +18674,23 @@ var UPG_FRAMEWORKS = [
|
|
|
17493
18674
|
"role": "item"
|
|
17494
18675
|
},
|
|
17495
18676
|
{
|
|
17496
|
-
"type": "
|
|
18677
|
+
"type": "ideal_customer_profile",
|
|
17497
18678
|
"role": "item"
|
|
17498
18679
|
},
|
|
17499
18680
|
{
|
|
17500
|
-
"type": "
|
|
18681
|
+
"type": "positioning",
|
|
18682
|
+
"role": "item"
|
|
18683
|
+
},
|
|
18684
|
+
{
|
|
18685
|
+
"type": "messaging",
|
|
18686
|
+
"role": "item"
|
|
18687
|
+
},
|
|
18688
|
+
{
|
|
18689
|
+
"type": "launch",
|
|
18690
|
+
"role": "item"
|
|
18691
|
+
},
|
|
18692
|
+
{
|
|
18693
|
+
"type": "sales_motion",
|
|
17501
18694
|
"role": "item"
|
|
17502
18695
|
}
|
|
17503
18696
|
],
|
|
@@ -17519,8 +18712,8 @@ var UPG_FRAMEWORKS = [
|
|
|
17519
18712
|
]
|
|
17520
18713
|
},
|
|
17521
18714
|
"education": {
|
|
17522
|
-
"purpose": "
|
|
17523
|
-
"core_question": "
|
|
18715
|
+
"purpose": "Orchestrate every go-to-market activity (messaging, channels, pricing, enablement, launch timeline) into a single executable plan.",
|
|
18716
|
+
"core_question": "Do all go-to-market functions (product, marketing, sales, CS) have a shared plan for how we'll bring this to market?",
|
|
17524
18717
|
"when_to_use": [
|
|
17525
18718
|
"You are launching a new product, feature, or entering a new market",
|
|
17526
18719
|
"You need to coordinate cross-functional launch activities",
|
|
@@ -19733,6 +20926,173 @@ var UPG_FRAMEWORKS = [
|
|
|
19733
20926
|
"You are in a delivery phase with clear requirements"
|
|
19734
20927
|
]
|
|
19735
20928
|
}
|
|
20929
|
+
},
|
|
20930
|
+
{
|
|
20931
|
+
"id": "assumption-map",
|
|
20932
|
+
"approach_ids": [
|
|
20933
|
+
"reflect"
|
|
20934
|
+
],
|
|
20935
|
+
"name": "Assumption Map",
|
|
20936
|
+
"version": "1.0.0",
|
|
20937
|
+
"description": "Plot assumptions on axes of uncertainty vs risk. Most risky + most uncertain = test first.",
|
|
20938
|
+
"category": "validation",
|
|
20939
|
+
"origin": {
|
|
20940
|
+
"type": "practitioner",
|
|
20941
|
+
"attribution": "David Bland",
|
|
20942
|
+
"description": "Published in Testing Business Ideas (Wiley). Prioritises which assumptions to validate first.",
|
|
20943
|
+
"url": "https://www.strategyzer.com/books/testing-business-ideas",
|
|
20944
|
+
"year": 2019,
|
|
20945
|
+
"license": "published_methodology"
|
|
20946
|
+
},
|
|
20947
|
+
"tags": [
|
|
20948
|
+
"validation",
|
|
20949
|
+
"quadrant"
|
|
20950
|
+
],
|
|
20951
|
+
"slots": [
|
|
20952
|
+
{
|
|
20953
|
+
"label": "Test First",
|
|
20954
|
+
"entityTypeId": "assumption",
|
|
20955
|
+
"role": "test_first",
|
|
20956
|
+
"description": "High risk, low confidence: validate immediately",
|
|
20957
|
+
"predicate": [
|
|
20958
|
+
{
|
|
20959
|
+
"scope": "entity",
|
|
20960
|
+
"property": "risk_level",
|
|
20961
|
+
"op": "band",
|
|
20962
|
+
"value": [
|
|
20963
|
+
4,
|
|
20964
|
+
6
|
|
20965
|
+
]
|
|
20966
|
+
},
|
|
20967
|
+
{
|
|
20968
|
+
"scope": "entity",
|
|
20969
|
+
"property": "confidence",
|
|
20970
|
+
"op": "band",
|
|
20971
|
+
"value": [
|
|
20972
|
+
1,
|
|
20973
|
+
4
|
|
20974
|
+
]
|
|
20975
|
+
}
|
|
20976
|
+
]
|
|
20977
|
+
},
|
|
20978
|
+
{
|
|
20979
|
+
"label": "Research",
|
|
20980
|
+
"entityTypeId": "assumption",
|
|
20981
|
+
"role": "research",
|
|
20982
|
+
"description": "Low risk, low confidence: learn more",
|
|
20983
|
+
"predicate": [
|
|
20984
|
+
{
|
|
20985
|
+
"scope": "entity",
|
|
20986
|
+
"property": "risk_level",
|
|
20987
|
+
"op": "band",
|
|
20988
|
+
"value": [
|
|
20989
|
+
1,
|
|
20990
|
+
4
|
|
20991
|
+
]
|
|
20992
|
+
},
|
|
20993
|
+
{
|
|
20994
|
+
"scope": "entity",
|
|
20995
|
+
"property": "confidence",
|
|
20996
|
+
"op": "band",
|
|
20997
|
+
"value": [
|
|
20998
|
+
1,
|
|
20999
|
+
4
|
|
21000
|
+
]
|
|
21001
|
+
}
|
|
21002
|
+
]
|
|
21003
|
+
},
|
|
21004
|
+
{
|
|
21005
|
+
"label": "Monitor",
|
|
21006
|
+
"entityTypeId": "assumption",
|
|
21007
|
+
"role": "monitor",
|
|
21008
|
+
"description": "High risk but well understood: watch closely",
|
|
21009
|
+
"predicate": [
|
|
21010
|
+
{
|
|
21011
|
+
"scope": "entity",
|
|
21012
|
+
"property": "risk_level",
|
|
21013
|
+
"op": "band",
|
|
21014
|
+
"value": [
|
|
21015
|
+
4,
|
|
21016
|
+
6
|
|
21017
|
+
]
|
|
21018
|
+
},
|
|
21019
|
+
{
|
|
21020
|
+
"scope": "entity",
|
|
21021
|
+
"property": "confidence",
|
|
21022
|
+
"op": "band",
|
|
21023
|
+
"value": [
|
|
21024
|
+
4,
|
|
21025
|
+
6
|
|
21026
|
+
]
|
|
21027
|
+
}
|
|
21028
|
+
]
|
|
21029
|
+
},
|
|
21030
|
+
{
|
|
21031
|
+
"label": "Accept",
|
|
21032
|
+
"entityTypeId": "assumption",
|
|
21033
|
+
"role": "accept",
|
|
21034
|
+
"description": "Low risk, high confidence: safe to proceed",
|
|
21035
|
+
"predicate": [
|
|
21036
|
+
{
|
|
21037
|
+
"scope": "entity",
|
|
21038
|
+
"property": "risk_level",
|
|
21039
|
+
"op": "band",
|
|
21040
|
+
"value": [
|
|
21041
|
+
1,
|
|
21042
|
+
4
|
|
21043
|
+
]
|
|
21044
|
+
},
|
|
21045
|
+
{
|
|
21046
|
+
"scope": "entity",
|
|
21047
|
+
"property": "confidence",
|
|
21048
|
+
"op": "band",
|
|
21049
|
+
"value": [
|
|
21050
|
+
4,
|
|
21051
|
+
6
|
|
21052
|
+
]
|
|
21053
|
+
}
|
|
21054
|
+
]
|
|
21055
|
+
}
|
|
21056
|
+
],
|
|
21057
|
+
"data": {
|
|
21058
|
+
"entity_types": [
|
|
21059
|
+
{
|
|
21060
|
+
"type": "assumption",
|
|
21061
|
+
"role": "item"
|
|
21062
|
+
}
|
|
21063
|
+
],
|
|
21064
|
+
"required_properties": {}
|
|
21065
|
+
},
|
|
21066
|
+
"structure": {
|
|
21067
|
+
"pattern": "quadrant"
|
|
21068
|
+
},
|
|
21069
|
+
"presentation": {
|
|
21070
|
+
"layout": {
|
|
21071
|
+
"type": "quadrant",
|
|
21072
|
+
"x_axis": "confidence",
|
|
21073
|
+
"y_axis": "risk_level",
|
|
21074
|
+
"x_label": "Confidence",
|
|
21075
|
+
"y_label": "Risk"
|
|
21076
|
+
},
|
|
21077
|
+
"colour_by": "group",
|
|
21078
|
+
"card_fields": [
|
|
21079
|
+
"title",
|
|
21080
|
+
"description"
|
|
21081
|
+
]
|
|
21082
|
+
},
|
|
21083
|
+
"education": {
|
|
21084
|
+
"purpose": "Plot assumptions on a risk/evidence matrix so teams test the most dangerous unknowns first instead of building on unvalidated beliefs.",
|
|
21085
|
+
"core_question": "Which assumptions carry the most risk and the least evidence? Where do we run experiments first?",
|
|
21086
|
+
"when_to_use": [
|
|
21087
|
+
"You have hypotheses about user needs or solutions that need testing",
|
|
21088
|
+
"You want to reduce risk before committing engineering resources",
|
|
21089
|
+
"The team is debating assumptions that can be tested empirically"
|
|
21090
|
+
],
|
|
21091
|
+
"when_not_to_use": [
|
|
21092
|
+
"The solution is already validated through real usage data",
|
|
21093
|
+
"Speed of shipping matters more than certainty about assumptions"
|
|
21094
|
+
]
|
|
21095
|
+
}
|
|
19736
21096
|
}
|
|
19737
21097
|
];
|
|
19738
21098
|
var UPG_FRAMEWORKS_BY_ID = Object.fromEntries(
|
|
@@ -20403,6 +21763,8 @@ var ENTITY_EMOJI = {
|
|
|
20403
21763
|
competitor_signal: "\u{1F4E1}",
|
|
20404
21764
|
compliance_framework: "\u{1F3DF}\uFE0F",
|
|
20405
21765
|
compliance_requirement: "\u26F2",
|
|
21766
|
+
capture: "\u{1F4F8}",
|
|
21767
|
+
composition: "\u{1F5BC}\uFE0F",
|
|
20406
21768
|
constraint: "\u{1F512}",
|
|
20407
21769
|
contact: "\u{1F4C7}",
|
|
20408
21770
|
content_calendar: "\u{1F4C5}",
|
|
@@ -23781,9 +25143,9 @@ var PRODUCT_SPEC_GUIDE = {
|
|
|
23781
25143
|
},
|
|
23782
25144
|
{
|
|
23783
25145
|
name: "Planning Cadence",
|
|
23784
|
-
description: "The cadence axis. Create a planning_cycle (a sprint, iteration, quarter, or PI), nest finer cycles inside a coarse one, schedule the
|
|
23785
|
-
entity_types: ["planning_cycle", "user_story", "objective"],
|
|
23786
|
-
edge_chain: ["planning_cycle_contains_planning_cycle", "
|
|
25146
|
+
description: "The cadence axis. Create a planning_cycle (a sprint, iteration, quarter, or PI), nest finer cycles inside a coarse one, schedule the work that flows through it (a task, story, epic, feature or bug), and scope the objectives the cycle serves. Scheduling and scoping are deliberate links, not containment: the scheduled item keeps its feature/epic parent.",
|
|
25147
|
+
entity_types: ["planning_cycle", "task", "user_story", "objective"],
|
|
25148
|
+
edge_chain: ["planning_cycle_contains_planning_cycle", "planning_cycle_schedules_work_item", "objective_scoped_to_planning_cycle"]
|
|
23787
25149
|
}
|
|
23788
25150
|
],
|
|
23789
25151
|
required_bridges: [
|
|
@@ -24480,12 +25842,21 @@ var PORTFOLIO_GUIDE = {
|
|
|
24480
25842
|
var WORKSPACE_GUIDE = {
|
|
24481
25843
|
domain_id: "workspace",
|
|
24482
25844
|
anchor_entity: "workspace",
|
|
24483
|
-
creation_sequence: ["workspace", "framework_exercise"],
|
|
24484
|
-
patterns: [
|
|
25845
|
+
creation_sequence: ["workspace", "framework_exercise", "composition", "capture"],
|
|
25846
|
+
patterns: [
|
|
25847
|
+
{
|
|
25848
|
+
name: "Canvas to published view",
|
|
25849
|
+
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",
|
|
25850
|
+
entity_types: ["workspace", "composition"],
|
|
25851
|
+
edge_chain: ["workspace_arranges_node", "workspace_produced_node", "composition_focuses_node"]
|
|
25852
|
+
}
|
|
25853
|
+
],
|
|
24485
25854
|
required_bridges: [],
|
|
24486
25855
|
anti_patterns: [
|
|
24487
25856
|
{ description: "Workspaces are containers for exploration: do not over-structure them" },
|
|
24488
|
-
{ description: "Workspace content is transient by default: promote discoveries to the graph, do not leave them in the workspace" }
|
|
25857
|
+
{ description: "Workspace content is transient by default: promote discoveries to the graph, do not leave them in the workspace" },
|
|
25858
|
+
{ 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" },
|
|
25859
|
+
{ description: "Writing every scratch canvas into a shared graph: only a workspace someone deliberately kept (retention: durable) belongs in a file other people pull" }
|
|
24489
25860
|
]
|
|
24490
25861
|
};
|
|
24491
25862
|
var FOUNDATIONS_GUIDE = {
|
|
@@ -27085,7 +28456,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27085
28456
|
{
|
|
27086
28457
|
id: "planning-cycle-without-scheduled-work",
|
|
27087
28458
|
name: "Planning cycle with no scheduled work",
|
|
27088
|
-
description: "A planning_cycle exists but neither schedules any
|
|
28459
|
+
description: "A planning_cycle exists but neither schedules any work item nor contains a finer sub-cycle. An empty cadence box is a date range with nothing flowing through it: a sprint or iteration nobody planned work into, or a coarse period that was never broken down.",
|
|
27089
28460
|
structured_condition: {
|
|
27090
28461
|
operator: "and",
|
|
27091
28462
|
checks: [
|
|
@@ -27094,8 +28465,8 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27094
28465
|
check: {
|
|
27095
28466
|
type: "relationship",
|
|
27096
28467
|
source_type: "planning_cycle",
|
|
27097
|
-
edge_type: "
|
|
27098
|
-
target_type: "
|
|
28468
|
+
edge_type: "planning_cycle_schedules_work_item",
|
|
28469
|
+
target_type: "node",
|
|
27099
28470
|
comparison: "not_exists"
|
|
27100
28471
|
}
|
|
27101
28472
|
},
|
|
@@ -27111,10 +28482,16 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27111
28482
|
]
|
|
27112
28483
|
},
|
|
27113
28484
|
why_it_matters: "A cadence layer only earns its keep when work is planned through it. An interval with no scheduled stories and no nested cycles adds ceremony without telling anyone what the period is for.",
|
|
27114
|
-
remediation: "Schedule the
|
|
28485
|
+
remediation: "Schedule the work the cycle will carry via `planning_cycle_schedules_work_item`, or break a coarse period into finer cycles via `planning_cycle_contains_planning_cycle`. If neither applies, the interval is not yet a real cadence box.",
|
|
27115
28486
|
stages: ["build", "beta", "launch", "growth", "mature"],
|
|
27116
28487
|
severity: "low",
|
|
27117
28488
|
since: "0.20.0"
|
|
28489
|
+
// RETARGETED at 0.32.0 from planning_cycle_schedules_user_story. Left
|
|
28490
|
+
// unchanged it would have become a false-positive generator the moment
|
|
28491
|
+
// cycles could hold tasks: a cycle full of scheduled tasks and no stories
|
|
28492
|
+
// would have reported as empty. Labeled fixtures cover the near-miss this
|
|
28493
|
+
// is most likely to get wrong — a coarse `period` cycle that legitimately
|
|
28494
|
+
// holds only sub-cycles and schedules nothing directly.
|
|
27118
28495
|
},
|
|
27119
28496
|
// ── Market intelligence layer ───────────────────────────────────────────
|
|
27120
28497
|
{
|
|
@@ -27454,7 +28831,7 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27454
28831
|
id: "contended-surface-without-arbitration",
|
|
27455
28832
|
since: "0.27.0",
|
|
27456
28833
|
name: "Contended surface without arbitration",
|
|
27457
|
-
description: 'At least one surface
|
|
28834
|
+
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.',
|
|
27458
28835
|
// 0.28.0 (feedback 852a9721, from a 30-surface field audit). Three changes,
|
|
27459
28836
|
// each earned by a reported false positive or a reported blind spot:
|
|
27460
28837
|
//
|
|
@@ -27548,12 +28925,26 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27548
28925
|
// the natural move if the ordering detector is ever minted; it is not worth a
|
|
27549
28926
|
// breaking property change before then.
|
|
27550
28927
|
//
|
|
27551
|
-
//
|
|
27552
|
-
// slot that honestly
|
|
27553
|
-
// branch (c), which carries no chained exemption.
|
|
27554
|
-
//
|
|
27555
|
-
//
|
|
27556
|
-
//
|
|
28928
|
+
// CLOSED (banked at 0.28.0, field-confirmed and resolved here): a `chained`
|
|
28929
|
+
// slot that honestly declared `arbitration_state: 'none'` used to re-fire
|
|
28930
|
+
// through branch (c), which carries no chained exemption. The field
|
|
28931
|
+
// confirmation arrived from a second reporter whose census left the state
|
|
28932
|
+
// unset on every one of its chained frames and named this branch as the
|
|
28933
|
+
// reason. THE FIX WAS THE VOCABULARY, NOT THE DETECTOR. Branch (c) matches
|
|
28934
|
+
// the literal string 'none', so the new fifth `SurfaceArbitrationState`
|
|
28935
|
+
// member `no_contention_by_design` never matches it and NOTHING in this
|
|
28936
|
+
// file, the evaluator, or the collector changed.
|
|
28937
|
+
//
|
|
28938
|
+
// Exempting `chained` from (c) was the alternative and it lost on both
|
|
28939
|
+
// correctness and cost. Correctness: `none` is defined as "nobody ever
|
|
28940
|
+
// decided", which is FALSE on a chained shell that has no decision to make,
|
|
28941
|
+
// and silencing a check on a false statement is worse than the empty field
|
|
28942
|
+
// it replaces. Cost: `evaluateEntityCount`'s except form covers the
|
|
28943
|
+
// PRESENCE filter only; the value-keyed path is a bare indexed lookup with
|
|
28944
|
+
// no exception arm, so giving it one means a parallel derived spec set, a
|
|
28945
|
+
// new collector aggregate keyed by (type, property, value, except_property,
|
|
28946
|
+
// except_value), the derivation walk, and its tests. That is the whole
|
|
28947
|
+
// 0.28.0 evaluator rider again, spent to make a false statement quiet.
|
|
27557
28948
|
structured_condition: {
|
|
27558
28949
|
operator: "and",
|
|
27559
28950
|
checks: [
|
|
@@ -27606,7 +28997,23 @@ var UPG_ANTI_PATTERNS = [
|
|
|
27606
28997
|
]
|
|
27607
28998
|
},
|
|
27608
28999
|
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.",
|
|
27609
|
-
|
|
29000
|
+
// Remediation is deliberately the ACTIONABLE CORE only. It is read by
|
|
29001
|
+
// someone who has already been told there is a problem and now needs the
|
|
29002
|
+
// next move, so rationale in it is strictly in the way. The reasoning that
|
|
29003
|
+
// used to live here (why transcription is ten minutes rather than a
|
|
29004
|
+
// meeting, why safe_by_coincidence is the dangerous state, why declaring
|
|
29005
|
+
// capacity is the honest fix rather than inventing a rule, and why the
|
|
29006
|
+
// chained exemption is a factual claim rather than a mute button) is all
|
|
29007
|
+
// above in this comment block, which is where a reader who wants it looks.
|
|
29008
|
+
remediation: (
|
|
29009
|
+
// Remediation is read by someone already told there is a problem and now
|
|
29010
|
+
// wanting the action, so rationale is strictly in the way. The reasoning
|
|
29011
|
+
// trimmed out in 0.31.0: a surface at its designed capacity should state
|
|
29012
|
+
// that capacity rather than invent an arbitration rule it does not need,
|
|
29013
|
+
// and leaving the state empty on a chained surface reads as unassessed
|
|
29014
|
+
// rather than as "nothing to arbitrate".
|
|
29015
|
+
"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'`."
|
|
29016
|
+
),
|
|
27610
29017
|
stages: ["build", "beta", "launch", "growth", "mature", "maintenance"],
|
|
27611
29018
|
severity: "medium",
|
|
27612
29019
|
source: { kind: "fundamental" }
|
|
@@ -29210,7 +30617,7 @@ var UPG_REGIONS = [
|
|
|
29210
30617
|
"roadmap_item_references_feature",
|
|
29211
30618
|
"feature_request_voted_on_by_feedback_vote",
|
|
29212
30619
|
"planning_cycle_contains_planning_cycle",
|
|
29213
|
-
"
|
|
30620
|
+
"planning_cycle_schedules_work_item"
|
|
29214
30621
|
],
|
|
29215
30622
|
boundary_edges: [
|
|
29216
30623
|
{
|
|
@@ -30578,6 +31985,63 @@ var UPG_STRUCTURE_PATTERNS = [
|
|
|
30578
31985
|
"flow"
|
|
30579
31986
|
];
|
|
30580
31987
|
|
|
31988
|
+
// src/frameworks/relational-paths.ts
|
|
31989
|
+
function resolveRelationalPath(spine, path, catalog = UPG_EDGE_CATALOG) {
|
|
31990
|
+
const steps = [];
|
|
31991
|
+
if (path.length === 0) {
|
|
31992
|
+
return { steps, endpoint: null, reason: "path is empty; a projection must declare at least one step", failedAt: 0 };
|
|
31993
|
+
}
|
|
31994
|
+
let current = spine;
|
|
31995
|
+
for (let i = 0; i < path.length; i++) {
|
|
31996
|
+
const step = path[i];
|
|
31997
|
+
const entry = catalog[step.edge];
|
|
31998
|
+
if (!entry) {
|
|
31999
|
+
return {
|
|
32000
|
+
steps,
|
|
32001
|
+
endpoint: null,
|
|
32002
|
+
failedAt: i,
|
|
32003
|
+
reason: `unknown edge type "${step.edge}" \u2014 not in the UPG edge catalog`
|
|
32004
|
+
};
|
|
32005
|
+
}
|
|
32006
|
+
if (step.direction !== "forward" && step.direction !== "reverse") {
|
|
32007
|
+
return {
|
|
32008
|
+
steps,
|
|
32009
|
+
endpoint: null,
|
|
32010
|
+
failedAt: i,
|
|
32011
|
+
reason: `step direction must be "forward" or "reverse", got "${String(step.direction)}"`
|
|
32012
|
+
};
|
|
32013
|
+
}
|
|
32014
|
+
const entersAt = step.direction === "forward" ? entry.source_type : entry.target_type;
|
|
32015
|
+
const landsOn = step.direction === "forward" ? entry.target_type : entry.source_type;
|
|
32016
|
+
if (entersAt !== current) {
|
|
32017
|
+
return {
|
|
32018
|
+
steps,
|
|
32019
|
+
endpoint: null,
|
|
32020
|
+
failedAt: i,
|
|
32021
|
+
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}".`
|
|
32022
|
+
};
|
|
32023
|
+
}
|
|
32024
|
+
steps.push({ from: current, to: landsOn });
|
|
32025
|
+
current = landsOn;
|
|
32026
|
+
}
|
|
32027
|
+
return { steps, endpoint: current, reason: null, failedAt: -1 };
|
|
32028
|
+
}
|
|
32029
|
+
function relationalCoveredEntityTypes(fw) {
|
|
32030
|
+
const covered = /* @__PURE__ */ new Set();
|
|
32031
|
+
const rel = fw.relational;
|
|
32032
|
+
if (!rel) return covered;
|
|
32033
|
+
covered.add(rel.spine);
|
|
32034
|
+
for (const column of rel.columns) {
|
|
32035
|
+
if (column.kind !== "projection") continue;
|
|
32036
|
+
const resolved = resolveRelationalPath(rel.spine, column.path);
|
|
32037
|
+
for (const step of resolved.steps) {
|
|
32038
|
+
covered.add(step.from);
|
|
32039
|
+
covered.add(step.to);
|
|
32040
|
+
}
|
|
32041
|
+
}
|
|
32042
|
+
return covered;
|
|
32043
|
+
}
|
|
32044
|
+
|
|
30581
32045
|
// src/frameworks/validate.ts
|
|
30582
32046
|
function validateUPGFramework(framework) {
|
|
30583
32047
|
const errors = [];
|
|
@@ -30682,6 +32146,25 @@ function validateUPGFramework(framework) {
|
|
|
30682
32146
|
}
|
|
30683
32147
|
}
|
|
30684
32148
|
}
|
|
32149
|
+
if (f.relational !== void 0) {
|
|
32150
|
+
if (typeof f.relational !== "object" || f.relational === null || Array.isArray(f.relational)) {
|
|
32151
|
+
errors.push('"relational" must be an object when present');
|
|
32152
|
+
} else {
|
|
32153
|
+
const rel = f.relational;
|
|
32154
|
+
errors.push(...validateRelationalSurface(rel));
|
|
32155
|
+
if (Array.isArray(f.slots) && f.slots.length > 0) {
|
|
32156
|
+
warnings.push(
|
|
32157
|
+
'"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.'
|
|
32158
|
+
);
|
|
32159
|
+
}
|
|
32160
|
+
const presLayout = f.presentation?.layout;
|
|
32161
|
+
if (presLayout?.type === "table" && Array.isArray(presLayout.columns) && presLayout.columns.length > 0) {
|
|
32162
|
+
errors.push(
|
|
32163
|
+
`"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.`
|
|
32164
|
+
);
|
|
32165
|
+
}
|
|
32166
|
+
}
|
|
32167
|
+
}
|
|
30685
32168
|
if (!f.education || typeof f.education !== "object") {
|
|
30686
32169
|
errors.push('"education" is required and must be an object');
|
|
30687
32170
|
} else {
|
|
@@ -30699,8 +32182,144 @@ function validateUPGFramework(framework) {
|
|
|
30699
32182
|
errors.push('"education.when_not_to_use" is required and must be an array');
|
|
30700
32183
|
}
|
|
30701
32184
|
}
|
|
32185
|
+
validateSlotPredicates(f.slots, errors);
|
|
30702
32186
|
return { valid: errors.length === 0, errors, warnings };
|
|
30703
32187
|
}
|
|
32188
|
+
var PREDICATE_SCOPES = ["entity", "framework"];
|
|
32189
|
+
var PREDICATE_OPS = ["eq", "in", "gte", "lt", "band"];
|
|
32190
|
+
function admits(atom) {
|
|
32191
|
+
switch (atom.op) {
|
|
32192
|
+
case "eq":
|
|
32193
|
+
return { kind: "discrete", values: [atom.value] };
|
|
32194
|
+
case "in":
|
|
32195
|
+
return { kind: "discrete", values: atom.value };
|
|
32196
|
+
case "gte":
|
|
32197
|
+
return { kind: "interval", lo: atom.value, hi: Number.POSITIVE_INFINITY };
|
|
32198
|
+
case "lt":
|
|
32199
|
+
return { kind: "interval", lo: Number.NEGATIVE_INFINITY, hi: atom.value };
|
|
32200
|
+
case "band":
|
|
32201
|
+
return { kind: "interval", lo: atom.value[0], hi: atom.value[1] };
|
|
32202
|
+
}
|
|
32203
|
+
}
|
|
32204
|
+
function atomsCannotBothHold(a, b) {
|
|
32205
|
+
const x = admits(a);
|
|
32206
|
+
const y = admits(b);
|
|
32207
|
+
if (x.kind === "discrete" && y.kind === "discrete") {
|
|
32208
|
+
return !x.values.some((v) => y.values.includes(v));
|
|
32209
|
+
}
|
|
32210
|
+
if (x.kind === "interval" && y.kind === "interval") {
|
|
32211
|
+
return !(x.lo < y.hi && y.lo < x.hi);
|
|
32212
|
+
}
|
|
32213
|
+
const set = x.kind === "discrete" ? x : y;
|
|
32214
|
+
const range = x.kind === "interval" ? x : y;
|
|
32215
|
+
return !set.values.some((v) => typeof v === "number" && v >= range.lo && v < range.hi);
|
|
32216
|
+
}
|
|
32217
|
+
function predicatesProvablyDisjoint(a, b) {
|
|
32218
|
+
for (const atomA of a) {
|
|
32219
|
+
for (const atomB of b) {
|
|
32220
|
+
if (atomA.scope === atomB.scope && atomA.property === atomB.property && atomsCannotBothHold(atomA, atomB)) {
|
|
32221
|
+
return true;
|
|
32222
|
+
}
|
|
32223
|
+
}
|
|
32224
|
+
}
|
|
32225
|
+
return false;
|
|
32226
|
+
}
|
|
32227
|
+
function parseAtom(atom, where, errors) {
|
|
32228
|
+
if (!atom || typeof atom !== "object" || Array.isArray(atom)) {
|
|
32229
|
+
errors.push(`${where} must be an object`);
|
|
32230
|
+
return null;
|
|
32231
|
+
}
|
|
32232
|
+
const a = atom;
|
|
32233
|
+
let ok = true;
|
|
32234
|
+
if (typeof a.scope !== "string" || !PREDICATE_SCOPES.includes(a.scope)) {
|
|
32235
|
+
errors.push(`${where}.scope must be one of: ${PREDICATE_SCOPES.join(", ")}`);
|
|
32236
|
+
ok = false;
|
|
32237
|
+
}
|
|
32238
|
+
if (typeof a.property !== "string" || a.property.length === 0) {
|
|
32239
|
+
errors.push(`${where}.property is required and must be a non-empty string`);
|
|
32240
|
+
ok = false;
|
|
32241
|
+
}
|
|
32242
|
+
if (typeof a.op !== "string" || !PREDICATE_OPS.includes(a.op)) {
|
|
32243
|
+
errors.push(`${where}.op must be one of: ${PREDICATE_OPS.join(", ")}`);
|
|
32244
|
+
return null;
|
|
32245
|
+
}
|
|
32246
|
+
switch (a.op) {
|
|
32247
|
+
case "eq":
|
|
32248
|
+
if (!["string", "number", "boolean"].includes(typeof a.value)) {
|
|
32249
|
+
errors.push(`${where}.value must be a string, number, or boolean for op "eq"`);
|
|
32250
|
+
ok = false;
|
|
32251
|
+
}
|
|
32252
|
+
break;
|
|
32253
|
+
case "in":
|
|
32254
|
+
if (!Array.isArray(a.value) || a.value.length === 0 || !a.value.every((v) => typeof v === "string" || typeof v === "number")) {
|
|
32255
|
+
errors.push(`${where}.value must be a non-empty array of strings or numbers for op "in"`);
|
|
32256
|
+
ok = false;
|
|
32257
|
+
}
|
|
32258
|
+
break;
|
|
32259
|
+
case "gte":
|
|
32260
|
+
case "lt":
|
|
32261
|
+
if (typeof a.value !== "number" || !Number.isFinite(a.value)) {
|
|
32262
|
+
errors.push(`${where}.value must be a finite number for op "${a.op}"`);
|
|
32263
|
+
ok = false;
|
|
32264
|
+
}
|
|
32265
|
+
break;
|
|
32266
|
+
case "band":
|
|
32267
|
+
if (!Array.isArray(a.value) || a.value.length !== 2 || !a.value.every((v) => typeof v === "number" && Number.isFinite(v))) {
|
|
32268
|
+
errors.push(`${where}.value must be a [min, max] pair of finite numbers for op "band"`);
|
|
32269
|
+
ok = false;
|
|
32270
|
+
} else if (a.value[0] >= a.value[1]) {
|
|
32271
|
+
errors.push(
|
|
32272
|
+
`${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)`
|
|
32273
|
+
);
|
|
32274
|
+
ok = false;
|
|
32275
|
+
}
|
|
32276
|
+
break;
|
|
32277
|
+
}
|
|
32278
|
+
return ok ? a : null;
|
|
32279
|
+
}
|
|
32280
|
+
function validateSlotPredicates(slots, errors) {
|
|
32281
|
+
if (!Array.isArray(slots)) return;
|
|
32282
|
+
const zones = [];
|
|
32283
|
+
slots.forEach((slot, i) => {
|
|
32284
|
+
if (!slot || typeof slot !== "object") return;
|
|
32285
|
+
const s = slot;
|
|
32286
|
+
if (s.predicate === void 0 || s.predicate === null) return;
|
|
32287
|
+
const label = typeof s.label === "string" && s.label.length > 0 ? ` ("${s.label}")` : "";
|
|
32288
|
+
const where = `"slots[${i}]${label}.predicate"`;
|
|
32289
|
+
if (!Array.isArray(s.predicate)) {
|
|
32290
|
+
errors.push(
|
|
32291
|
+
`${where} must be an array of atoms. A slot's membership rule is a conjunction, even when it has one atom`
|
|
32292
|
+
);
|
|
32293
|
+
return;
|
|
32294
|
+
}
|
|
32295
|
+
if (s.predicate.length === 0) {
|
|
32296
|
+
errors.push(
|
|
32297
|
+
`${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`
|
|
32298
|
+
);
|
|
32299
|
+
return;
|
|
32300
|
+
}
|
|
32301
|
+
const atoms = s.predicate.map((atom, j) => parseAtom(atom, `${where}[${j}]`, errors));
|
|
32302
|
+
if (atoms.some((a) => a === null)) return;
|
|
32303
|
+
zones.push({
|
|
32304
|
+
where: `slots[${i}]${label}`,
|
|
32305
|
+
entityTypeId: typeof s.entityTypeId === "string" ? s.entityTypeId : null,
|
|
32306
|
+
predicate: atoms
|
|
32307
|
+
});
|
|
32308
|
+
});
|
|
32309
|
+
for (let i = 0; i < zones.length; i++) {
|
|
32310
|
+
for (let j = i + 1; j < zones.length; j++) {
|
|
32311
|
+
const a = zones[i];
|
|
32312
|
+
const b = zones[j];
|
|
32313
|
+
if (a.entityTypeId && b.entityTypeId && a.entityTypeId !== b.entityTypeId) continue;
|
|
32314
|
+
if (predicatesProvablyDisjoint(a.predicate, b.predicate)) continue;
|
|
32315
|
+
const shared = a.predicate.filter((x) => b.predicate.some((y) => y.scope === x.scope && y.property === x.property)).map((x) => `${x.scope}.${x.property}`);
|
|
32316
|
+
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`;
|
|
32317
|
+
errors.push(
|
|
32318
|
+
`"${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`
|
|
32319
|
+
);
|
|
32320
|
+
}
|
|
32321
|
+
}
|
|
32322
|
+
}
|
|
30704
32323
|
function validateExpression(expr) {
|
|
30705
32324
|
if (expr.trim().length === 0) {
|
|
30706
32325
|
return "Expression must not be empty";
|
|
@@ -30720,6 +32339,122 @@ function validateExpression(expr) {
|
|
|
30720
32339
|
}
|
|
30721
32340
|
return null;
|
|
30722
32341
|
}
|
|
32342
|
+
function validateRelationalSurface(rel) {
|
|
32343
|
+
const errors = [];
|
|
32344
|
+
const spine = rel.spine;
|
|
32345
|
+
if (!spine || typeof spine !== "string") {
|
|
32346
|
+
errors.push('"relational.spine" is required and must be a string (the entity type whose instances are the rows)');
|
|
32347
|
+
}
|
|
32348
|
+
if (!Array.isArray(rel.columns) || rel.columns.length === 0) {
|
|
32349
|
+
errors.push('"relational.columns" is required and must be a non-empty array');
|
|
32350
|
+
return errors;
|
|
32351
|
+
}
|
|
32352
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
32353
|
+
for (let i = 0; i < rel.columns.length; i++) {
|
|
32354
|
+
const col = rel.columns[i];
|
|
32355
|
+
const at = `"relational.columns[${i}]"`;
|
|
32356
|
+
if (!col || typeof col !== "object" || Array.isArray(col)) {
|
|
32357
|
+
errors.push(`${at} must be an object`);
|
|
32358
|
+
continue;
|
|
32359
|
+
}
|
|
32360
|
+
const c = col;
|
|
32361
|
+
if (!c.id || typeof c.id !== "string") {
|
|
32362
|
+
errors.push(`${at}.id is required and must be a string`);
|
|
32363
|
+
} else if (seenIds.has(c.id)) {
|
|
32364
|
+
errors.push(`${at}.id "${c.id}" is duplicated; column ids must be unique within a surface`);
|
|
32365
|
+
} else {
|
|
32366
|
+
seenIds.add(c.id);
|
|
32367
|
+
}
|
|
32368
|
+
if (!c.label || typeof c.label !== "string") {
|
|
32369
|
+
errors.push(`${at}.label is required and must be a string`);
|
|
32370
|
+
}
|
|
32371
|
+
switch (c.kind) {
|
|
32372
|
+
case "field": {
|
|
32373
|
+
if (!c.property || typeof c.property !== "string") {
|
|
32374
|
+
errors.push(`${at}.property is required on a field column and must be a string`);
|
|
32375
|
+
}
|
|
32376
|
+
if ("entityTypeId" in c) {
|
|
32377
|
+
errors.push(
|
|
32378
|
+
`${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.`
|
|
32379
|
+
);
|
|
32380
|
+
}
|
|
32381
|
+
if ("path" in c) {
|
|
32382
|
+
errors.push(`${at} is a field column and must NOT declare "path"; use kind:"projection" to traverse edges`);
|
|
32383
|
+
}
|
|
32384
|
+
break;
|
|
32385
|
+
}
|
|
32386
|
+
case "computed": {
|
|
32387
|
+
if (!c.expression || typeof c.expression !== "string") {
|
|
32388
|
+
errors.push(`${at}.expression is required on a computed column and must be a string`);
|
|
32389
|
+
} else {
|
|
32390
|
+
const exprError = validateExpression(c.expression);
|
|
32391
|
+
if (exprError) errors.push(`${at}.expression: ${exprError}`);
|
|
32392
|
+
}
|
|
32393
|
+
break;
|
|
32394
|
+
}
|
|
32395
|
+
case "projection": {
|
|
32396
|
+
if (!Array.isArray(c.path) || c.path.length === 0) {
|
|
32397
|
+
errors.push(
|
|
32398
|
+
`${at}.path is required on a projection column and must be a non-empty array of { edge, direction } steps`
|
|
32399
|
+
);
|
|
32400
|
+
} else {
|
|
32401
|
+
let stepShapeOk = true;
|
|
32402
|
+
for (let s = 0; s < c.path.length; s++) {
|
|
32403
|
+
const step = c.path[s];
|
|
32404
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
32405
|
+
errors.push(`${at}.path[${s}] must be an object { edge, direction }`);
|
|
32406
|
+
stepShapeOk = false;
|
|
32407
|
+
continue;
|
|
32408
|
+
}
|
|
32409
|
+
const st = step;
|
|
32410
|
+
if (!st.edge || typeof st.edge !== "string") {
|
|
32411
|
+
errors.push(`${at}.path[${s}].edge is required and must be a string`);
|
|
32412
|
+
stepShapeOk = false;
|
|
32413
|
+
}
|
|
32414
|
+
if (st.direction !== "forward" && st.direction !== "reverse") {
|
|
32415
|
+
errors.push(
|
|
32416
|
+
`${at}.path[${s}].direction is required and must be "forward" or "reverse" (never inferred)`
|
|
32417
|
+
);
|
|
32418
|
+
stepShapeOk = false;
|
|
32419
|
+
}
|
|
32420
|
+
}
|
|
32421
|
+
if (stepShapeOk && typeof spine === "string") {
|
|
32422
|
+
const resolved = resolveRelationalPath(spine, c.path);
|
|
32423
|
+
if (resolved.reason) {
|
|
32424
|
+
errors.push(`${at}.path does not compose from spine "${spine}": ${resolved.reason}`);
|
|
32425
|
+
}
|
|
32426
|
+
}
|
|
32427
|
+
}
|
|
32428
|
+
if (!Array.isArray(c.fields) || c.fields.length === 0) {
|
|
32429
|
+
errors.push(`${at}.fields is required on a projection column and must be a non-empty array of property keys`);
|
|
32430
|
+
}
|
|
32431
|
+
if (c.limit !== void 0 && (typeof c.limit !== "number" || !Number.isInteger(c.limit) || c.limit < 1)) {
|
|
32432
|
+
errors.push(`${at}.limit must be a positive integer when present (it truncates rendering, never membership)`);
|
|
32433
|
+
}
|
|
32434
|
+
break;
|
|
32435
|
+
}
|
|
32436
|
+
default:
|
|
32437
|
+
errors.push(`${at}.kind must be one of: field, computed, projection. Got "${String(c.kind)}"`);
|
|
32438
|
+
}
|
|
32439
|
+
}
|
|
32440
|
+
const sort = rel.sort;
|
|
32441
|
+
if (!sort || typeof sort !== "object" || Array.isArray(sort)) {
|
|
32442
|
+
errors.push('"relational.sort" is required and must be an object { column, direction }');
|
|
32443
|
+
} else {
|
|
32444
|
+
const s = sort;
|
|
32445
|
+
if (!s.column || typeof s.column !== "string") {
|
|
32446
|
+
errors.push('"relational.sort.column" is required and must be a string (a column id)');
|
|
32447
|
+
} else if (seenIds.size > 0 && !seenIds.has(s.column)) {
|
|
32448
|
+
errors.push(
|
|
32449
|
+
`"relational.sort.column" is "${s.column}", which is not a declared column id. Declared ids: ${[...seenIds].join(", ")}`
|
|
32450
|
+
);
|
|
32451
|
+
}
|
|
32452
|
+
if (s.direction !== "asc" && s.direction !== "desc") {
|
|
32453
|
+
errors.push('"relational.sort.direction" is required and must be "asc" or "desc"');
|
|
32454
|
+
}
|
|
32455
|
+
}
|
|
32456
|
+
return errors;
|
|
32457
|
+
}
|
|
30723
32458
|
|
|
30724
32459
|
// src/format/canonical.ts
|
|
30725
32460
|
import * as nodeCrypto from "crypto";
|
|
@@ -30810,9 +32545,12 @@ var NODE_KEY_ORDER = [
|
|
|
30810
32545
|
"title",
|
|
30811
32546
|
"slug",
|
|
30812
32547
|
"aliases",
|
|
32548
|
+
"key",
|
|
30813
32549
|
"description",
|
|
30814
32550
|
"tags",
|
|
30815
32551
|
"status",
|
|
32552
|
+
"archived",
|
|
32553
|
+
"archived_at",
|
|
30816
32554
|
"lifecycle_status",
|
|
30817
32555
|
"source_id",
|
|
30818
32556
|
"source_type",
|
|
@@ -30850,12 +32588,13 @@ function canonicalProduct(product) {
|
|
|
30850
32588
|
function effectiveRootProduct(doc) {
|
|
30851
32589
|
const node = doc.nodes?.find((n) => n.type === "product" && n.id === doc.product.id);
|
|
30852
32590
|
if (!node) return doc.product;
|
|
32591
|
+
const propStage = node.properties?.stage;
|
|
32592
|
+
const nodeStage = (typeof propStage === "string" ? propStage : void 0) ?? node.status;
|
|
30853
32593
|
return {
|
|
30854
32594
|
...doc.product,
|
|
30855
32595
|
title: node.title ?? doc.product.title,
|
|
30856
32596
|
description: node.description ?? doc.product.description,
|
|
30857
|
-
|
|
30858
|
-
stage: node.status ?? doc.product.stage
|
|
32597
|
+
stage: nodeStage ?? doc.product.stage
|
|
30859
32598
|
};
|
|
30860
32599
|
}
|
|
30861
32600
|
function singleBody(doc) {
|
|
@@ -31101,7 +32840,7 @@ function checkHeaderSealText(text) {
|
|
|
31101
32840
|
}
|
|
31102
32841
|
|
|
31103
32842
|
// src/index.ts
|
|
31104
|
-
var UPG_VERSION = "0.
|
|
32843
|
+
var UPG_VERSION = "0.32.0";
|
|
31105
32844
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
31106
32845
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
31107
32846
|
var UPG_TYPES = getTypes();
|
|
@@ -31276,6 +33015,7 @@ export {
|
|
|
31276
33015
|
USERS_NEEDS_PLAYBOOK,
|
|
31277
33016
|
UnknownEntityTypeError,
|
|
31278
33017
|
VARIES_BY_EDGE,
|
|
33018
|
+
WORKSPACE_ARRANGEMENT_EDGE_PROPERTY_SCHEMA,
|
|
31279
33019
|
backfillSlug,
|
|
31280
33020
|
buildTypeAliases,
|
|
31281
33021
|
canBeChildOf,
|
|
@@ -31391,14 +33131,17 @@ export {
|
|
|
31391
33131
|
normalizeDocument,
|
|
31392
33132
|
parseUpg,
|
|
31393
33133
|
pickCanonicalEdge,
|
|
33134
|
+
predicatesProvablyDisjoint,
|
|
31394
33135
|
presenceExceptKey,
|
|
31395
33136
|
projectGraph,
|
|
31396
33137
|
readActiveWhen,
|
|
31397
33138
|
readStringArray,
|
|
33139
|
+
relationalCoveredEntityTypes,
|
|
31398
33140
|
resolveAllEdges,
|
|
31399
33141
|
resolveContainmentEdge,
|
|
31400
33142
|
resolveEntityType,
|
|
31401
33143
|
resolveLabel,
|
|
33144
|
+
resolveRelationalPath,
|
|
31402
33145
|
resolveSlugCollision,
|
|
31403
33146
|
resolveTreePatternEdges,
|
|
31404
33147
|
ringOrderedDomainIds,
|