diff --git a/.gitignore b/.gitignore index 6f8795f..b4be20d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ docs/superpowers/plans/ docs/superpowers/specs/ docs/copilot/plans/ docs/copilot/specs/ +bruno/docs/diagrams/*.png diff --git a/bruno/SIS/logging.js b/bruno/SIS/logging.js index c6d7583..d1b8c0e 100644 --- a/bruno/SIS/logging.js +++ b/bruno/SIS/logging.js @@ -479,6 +479,17 @@ const logSpecStudentEdOrgAssociation = { raceDescriptors: r => (r?.races || []).map(rc => extractDescriptor(rc.raceDescriptor)), }; +// StudentEdOrgResponsibilityAssoc spec map (StudentEnrollment > StudentEdOrgResponsibilityAssocs) +// Primary keys: beginDate, educationOrganizationId, responsibilityDescriptor, studentUniqueId +// Include identifiers and mutated field (responsibilityDescriptor) plus endDate. +const logSpecStudentEdOrgResponsibilityAssoc = { + beginDate: r => r?.beginDate, + responsibilityDescriptor: r => extractDescriptor(r?.responsibilityDescriptor), + educationOrganizationId: r => r?.educationOrganizationReference?.educationOrganizationId, + studentUniqueId: r => r?.studentReference?.studentUniqueId, + endDate: r => r?.endDate, +}; + // StudentSchoolAssociation spec map (StudentEnrollment > StudentSchoolAssociations) // Primary keys per config: schoolId, studentUniqueId, entryDate (naturalIdField null) // Include required descriptors & mutated-capable fields (entryDate, exitWithdrawDate, exitWithdrawTypeDescriptor, graduationPlanReference constituents, calendarReference.calendarCode) @@ -938,6 +949,7 @@ module.exports = { ,logSpecStudent ,logSpecGraduationPlan ,logSpecStudentEdOrgAssociation + ,logSpecStudentEdOrgResponsibilityAssoc ,logSpecStudentSchoolAssociation ,logSpecStudentSectionAssociation ,logSpecStudentCTEProgramAssociation diff --git a/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/01 - 1st StudentEdOrgResponsibilityAssoc is valid.bru b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/01 - 1st StudentEdOrgResponsibilityAssoc is valid.bru new file mode 100644 index 0000000..7cd095f --- /dev/null +++ b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/01 - 1st StudentEdOrgResponsibilityAssoc is valid.bru @@ -0,0 +1,73 @@ +meta { + name: 01 - 1st StudentEdOrgResponsibilityAssoc is valid + type: http + seq: 1 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations?beginDate=[ENTER FIRST BEGIN DATE YYYY-MM-DD]&educationOrganizationId=[ENTER FIRST EDUCATION ORGANIZATION ID]&studentUniqueId=[ENTER FIRST STUDENT UNIQUE ID]&responsibilityDescriptor={{firstResponsibilityDescriptorEncoded}}&responsibilityDescriptor_KEEP_IT_AT_THE_END=[ENTER FIRST RESPONSIBILITY DESCRIPTOR] + body: none + auth: inherit +} + +params:query { + beginDate: [ENTER FIRST BEGIN DATE YYYY-MM-DD] + educationOrganizationId: [ENTER FIRST EDUCATION ORGANIZATION ID] + studentUniqueId: [ENTER FIRST STUDENT UNIQUE ID] + responsibilityDescriptor: {{firstResponsibilityDescriptorEncoded}} + responsibilityDescriptor_KEEP_IT_AT_THE_END: [ENTER FIRST RESPONSIBILITY DESCRIPTOR] +} + +assert { + res.status: eq 200 + res.body: isArray + res.body: isNotEmpty + res.body[0].id: isString + res.body[0].id: isNotEmpty + res.body[0].beginDate: isString + res.body[0].beginDate: isNotEmpty + res.body[0].responsibilityDescriptor: isString + res.body[0].responsibilityDescriptor: isNotEmpty + res.body[0].educationOrganizationReference: isDefined + res.body[0].educationOrganizationReference.educationOrganizationId: isNumber + res.body[0].educationOrganizationReference.educationOrganizationId: neq 0 + res.body[0].studentReference: isDefined + res.body[0].studentReference.studentUniqueId: isString + res.body[0].studentReference.studentUniqueId: isNotEmpty + res.body[0].endDate: isString + res.body[0].endDate: isNotEmpty +} + +script:pre-request { + const { encodeDescriptorParameter, setVar } = require('./utils'); + const responsibilityDescriptorEncoded = encodeDescriptorParameter(req.url, 'responsibilityDescriptor_KEEP_IT_AT_THE_END'); + + setVar(bru, 'firstResponsibilityDescriptorEncoded', responsibilityDescriptorEncoded); +} + +script:post-response { + const { pickSingle, setVars, wipeVars, extractDescriptor } = require('./utils'); + const { logSpecStudentEdOrgResponsibilityAssoc, logScenario } = require('./logging'); + const entityName = 'StudentEdOrgResponsibilityAssoc'; + const scenarioName = this.req.name; + const single = pickSingle(res.getBody()); + + if (!single) { + wipeVars(bru, [ + 'firstStudentEdOrgResponsibilityAssocUniqueId', + 'firstStudentEdOrgResponsibilityAssocResponsibilityDescriptor' + ], entityName, true); + } + + setVars(bru, { + firstStudentEdOrgResponsibilityAssocUniqueId: single.id, + firstStudentEdOrgResponsibilityAssocResponsibilityDescriptor: extractDescriptor(single.responsibilityDescriptor) + }, entityName); + + logScenario(entityName, scenarioName, single, logSpecStudentEdOrgResponsibilityAssoc); +} + +settings { + encodeUrl: false + timeout: 0 +} diff --git a/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/02 - 2nd StudentEdOrgResponsibilityAssoc is valid.bru b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/02 - 2nd StudentEdOrgResponsibilityAssoc is valid.bru new file mode 100644 index 0000000..13087d6 --- /dev/null +++ b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/02 - 2nd StudentEdOrgResponsibilityAssoc is valid.bru @@ -0,0 +1,73 @@ +meta { + name: 02 - 2nd StudentEdOrgResponsibilityAssoc is valid + type: http + seq: 2 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations?beginDate=[ENTER SECOND BEGIN DATE YYYY-MM-DD]&educationOrganizationId=[ENTER SECOND EDUCATION ORGANIZATION ID]&responsibilityDescriptor={{secondResponsibilityDescriptorEncoded}}&studentUniqueId=[ENTER SECOND STUDENT UNIQUE ID]&responsibilityDescriptor_KEEP_IT_AT_THE_END=[ENTER SECOND RESPONSIBILITY DESCRIPTOR] + body: none + auth: inherit +} + +params:query { + beginDate: [ENTER SECOND BEGIN DATE YYYY-MM-DD] + educationOrganizationId: [ENTER SECOND EDUCATION ORGANIZATION ID] + responsibilityDescriptor: {{secondResponsibilityDescriptorEncoded}} + studentUniqueId: [ENTER SECOND STUDENT UNIQUE ID] + responsibilityDescriptor_KEEP_IT_AT_THE_END: [ENTER SECOND RESPONSIBILITY DESCRIPTOR] +} + +assert { + res.status: eq 200 + res.body: isArray + res.body: isNotEmpty + res.body[0].id: isString + res.body[0].id: isNotEmpty + res.body[0].beginDate: isString + res.body[0].beginDate: isNotEmpty + res.body[0].responsibilityDescriptor: isString + res.body[0].responsibilityDescriptor: isNotEmpty + res.body[0].educationOrganizationReference: isDefined + res.body[0].educationOrganizationReference.educationOrganizationId: isNumber + res.body[0].educationOrganizationReference.educationOrganizationId: neq 0 + res.body[0].studentReference: isDefined + res.body[0].studentReference.studentUniqueId: isString + res.body[0].studentReference.studentUniqueId: isNotEmpty + res.body[0].endDate: isString + res.body[0].endDate: isNotEmpty +} + +script:pre-request { + const { encodeDescriptorParameter, setVar } = require('./utils'); + const responsibilityDescriptorEncoded = encodeDescriptorParameter(req.url, 'responsibilityDescriptor_KEEP_IT_AT_THE_END'); + + setVar(bru, 'secondResponsibilityDescriptorEncoded', responsibilityDescriptorEncoded); +} + +script:post-response { + const { pickSingle, setVars, wipeVars } = require('./utils'); + const { logSpecStudentEdOrgResponsibilityAssoc, logScenario } = require('./logging'); + const entityName = 'StudentEdOrgResponsibilityAssoc'; + const scenarioName = this.req.name; + const single = pickSingle(res.getBody()); + + if (!single) { + wipeVars(bru, [ + 'secondStudentEdOrgResponsibilityAssocUniqueId', + 'secondStudentEdOrgResponsibilityAssocEndDate' + ], entityName, true); + } + + setVars(bru, { + secondStudentEdOrgResponsibilityAssocUniqueId: single.id, + secondStudentEdOrgResponsibilityAssocEndDate: single.endDate + }, entityName); + + logScenario(entityName, scenarioName, single, logSpecStudentEdOrgResponsibilityAssoc); +} + +settings { + encodeUrl: false + timeout: 0 +} diff --git a/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/03 - 2nd StudentEdOrgResponsibilityAssoc - endDate was Updated.bru b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/03 - 2nd StudentEdOrgResponsibilityAssoc - endDate was Updated.bru new file mode 100644 index 0000000..bf06cca --- /dev/null +++ b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/03 - 2nd StudentEdOrgResponsibilityAssoc - endDate was Updated.bru @@ -0,0 +1,65 @@ +meta { + name: 03 - 2nd StudentEdOrgResponsibilityAssoc - endDate was Updated + type: http + seq: 3 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations/{{secondStudentEdOrgResponsibilityAssocUniqueId}} + body: none + auth: inherit +} + +assert { + res.status: eq 200 + res.body: isDefined + res.body.id: isString + res.body.id: isNotEmpty + res.body.beginDate: isString + res.body.beginDate: isNotEmpty + res.body.responsibilityDescriptor: isString + res.body.responsibilityDescriptor: isNotEmpty + res.body.educationOrganizationReference: isDefined + res.body.educationOrganizationReference.educationOrganizationId: isNumber + res.body.educationOrganizationReference.educationOrganizationId: neq 0 + res.body.studentReference: isDefined + res.body.studentReference.studentUniqueId: isString + res.body.studentReference.studentUniqueId: isNotEmpty + res.body.endDate: isString + res.body.endDate: isNotEmpty +} + +script:pre-request { + const { validateDependency } = require('./utils'); + + validateDependency(bru, 'secondStudentEdOrgResponsibilityAssocUniqueId', '02 - 2nd StudentEdOrgResponsibilityAssoc is valid', { actionHint: 'Ensure you ran the second certification scenario successfully before continuing.' }); + + validateDependency(bru, 'secondStudentEdOrgResponsibilityAssocEndDate', '02 - 2nd StudentEdOrgResponsibilityAssoc is valid', { actionHint: 'Ensure you ran the second certification scenario successfully before continuing.' }); +} + +script:post-response { + const { getVar, expectChanged, throwNotFoundOrSpecificError } = require('./utils'); + const { logSpecStudentEdOrgResponsibilityAssoc, logScenario } = require('./logging'); + const entityName = 'StudentEdOrgResponsibilityAssoc'; + const scenarioName = this.req.name; + + if (res.status !== 200 || !res.body) { + throwNotFoundOrSpecificError(entityName); + } + + const current = res.getBody(); + const previousEndDate = getVar(bru, 'secondStudentEdOrgResponsibilityAssocEndDate'); + + expectChanged(previousEndDate, current.endDate, 'endDate'); + + logScenario(entityName, scenarioName, current, logSpecStudentEdOrgResponsibilityAssoc, [ + 'endDate' + ]); +} + +settings { + encodeUrl: false + timeout: 0 + followRedirects: true + maxRedirects: 5 +} diff --git a/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/entity.config.json b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/entity.config.json new file mode 100644 index 0000000..54a4948 --- /dev/null +++ b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/entity.config.json @@ -0,0 +1,14 @@ +{ + "$schema": "../../../../schemas/entity-config.schema.json", + "version": 1, + "identity": { + "primaryKeyFields": ["beginDate", "educationOrganizationId", "responsibilityDescriptor", "studentUniqueId"], + "naturalIdField": null + }, + "overrides": { + "entityName": "StudentEdOrgResponsibilityAssoc", + "endpointSegment": "studentEducationOrganizationResponsibilityAssociations", + "loggingSpecName": "StudentEdOrgResponsibilityAssoc", + "fileNameAlias": "StudentEdOrgResponsibilityAssoc" + } +} diff --git a/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/folder.bru b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/folder.bru new file mode 100644 index 0000000..5ffc0ee --- /dev/null +++ b/bruno/SIS/v5/StudentEnrollment/StudentEdOrgResponsibilityAssocs/folder.bru @@ -0,0 +1,65 @@ +meta { + name: Student Education Organization Responsibility Associations +} + +auth { + mode: inherit +} + +docs { + # v5 Student Enrollment > StudentEdOrgResponsibilityAssociation Scenarios + + The StudentEducationOrganizationResponsibilityAssociation (SEORA) indicates a relationship between a student and an education organization other than an enrollment relationship, and generally indicates some kind of accountability or responsibility of the education organization for the student. The kind of responsibility is specified in the Responsibility descriptor value according to policy. + + This association represents student information that is specific to a student's relationship with an EducationOrganization. Enrollment relationship semantics are covered by [StudentSchoolAssociation. Domain: Enrollment](https://docs.ed-fi.org/reference/data-exchange/data-standard/model-reference/enrollment-domain/best-practices/#best-practices-related-to-the-studenteducationorganizationresponsibilityassociation-seora) + + ## Prerequisites + + * Student + * School + * StudentSchoolAssociation (enrollment) + + ## Scenarios tasks + + 1. __CREATE__ a `first` `Student EdOrg Responsibility Association` for an `elementary Student` at Grand Bend Elementary School District. + 1. __CREATE__ a `second` `Student EdOrg Responsibility Association` for the same `elementary Student` at Grand Bend High School District. + 2. __UPDATE__ the _endDate_ on the second `Student EdOrg Responsibility Association`. + + ### Additional Notes + + * A SEORA is written when the responsibility for a student is a different education organization than the enrollment school in the SSA. + * The _BeginDate_ and _EndDate_ of the SEORA should be informed by the _EntryDate_ and _ExitWithdrawDate_ of a student’s SSA(s). There may be circumstances when they may be legitimately different. + + ## Scenarios example data + + | Resource | Property Name | Is Collection | Data Type | Required | Scenario 1: POST | Scenario 2: POST | Scenario 3: PUT | + | ------------------------------------------------------------------ | --------------------------------------- | ------------- | ------------------------------------- | ----------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | + | StudentEducationOrganizationResponsibilityAssociations | educationOrganizationReference | FALSE | educationOrganizationReference | REQUIRED | | | | + | educationOrganizationReference | educationOrganizationId | FALSE | integer | REQUIRED | 255901107 | 255901001 | 255901001 | + | StudentEducationOrganizationResponsibilityAssociations | studentReference | FALSE | studentReference | REQUIRED | | | | + | studentReference | studentUniqueId | FALSE | string | REQUIRED | 111111 | 111111 | 111111 | + | StudentEducationOrganizationResponsibilityAssociations | beginDate | FALSE | string | REQUIRED | [current school year]-08-23 | [current school year]-08-23 | [current school year]-08-23 | + | StudentEducationOrganizationResponsibilityAssociations | responsibilityDescriptor | FALSE | string | REQUIRED | Residency | Individualized Education Program | Individualized Education Program | + | StudentEducationOrganizationResponsibilityAssociations | endDate | FALSE | string | REQUIRED | [current school year]-06-01 | [current school year]-06-01 | [current school year]-10-01 | + + ## API response format + + ```json + { + "id": "string", + "beginDate": "2026-06-23", + "responsibilityDescriptor": "string", + "educationOrganizationReference": { + "educationOrganizationId": 0 + }, + "responsibleEducationOrganizationReference": { + "educationOrganizationId": 0 + }, + "studentReference": { + "studentUniqueId": "string" + }, + "endDate": "2026-06-23" + } + ``` + +} diff --git a/bruno/Sample Data/Descriptors/ResponsibilityDescriptors.bru b/bruno/Sample Data/Descriptors/ResponsibilityDescriptors.bru new file mode 100644 index 0000000..dd69e05 --- /dev/null +++ b/bruno/Sample Data/Descriptors/ResponsibilityDescriptors.bru @@ -0,0 +1,25 @@ +meta { + name: ResponsibilityDescriptors + type: http + seq: 27 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/responsibilityDescriptors + body: none + auth: bearer +} + +params:query { + ~description: Individualized Education Program + ~codeValue: Residency +} + +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 1 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 1 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..e03f13b --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 1 - StaffEdOrgAssociations.bru @@ -0,0 +1,39 @@ +meta { + name: Aux 1 - StaffEdOrgAssociations + type: http + seq: 1 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations?staffUniqueId=[ENTER_STAFF_UNIQUE_ID]&educationOrganizationId=[ENTER_EDORG_ID]&beginDate=[ENTER_BEGIN_DATE] + body: none + auth: bearer +} + +params:query { + staffUniqueId: [ENTER_STAFF_UNIQUE_ID] + educationOrganizationId: [ENTER_EDORG_ID] + beginDate: [ENTER_BEGIN_DATE] +} + +auth:bearer { + token: {{edFiCertToken}} +} + +script:post-response { + const { setVar, setVarsMessage, wipeVar, wipeVarsWarning, pickSingle } = require('./utils'); + const item = pickSingle(res.getBody()); + + if (item) { + setVar(bru, 'tempStaffEdOrgAssociationsUUID', item.id); + setVarsMessage('StaffEdOrgAssociations'); + } else { + wipeVar(bru, 'tempStaffEdOrgAssociationsUUID'); + wipeVarsWarning('StaffEdOrgAssociations'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 2 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 2 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..7c7d9c3 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 2 - StaffEdOrgAssociations.bru @@ -0,0 +1,44 @@ +meta { + name: Aux 2 - StaffEdOrgAssociations + type: http + seq: 2 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "staffReference": { + "staffUniqueId": "[ENTER_STAFF_UNIQUE_ID]" + }, + "educationOrganizationReference": { + "educationOrganizationId": "[ENTER_EDORG_ID]" + }, + "beginDate": "[ENTER_BEGIN_DATE]", + "staffClassificationDescriptor": "uri://ed-fi.org/StaffClassificationDescriptor#Teacher", + "positionTitle": "1st Grade teacher", + "endDate": "[ENTER_END_DATE]" + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const _UUID = locationHeader.split('/').pop(); + bru.setVar('tempStaffEdOrgAssociationsUUID', _UUID); + console.log('StaffEdOrgAssociation data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 3 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 3 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..919c6d6 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 3 - StaffEdOrgAssociations.bru @@ -0,0 +1,36 @@ +meta { + name: Aux 3 - StaffEdOrgAssociations + type: http + seq: 3 +} + +put { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations/{{tempStaffEdOrgAssociationsUUID}} + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "id": "{{tempStaffEdOrgAssociationsUUID}}", + "staffReference": { + "staffUniqueId": "[ENTER_STAFF_UNIQUE_ID]" + }, + "educationOrganizationReference": { + "educationOrganizationId": "[ENTER_EDORG_ID]" + }, + "beginDate": "[ENTER_BEGIN_DATE]", + "staffClassificationDescriptor": "uri://ed-fi.org/StaffClassificationDescriptor#Teacher", + "positionTitle": "1st Grade teacher", + "endDate": "[ENTER_END_DATE]" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 4 - StaffEdOrgAssociations by Id.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 4 - StaffEdOrgAssociations by Id.bru new file mode 100644 index 0000000..22797d8 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 4 - StaffEdOrgAssociations by Id.bru @@ -0,0 +1,20 @@ +meta { + name: Aux 4 - StaffEdOrgAssociations by Id + type: http + seq: 4 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations/{{tempStaffEdOrgAssociationsUUID}} + body: none + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 5 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 5 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..e4444e8 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Aux 5 - StaffEdOrgAssociations.bru @@ -0,0 +1,20 @@ +meta { + name: Aux 5 - StaffEdOrgAssociations + type: http + seq: 5 +} + +delete { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations/{{tempStaffEdOrgAssociationsUUID}} + body: none + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 1 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 1 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..b3f3520 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 1 - StaffEdOrgAssociations.bru @@ -0,0 +1,44 @@ +meta { + name: Step 1 - StaffEdOrgAssociations + type: http + seq: 6 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "staffReference": { + "staffUniqueId": "207220-test01" + }, + "educationOrganizationReference": { + "educationOrganizationId": "9999999901" + }, + "beginDate": "01/02/2026", + "staffClassificationDescriptor": "uri://ed-fi.org/StaffClassificationDescriptor#Teacher", + "positionTitle": "1st Grade teacher", + "endDate": "01/03/2026" + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const _UUID = locationHeader.split('/').pop(); + bru.setVar('tempFirstStaffEdOrgAssociationUUID', _UUID); + console.log('StaffEdOrgAssociation data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 2 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 2 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..032e8b9 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 2 - StaffEdOrgAssociations.bru @@ -0,0 +1,44 @@ +meta { + name: Step 2 - StaffEdOrgAssociations + type: http + seq: 7 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "staffReference": { + "staffUniqueId": "207269-test02" + }, + "educationOrganizationReference": { + "educationOrganizationId": "9999999902" + }, + "beginDate": "09/01/2026", + "staffClassificationDescriptor": "uri://ed-fi.org/StaffClassificationDescriptor#Teacher", + "positionTitle": "9th Grade Teacher", + "endDate": null + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const _UUID = locationHeader.split('/').pop(); + bru.setVar('tempSecondStaffEdOrgAssociationUUID', _UUID); + console.log('StaffEdOrgAssociation data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 3 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 3 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..782be21 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 3 - StaffEdOrgAssociations.bru @@ -0,0 +1,36 @@ +meta { + name: Step 3 - StaffEdOrgAssociations + type: http + seq: 8 +} + +put { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations/{{tempFirstStaffEdOrgAssociationUUID}} + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "id": "{{tempFirstStaffEdOrgAssociationUUID}}", + "staffReference": { + "staffUniqueId": "207220-test01" + }, + "educationOrganizationReference": { + "educationOrganizationId": "9999999901" + }, + "beginDate": "01/02/2026", // Begin date cannot be updated as it is part of the primary key + "staffClassificationDescriptor": "uri://ed-fi.org/StaffClassificationDescriptor#Teacher", + "positionTitle": "2nd Grade teacher", + "endDate": null + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 4 - StaffEdOrgAssociations.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 4 - StaffEdOrgAssociations.bru new file mode 100644 index 0000000..184bb55 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/Step 4 - StaffEdOrgAssociations.bru @@ -0,0 +1,36 @@ +meta { + name: Step 4 - StaffEdOrgAssociations + type: http + seq: 9 +} + +put { + url: {{resourceBaseUrl}}/ed-fi/staffEducationOrganizationAssignmentAssociations/{{tempSecondStaffEdOrgAssociationUUID}} + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "id": "{{tempSecondStaffEdOrgAssociationUUID}}", + "staffReference": { + "staffUniqueId": "207269-test02" + }, + "educationOrganizationReference": { + "educationOrganizationId": "9999999902" + }, + "beginDate": "09/01/2026", + "staffClassificationDescriptor": "uri://ed-fi.org/StaffClassificationDescriptor#Teacher", + "positionTitle": "10th Grade Teacher", + "endDate": null + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/folder.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/folder.bru new file mode 100644 index 0000000..1a71f99 --- /dev/null +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations V5/folder.bru @@ -0,0 +1,8 @@ +meta { + name: StaffEdOrgAssociations V5 + seq: 29 +} + +auth { + mode: inherit +} diff --git a/bruno/Sample Data/Resources/StaffEdOrgAssociations/folder.bru b/bruno/Sample Data/Resources/StaffEdOrgAssociations/folder.bru index 9425fec..2addd9d 100644 --- a/bruno/Sample Data/Resources/StaffEdOrgAssociations/folder.bru +++ b/bruno/Sample Data/Resources/StaffEdOrgAssociations/folder.bru @@ -1,6 +1,6 @@ meta { name: StaffEdOrgAssociations - seq: 24 + seq: 28 } auth { diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 1 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 1 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..a713b92 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 1 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,50 @@ +meta { + name: Aux 1 - StudentEdOrgResponsibilityAssocs + type: http + seq: 1 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations?beginDate=[ENTER_BEGIN_DATE]&studentUniqueId=[ENTER_STUDENT_UNIQUE_ID]&educationOrganizationId=[ENTER_SCHOOL_UNIQUE_ID]&responsibilityDescriptor={{tempResponsibilityDescriptorEncoded}}&responsibilityDescriptor_KEEP_IT_AT_THE_END=[ENTER_RESPONSIBILITY_DESCRIPTOR] + body: none + auth: bearer +} + +params:query { + beginDate: [ENTER_BEGIN_DATE] + studentUniqueId: [ENTER_STUDENT_UNIQUE_ID] + educationOrganizationId: [ENTER_SCHOOL_UNIQUE_ID] + responsibilityDescriptor: {{tempResponsibilityDescriptorEncoded}} + responsibilityDescriptor_KEEP_IT_AT_THE_END: [ENTER_RESPONSIBILITY_DESCRIPTOR] +} + +auth:bearer { + token: {{edFiCertToken}} +} + +script:pre-request { + const { encodeDescriptorParameter, setVar } = require('./utils'); + const responsibilityDescriptorEncoded = encodeDescriptorParameter(req.url, 'responsibilityDescriptor_KEEP_IT_AT_THE_END'); + + setVar(bru, 'tempResponsibilityDescriptorEncoded', responsibilityDescriptorEncoded); +} + +script:post-response { + const { setVar, setVarsMessage, wipeVar, wipeVarsWarning, pickSingle } = require('./utils'); + const item = pickSingle(res.getBody()); + + if (item) { + setVar(bru, 'tempStudentEdOrgResponsibilityAssocUUID', item.id); + setVarsMessage('StudentEdOrgResponsibilityAssoc'); + } else { + wipeVar(bru, 'tempStudentEdOrgResponsibilityAssocUUID'); + wipeVarsWarning('StudentEdOrgResponsibilityAssoc'); + } +} + +settings { + encodeUrl: false + timeout: 0 + followRedirects: true + maxRedirects: 5 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 2 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 2 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..ebbd7e7 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 2 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,43 @@ +meta { + name: Aux 2 - StudentEdOrgResponsibilityAssocs + type: http + seq: 2 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "responsibilityDescriptor": "[ENTER_RESPONSIBILITY_DESCRIPTOR]", + "educationOrganizationReference": { + "educationOrganizationId": "[ENTER_SCHOOL_UNIQUE_ID]" + }, + "studentReference": { + "studentUniqueId": "[ENTER_STUDENT_UNIQUE_ID]" + }, + "beginDate": "[ENTER_BEGIN_DATE]", + "endDate": "[ENTER_END_DATE]" + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const assocUUID = locationHeader.split('/').pop(); + bru.setVar('tempStudentEdOrgResponsibilityAssocUUID', assocUUID); + console.log('StudentEdOrgResponsibilityAssoc data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 3 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 3 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..054dae4 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 3 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,35 @@ +meta { + name: Aux 3 - StudentEdOrgResponsibilityAssocs + type: http + seq: 3 +} + +put { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations/{{tempStudentSchoolAssociationUUID}} + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "id": "{{tempStudentSchoolAssociationUUID}}", + "responsibilityDescriptor": "[ENTER_RESPONSIBILITY_DESCRIPTOR]", + "educationOrganizationReference": { + "educationOrganizationId": "[ENTER_SCHOOL_UNIQUE_ID]" + }, + "studentReference": { + "studentUniqueId": "[ENTER_STUDENT_UNIQUE_ID]" + }, + "beginDate": "[ENTER_BEGIN_DATE]", + "endDate": "[ENTER_END_DATE]" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 4 - StudentEdOrgResponsibilityAssocs by Id.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 4 - StudentEdOrgResponsibilityAssocs by Id.bru new file mode 100644 index 0000000..110ae1b --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 4 - StudentEdOrgResponsibilityAssocs by Id.bru @@ -0,0 +1,20 @@ +meta { + name: Aux 4 - StudentEdOrgResponsibilityAssocs by Id + type: http + seq: 4 +} + +get { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations/{{tempStudentEdOrgResponsibilityAssocUUID}} + body: none + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 5 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 5 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..9679ba7 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Aux 5 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,20 @@ +meta { + name: Aux 5 - StudentEdOrgResponsibilityAssocs + type: http + seq: 5 +} + +delete { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations/{{tempStudentEdOrgResponsibilityAssocUUID}} + body: none + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Dep 1 - StudentSchoolAssociations.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Dep 1 - StudentSchoolAssociations.bru new file mode 100644 index 0000000..561818a --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Dep 1 - StudentSchoolAssociations.bru @@ -0,0 +1,63 @@ +meta { + name: Dep 1 - StudentSchoolAssociations + type: http + seq: 6 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/studentSchoolAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "schoolReference": { + "schoolId": "9999999901", + "link": { + "rel": "School", + "href": "/ed-fi/schools/2ed6f66667ea4961be5b6272987cb96d" + } + }, + "studentReference": { + "studentUniqueId": "99999901", + "link": { + "rel": "Student", + "href": "/ed-fi/students/645d84ccb94a44be8b3534a6f9286173" + } + }, + "calendarReference": { + "calendarCode": "CC-9999999901", + "schoolId": "9999999901", + "schoolYear": 2026 + }, + "primarySchool": true, + "entryDate": "2026-08-31", + "enrollmentTypeDescriptor": "uri://ed-fi.org/EnrollmentTypeDescriptor#Current", + "entryGradeLevelDescriptor": "uri://ed-fi.org/GradeLevelDescriptor#First grade", + "entryTypeDescriptor": "uri://ed-fi.org/EntryTypeDescriptor#Next year school", + "fullTimeEquivalency": 1.00, + "exitWithdrawDate": "2026-09-01", + "exitWithdrawTypeDescriptor": "uri://ed-fi.org/ExitWithdrawTypeDescriptor#Transferred", + "residencyStatusDescriptor": "uri://ed-fi.org/ResidencyStatusDescriptor#Resident of admin unit and school area", + "repeatGradeIndicator": false + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const assocUUID = locationHeader.split('/').pop(); + bru.setVar('tempFirstStudentSchoolAssociationUUID', assocUUID); + console.log('StudentSchoolAssociation data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Dep 2 - StudentSchoolAssociations.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Dep 2 - StudentSchoolAssociations.bru new file mode 100644 index 0000000..8bb2e97 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Dep 2 - StudentSchoolAssociations.bru @@ -0,0 +1,61 @@ +meta { + name: Dep 2 - StudentSchoolAssociations + type: http + seq: 8 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/studentSchoolAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "schoolReference": { + "schoolId": "9999999902", + "link": { + "rel": "School", + "href": "/ed-fi/schools/2ed6f66667ea4961be5b6272987cb96d" + } + }, + "studentReference": { + "studentUniqueId": "99999901", + "link": { + "rel": "Student", + "href": "/ed-fi/students/645d84ccb94a44be8b3534a6f9286173" + } + }, + "calendarReference": { + "calendarCode": "CC-9999999902", + "schoolId": "9999999902", + "schoolYear": 2026 + }, + "primarySchool": true, + "entryDate": "2026-08-31", + "enrollmentTypeDescriptor": "uri://ed-fi.org/EnrollmentTypeDescriptor#Current", + "entryGradeLevelDescriptor": "uri://ed-fi.org/GradeLevelDescriptor#First grade", + "entryTypeDescriptor": "uri://ed-fi.org/EntryTypeDescriptor#Next year school", + "fullTimeEquivalency": 1.00, + "residencyStatusDescriptor": "uri://ed-fi.org/ResidencyStatusDescriptor#Resident of admin unit and school area", + "repeatGradeIndicator": false + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const assocUUID = locationHeader.split('/').pop(); + bru.setVar('tempSecondStudentSchoolAssociationUUID', assocUUID); + console.log('StudentSchoolAssociation data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 1 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 1 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..f266f19 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 1 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,43 @@ +meta { + name: Step 1 - StudentEdOrgResponsibilityAssocs + type: http + seq: 7 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "responsibilityDescriptor": "uri://ed-fi.org/ResponsibilityDescriptor#Residency", + "educationOrganizationReference": { + "educationOrganizationId": "9999999901" + }, + "studentReference": { + "studentUniqueId": "99999901" + }, + "beginDate": "2026-08-23", + "endDate": "2026-06-01" + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const assocUUID = locationHeader.split('/').pop(); + bru.setVar('tempFirstStudentEdOrgResponsibilityAssocUUID', assocUUID); + console.log('StudentEdOrgResponsibilityAssoc data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 2 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 2 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..3d0ec73 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 2 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,43 @@ +meta { + name: Step 2 - StudentEdOrgResponsibilityAssocs + type: http + seq: 9 +} + +post { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "responsibilityDescriptor": "uri://ed-fi.org/ResponsibilityDescriptor#Individualized Education Program", + "educationOrganizationReference": { + "educationOrganizationId": "9999999902" + }, + "studentReference": { + "studentUniqueId": "99999901" // This is not a typo. The test is "Same Student with a different EdOrg" + }, + "beginDate": "2026-08-23", + "endDate": "2026-06-01" + } +} + +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const assocUUID = locationHeader.split('/').pop(); + bru.setVar('tempSecondStudentEdOrgResponsibilityAssocUUID', assocUUID); + console.log('StudentEdOrgResponsibilityAssoc data was created correctly.'); + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 3 - StudentEdOrgResponsibilityAssocs.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 3 - StudentEdOrgResponsibilityAssocs.bru new file mode 100644 index 0000000..b692744 --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/Step 3 - StudentEdOrgResponsibilityAssocs.bru @@ -0,0 +1,35 @@ +meta { + name: Step 3 - StudentEdOrgResponsibilityAssocs + type: http + seq: 10 +} + +put { + url: {{resourceBaseUrl}}/ed-fi/studentEducationOrganizationResponsibilityAssociations/{{tempSecondStudentEdOrgResponsibilityAssocUUID}} + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + "id":"{{tempSecondStudentEdOrgResponsibilityAssocUUID}}", + "responsibilityDescriptor": "uri://ed-fi.org/ResponsibilityDescriptor#Individualized Education Program", + "educationOrganizationReference": { + "educationOrganizationId": "9999999902" + }, + "studentReference": { + "studentUniqueId": "99999901" // This is not a typo. The test is "Same Student with a different EdOrg" + }, + "beginDate": "2026-08-23", + "endDate": "2026-10-01" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/folder.bru b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/folder.bru new file mode 100644 index 0000000..df223ac --- /dev/null +++ b/bruno/Sample Data/Resources/StudentEdOrgResponsibilityAssocs/folder.bru @@ -0,0 +1,8 @@ +meta { + name: StudentEdOrgResponsibilityAssocs + seq: 41 +} + +auth { + mode: inherit +} diff --git a/bruno/ai-docs/sample-data.spec.md b/bruno/ai-docs/sample-data.spec.md new file mode 100644 index 0000000..1667a63 --- /dev/null +++ b/bruno/ai-docs/sample-data.spec.md @@ -0,0 +1,468 @@ +# Ed-Fi Sample Data Script Generation Specification + +**Version:** 1.0 +**Last Updated:** 2026-06-26 +**Status:** Active + +This specification defines rules for generating Bruno sample data seed scripts for Ed-Fi ODS/API entities. These scripts live in `Sample Data/Resources/ V/` and are companion scripts to the read-only certification test scenarios in `SIS/v///`. + +--- + +## Table of Contents + +1. [Purpose & Scope](#1-purpose--scope) +2. [Inputs](#2-inputs) +3. [Output Folder Naming & Legacy Detection](#3-output-folder-naming--legacy-detection) +4. [Output Files Overview](#4-output-files-overview) +5. [Step-N File Rules (Data Seed)](#5-step-n-file-rules-data-seed) +6. [Aux-N File Rules (On-Demand Helpers)](#6-aux-n-file-rules-on-demand-helpers) +7. [Variable Naming Conventions](#7-variable-naming-conventions) +8. [Descriptor Encoding](#8-descriptor-encoding) +9. [Placeholder Format](#9-placeholder-format) +10. [Null & Empty Field Handling](#10-null--empty-field-handling) +11. [Sequence Numbering](#11-sequence-numbering) +12. [Consistency Checking](#12-consistency-checking) + +--- + +## 1. Purpose & Scope + +Sample data scripts seed test data into the Ed-Fi ODS/API so that read-only certification tests can validate it. The execution order interleaves sample data and certification steps: + +``` +Sample Data Step 1 → SIS Step 1 → Sample Data Step 2 → SIS Step 2 → ... +``` + +**In scope:** +- Generating `Step-N` and `Aux-N` `.bru` files in `Sample Data/Resources/ V/` +- Generating the minimal `folder.bru` for the output folder +- V4 and V5 entity support (version-agnostic rules) + +**Out of scope:** +- Generating SIS certification test files (`SIS/v/...`) — covered by `spec.md` +- Executing or running Bruno scripts + +--- + +## 2. Inputs + +### 2.1 Primary Input: `folder.bru` docs block + +Located at `bruno/SIS/v///folder.bru`. + +Required sections parsed from the `docs {}` block: + +| Section | Purpose | +|---------|---------| +| `## Scenarios tasks` | Defines scenario operations (`__CREATE__`, `__UPDATE__`, `__DELETE__`) and ordinals | +| `## Scenarios example data` | Tabular data: field names, types, required status, per-scenario values | + +### 2.2 Secondary Input: `entity.config.json` + +Located at `bruno/SIS/v///entity.config.json`. + +Key fields used: + +| Field | Purpose | +|-------|---------| +| `identity.primaryKeyFields` | Determines which fields get `[ENTER ...]` placeholders in Aux files | +| `identity.naturalIdField` | Used as the primary Aux 1 query parameter when a single natural key exists | +| `identity.irregularPlural.singular` | Override for entity singular name | +| `overrides.entityName` | Override for entity singular name (highest precedence) | +| `overrides.endpointSegment` | Override for REST endpoint segment | + +### 2.3 Version & Path Inference + +| Component | Derivation | +|-----------|-----------| +| **version** | Extracted from path: `SIS/v5/...` → `5`, `SIS/v4/...` → `4` | +| **entityGroup** | Parent folder of entity (e.g. `StaffAssociation`) | +| **entityFolder** | Entity folder name (e.g. `Staffs`) | +| **entityPlural** | `entityFolder` value, used as-is for file names | +| **entitySingular** | 1. `overrides.entityName` → 2. `identity.irregularPlural.singular` → 3. Remove trailing `s` from `entityFolder` | +| **endpointSegment** | 1. `overrides.endpointSegment` → 2. `identity.irregularPlural.endpointSegment` → 3. Lowercase first letter of `entityFolder` | + +--- + +## 3. Output Folder Naming & Legacy Detection + +### 3.1 Standard Naming + +- **V5:** `bruno/Sample Data/Resources/ V5/` +- **V4:** `bruno/Sample Data/Resources/ V4/` + +`` matches the `entityFolder` name exactly (e.g. `Staffs`, `GradingPeriods`). + +### 3.2 Legacy Detection + +Before generating, check if a folder named `Sample Data/Resources//` (without version suffix) already exists. + +**If found:** Pause and notify the user: + +> "A legacy folder `` already exists without a version suffix. Before continuing, please confirm: should I (A) rename it to ` V`, (B) delete it and generate a fresh ` V` folder from scratch, or (C) keep it as-is and generate a new ` V` folder?" + +Do not proceed until the user responds. + +**If the versioned target folder already exists:** Pause and ask: + +> "The folder ` V` already exists. Should I (A) overwrite its contents, or (B) abort?" + +### 3.3 Output `folder.bru` + +Generate a minimal `folder.bru` in the output folder: + +``` +meta { + name: V + seq: +} + +auth { + mode: inherit +} +``` + +``: inspect all existing `folder.bru` files in `Sample Data/Resources/` and use `max(seq) + 1`. If none found, default to `1`. + +--- + +## 4. Output Files Overview + +All generated files live in `Sample Data/Resources/ V/`: + +| File | Method | seq | Body data | Purpose | +|------|--------|-----|-----------|---------| +| `Aux 1 - .bru` | GET | 1 | All PK fields as placeholders | Query by natural key | +| `Aux 2 - .bru` | POST | 2 | PK fields as placeholders; non-PK from Scenario 1 | Create helper | +| `Aux 3 - .bru` | PUT | 3 | PK fields as placeholders; non-PK from Scenario 1 | Update helper | +| `Aux 4 - by Id.bru` | GET | 4 | No body | Fetch by UUID | +| `Aux 5 - .bru` | DELETE | 5 | No body | Delete helper | +| `Step 1 - .bru` | POST or PUT | 6 | Scenario 1 values | First data seed step | +| `Step 2 - .bru` | POST or PUT | 7 | Scenario 2 values | Second data seed step | +| `Step N - .bru` | POST or PUT | 5+N | Scenario N values | Nth data seed step | + +--- + +## 5. Step-N File Rules (Data Seed) + +### 5.1 Scenario Numbering + +Steps are numbered by **execution order** (position in the `## Scenarios tasks` list, top to bottom), **not** by the numeric label on the task line. This prevents bugs from out-of-order or misnumbered task lists in `folder.bru`. + +**Example:** If the task list reads `1, 2, 4, 5, 3, 6`, the generated files are still `Step 1` through `Step 6` based on position. + +### 5.2 HTTP Method + +| Task token | HTTP Method | +|------------|-------------| +| `__CREATE__` | POST | +| `__UPDATE__` | PUT | +| `__DELETE__` | DELETE | + +### 5.3 URL Construction + +- **POST:** `{{resourceBaseUrl}}/ed-fi/` +- **PUT / DELETE:** `{{resourceBaseUrl}}/ed-fi//{{tempUUID}}` + +`` is the capitalized ordinal referenced in the task line (e.g. `__UPDATE__ ... the 'first' added ...` → `First`). + +### 5.4 Request Body — POST (`__CREATE__`) + +Include all fields from the corresponding scenario column that are **REQUIRED** or **CONDITIONAL**: +- Descriptor fields: encode as `uri://ed-fi.org/#` (see Section 8) +- Empty/blank scenario values for REQUIRED or CONDITIONAL fields: use `null` (see Section 10) +- OPTIONAL fields with empty/blank values: omit entirely + +### 5.5 Request Body — PUT (`__UPDATE__`) + +Same as POST body rules, plus: +- Include `"id": "{{tempUUID}}"` as the **first field** in the body +- Use the PUT scenario column values + +### 5.6 `script:post-response` (POST steps only) + +```javascript +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const _UUID = locationHeader.split('/').pop(); + bru.setVar('tempUUID', _UUID); + console.log(' data was created correctly.'); + } +} +``` + +- Local capture variable is always named `_UUID` +- Stored Bruno variable: `tempUUID` (e.g. `tempFirstStaffUUID`, `tempThirdGradingPeriodUUID`) +- PUT and DELETE steps do **not** include a `script:post-response` + +### 5.7 Auth & Settings + +Every Step file includes: + +``` +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} +``` + +### 5.8 Full Step File Template + +``` +meta { + name: Step - + type: http + seq: <5+N> +} + + { + url: + body: json + auth: bearer +} + +auth:bearer { + token: {{edFiCertToken}} +} + +body:json { + { + + } +} + + + +settings { + encodeUrl: true + timeout: 0 +} +``` + +--- + +## 6. Aux-N File Rules (On-Demand Helpers) + +Aux files are manually-executed helpers, not part of the automated sequence. The user edits placeholder values before running them. + +### 6.1 Aux 1 — GET by Query Parameters + +- Method: GET +- URL: `{{resourceBaseUrl}}/ed-fi/?` +- `params:query {}` block mirrors the URL parameters exactly +- **All PK fields** → `[ENTER FIELD NAME]` placeholder (see Section 9) +- **Descriptor PK fields** → also use the sentinel pattern (see Section 8.2) + +`script:post-response` — always present, captures UUID using `pickSingle()`: + +```javascript +script:post-response { + const { setVar, setVarsMessage, wipeVar, wipeVarsWarning, pickSingle } = require('./utils'); + const item = pickSingle(res.getBody()); + + if (item) { + setVar(bru, 'tempUUID', item.id); + setVarsMessage(''); + } else { + wipeVar(bru, 'tempUUID'); + wipeVarsWarning(''); + } +} +``` + +Note: The Aux UUID variable has **no ordinal** — it is a single working copy for ad-hoc use. + +### 6.2 Aux 2 — POST (Create Helper) + +- Method: POST +- Body: JSON with all REQUIRED and CONDITIONAL fields: + - PK fields → `[ENTER FIELD NAME]` placeholders + - Non-PK fields → **exact Scenario 1 values** (fully encoded; user updates as needed) + - OPTIONAL fields → omit + +`script:post-response` — captures UUID into `tempUUID`: + +```javascript +script:post-response { + const locationHeader = res.headers.Location || res.headers.location; + if (locationHeader) { + const _UUID = locationHeader.split('/').pop(); + bru.setVar('tempUUID', _UUID); + console.log(' data was created correctly.'); + } +} +``` + +### 6.3 Aux 3 — PUT (Update Helper) + +- Method: PUT +- URL: `{{resourceBaseUrl}}/ed-fi//{{tempUUID}}` +- Body: same structure as Aux 2, plus `"id": "{{tempUUID}}"` as the **first field** + - PK fields → `[ENTER FIELD NAME]` placeholders + - Non-PK fields → Scenario 1 values +- No `script:post-response` + +### 6.4 Aux 4 — GET by Id + +- Method: GET +- URL: `{{resourceBaseUrl}}/ed-fi//{{tempUUID}}` +- No body, no scripts + +### 6.5 Aux 5 — DELETE + +- Method: DELETE +- URL: `{{resourceBaseUrl}}/ed-fi//{{tempUUID}}` +- No body, no scripts + +### 6.6 Auth & Settings (All Aux Files) + +``` +auth:bearer { + token: {{edFiCertToken}} +} + +settings { + encodeUrl: true + timeout: 0 +} +``` + +--- + +## 7. Variable Naming Conventions + +| Variable | Pattern | Example | +|----------|---------|---------| +| Step POST UUID | `tempUUID` | `tempFirstStaffUUID` | +| Aux UUID (no ordinal) | `tempUUID` | `tempStaffUUID` | +| Local capture variable | `_UUID` (always this name) | `const _UUID = locationHeader.split('/').pop()` | +| Descriptor sentinel (Aux 1) | `tempEncoded` | `tempGradingPeriodDescriptorEncoded` | + +**Ordinal capitalization (authoritative list):** + +| Position | Capitalized | Lowercase | +|----------|-------------|-----------| +| 1 | `First` | `first` | +| 2 | `Second` | `second` | +| 3 | `Third` | `third` | +| 4 | `Fourth` | `fourth` | +| 5 | `Fifth` | `fifth` | +| 6 | `Sixth` | `sixth` | +| 7 | `Seventh` | `seventh` | +| 8 | `Eighth` | `eighth` | +| 9 | `Ninth` | `ninth` | +| 10 | `Tenth` | `tenth` | + +--- + +## 8. Descriptor Encoding + +### 8.1 Body Values + +Descriptor field values in request bodies are encoded as: + +``` +uri://ed-fi.org/# +``` + +`` is the field name with the first letter uppercased: + +| Field name | Encoded type | +|------------|-------------| +| `sexDescriptor` | `SexDescriptor` | +| `gradingPeriodDescriptor` | `GradingPeriodDescriptor` | +| `highestCompletedLevelOfEducationDescriptor` | `HighestCompletedLevelOfEducationDescriptor` | +| `electronicMailTypeDescriptor` | `ElectronicMailTypeDescriptor` | + +**Full example:** `"uri://ed-fi.org/SexDescriptor#Male"` + +### 8.2 Aux 1 Sentinel Pattern (Descriptor as PK Query Parameter) + +When a descriptor field appears in `identity.primaryKeyFields`, it must use the sentinel pattern in Aux 1 to handle URL encoding correctly: + +``` +params:query { + : {{tempEncoded}} + _KEEP_IT_AT_THE_END: [ENTER DESCRIPTOR VALUE] +} + +script:pre-request { + const { encodeDescriptorParameter } = require('./utils'); + const encoded = encodeDescriptorParameter(req.url, '_KEEP_IT_AT_THE_END'); + bru.setVar('tempEncoded', encoded); +} +``` + +If no PK field is a descriptor type, the sentinel pattern and `script:pre-request` are omitted entirely from Aux 1. + +--- + +## 9. Placeholder Format + +All placeholder values use the format: + +``` +[ENTER FIELD NAME] +``` + +Rules: +- Words separated by **spaces** (never underscores) +- **All uppercase** +- Derived by splitting the camelCase field name on word boundaries + +| Field name | Placeholder | +|------------|-------------| +| `schoolId` | `[ENTER SCHOOL ID]` | +| `staffUniqueId` | `[ENTER STAFF UNIQUE ID]` | +| `beginDate` | `[ENTER BEGIN DATE]` | +| `gradingPeriodDescriptor` | `[ENTER GRADING PERIOD DESCRIPTOR]` | +| `educationOrganizationId` | `[ENTER EDUCATION ORGANIZATION ID]` | + +--- + +## 10. Null & Empty Field Handling + +| Scenario | Rule | +|----------|------| +| REQUIRED field — empty/blank scenario column value | Include field with `null` value | +| CONDITIONAL field — empty/blank scenario column value | Include field with `null` value | +| OPTIONAL field — empty/blank scenario column value | Omit field entirely | +| Field not listed in data table | Omit field entirely | + +--- + +## 11. Sequence Numbering + +| File | `seq` value | +|------|-------------| +| `Aux 1` | `1` | +| `Aux 2` | `2` | +| `Aux 3` | `3` | +| `Aux 4` | `4` | +| `Aux 5` | `5` | +| `Step N` | `5 + N` | +| `folder.bru` | `max(existing seq values in Sample Data/Resources/) + 1` | + +--- + +## 12. Consistency Checking + +Before generating any files, validate all inputs. **Pause and ask the user** whenever any of the following conditions are found. Never assume — inconsistencies may be intentional or typos. The user must confirm before the skill proceeds. + +| Check | Action on issue | +|-------|----------------| +| Task list numbering is not sequential by position (e.g. `1, 2, 4, 5, 3, 6`) | Warn user; confirm that execution-order position will be used for Step numbering | +| Scenario column count in data table does not match task count | Pause; ask user to confirm expected count | +| A REQUIRED field has no value in any scenario column (not blank — completely absent from the table) | Pause; ask for the correct value before generating | +| Descriptor field value is present but does not use a recognizable short-form (e.g. contains `uri://` — already encoded) | Pause; confirm whether to use as-is or re-encode | +| A PK field listed in `entity.config.json` is not present as a column in the data table | Pause; flag the missing field; ask how to handle it | +| A PUT scenario column references an ordinal whose CREATE step does not exist | Pause; flag the inconsistency | +| Output folder already exists in any form (with or without version suffix) | Pause; ask user whether to overwrite, rename, or abort (see Section 3.2) | +| Any pattern in existing reference files for this entity deviates from this spec | Surface the deviation explicitly; ask user to confirm canonical behavior before replicating or correcting it | + +**Principle:** If in doubt, stop and ask. It is always better to pause for a 10-second clarification than to generate files with a baked-in assumption that later causes a test failure. diff --git a/bruno/ai-docs/skills/README.md b/bruno/ai-docs/skills/README.md new file mode 100644 index 0000000..2bdcc74 --- /dev/null +++ b/bruno/ai-docs/skills/README.md @@ -0,0 +1,113 @@ +# Repository Skills + +This folder contains custom AI agent skills for the `certification-testing` project. Skills are reusable workflow guides that the GitHub Copilot agent follows when invoked via `/superpowers:`. + +--- + +## What Is a Skill? + +A skill is a `SKILL.md` file that instructs the AI agent on *how* to perform a specific, repeatable task. Skills in this folder are project-specific — they encode domain knowledge (Ed-Fi entities, Bruno patterns, folder conventions) that a generic agent wouldn't otherwise know. + +Each skill folder follows this structure: + +``` +skills/ + / + SKILL.md # Required — the skill instructions + # Optional supporting files (templates, reference docs) +``` + +--- + +## Available Skills + +| Skill | Invocation | Purpose | +|-------|------------|---------| +| `generate-sample-data` | `/superpowers:generate-sample-data` | Generates Bruno `Step-N` and `Aux-N` seed scripts for one Ed-Fi entity from its `folder.bru` scenario data table | + +--- + +## How to Use a Skill + +1. Open GitHub Copilot Chat in VS Code +2. Type the invocation command, e.g.: + ``` + /superpowers:generate-sample-data + ``` +3. The skill will guide you step-by-step — it will ask for the entity, validate inputs, confirm the plan, and generate the files + +> **Note:** Skills must be deployed before they can be invoked. See [Deploying a Skill](#deploying-a-skill) below. + +--- + +## Deploying a Skill + +Skills in this repo are the **source of truth** but must be copied to the local superpowers directory to be invokable. The deployment target is: + +``` +~/.copilot/installed-plugins/superpowers-marketplace/superpowers/skills//SKILL.md +``` + +### Option A — Using the `writing-skills` superpowers skill (Recommended) + +``` +/superpowers:writing-skills +``` + +Tell it: *"Deploy the skill at `bruno/ai-docs/skills//SKILL.md` to the superpowers skills folder."* + +It will handle copying and verification. + +### Option B — Manual copy (PowerShell) + +```powershell +$skillName = "generate-sample-data" +$src = "bruno\ai-docs\skills\$skillName" +$dst = "$env:USERPROFILE\.copilot\installed-plugins\superpowers-marketplace\superpowers\skills\$skillName" + +New-Item -ItemType Directory -Force -Path $dst +Copy-Item -Path "$src\*" -Destination $dst -Recurse -Force + +Write-Host "Deployed: $skillName" +``` + +Run this from the repository root. + +### Verifying Deployment + +After deploying, verify the skill is available: +1. Open GitHub Copilot Chat +2. Type `/superpowers:` — the skill name should appear in the autocomplete list +3. Or invoke it directly and confirm it responds with the expected first step + +--- + +## Adding a New Skill + +1. Create a folder: `bruno/ai-docs/skills//` +2. Create `SKILL.md` with a YAML frontmatter header: + ```markdown + --- + name: skill-name + description: "Use when [specific triggering condition]." + --- + ``` +3. Write the skill workflow (see `generate-sample-data/SKILL.md` as an example) +4. Add a row to the [Available Skills](#available-skills) table above +5. Deploy using Option A or Option B + +### Guidelines + +- **One skill = one focused task.** If a skill is doing two unrelated things, split it. +- **Reference spec docs, don't duplicate them.** Skills should instruct the agent to read `sample-data.spec.md` or `spec.md` — not copy their content inline. +- **Always include a consistency-check step.** Skills that generate files must pause and ask the user when they detect ambiguities or unexpected patterns. +- **Source lives here; deployed copy is disposable.** Always edit the file in this repo — never edit the deployed copy directly, as it will be overwritten on next deploy. + +--- + +## Related Files + +| File | Purpose | +|------|---------| +| `bruno/ai-docs/spec.md` | Rules for generating SIS read-only certification test scenarios | +| `bruno/ai-docs/sample-data.spec.md` | Rules for generating Sample Data seed scripts (used by `generate-sample-data` skill) | diff --git a/bruno/ai-docs/skills/generate-sample-data/SKILL.md b/bruno/ai-docs/skills/generate-sample-data/SKILL.md new file mode 100644 index 0000000..d7c21e4 --- /dev/null +++ b/bruno/ai-docs/skills/generate-sample-data/SKILL.md @@ -0,0 +1,181 @@ +--- +name: generate-sample-data +description: "Generates Bruno sample data seed scripts (Step-N and Aux-N .bru files) for an Ed-Fi entity from its folder.bru scenario data table. Use when adding or updating Sample Data scripts in Sample Data/Resources/ V/." +--- + +# Generate Sample Data Scripts + +Generate Bruno `.bru` sample data seed scripts for a single Ed-Fi entity. These scripts live in `Sample Data/Resources/ V/` and seed the ODS/API with test data used by the read-only certification scenarios in `SIS/v/`. + +## Before You Start + +Read `bruno/ai-docs/sample-data.spec.md` in full before doing anything else. All rules in that document are authoritative. This skill defines the workflow; the spec defines the rules. When they conflict, the spec wins — but surface the conflict to the user. + +## Workflow + +Complete these steps in order. Do not skip steps. Do not generate files until Step 4 is confirmed. + +--- + +### Step 1 — Ask for the entity + +Ask the user: + +> "Which entity do you want to generate sample data for? Please provide the path relative to `bruno/SIS/` (e.g. `v5/StaffAssociation/Staffs` or `v4/EducationOrganizationCalendar/GradingPeriods`)." + +Wait for the user's response. + +--- + +### Step 2 — Read inputs + +Read both input files in parallel: + +1. `bruno/SIS//folder.bru` — parse the `docs {}` block: + - `## Scenarios tasks` — extract operations (`__CREATE__`, `__UPDATE__`, `__DELETE__`), ordinals (`first`, `second`, ...), and referenced field names + - `## Scenarios example data` — extract the full data table (all columns, all rows, Required/Optional/Conditional status) + +2. `bruno/SIS//entity.config.json` — extract: + - `identity.primaryKeyFields` + - `identity.naturalIdField` + - `identity.irregularPlural` (if present) + - `overrides.entityName`, `overrides.endpointSegment` (if present) + +--- + +### Step 3 — Consistency check + +Before generating anything, validate all inputs against Section 12 of `sample-data.spec.md`. + +**On any inconsistency — no matter how minor — pause immediately.** + +For each issue found: +1. Describe it clearly to the user +2. State whether it looks like a typo or a possibly intentional pattern, and why +3. Ask for explicit confirmation on how to proceed + +Do not group all issues into one message if they require independent decisions. Address them one at a time if needed. + +Do not proceed to Step 4 until **all** flagged issues are resolved. + +--- + +### Step 4 — Confirm plan with user + +Present a summary for the user to approve before any files are created: + +> "Here's what I'll generate for **` V`**: +> +> - **Output folder:** `Sample Data/Resources/ V/` +> - **folder.bru** (seq: ``) +> - **5 Aux files** (Aux 1–5, seq 1–5) +> - **`` Step files:** +> - Step 1 (seq 6) — `` — Scenario 1 (`` `____`) +> - Step 2 (seq 7) — `` — Scenario 2 (`` `____`) +> - ... +> +> Shall I proceed?" + +Wait for user confirmation before continuing. + +--- + +### Step 5 — Legacy folder check + +Before creating any files, check whether any of these folders already exist: + +- `Sample Data/Resources//` (no version suffix — legacy) +- `Sample Data/Resources/ V/` (versioned target — already exists) + +If the **legacy folder** exists without a version suffix, pause and ask: + +> "A legacy folder `` exists without a version suffix. Should I: **(A)** rename it to ` V`, **(B)** delete it and create a fresh ` V` folder from scratch, or **(C)** keep it as-is and generate a new ` V` folder?" + +If the **versioned target folder** already exists, pause and ask: + +> "The folder ` V` already exists. Should I: **(A)** overwrite its contents, or **(B)** abort?" + +Wait for the user's choice before touching anything. + +--- + +### Step 6 — Generate files + +Create the output folder and all files per `sample-data.spec.md`. Generate in this order: + +1. `folder.bru` +2. `Aux 1 - .bru` (GET, seq 1) +3. `Aux 2 - .bru` (POST, seq 2) +4. `Aux 3 - .bru` (PUT, seq 3) +5. `Aux 4 - by Id.bru` (GET, seq 4) +6. `Aux 5 - .bru` (DELETE, seq 5) +7. `Step 1 - .bru` through `Step - .bru` (seq 6 through 5+N) + +Apply all rules from `sample-data.spec.md` exactly: +- Step numbering by execution order (position), not by numeric task label +- Local UUID capture always uses `const _UUID = ...` +- Step UUID variables: `tempUUID` +- Aux UUID variables: `tempUUID` (no ordinal) +- Placeholders: `[ENTER FIELD NAME]` (spaces, all caps) +- Non-PK fields in Aux 2 and Aux 3: Scenario 1 values +- Descriptor encoding: `uri://ed-fi.org/#` +- Descriptor PKs in Aux 1: use sentinel pattern + +--- + +### Step 7 — Verify + +After generating all files, self-verify before presenting results: + +- [ ] HTTP method is correct for each Step (POST for `__CREATE__`, PUT for `__UPDATE__`) +- [ ] All Step UUID variables follow `tempUUID` +- [ ] All Aux UUID variables follow `tempUUID` (no ordinal) +- [ ] Local capture variable is `_UUID` in every `script:post-response` +- [ ] All PK fields in Aux files use `[ENTER FIELD NAME]` placeholders (spaces, all caps) +- [ ] All non-PK Scenario 1 values are correctly populated in Aux 2 and Aux 3 +- [ ] All descriptor values encoded as `uri://ed-fi.org/#` +- [ ] Descriptor PK fields in Aux 1 use sentinel pattern +- [ ] Seq numbers are correct: Aux 1–5 = seq 1–5; Step N = seq 5+N +- [ ] Step numbering follows execution order, not task-line numeric labels +- [ ] No OPTIONAL field with a blank value was included (should be omitted) +- [ ] No REQUIRED or CONDITIONAL field with a blank value was omitted (should be `null`) + +List all generated files with their seq, HTTP method, and scenario label. + +--- + +### Step 8 — Ask user to review + +> "Files generated in `Sample Data/Resources/ V/`. Please review them in Bruno and let me know if any corrections are needed." + +--- + +## Deployment Note + +This skill's source lives in the repository at: + +``` +bruno/ai-docs/skills/generate-sample-data/SKILL.md +``` + +To enable the `/superpowers:generate-sample-data` invocation, it must be deployed to: + +``` +~/.copilot/installed-plugins/superpowers-marketplace/superpowers/skills/generate-sample-data/SKILL.md +``` + +Use the `writing-skills` skill to deploy or update it after any changes. + +--- + +## Key Principles + +| Principle | Rule | +|-----------|------| +| **One entity at a time** | Stay focused on the entity the user specified. Do not generate files for other entities. | +| **Pause on inconsistency** | Never assume. Always describe the issue and ask the user to confirm before proceeding. | +| **Spec is authoritative** | `sample-data.spec.md` overrides any pattern observed in existing reference files. | +| **Scenario 1 for Aux** | Aux 2 and Aux 3 always use Scenario 1 data for non-PK fields. | +| **Execution order for Step numbering** | Use position in task list, not the numeric label on the task line. | +| **`_UUID` is always the local variable** | Never rename it per-entity. Consistency prevents typos. | +| **Confirm before creating** | Never create files without user approval of the plan (Step 4). | diff --git a/bruno/docs/Certification 2.1 Roadmap.confluence.md.md b/bruno/docs/Certification 2.1 Roadmap.confluence.md.md new file mode 100644 index 0000000..b10d6e7 --- /dev/null +++ b/bruno/docs/Certification 2.1 Roadmap.confluence.md.md @@ -0,0 +1,122 @@ +# Roadmap for Certification 2.1 (DataStandard 5) + +Spike of this research: https://edfi.atlassian.net/browse/CERT-232 + +**Team size:** 3 developers, 1 tester, 1 product owner +**Team capacity:** 40% (due to many running projects in parallel) +**Sprint length:** 2 weeks · **Full-capacity velocity:** ~35 pts/sprint · **Effective velocity at 40%:** ~14 pts/sprint +**Roadmap start date:** May 28, 2026 + +--- + +## Summary + +| Metric | Value | +|---|---| +| Total dev story points | 42 | +| Completed (Sprint 1 + CERT-241) | 17 ✅ | +| **Remaining dev points** | **25 pts** | +| Phase 1 points (gated) | 8 pts | +| Phase 2 points (parallel) | 28 pts | +| Sprints needed | 3 | +| **Projected dev ETA** | **July 8, 2026** | +| With QA buffer sprint | July 15, 2026 | +| Documentation tickets | 7 (parallel, unestimated) | + +--- + +## Tickets missing to develop as part of Certification 2.1 + +### Development Tickets + +| Ticket | Points | Status | Phase | Category | +|---|---|---|---|---| +| [CERT-241](https://edfi.atlassian.net/browse/CERT-241) | 3 | ✅ Done | 1 | DS5 boilerplate | +| [CERT-247](https://edfi.atlassian.net/browse/CERT-247) | 5 | ✅ Done | 1 – Gated | Regression tests | +| [CERT-257](https://edfi.atlassian.net/browse/CERT-257) | 3 | ✅ Done | 1 – Gated | Pipeline stabilization | +| [CERT-242](https://edfi.atlassian.net/browse/CERT-242) | 2 | 🔵 Open | 2 – Parallel | Scenario updates | +| [CERT-243](https://edfi.atlassian.net/browse/CERT-243) | 3 | 🔵 Open | 2 – Parallel | New scenario | +| [CERT-244](https://edfi.atlassian.net/browse/CERT-244) | 3 | 🔵 Open | 2 – Parallel | New scenario | +| [CERT-245](https://edfi.atlassian.net/browse/CERT-245) | 3 | 🔵 Open | 2 – Parallel | New scenario | +| [CERT-246](https://edfi.atlassian.net/browse/CERT-246) | 3 | 🔵 Open | 2 – Parallel | Scenario updates | +| [CERT-239](https://edfi.atlassian.net/browse/CERT-239) | 3 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-237](https://edfi.atlassian.net/browse/CERT-237) | 3 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-250](https://edfi.atlassian.net/browse/CERT-250) | 1 | 🔵 Open | 2 – Parallel | Scenario updates | +| [CERT-252](https://edfi.atlassian.net/browse/CERT-252) | 2 | 🔵 Open | 2 – Parallel | Scenario updates | +| [CERT-256](https://edfi.atlassian.net/browse/CERT-256) | 5 | 🔵 Open | 2 – Parallel | Scenario updates | +| [CERT-258](https://edfi.atlassian.net/browse/CERT-258) | 1 | 🔵 Open | 2 – Parallel | Regression follow-up | +| [CERT-259](https://edfi.atlassian.net/browse/CERT-259) | 2 | 🔵 Open | 2 – Parallel | Regression follow-up | + +> **Note:** Estimations are approximations based on original ticket estimates and complexity. They do **not** account for AI-assisted development using `bruno/ai-docs/spec.md`, which could meaningfully accelerate Phase 2 scenario tickets. + +### Documentation Tickets + +| Ticket | Status | Notes | +|---|---|---| +| [CERT-249](https://edfi.atlassian.net/browse/CERT-249) | 🔵 Open | Documentation updates | +| [CERT-248](https://edfi.atlassian.net/browse/CERT-248) | 🔵 Open | Documentation updates | +| [CERT-235](https://edfi.atlassian.net/browse/CERT-235) | 🔵 Open | Documentation updates | +| [CERT-251](https://edfi.atlassian.net/browse/CERT-251) | 🔵 Open | Documentation updates | +| [CERT-253](https://edfi.atlassian.net/browse/CERT-253) | 🔵 Open | Documentation updates | +| [CERT-254](https://edfi.atlassian.net/browse/CERT-254) | 🔵 Open | Documentation updates | +| [CERT-255](https://edfi.atlassian.net/browse/CERT-255) | 🔵 Open | Documentation updates | + +> Although these tickets are assigned to Certification 2.2, they will be moved to Certification 2.1, as they are part of the work that needs to be done for Certification 2.1, and they are not being worked on as part of Certification 2.2. + +--- + +## Dependency Graph + +Sprint flow: CERT-241 (Done) enables Sprint 1, which unblocks Sprint 2 and Sprint 3 sequentially. Most tickets within each sprint run in parallel. Ticket details are grouped by sprint assignment. + +[DIAGRAM: dependency_graph.png] + +--- + +## Sprint Gantt Chart + +[DIAGRAM: sprint_gantt.png] + +--- + +## Burndown Chart + +Story points remaining at the end of each sprint, based on the dependency-gated execution plan. + +[DIAGRAM: burndown_xy.png] + +> Sprint 1 burns 14 pts (exactly at capacity). Sprint 2 burns 11 pts (~3 pts buffer). Sprint 3 burns 11 pts including CERT-256 (5 pts, high complexity) — ~3 pts of buffer remaining for bugs or unplanned work. + +--- + +## ETA & Risks + +### Projected Timeline + +| Milestone | Date | +|---|---| +| Roadmap start | May 28, 2026 | +| Sprint 1 end — Phase 1 parallel + scenario updates (14 pts) | June 10, 2026 | +| Sprint 2 end — scenario updates + new scenarios (11 pts) | June 24, 2026 | +| Sprint 3 end — **Dev complete**, CERT-256 + new scenarios (11 pts) | **July 8, 2026** | +| QA buffer end (1 week) | July 15, 2026 | +| Documentation complete (target) | July 8, 2026 | + +### Risk Register + +| Risk | Impact | Mitigation | +|---|---|---| +| CERT-247 regression tests may uncover new defects or unestimated issues | Unplanned scope added mid-Sprint 1 or Sprint 2 | Log all findings as new tickets during Sprint 1; triage and size at Sprint 1 review before locking Sprint 2 scope | +| Point estimates were sized by a domain expert; 2 of 3 developers are new to the codebase | Sprint 1–2 effective velocity may drop to ~10–12 pts — could add 1 sprint (worst-case ETA: **July 22, 2026**) | Pair new devs on simpler scenario update tickets first; re-baseline velocity at Sprint 1 review | +| Estimations not yet refined | Scope creep in Phase 2 | Refine tickets at Sprint 1 kickoff; add buffer sprint if >35 pts after refinement | +| AI tooling (`spec.md`) not factored into estimates | ETA may be pessimistic | Re-baseline after Sprint 1 actuals are known | +| 7 doc tickets have no point estimates | Doc lane may slip | Size docs at Sprint 1 planning; allocate dedicated doc assignee | +| CERT-236 explicitly out of scope | — | Confirmed won't-do; no action needed | + +--- + +(Notes) + +- Mermaid source files live in `bruno/docs/diagrams/`. If you want editable diagrams in Confluence, consider installing a Mermaid macro from the Atlassian Marketplace; otherwise upload the generated PNGs. + +- Added tickets from regression testing: [CERT-258](https://edfi.atlassian.net/browse/CERT-258) (1pt) and [CERT-259](https://edfi.atlassian.net/browse/CERT-259) (2pt). Both are scheduled in Sprint 2. diff --git a/bruno/docs/Certification 2.1 Roadmap.md b/bruno/docs/Certification 2.1 Roadmap.md index b5ca450..227b0e9 100644 --- a/bruno/docs/Certification 2.1 Roadmap.md +++ b/bruno/docs/Certification 2.1 Roadmap.md @@ -13,15 +13,15 @@ Spike of this research: https://edfi.atlassian.net/browse/CERT-232 | Metric | Value | |---|---| -| Total dev story points | 39 | -| Completed (CERT-241) | 3 pts ✅ | -| **Remaining dev points** | **36 pts** | +| Total dev story points | 42 | +| Completed (All Sprints) | 42 pts ✅ | +| **Remaining dev points** | **0 pts** | | Phase 1 points (gated) | 8 pts | | Phase 2 points (parallel) | 28 pts | | Sprints needed | 3 | | **Projected dev ETA** | **July 8, 2026** | | With QA buffer sprint | July 15, 2026 | -| Documentation tickets | 7 (parallel, unestimated) | +| Documentation tickets | 8 (parallel, unestimated) | ## Tickets missing to develop as part of Certification 2.1 @@ -30,32 +30,33 @@ Spike of this research: https://edfi.atlassian.net/browse/CERT-232 | Ticket | Points | Status | Phase | Category | |---|---|---|---|---| | [CERT-241](https://edfi.atlassian.net/browse/CERT-241) | 3 | ✅ Done | 1 | DS5 boilerplate | -| [CERT-247](https://edfi.atlassian.net/browse/CERT-247) | 5 | 🔵 Open | 1 – Gated | Regression tests | -| [CERT-257](https://edfi.atlassian.net/browse/CERT-257) | 3 | 🔵 Open | 1 – Gated | Pipeline stabilization | -| [CERT-242](https://edfi.atlassian.net/browse/CERT-242) | 2 | 🔵 Open | 2 – Parallel | Scenario updates | -| [CERT-243](https://edfi.atlassian.net/browse/CERT-243) | 3 | 🔵 Open | 2 – Parallel | New scenario | -| [CERT-244](https://edfi.atlassian.net/browse/CERT-244) | 3 | 🔵 Open | 2 – Parallel | New scenario | -| [CERT-245](https://edfi.atlassian.net/browse/CERT-245) | 3 | 🔵 Open | 2 – Parallel | New scenario | -| [CERT-246](https://edfi.atlassian.net/browse/CERT-246) | 3 | 🔵 Open | 2 – Parallel | Scenario updates | -| [CERT-239](https://edfi.atlassian.net/browse/CERT-239) | 3 | 🔵 Open | 2 – Parallel | Scenario updates | -| [CERT-237](https://edfi.atlassian.net/browse/CERT-237) | 3 | 🔵 Open | 2 – Parallel | Scenario updates | -| [CERT-250](https://edfi.atlassian.net/browse/CERT-250) | 1 | 🔵 Open | 2 – Parallel | Scenario updates | -| [CERT-252](https://edfi.atlassian.net/browse/CERT-252) | 2 | 🔵 Open | 2 – Parallel | Scenario updates | -| [CERT-256](https://edfi.atlassian.net/browse/CERT-256) | 5 | 🔵 Open | 2 – Parallel | Scenario updates | - +| [CERT-247](https://edfi.atlassian.net/browse/CERT-247) | 5 | ✅ Done | 1 – Gated | Regression tests | +| [CERT-257](https://edfi.atlassian.net/browse/CERT-257) | 3 | ✅ Done | 1 – Gated | Pipeline stabilization | +| [CERT-242](https://edfi.atlassian.net/browse/CERT-242) | 2 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-243](https://edfi.atlassian.net/browse/CERT-243) | 3 | ✅ Done | 2 – Parallel | New scenario | +| [CERT-244](https://edfi.atlassian.net/browse/CERT-244) | 3 | ✅ Done | 2 – Parallel | New scenario | +| [CERT-245](https://edfi.atlassian.net/browse/CERT-245) | 3 | ✅ Done | 2 – Parallel | New scenario | +| [CERT-246](https://edfi.atlassian.net/browse/CERT-246) | 3 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-239](https://edfi.atlassian.net/browse/CERT-239) | 3 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-237](https://edfi.atlassian.net/browse/CERT-237) | 3 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-250](https://edfi.atlassian.net/browse/CERT-250) | 1 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-256](https://edfi.atlassian.net/browse/CERT-256) | 5 | ✅ Done | 2 – Parallel | Scenario updates | +| [CERT-258](https://edfi.atlassian.net/browse/CERT-258) | 1 | ✅ Done | 2 – Parallel | Regression follow-up | +| [CERT-259](https://edfi.atlassian.net/browse/CERT-259) | 2 | ✅ Done | 2 – Parallel | Regression follow-up | > **Note:** Estimations are approximations based on original ticket estimates and complexity. They do **not** account for AI-assisted development using `bruno/ai-docs/spec.md`, which could meaningfully accelerate Phase 2 scenario tickets. ### Documentation Tickets | Ticket | Status | Notes | |---|---|---| -| [CERT-249](https://edfi.atlassian.net/browse/CERT-249) | 🔵 Open | Documentation updates | -| [CERT-248](https://edfi.atlassian.net/browse/CERT-248) | 🔵 Open | Documentation updates | -| [CERT-235](https://edfi.atlassian.net/browse/CERT-235) | 🔵 Open | Documentation updates | -| [CERT-251](https://edfi.atlassian.net/browse/CERT-251) | 🔵 Open | Documentation updates | -| [CERT-253](https://edfi.atlassian.net/browse/CERT-253) | 🔵 Open | Documentation updates | -| [CERT-254](https://edfi.atlassian.net/browse/CERT-254) | 🔵 Open | Documentation updates | -| [CERT-255](https://edfi.atlassian.net/browse/CERT-255) | 🔵 Open | Documentation updates | +| [CERT-249](https://edfi.atlassian.net/browse/CERT-249) | ✅ Done | Documentation updates | +| [CERT-248](https://edfi.atlassian.net/browse/CERT-248) | ✅ Done | Documentation updates | +| [CERT-235](https://edfi.atlassian.net/browse/CERT-235) | ✅ Done | Documentation updates | +| [CERT-251](https://edfi.atlassian.net/browse/CERT-251) | ✅ Done | Documentation updates | +| [CERT-253](https://edfi.atlassian.net/browse/CERT-253) | ✅ Done | Documentation updates | +| [CERT-254](https://edfi.atlassian.net/browse/CERT-254) | ✅ Done | Documentation updates | +| [CERT-255](https://edfi.atlassian.net/browse/CERT-255) | ✅ Done | Documentation updates | +| [CERT-260](https://edfi.atlassian.net/browse/CERT-260) | ✅ Done | Documentation updates | > Although these tickets are assigned to Certification 2.2, they will be moved to Certification 2.1, as they are part of the work that needs to be done for Certification 2.1, and they are not being worked on as part of Certification 2.2. @@ -66,28 +67,35 @@ Spike of this research: https://edfi.atlassian.net/browse/CERT-232 Sprint flow: CERT-241 (Done) enables Sprint 1, which unblocks Sprint 2 and Sprint 3 sequentially. Most tickets within each sprint run in parallel. Ticket details are grouped by sprint assignment. ```mermaid -flowchart LR +flowchart TB C241["✅ CERT-241\nDS5 Boilerplate · Done"] - subgraph S1 ["Sprint 1 · May 28–Jun 10 · 14 pts"] - C247["CERT-247\nRegression Tests\n5 pts · QA"] - C257["CERT-257\nPipeline Stabilization\n3 pts · Dev"] - C239["CERT-239 · 3 pts"] - C237["CERT-237 · 3 pts"] + direction LR + C247["✅ CERT-247\nRegression Tests\n5 pts · QA"] + C257["✅ CERT-257\nPipeline Stabilization\n3 pts · Dev"] + C239["✅ CERT-239 · 3 pts"] + C237["✅ CERT-237 · 3 pts"] + C247 ~~~ C257 ~~~ C239 ~~~ C237 end - subgraph S2 ["Sprint 2 · Jun 11–Jun 24 · 11 pts"] - C252["CERT-252 · 2 pts"] - C242["CERT-242 · 2 pts"] - C250["CERT-250 · 1 pt"] - C243["CERT-243 · 3 pts"] - C244["CERT-244 · 3 pts"] + subgraph S2 ["Sprint 2 · Jun 11–Jun 24 · 14 pts"] + direction LR + C252["✅ CERT-252 · 2 pts"] + C242["✅ CERT-242 · 2 pts"] + C250["✅ CERT-250 · 1 pt"] + C243["✅ CERT-243 · 3 pts"] + C244["✅ CERT-244 · 3 pts"] + C258["✅ CERT-258 · 1 pt"] + C259["✅ CERT-259 · 2 pts"] + C252 ~~~ C242 ~~~ C250 ~~~ C243 ~~~ C244 ~~~ C258 ~~~ C259 end subgraph S3 ["Sprint 3 · Jun 25–Jul 8 · 11 pts"] - C256["CERT-256 · 5 pts"] - C245["CERT-245 · 3 pts"] - C246["CERT-246 · 3 pts"] + direction LR + C256["✅ CERT-256 · 5 pts"] + C245["✅ CERT-245 · 3 pts"] + C246["✅ CERT-246 · 3 pts"] + C256 ~~~ C245 ~~~ C246 end C241 --> S1 --> S2 --> S3 @@ -106,30 +114,32 @@ gantt axisFormat %b %d section Sprint 1 · May 28–Jun 10 (14 pts) - CERT-241 DS5 Boilerplate (Done) :done, cert241, 2026-05-01, 2026-05-27 - CERT-247 Regression Tests (QA) :active, cert247, 2026-05-28, 2026-06-10 - CERT-257 Pipeline Stabilization (Dev) :active, cert257, 2026-05-28, 2026-06-10 - CERT-239 Scenario Updates :cert239, 2026-05-28, 2026-06-10 - CERT-237 Scenario Updates :cert237, 2026-05-28, 2026-06-10 - - section Sprint 2 · Jun 11–Jun 24 (11 pts) - CERT-252 Scenario Updates :cert252, 2026-06-11, 2026-06-24 - CERT-242 Scenario Updates :cert242, 2026-06-11, 2026-06-24 - CERT-250 Scenario Updates :cert250, 2026-06-11, 2026-06-24 - CERT-243 New Scenario :cert243, 2026-06-11, 2026-06-24 - CERT-244 New Scenario :cert244, 2026-06-11, 2026-06-24 + CERT-241 DS5 Boilerplate :done, cert241, 2026-05-15, 2026-05-27 + CERT-247 Regression Tests (QA) :done, cert247, 2026-05-28, 2026-06-10 + CERT-257 Pipeline Stabilization :done, cert257, 2026-05-28, 2026-06-10 + CERT-239 Scenario Updates :done, cert239, 2026-05-28, 2026-06-10 + CERT-237 Scenario Updates :done, cert237, 2026-05-28, 2026-06-10 + + section Sprint 2 · Jun 11–Jun 24 (14 pts) + CERT-252 Scenario Updates :done, cert252, 2026-06-11, 2026-06-24 + CERT-242 Scenario Updates :done, cert242, 2026-06-11, 2026-06-24 + CERT-250 Scenario Updates :done, cert250, 2026-06-11, 2026-06-24 + CERT-243 New Scenario :done, cert243, 2026-06-11, 2026-06-24 + CERT-244 New Scenario :done, cert244, 2026-06-11, 2026-06-24 + CERT-258 Regression follow-up :done, cert258, 2026-06-11, 2026-06-24 + CERT-259 SampleData scripts :done, cert259, 2026-06-11, 2026-06-24 section Sprint 3 · Jun 25–Jul 8 (11 pts) - CERT-256 Scenario Updates :cert256, 2026-06-25, 2026-07-08 - CERT-245 New Scenario :cert245, 2026-06-25, 2026-07-08 - CERT-246 Scenario Updates :cert246, 2026-06-25, 2026-07-08 + CERT-256 Scenario Updates :done, cert256, 2026-06-25, 2026-07-08 + CERT-245 New Scenario :done, cert245, 2026-06-25, 2026-07-08 + CERT-246 Scenario Updates :done, cert246, 2026-06-25, 2026-07-08 section Documentation (Parallel) - CERT-248 / 235 / 249 / 251 :doc1, 2026-05-28, 2026-06-10 - CERT-253 / 254 / 255 :doc2, 2026-06-11, 2026-07-08 + CERT-248 / 235 / 249 / 251 :doc1, 2026-05-28, 2026-06-10 + CERT-253 / 254 / 255 :doc2, 2026-06-11, 2026-07-08 section QA Buffer (Optional) - QA Validation & Sign-off :crit, qa, 2026-07-09, 2026-07-15 + QA & Sign-off :crit, qa, 2026-07-09, 2026-07-15 ``` --- @@ -143,8 +153,8 @@ xychart-beta title "Story Point Burndown — Remaining Dev Points" x-axis ["Start (May 28)", "Sprint 1 End (Jun 10)", "Sprint 2 End (Jun 24)", "Sprint 3 End (Jul 8)"] y-axis "Story Points Remaining" 0 --> 40 - line [36, 22, 11, 0] - bar [36, 22, 11, 0] + line [36, 25, 14, 0] + bar [36, 25, 14, 0] ``` > Sprint 1 burns 14 pts (exactly at capacity). Sprint 2 burns 11 pts (~3 pts buffer). Sprint 3 burns 11 pts including CERT-256 (5 pts, high complexity) — ~3 pts of buffer remaining for bugs or unplanned work. diff --git a/bruno/docs/diagrams/README.md b/bruno/docs/diagrams/README.md new file mode 100644 index 0000000..9e5ee15 --- /dev/null +++ b/bruno/docs/diagrams/README.md @@ -0,0 +1,119 @@ +# Diagrams — Maintenance Guide + +This folder holds everything needed to maintain and regenerate the roadmap diagrams for the Confluence page and the original Markdown document. + +--- + +## File inventory + +| File | Type | Purpose | +|---|---|---| +| `dependency_graph.mmd` | Mermaid source | Flowchart — sprint dependency flow (TB layout, LR inside subgraphs) | +| `sprint_gantt.mmd` | Mermaid source | Gantt — sprint timeline with ticket labels and dates | +| `burndown_xy.mmd` | Mermaid source | XY chart — story point burndown across sprints | +| `render_diagrams.ps1` | PowerShell script | Renders all `.mmd` files → `.png` using mermaid-cli | +| `*.png` | Generated images | Upload these to Confluence; re-generate as needed | + +The `.mmd` files are the **source of truth**. Never edit the PNGs directly — always edit the `.mmd` then re-render. + +--- + +## Tools used and why + +### mermaid-cli (`@mermaid-js/mermaid-cli`) +Used to render `.mmd` diagram sources into PNG images. It runs headless Chrome under the hood via Puppeteer to produce pixel-accurate renders that match what VS Code and GitHub Markdown previews show. + +- **Not** used for the Confluence text content — only for diagrams. +- PNGs are preferred over SVGs here because the SVG renderer produces minor layout differences compared to the browser preview. + +### Pandoc (not currently used) +Pandoc could convert the Markdown to Confluence Storage Format (XHTML) automatically. It is **not used in this workflow** because: +- The Confluence document (`Certification 2.1 Roadmap.confluence.md`) is maintained manually as a close mirror of the original Markdown. +- Mermaid blocks are replaced with `[DIAGRAM: xxx.png]` placeholders and actual PNGs are uploaded manually. + +If you want to automate the Markdown → Confluence conversion in the future, look into `pandoc --to=jira` or the Confluence REST API with Pandoc's `--to=html`. + +--- + +## Prerequisites (one-time setup) + +1. **Node.js** (v18+): https://nodejs.org/ + +2. **mermaid-cli**: + ```powershell + npm install -g @mermaid-js/mermaid-cli + ``` + +3. **Chrome or Edge** must be installed (used by Puppeteer internally). The render script auto-detects common install paths. If it can't find a browser, set: + ```powershell + $env:PUPPETEER_EXECUTABLE_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe" + ``` + Or install a headless shell directly: + ```powershell + npx puppeteer browsers install chrome-headless-shell + ``` + +--- + +## How to update a diagram + +1. Open the relevant `.mmd` file (e.g., `sprint_gantt.mmd`). +2. Edit the Mermaid source — refer to https://mermaid.js.org/syntax/ for syntax help. +3. Preview changes in VS Code using the **Markdown Preview** or a Mermaid extension. +4. Keep the corresponding block in `../Certification 2.1 Roadmap.md` in sync with the same edits. +5. Re-render (see below). + +--- + +## How to re-generate PNGs + +From this folder in PowerShell: + +```powershell +.\render_diagrams.ps1 +``` + +This regenerates all three PNGs in place. The script: +- Auto-detects Chrome/Edge for Puppeteer. +- Applies per-diagram width/height/scale settings for best quality. +- Falls back from a global `mmdc` install to `npx @mermaid-js/mermaid-cli` if needed. + +To render a single diagram manually: +```powershell +npx @mermaid-js/mermaid-cli -i sprint_gantt.mmd -o sprint_gantt.png -w 1600 -H 700 --scale 2 +``` + +--- + +## The Confluence document + +`../Certification 2.1 Roadmap.confluence.md` is the Confluence-ready mirror of `../Certification 2.1 Roadmap.md`. Differences from the original: + +- Mermaid code blocks are replaced with `[DIAGRAM: xxx.png]` placeholders (images are uploaded manually). +- Intended to be copy-pasted or uploaded to Confluence as-is; no Mermaid plugin required. + +**Keep it in sync with the original Markdown** whenever either document changes. The two files have identical sections — update both at the same time. + +--- + +## How to update the Confluence page + +1. Edit `../Certification 2.1 Roadmap.md` (the source of truth). +2. Apply the same text changes to `../Certification 2.1 Roadmap.confluence.md` — replace any Mermaid blocks with `[DIAGRAM: xxx.png]` if you added new diagrams. +3. Re-generate PNGs if any `.mmd` was changed (see above). +4. Open the Confluence page → Edit. +5. For each updated diagram placeholder: + - Remove the old image on the page. + - Re-upload the new PNG from this folder (Insert → Files and images). +6. Paste any updated text sections from `Certification 2.1 Roadmap.confluence.md`. +7. Publish. + +> **Tip:** if your Confluence instance has a Mermaid macro (available from the Atlassian Marketplace), you can replace the PNG images with the macro and paste the `.mmd` contents directly — no rendering step needed. + +--- + +## Diagram layout notes + +- **`dependency_graph.mmd`**: top-level `flowchart TB` (sprints flow top→bottom). Each subgraph uses `direction LR` with invisible links (`~~~`) between nodes to force the tickets inside each sprint to display left→right. +- **`sprint_gantt.mmd`**: standard Mermaid `gantt` block. Labels are plain text (no quotes, no HTML) to avoid rendering artefacts. Dates reflect actual sprint boundaries. +- **`burndown_xy.mmd`**: `xychart-beta` with both a `line` and `bar` series showing remaining story points at each sprint boundary. diff --git a/bruno/docs/diagrams/burndown_xy.mmd b/bruno/docs/diagrams/burndown_xy.mmd new file mode 100644 index 0000000..c44c3a3 --- /dev/null +++ b/bruno/docs/diagrams/burndown_xy.mmd @@ -0,0 +1,6 @@ +xychart-beta + title "Story Point Burndown — Remaining Dev Points" + x-axis ["Start (May 28)", "Sprint 1 End (Jun 10)", "Sprint 2 End (Jun 24)", "Sprint 3 End (Jul 8)"] + y-axis "Story Points Remaining" 0 --> 40 + line [36, 25, 14, 0] + bar [36, 25, 14, 0] diff --git a/bruno/docs/diagrams/dependency_graph.mmd b/bruno/docs/diagrams/dependency_graph.mmd new file mode 100644 index 0000000..7f44574 --- /dev/null +++ b/bruno/docs/diagrams/dependency_graph.mmd @@ -0,0 +1,35 @@ +flowchart TB + C241["✅ CERT-241\nDS5 Boilerplate · Done"] + + subgraph S1 ["Sprint 1 · May 28–Jun 10 · 14 pts"] + direction LR + C247["✅ CERT-247\nRegression Tests\n5 pts · QA"] + C257["✅ CERT-257\nPipeline Stabilization\n3 pts · Dev"] + C239["✅ CERT-239 · 3 pts"] + C237["✅ CERT-237 · 3 pts"] + C247 ~~~ C257 ~~~ C239 ~~~ C237 + end + + subgraph S2 ["Sprint 2 · Jun 11–Jun 24 · 14 pts"] + direction LR + C252["✅ CERT-252 · 2 pts"] + C242["✅ CERT-242 · 2 pts"] + C250["✅ CERT-250 · 1 pt"] + C243["✅ CERT-243 · 3 pts"] + C244["✅ CERT-244 · 3 pts"] + C258["✅ CERT-258 · 1 pt"] + C259["✅ CERT-259 · 2 pts"] + C252 ~~~ C242 ~~~ C250 ~~~ C243 ~~~ C244 ~~~ C258 ~~~ C259 + end + + subgraph S3 ["Sprint 3 · Jun 25–Jul 8 · 11 pts"] + direction LR + C256["✅ CERT-256 · 5 pts"] + C245["✅ CERT-245 · 3 pts"] + C246["✅ CERT-246 · 3 pts"] + C256 ~~~ C245 ~~~ C246 + end + + C241 --> S1 --> S2 --> S3 + + style C241 fill:#22c55e,color:#fff diff --git a/bruno/docs/diagrams/render_diagrams.ps1 b/bruno/docs/diagrams/render_diagrams.ps1 new file mode 100644 index 0000000..a0530c1 --- /dev/null +++ b/bruno/docs/diagrams/render_diagrams.ps1 @@ -0,0 +1,104 @@ +# Render Mermaid diagrams to PNG using mermaid-cli (mmdc) +# Usage: +# 1. Install Node.js + npm +# 2. npm install -g @mermaid-js/mermaid-cli +# 3. Run this script in PowerShell: .\render_diagrams.ps1 + +$diagrams = @( + 'dependency_graph.mmd', + 'sprint_gantt.mmd', + 'burndown_xy.mmd' +) + +# Try to find a system mmdc, otherwise fall back to npx +$mmdcCmd = (Get-Command mmdc -ErrorAction SilentlyContinue)?.Source +$npxCmd = (Get-Command npx -ErrorAction SilentlyContinue)?.Source + +if (-not $mmdcCmd -and -not $npxCmd) { + Write-Error "Neither 'mmdc' nor 'npx' were found in PATH.\nInstall mermaid-cli (npm i -g @mermaid-js/mermaid-cli) or ensure Node/npm is available for npx." + exit 1 +} + +# Try to locate a local Chrome/Edge executable and set PUPPETEER_EXECUTABLE_PATH +function Find-ChromeExecutable { + # Prefer explicit common install locations for this environment first + $candidates = @( + 'C:\Program Files\Google\Chrome\Application\chrome.exe', + 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe', + "$Env:ProgramFiles\Google\Chrome\Application\chrome.exe", + "$Env:ProgramFiles(x86)\Google\Chrome\Application\chrome.exe", + "$Env:LocalAppData\Google\Chrome\Application\chrome.exe", + "$Env:ProgramFiles\Microsoft\Edge\Application\msedge.exe", + "$Env:ProgramFiles(x86)\Microsoft\Edge\Application\msedge.exe", + "$Env:LocalAppData\Microsoft\Edge\Application\msedge.exe" + ) + + foreach ($p in $candidates) { + if ($p -and (Test-Path $p)) { return $p } + } + + # Fall back to where.exe lookup if available + try { + $chromeWhere = (where.exe chrome 2>$null) -split "\r?\n" | Select-Object -First 1 + if ($chromeWhere -and (Test-Path $chromeWhere)) { return $chromeWhere } + } catch { } + try { + $edgeWhere = (where.exe msedge 2>$null) -split "\r?\n" | Select-Object -First 1 + if ($edgeWhere -and (Test-Path $edgeWhere)) { return $edgeWhere } + } catch { } + + return $null +} + +$chromePath = Find-ChromeExecutable +if ($env:PUPPETEER_EXECUTABLE_PATH -and (Test-Path $env:PUPPETEER_EXECUTABLE_PATH)) { + Write-Host "Using PUPPETEER_EXECUTABLE_PATH from environment: $env:PUPPETEER_EXECUTABLE_PATH" -ForegroundColor Green +} else { + $chromePath = Find-ChromeExecutable + if ($chromePath) { + Write-Host "Found browser executable: $chromePath" -ForegroundColor Green + $env:PUPPETEER_EXECUTABLE_PATH = $chromePath + } else { + Write-Host "No Chrome/Edge executable found in common locations." -ForegroundColor Yellow + Write-Host "If you have Chrome/Edge installed, set the environment variable PUPPETEER_EXECUTABLE_PATH to its path, or run 'npx puppeteer browsers install chrome-headless-shell' to install a headless browser." -ForegroundColor Yellow + } +} + +foreach ($d in $diagrams) { + $in = Join-Path -Path $PSScriptRoot -ChildPath $d + if (-not (Test-Path $in)) { + Write-Warning "Source not found: $in -- skipping." + continue + } + $out = [System.IO.Path]::ChangeExtension($in, '.png') + Write-Host "Rendering $in -> $out (PNG)" + + # Per-diagram rendering hints to improve clarity + $opts = @() + if ($d -match 'dependency') { + $opts = @('-w','2000','-H','400','--scale','2') + } elseif ($d -match 'gantt') { + $opts = @('-w','1600','-H','700','--scale','2') + } elseif ($d -match 'burndown') { + $opts = @('-w','1000','-H','500','--scale','2') + } else { + $opts = @('--scale','2') + } + + if ($mmdcCmd) { + & $mmdcCmd -i $in -o $out @opts + $rc = $LASTEXITCODE + if ($rc -ne 0) { + Write-Warning "mmdc exited with code $rc for $d. Trying npx fallback if available." + if (-not $npxCmd) { throw "Rendering failed and no npx available to fallback." } + } else { continue } + } + + if ($npxCmd) { + & $npxCmd @mermaid-js/mermaid-cli -i $in -o $out @opts + $rc = $LASTEXITCODE + if ($rc -ne 0) { throw "npx mermaid-cli failed with exit code $rc for $d" } + } +} + +Write-Host "Done. Generated PNGs are next to the .mmd sources. Upload them to the Confluence page." diff --git a/bruno/docs/diagrams/sprint_gantt.mmd b/bruno/docs/diagrams/sprint_gantt.mmd new file mode 100644 index 0000000..b74e89f --- /dev/null +++ b/bruno/docs/diagrams/sprint_gantt.mmd @@ -0,0 +1,32 @@ +gantt + title Certification 2.1 — Sprint Plan + dateFormat YYYY-MM-DD + axisFormat %b %d + + section Sprint 1 · May 28–Jun 10 (14 pts) + CERT-241 DS5 Boilerplate :done, cert241, 2026-05-15, 2026-05-27 + CERT-247 Regression Tests (QA) :done, cert247, 2026-05-28, 2026-06-10 + CERT-257 Pipeline Stabilization :done, cert257, 2026-05-28, 2026-06-10 + CERT-239 Scenario Updates :done, cert239, 2026-05-28, 2026-06-10 + CERT-237 Scenario Updates :done, cert237, 2026-05-28, 2026-06-10 + + section Sprint 2 · Jun 11–Jun 24 (14 pts) + CERT-252 Scenario Updates :done, cert252, 2026-06-11, 2026-06-24 + CERT-242 Scenario Updates :done, cert242, 2026-06-11, 2026-06-24 + CERT-250 Scenario Updates :done, cert250, 2026-06-11, 2026-06-24 + CERT-243 New Scenario :done, cert243, 2026-06-11, 2026-06-24 + CERT-244 New Scenario :done, cert244, 2026-06-11, 2026-06-24 + CERT-258 Regression follow-up :done, cert258, 2026-06-11, 2026-06-24 + CERT-259 SampleData scripts :done, cert259, 2026-06-11, 2026-06-24 + + section Sprint 3 · Jun 25–Jul 8 (11 pts) + CERT-256 Scenario Updates :done, cert256, 2026-06-25, 2026-07-08 + CERT-245 New Scenario :done, cert245, 2026-06-25, 2026-07-08 + CERT-246 Scenario Updates :done, cert246, 2026-06-25, 2026-07-08 + + section Documentation (Parallel) + CERT-248 / 235 / 249 / 251 :doc1, 2026-05-28, 2026-06-10 + CERT-253 / 254 / 255 :doc2, 2026-06-11, 2026-07-08 + + section QA Buffer (Optional) + QA & Sign-off :crit, qa, 2026-07-09, 2026-07-15