Skip to content

Commit

Permalink
Enforcing the Requirement Identification principle
Browse files Browse the repository at this point in the history
- Added reqId to entities
- Created Epic entity
- Goals -> Scenario page now lists Epics instead of User Stories
- Created /api/epic endpoints
- lifted the UserStory.outcome and UseCase.goalInContext fields to Scenario.outcome
- Added reqId labels to UI panels
- Added reqId column to data tables
- Context and Objective entity changed from Goal -> Outcome
- Removed the /api/goal endpoints
- Obstacles page merged into Situation page
  • Loading branch information
mlhaufe authored Nov 1, 2024
1 parent 08e63d1 commit 7fecb87
Show file tree
Hide file tree
Showing 115 changed files with 1,207 additions and 536 deletions.
4 changes: 1 addition & 3 deletions components/XDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const props = defineProps<{
const dataTable = ref<DataTable>(),
createDisabled = ref(false),
sortField = ref<string | undefined>('name'),
confirm = useConfirm(),
createDialog = ref<Dialog>(),
createDialogVisible = ref(false),
Expand Down Expand Up @@ -190,8 +189,7 @@ const onEditDialogCancel = () => {
</template>
</Toolbar>
<DataTable ref="dataTable" :value="props.datasource as unknown as any[]" dataKey="id" v-model:filters="filters"
:globalFilterFields="Object.keys(props.datasource?.[0] ?? {})" :sortField="sortField" :sortOrder="1"
:loading="props.loading" stripedRows>
:globalFilterFields="Object.keys(props.datasource?.[0] ?? {})" :loading="props.loading" stripedRows>
<Column
v-for="key of Object.keys(props.viewModel).filter(k => props.viewModel[k as keyof RowType] !== 'hidden')"
:key="key" :field="key" :header="camelCaseToTitle(key)" sortable>
Expand Down
6 changes: 6 additions & 0 deletions domain/relations/RequirementRelation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export abstract class RequirementRelation extends BaseEntity {
@Property({ type: 'uuid', primary: true })
id: string;

/**
* The left-hand side of the relation
*/
@ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] })
left: Requirement

/**
* The right-hand side of the relation
*/
@ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] })
right: Requirement
}
6 changes: 6 additions & 0 deletions domain/requirements/Assumption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const assumptionReqIdPrefix = 'E.4.' as const;
export type AssumptionReqId = `${typeof assumptionReqIdPrefix}${number}`;

/**
* Posited property of the environment
*/
Expand All @@ -12,4 +15,7 @@ export class Assumption extends Requirement {
super(props);
this.req_type = ReqType.ASSUMPTION;
}

override get reqId(): AssumptionReqId | undefined { return super.reqId as AssumptionReqId | undefined }
override set reqId(value: AssumptionReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { ConstraintCategory } from './ConstraintCategory.js';
import { type Properties } from '../types/index.js';
import { ReqType } from './ReqType.js';

export const constraintReqIdPrefix = 'E.3.' as const;
export type ConstraintReqId = `${typeof constraintReqIdPrefix}${number}`;

/**
* A Constraint is a property imposed by the environment
*/
Expand All @@ -15,6 +18,9 @@ export class Constraint extends Requirement {
this.req_type = ReqType.CONSTRAINT;
}

override get reqId(): ConstraintReqId | undefined { return super.reqId as ConstraintReqId | undefined }
override set reqId(value: ConstraintReqId | undefined) { super.reqId = value }

/**
* Category of the constraint
*/
Expand Down
6 changes: 6 additions & 0 deletions domain/requirements/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const effectReqIdPrefix = 'E.5.' as const;
export type EffectReqId = `${typeof effectReqIdPrefix}${number}`;

/**
* Environment property affected by the system
*/
Expand All @@ -12,4 +15,7 @@ export class Effect extends Requirement {
super(props);
this.req_type = ReqType.EFFECT;
}

override get reqId(): EffectReqId | undefined { return super.reqId as EffectReqId | undefined }
override set reqId(value: EffectReqId | undefined) { super.reqId = value }
}
8 changes: 7 additions & 1 deletion domain/requirements/EnvironmentComponent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Entity, ManyToOne } from "@mikro-orm/core";
import { Entity } from "@mikro-orm/core";
import { Component } from "./Component.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const environmentComponentReqIdPrefix = 'E.2.' as const;
export type EnvironmentComponentReqId = `${typeof environmentComponentReqIdPrefix}${number}`;

/**
* Represents a component that is part of an environment.
*/
Expand All @@ -12,4 +15,7 @@ export class EnvironmentComponent extends Component {
super(props);
this.req_type = ReqType.ENVIRONMENT_COMPONENT;
}

override get reqId(): EnvironmentComponentReqId | undefined { return super.reqId as EnvironmentComponentReqId | undefined }
override set reqId(value: EnvironmentComponentReqId | undefined) { super.reqId = value }
}
21 changes: 21 additions & 0 deletions domain/requirements/Epic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Entity } from "@mikro-orm/core";
import { Scenario } from "./Scenario.js";
import { ReqType } from "./ReqType.js";

