From 8a7987b9c3829d1e1051469d1314d353c44ca513 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 15 Nov 2025 07:09:56 +0200 Subject: [PATCH] ... --- examples/hero/heromodels/prd.vsh | 93 +++++++++ lib/hero/heromodels/factory.v | 4 + lib/hero/heromodels/prd.v | 326 ++++++++++++++++++++++++++++++- lib/hero/heromodels/prd_test.v | 226 +++++++++++++++++++++ 4 files changed, 645 insertions(+), 4 deletions(-) create mode 100644 examples/hero/heromodels/prd.vsh create mode 100644 lib/hero/heromodels/prd_test.v diff --git a/examples/hero/heromodels/prd.vsh b/examples/hero/heromodels/prd.vsh new file mode 100644 index 00000000..b797011a --- /dev/null +++ b/examples/hero/heromodels/prd.vsh @@ -0,0 +1,93 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import incubaid.herolib.hero.heromodels + +// Initialize database +mut mydb := heromodels.new()! + +// Create goals +mut goals := [ + heromodels.Goal{ + id: 'G1' + title: 'Faster Requirements' + description: 'Reduce PRD creation time to under 1 day' + gtype: .product + } +] + +// Create use cases +mut use_cases := [ + heromodels.UseCase{ + id: 'UC1' + title: 'Generate PRD' + actor: 'Product Manager' + goal: 'Create validated PRD' + steps: ['Select template', 'Fill fields', 'Export to Markdown'] + success: 'Complete PRD generated' + failure: 'Validation failed' + } +] + +// Create requirements +mut criterion := heromodels.AcceptanceCriterion{ + id: 'AC1' + description: 'Display template list' + condition: 'List contains >= 5 templates' +} + +mut requirements := [ + heromodels.Requirement{ + id: 'R1' + category: 'Editor' + title: 'Template Selection' + rtype: .functional + description: 'User can select from templates' + priority: .high + criteria: [criterion] + dependencies: [] + } +] + +// Create constraints +mut constraints := [ + heromodels.Constraint{ + id: 'C1' + title: 'ARM64 Support' + description: 'Must run on ARM64 infrastructure' + ctype: .technica + } +] + +// Create risks +mut risks := map[string]string{} +risks['RISK1'] = 'Templates too limited → Add community contributions' +risks['RISK2'] = 'AI suggestions inaccurate → Add review workflow' + +// Create a new PRD object +mut prd := mydb.prd.new( + product_name: 'Lumina PRD Builder' + version: 'v1.0' + overview: 'Tool to create structured PRDs quickly' + vision: 'Enable teams to generate clear requirements in minutes' + goals: goals + use_cases: use_cases + requirements: requirements + constraints: constraints + risks: risks +)! + +// Save to database +prd = mydb.prd.set(prd)! +println('✓ Created PRD with ID: ${prd.id}') + +// Retrieve from database +mut retrieved := mydb.prd.get(prd.id)! +println('✓ Retrieved PRD: ${retrieved.product_name}') + +// List all PRDs +mut all_prds := mydb.prd.list()! +println('✓ Total PRDs in database: ${all_prds.len}') + +// Check if exists +exists := mydb.prd.exist(prd.id)! +println('✓ PRD exists: ${exists}') \ No newline at end of file diff --git a/lib/hero/heromodels/factory.v b/lib/hero/heromodels/factory.v index 1272ba7e..1172e103 100644 --- a/lib/hero/heromodels/factory.v +++ b/lib/hero/heromodels/factory.v @@ -31,6 +31,7 @@ pub mut: registration_desk DBRegistrationDesk messages DBMessages tags DBTags + prd DBPrd rpc_handler &Handler } @@ -91,6 +92,9 @@ pub fn new(args NewArgs) !&ModelsFactory { tags: DBTags{ db: &mydb } + prd: DBPrd{ + db: &mydb + } rpc_handler: &h } diff --git a/lib/hero/heromodels/prd.v b/lib/hero/heromodels/prd.v index 5b39f173..05514de1 100644 --- a/lib/hero/heromodels/prd.v +++ b/lib/hero/heromodels/prd.v @@ -1,10 +1,19 @@ -module prd +module heromodels + +import incubaid.herolib.data.encoder +import incubaid.herolib.data.ourtime +import incubaid.herolib.hero.db +import incubaid.herolib.schemas.jsonrpc { Response, new_error, new_response, new_response_false, new_response_int, new_response_true } +import incubaid.herolib.hero.user { UserRef } +import json // Basic enums for clarity // Core PRD type, this is the root object +@[heap] pub struct ProductRequirementsDoc { -pub: + db.Base +pub mut: product_name string version string overview string @@ -13,9 +22,12 @@ pub: use_cases []UseCase requirements []Requirement constraints []Constraint - risks map[string]string // risk_id -> mitigation } +pub struct DBPrd { +pub mut: + db &db.DB @[skip; str: skip] +} pub enum PRDPriority { low @@ -87,7 +99,7 @@ pub enum ConstraintType { design } -pub struct constraint { +pub struct Constraint { pub: id string title string @@ -95,3 +107,309 @@ pub: ctype ConstraintType } +pub fn (self ProductRequirementsDoc) type_name() string { + return 'prd' +} + +pub fn (self ProductRequirementsDoc) description(methodname string) string { + match methodname { + 'set' { + return 'Create or update a product requirements document. Returns the ID of the PRD.' + } + 'get' { + return 'Retrieve a PRD by ID. Returns the complete PRD object.' + } + 'delete' { + return 'Delete a PRD by ID. Returns true if successful.' + } + 'exist' { + return 'Check if a PRD exists by ID. Returns true or false.' + } + 'list' { + return 'List all PRDs. Returns an array of PRD objects.' + } + else { + return 'Generic method for PRD operations.' + } + } +} + +pub fn (self ProductRequirementsDoc) example(methodname string) (string, string) { + match methodname { + 'set' { + return '{"product_name": "Test Product", "version": "v1.0", "overview": "A test product", "vision": "To test the system", "goals": [], "use_cases": [], "requirements": [], "constraints": []}', '1' + } + 'get' { + return '{"id": 1}', '{"product_name": "Test Product", "version": "v1.0", "overview": "A test product", "vision": "To test the system", "goals": [], "use_cases": [], "requirements": [], "constraints": []}' + } + 'delete' { + return '{"id": 1}', 'true' + } + 'exist' { + return '{"id": 1}', 'true' + } + 'list' { + return '{}', '[{"product_name": "Test Product", "version": "v1.0"}]' + } + else { + return '{}', '{}' + } + } +} + +pub fn (self ProductRequirementsDoc) dump(mut e encoder.Encoder) ! { + e.add_string(self.product_name) + e.add_string(self.version) + e.add_string(self.overview) + e.add_string(self.vision) + + // Encode goals array + e.add_u16(u16(self.goals.len)) + for goal in self.goals { + e.add_string(goal.id) + e.add_string(goal.title) + e.add_string(goal.description) + e.add_u8(u8(goal.gtype)) + } + + // Encode use_cases array + e.add_u16(u16(self.use_cases.len)) + for uc in self.use_cases { + e.add_string(uc.id) + e.add_string(uc.title) + e.add_string(uc.actor) + e.add_string(uc.goal) + e.add_list_string(uc.steps) + e.add_string(uc.success) + e.add_string(uc.failure) + } + + // Encode requirements array + e.add_u16(u16(self.requirements.len)) + for req in self.requirements { + e.add_string(req.id) + e.add_string(req.category) + e.add_string(req.title) + e.add_u8(u8(req.rtype)) + e.add_string(req.description) + e.add_u8(u8(req.priority)) + + // Encode acceptance criteria + e.add_u16(u16(req.criteria.len)) + for criterion in req.criteria { + e.add_string(criterion.id) + e.add_string(criterion.description) + e.add_string(criterion.condition) + } + + // Encode dependencies + e.add_list_string(req.dependencies) + } + + // Encode constraints array + e.add_u16(u16(self.constraints.len)) + for constraint in self.constraints { + e.add_string(constraint.id) + e.add_string(constraint.title) + e.add_string(constraint.description) + e.add_u8(u8(constraint.ctype)) + } +} + +pub fn (mut self DBPrd) load(mut o ProductRequirementsDoc, mut e encoder.Decoder) ! { + o.product_name = e.get_string()! + o.version = e.get_string()! + o.overview = e.get_string()! + o.vision = e.get_string()! + + // Decode goals + goals_len := e.get_u16()! + mut goals := []Goal{} + for _ in 0 .. goals_len { + goals << Goal{ + id: e.get_string()! + title: e.get_string()! + description: e.get_string()! + gtype: unsafe { GoalType(e.get_u8()!) } + } + } + o.goals = goals + + // Decode use_cases + use_cases_len := e.get_u16()! + mut use_cases := []UseCase{} + for _ in 0 .. use_cases_len { + use_cases << UseCase{ + id: e.get_string()! + title: e.get_string()! + actor: e.get_string()! + goal: e.get_string()! + steps: e.get_list_string()! + success: e.get_string()! + failure: e.get_string()! + } + } + o.use_cases = use_cases + + // Decode requirements + requirements_len := e.get_u16()! + mut requirements := []Requirement{} + for _ in 0 .. requirements_len { + req_id := e.get_string()! + req_category := e.get_string()! + req_title := e.get_string()! + req_rtype := unsafe { RequirementType(e.get_u8()!) } + req_description := e.get_string()! + req_priority := unsafe { PRDPriority(e.get_u8()!) } + + // Decode criteria + criteria_len := e.get_u16()! + mut criteria := []AcceptanceCriterion{} + for _ in 0 .. criteria_len { + criteria << AcceptanceCriterion{ + id: e.get_string()! + description: e.get_string()! + condition: e.get_string()! + } + } + + // Decode dependencies + dependencies := e.get_list_string()! + + requirements << Requirement{ + id: req_id + category: req_category + title: req_title + rtype: req_rtype + description: req_description + priority: req_priority + criteria: criteria + dependencies: dependencies + } + } + o.requirements = requirements + + // Decode constraints + constraints_len := e.get_u16()! + mut constraints := []Constraint{} + for _ in 0 .. constraints_len { + constraints << Constraint{ + id: e.get_string()! + title: e.get_string()! + description: e.get_string()! + ctype: unsafe { ConstraintType(e.get_u8()!) } + } + } + o.constraints = constraints +} + +@[params] +pub struct PrdArg { +pub mut: + id u32 + product_name string @[required] + version string + overview string + vision string + goals []Goal + use_cases []UseCase + requirements []Requirement + constraints []Constraint + securitypolicy u32 + tags []string +} + +pub fn (mut self DBPrd) new(args PrdArg) !ProductRequirementsDoc { + mut o := ProductRequirementsDoc{ + product_name: args.product_name + version: args.version + overview: args.overview + vision: args.vision + goals: args.goals + use_cases: args.use_cases + requirements: args.requirements + constraints: args.constraints + updated_at: ourtime.now().unix() + } + + o.securitypolicy = args.securitypolicy + o.tags = self.db.tags_get(args.tags)! + + return o +} + +pub fn (mut self DBPrd) set(o ProductRequirementsDoc) !ProductRequirementsDoc { + return self.db.set[ProductRequirementsDoc](o)! +} + +pub fn (mut self DBPrd) delete(id u32) !bool { + if !self.db.exists[ProductRequirementsDoc](id)! { + return false + } + self.db.delete[ProductRequirementsDoc](id)! + return true +} + +pub fn (mut self DBPrd) exist(id u32) !bool { + return self.db.exists[ProductRequirementsDoc](id)! +} + +pub fn (mut self DBPrd) get(id u32) !ProductRequirementsDoc { + mut o, data := self.db.get_data[ProductRequirementsDoc](id)! + mut e_decoder := encoder.decoder_new(data) + self.load(mut o, mut e_decoder)! + return o +} + +pub fn (mut self DBPrd) list() ![]ProductRequirementsDoc { + return self.db.list[ProductRequirementsDoc]()!.map(self.get(it)!) +} + +pub fn prd_handle(mut f ModelsFactory, rpcid int, servercontext map[string]string, userref UserRef, method string, params string) !Response { + match method { + 'get' { + id := db.decode_u32(params)! + res := f.prd.get(id)! + return new_response(rpcid, json.encode(res)) + } + 'set' { + mut args := db.decode_generic[PrdArg](params)! + mut o := f.prd.new(args)! + if args.id != 0 { + o.id = args.id + } + o = f.prd.set(o)! + return new_response_int(rpcid, int(o.id)) + } + 'delete' { + id := db.decode_u32(params)! + deleted := f.prd.delete(id)! + if deleted { + return new_response_true(rpcid) + } else { + return new_error(rpcid, + code: 404 + message: 'PRD with ID ${id} not found' + ) + } + } + 'exist' { + id := db.decode_u32(params)! + if f.prd.exist(id)! { + return new_response_true(rpcid) + } else { + return new_response_false(rpcid) + } + } + 'list' { + res := f.prd.list()! + return new_response(rpcid, json.encode(res)) + } + else { + return new_error(rpcid, + code: 32601 + message: 'Method ${method} not found on prd' + ) + } + } +} diff --git a/lib/hero/heromodels/prd_test.v b/lib/hero/heromodels/prd_test.v new file mode 100644 index 00000000..ebd47ebc --- /dev/null +++ b/lib/hero/heromodels/prd_test.v @@ -0,0 +1,226 @@ +module heromodels + +import incubaid.herolib.hero.db + +fn test_prd_new() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + mut args := PrdArg{ + product_name: 'Test Product' + version: 'v1.0' + overview: 'This is a test product.' + vision: 'To revolutionize testing.' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + + prd := db_prd.new(args)! + + assert prd.product_name == 'Test Product' + assert prd.version == 'v1.0' + assert prd.overview == 'This is a test product.' + assert prd.vision == 'To revolutionize testing.' + assert prd.goals.len == 0 + assert prd.use_cases.len == 0 + assert prd.requirements.len == 0 + assert prd.constraints.len == 0 + assert prd.risks.len == 0 + assert prd.updated_at > 0 + + println('✓ PRD new test passed!') +} + +fn test_prd_crud_operations() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + // Create a new PRD + mut args := PrdArg{ + product_name: 'CRUD Test Product' + version: 'v1.0' + overview: 'This is a test product for CRUD.' + vision: 'To test CRUD operations.' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + + mut prd := db_prd.new(args)! + prd = db_prd.set(prd)! + original_id := prd.id + + // Test get + retrieved_prd := db_prd.get(original_id)! + assert retrieved_prd.product_name == 'CRUD Test Product' + assert retrieved_prd.version == 'v1.0' + assert retrieved_prd.id == original_id + + // Test exist + exists := db_prd.exist(original_id)! + assert exists == true + + // Test delete + db_prd.delete(original_id)! + exists_after_delete := db_prd.exist(original_id)! + assert exists_after_delete == false + + println('✓ PRD CRUD operations test passed!') +} + +fn test_prd_encoding_decoding_complex() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + mut goal := Goal{ + id: 'G1' + title: 'Speed' + description: 'Generate PRDs in minutes' + gtype: .product + } + + mut use_case := UseCase{ + id: 'UC1' + title: 'Create PRD' + actor: 'Product Manager' + goal: 'Produce PRD quickly' + steps: ['Click new', 'Fill data', 'Export'] + success: 'Valid PRD generated' + failure: 'Missing fields' + } + + mut criterion := AcceptanceCriterion{ + id: 'AC1' + description: 'System displays template list' + condition: 'List contains >= 5 templates' + } + + mut requirement := Requirement{ + id: 'R1' + category: 'Editor' + title: 'Template Selection' + rtype: .functional + description: 'User can select from predefined templates' + priority: .high + criteria: [criterion] + dependencies: [] + } + + mut constraint := Constraint{ + id: 'C1' + title: 'ARM64 Only' + description: 'Must run on ARM64 servers' + ctype: .technica + } + + mut risks := map[string]string{} + risks['RISK1'] = 'Mitigation strategy here' + + mut args := PrdArg{ + product_name: 'Complex Test Product' + version: 'v2.0' + overview: 'Complete test with all fields' + vision: 'Full feature test' + goals: [goal] + use_cases: [use_case] + requirements: [requirement] + constraints: [constraint] + risks: risks + } + + mut prd := db_prd.new(args)! + prd = db_prd.set(prd)! + prd_id := prd.id + + // Retrieve and verify + retrieved_prd := db_prd.get(prd_id)! + + assert retrieved_prd.product_name == 'Complex Test Product' + assert retrieved_prd.goals.len == 1 + assert retrieved_prd.goals[0].id == 'G1' + assert retrieved_prd.goals[0].gtype == .product + + assert retrieved_prd.use_cases.len == 1 + assert retrieved_prd.use_cases[0].id == 'UC1' + assert retrieved_prd.use_cases[0].steps.len == 3 + + assert retrieved_prd.requirements.len == 1 + assert retrieved_prd.requirements[0].id == 'R1' + assert retrieved_prd.requirements[0].criteria.len == 1 + assert retrieved_prd.requirements[0].priority == .high + + assert retrieved_prd.constraints.len == 1 + assert retrieved_prd.constraints[0].id == 'C1' + assert retrieved_prd.constraints[0].ctype == .technica + + assert retrieved_prd.risks.len == 1 + assert retrieved_prd.risks['RISK1'] == 'Mitigation strategy here' + + println('✓ PRD encoding/decoding complex test passed!') +} + +fn test_prd_type_name() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + mut args := PrdArg{ + product_name: 'Type Name Test' + version: 'v1.0' + overview: 'Test' + vision: 'Test' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + + prd := db_prd.new(args)! + type_name := prd.type_name() + assert type_name == 'prd' + + println('✓ PRD type_name test passed!') +} + +fn test_prd_list() ! { + mut mydb := db.new_test()! + mut db_prd := DBPrd{ + db: &mydb + } + + // Create multiple PRDs + for i in 0 .. 3 { + mut args := PrdArg{ + product_name: 'Product ${i}' + version: 'v1.0' + overview: 'Overview ${i}' + vision: 'Vision ${i}' + goals: [] + use_cases: [] + requirements: [] + constraints: [] + risks: {} + } + mut prd := db_prd.new(args)! + prd = db_prd.set(prd)! + } + + // List all PRDs + all_prds := db_prd.list()! + assert all_prds.len == 3 + + println('✓ PRD list test passed!') +}