export const epicReqIdPrefix = 'G.5.' as const;
export type EpicReqId = `${typeof epicReqIdPrefix}${number}`;

/**
* An Epic is a collection of Use Cases and User Stories all directed towards a common goal.
* Ex: "decrease the percentage of of fraudulent sellers by 20%"
*/
@Entity({ discriminatorValue: ReqType.EPIC })
export class Epic extends Scenario {
constructor(props: Omit<Epic, 'id' | 'req_type'>) {
super(props);
this.req_type = ReqType.EPIC;
}

override get reqId(): EpicReqId | undefined { return super.reqId as EpicReqId | undefined }
override set reqId(value: EpicReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/FunctionalBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Functionality } from "./Functionality.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const functionalBehaviorReqIdPrefix = 'S.2.' as const;
export type FunctionalBehaviorReqId = `${typeof functionalBehaviorReqIdPrefix}${number}`;

/**
* FunctionalBehavior specifies **what** behavior the system should exhibit, i.e.,
* the results or effects of the system's operation.
Expand All @@ -14,4 +17,7 @@ export class FunctionalBehavior extends Functionality {
super(props);
this.req_type = ReqType.FUNCTIONAL_BEHAVIOR;
}

override get reqId(): FunctionalBehaviorReqId | undefined { return super.reqId as FunctionalBehaviorReqId | undefined }
override set reqId(value: FunctionalBehaviorReqId | undefined) { super.reqId = value }
}
8 changes: 7 additions & 1 deletion domain/requirements/GlossaryTerm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Entity, ManyToOne } from "@mikro-orm/core";
import { Entity } from "@mikro-orm/core";
import { Component } from "./Component.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const glossaryTermReqIdPrefix = 'E.1.' as const;
export type GlossaryTermReqId = `${typeof glossaryTermReqIdPrefix}${number}`;

/**
* A word or phrase that is part of a glossary. Provides a definition for the term
*/
Expand All @@ -12,4 +15,7 @@ export class GlossaryTerm extends Component {
super(props);
this.req_type = ReqType.GLOSSARY_TERM;
}

override get reqId(): GlossaryTermReqId | undefined { return super.reqId as GlossaryTermReqId | undefined }
override set reqId(value: GlossaryTermReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Invariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const invariantReqIdPrefix = 'E.6.' as const;
export type InvariantReqId = `${typeof invariantReqIdPrefix}${number}`;

/**
* Environment property that must be maintained.
* It exists as both an assumption and an effect.
Expand All @@ -14,4 +17,7 @@ export class Invariant extends Requirement {
super(props);
this.req_type = ReqType.INVARIANT;
}

override get reqId(): InvariantReqId | undefined { return super.reqId as InvariantReqId | undefined }
override set reqId(value: InvariantReqId | undefined) { super.reqId = value }
}
3 changes: 2 additions & 1 deletion domain/requirements/Justification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

/**
* Explanation of a project or system property in reference to a goal or environment property
* Explanation of a project or system property in reference to a goal or environment property.
* A requirement is justified if it helps to achieve a goal or to satisfy an environment property (constraint).
*/
@Entity({ discriminatorValue: ReqType.JUSTIFICATION })
export class Justification extends MetaRequirement {
Expand Down
6 changes: 6 additions & 0 deletions domain/requirements/Limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const limitReqIdPrefix = 'G.6.' as const;
export type LimitReqId = `${typeof limitReqIdPrefix}${number}`;

/**
* An Exclusion from the scope of requirements
*/
Expand All @@ -12,4 +15,7 @@ export class Limit extends Requirement {
super(props);
this.req_type = ReqType.LIMIT;
}

override get reqId(): LimitReqId | undefined { return super.reqId as LimitReqId | undefined }
override set reqId(value: LimitReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/NonFunctionalBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Functionality } from "./Functionality.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const nonFunctionalBehaviorReqIdPrefix = 'S.2.' as const;
export type NonFunctionalBehaviorReqId = `${typeof nonFunctionalBehaviorReqIdPrefix}${number}`;

/**
* NonFunctionalBehavior is a type of Behavior that is not directly related to the functionality of a system.
* It specifies **how** the system should behave, i.e., the qualities that the system must exhibit.
Expand All @@ -14,4 +17,7 @@ export class NonFunctionalBehavior extends Functionality {
super(props);
this.req_type = ReqType.NON_FUNCTIONAL_BEHAVIOR;
}

override get reqId(): NonFunctionalBehaviorReqId | undefined { return super.reqId as NonFunctionalBehaviorReqId | undefined }
override set reqId(value: NonFunctionalBehaviorReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Obstacle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Goal } from "./Goal.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const obstacleReqIdPrefix = 'G.2.' as const;
export type ObstacleReqId = `${typeof obstacleReqIdPrefix}${number}`;

/**
* Obstacles are the challenges that prevent the goals from being achieved.
*/
Expand All @@ -12,4 +15,7 @@ export class Obstacle extends Goal {
super(props);
this.req_type = ReqType.OBSTACLE;
}

override get reqId(): ObstacleReqId | undefined { return super.reqId as ObstacleReqId | undefined }
override set reqId(value: ObstacleReqId | undefined) { super.reqId = value }
}
7 changes: 7 additions & 0 deletions domain/requirements/Outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Goal } from "./Goal.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

// FIXME: The Context and overall objective entry is an Outcome, but the req_id is G.1.0
export const outcomeReqIdPrefix = 'G.3.' as const;
export type OutcomeReqId = `${typeof outcomeReqIdPrefix}${number}`;

/**
* A result desired by an organization
*/
Expand All @@ -12,4 +16,7 @@ export class Outcome extends Goal {
super(props);
this.req_type = ReqType.OUTCOME;
}

override get reqId(): OutcomeReqId | undefined { return super.reqId as OutcomeReqId | undefined }
override set reqId(value: OutcomeReqId | undefined) { super.reqId = value }
}
6 changes: 6 additions & 0 deletions domain/requirements/Person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Actor } from "./Actor.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const personReqIdPrefix = 'P.1.' as const;
export type PersonReqId = `${typeof personReqIdPrefix}${number}`;

/**
* A person is a member of the Project staff
*/
Expand All @@ -14,6 +17,9 @@ export class Person extends Actor {
this.email = email;
}

override get reqId(): PersonReqId | undefined { return super.reqId as PersonReqId | undefined }
override set reqId(value: PersonReqId | undefined) { super.reqId = value }

/**
* Email address of the person
*/
Expand Down
1 change: 1 addition & 0 deletions domain/requirements/ReqType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ReqType {
CONSTRAINT = 'constraint',
EFFECT = 'effect',
ENVIRONMENT_COMPONENT = 'environment_component',
EPIC = 'epic',
EXAMPLE = 'example',
FUNCTIONAL_BEHAVIOR = 'functional_behavior',
FUNCTIONALITY = 'functionality',
Expand Down
13 changes: 13 additions & 0 deletions domain/requirements/Requirement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { type Properties } from '../types/index.js';
import { ReqType } from './ReqType.js';
import { AppUser } from '../application/AppUser.js';

export type ReqIdPrefix = `${'P' | 'E' | 'G' | 'S'}.${number}.`
export type ReqId = `${ReqIdPrefix}${number}`

/**
* A Requirement is a statement that specifies a property.
*/
Expand Down Expand Up @@ -33,6 +36,16 @@ export abstract class Requirement extends BaseEntity {
@Property({ type: 'uuid', primary: true })
id: string;

private _reqId?: ReqId

/**
* The user-friendly identifier of the requirement that is unique within its parent
*/
// This is nullable because MetaRequirements, Silence, and Noise do not have a reqId
@Property({ type: 'text', nullable: true })
get reqId(): ReqId | undefined { return this._reqId }
set reqId(value: ReqId | undefined) { this._reqId = value }

// A property is a Predicate formalizing its associated statement.
// see: https://github.com/final-hill/cathedral/issues/368
// property!: string
Expand Down
10 changes: 9 additions & 1 deletion domain/requirements/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { Example } from "./Example.js";
import { Stakeholder } from "./Stakeholder.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";
import { Outcome } from "./Outcome.js";

/**
* A Scenario specifies system behavior by describing paths
* of interaction between actors and the system.
*/
@Entity({ abstract: true, discriminatorValue: ReqType.SCENARIO })
export abstract class Scenario extends Example {
constructor({ primaryActor, ...rest }: Properties<Omit<Scenario, 'id' | 'req_type'>>) {
constructor({ primaryActor, outcome, ...rest }: Properties<Omit<Scenario, 'id' | 'req_type'>>) {
super(rest);
this.primaryActor = primaryActor;
this.outcome = outcome;
this.req_type = ReqType.SCENARIO;
}

Expand All @@ -21,4 +23,10 @@ export abstract class Scenario extends Example {
*/
@ManyToOne({ entity: () => Stakeholder })
primaryActor?: Stakeholder;

/**
* The outcome (goal) that the scenario is aiming to achieve.
*/
@ManyToOne({ entity: () => Outcome })
outcome?: Outcome;
}
8 changes: 7 additions & 1 deletion domain/requirements/Stakeholder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Entity, Enum, ManyToOne, Property } from "@mikro-orm/core";
import { Entity, Enum, Property } from "@mikro-orm/core";
import { Component } from "./Component.js";
import { StakeholderCategory } from "./StakeholderCategory.js";
import { StakeholderSegmentation } from "./StakeholderSegmentation.js";
import { type Properties } from "../types/index.js";
import { ReqType } from "./ReqType.js";

export const stakeholderReqIdPrefix = 'G.7.' as const;
export type StakeholderReqId = `${typeof stakeholderReqIdPrefix}${number}`;

/**
* A human actor who may affect or be affected by a project or its associated system
*/
Expand All @@ -19,6 +22,9 @@ export class Stakeholder extends Component {
this.category = props.category;
}

override get reqId(): StakeholderReqId | undefined { return super.reqId as StakeholderReqId | undefined }
override set reqId(value: StakeholderReqId | undefined) { super.reqId = value }

/**
* The segmentation of the stakeholder.
*/
Expand Down
Loading

0 comments on commit 7fecb87

Please sign in to comment.