diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..287c30e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,109 @@ +# GitHub Copilot Instructions for BOSS + +These instructions apply to AI-assisted work in this repository. Keep changes small, reviewable, and grounded in the repo's plan documents and tests. + +## Repository Context + +BOSS translates BuildingSync XML into OpenStudio workflows. The current production behavior is baseline-only: write `baseline/in.osw`, then run that baseline workflow. Preserve that behavior unless the current task explicitly changes it. + +Important project context lives in: + +- `README.md` — setup, CLI use, current baseline workflow, and test commands. +- `docs/scenario_workflow_translation_plan.md` — source of truth for the scenario workflow translation roadmap. +- `spec/files/v2.7.0/` — BuildingSync fixtures used by unit and integration tests. +- `spec/tests/` — existing RSpec coverage patterns. + +## Related Repositories and Dependencies + +BOSS sits between BuildingSync XML data and OpenStudio workflow execution. Several sibling repositories are useful for context, but this repository should remain the implementation target unless the user explicitly asks for cross-repo edits. + +- `../schema` — BuildingSync schema source and examples. Use it to verify XML structure, element names, `ScenarioType`, `PackageOfMeasures`, `Measure`, `MeasureID`, and schema-version behavior. Do not change schema files while working on BOSS unless the task explicitly requests schema work. +- `../BuildingSync-gem` — legacy Ruby translator with older scenario-to-OpenStudio workflow behavior. Use it as historical reference for workflow sequencing, scenario/result concepts, and mapping ideas. Do not copy code or mappings blindly; verify every behavior against current BOSS dependencies and BuildingSync v2.7 fixtures. +- `../buildingsync-measures-gem` — related BuildingSync/OpenStudio measure work. Use it to understand existing measures and BuildingSync-oriented OpenStudio measure patterns when mapping scenarios. Treat it as reference unless the user asks for changes there. +- `../openstudio-standards` — OpenStudio standards logic used by the `openstudio-standards` gem. BOSS currently depends on the gem version declared in `BOSS.gemspec`; do not edit the sibling standards repo as part of BOSS work unless the issue is proven to belong upstream and the user asks for a cross-repo change. +- OpenStudio measure gems declared in `BOSS.gemspec` — `openstudio-common-measures`, `openstudio-ee`, `openstudio-extension`, `openstudio-model-articulation`, and `openstudio-standards`. These define the measure directories, arguments, runner behavior, and standards templates BOSS can rely on. When adding scenario mappings, verify target measure names and arguments against the installed gem versions used by BOSS. + +Rule of thumb: inspect sibling repos freely for context; keep BOSS PRs scoped to BOSS files. If a required fix appears to belong in a sibling repo or upstream gem, record that clearly as follow-up work instead of mixing it into a BOSS PR. + +## Scenario Workflow Translation Work + +For any work related to translating BuildingSync `PackageOfMeasures` scenarios into OpenStudio workflows: + +1. Read `docs/scenario_workflow_translation_plan.md` before editing code. +2. Check the git state and recent commits to determine which PR steps have already landed. +3. Start with the first PR step in the plan whose green-light criteria are not satisfied. +4. Keep changes scoped to that PR step unless a small supporting edit is necessary to make the step coherent. +5. Treat each step's green-light criteria as the definition of done and red-light criteria as merge blockers. +6. Preserve existing baseline-only API, CLI behavior, output layout, and integration tests unless the current PR step explicitly changes them. +7. If implementation decisions change the roadmap, update `docs/scenario_workflow_translation_plan.md` in the same PR. + +Do not jump ahead to later PR steps just because the code is nearby. If a later concern is discovered, record it in the plan or final notes and keep the current PR focused. + +## Scope Discipline + +Before opening or finalizing a PR: + +- Review every changed file and drop exploratory edits that are not load-bearing. +- Keep generated outputs, local run artifacts, and unrelated formatting churn out of the PR. +- Prefer existing project patterns over new abstractions unless the new abstraction clearly removes repeated complexity. +- If a change is documentation-only, say so and do not run unnecessary simulation tests. + +## Tests and Verification + +Whenever you make code changes, decide whether the change is unit-testable. If it is, add or update focused tests in the same PR. + +Use the narrowest useful command first, then broaden as risk increases: + +```bash +bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb +bundle exec rspec spec/tests/unit/scenario_measure_mapper_spec.rb +bundle exec rspec spec/tests/integration/write_and_run_osws_spec.rb +bundle exec rake +``` + +If a test cannot be run locally because dependencies, OpenStudio, weather files, or runtime are unavailable, state that clearly and name the command that should be run later. + +## Ruby and Style Checks + +For Ruby code changes, run project commands through the bundle when possible: + +```bash +bundle exec rubocop +``` + +If RuboCop or the bundle is unavailable locally, do not guess. Say what failed and what the reviewer should run after dependencies are available. + +## Documentation and Comments + +Write for maintainers first. + +- Keep comments short and focused on non-obvious behavior. +- Do not narrate what the next line of code already says. +- Put roadmap, rationale, and cross-PR history in `docs/scenario_workflow_translation_plan.md`, not in source comments. +- Keep README updates matched to implemented behavior; do not document future result writeback or mapping coverage until it exists. + +## Pull Request Description Template + +Use a short, plain-language PR body: + +```md +## Why do we need this PR + + +## Core change explanation + + +## Which files are affected +- `path/to/file_or_folder` + +## What kind of tests are done +- +``` + +Optional `Out of scope` or `Follow-up work` sections are useful when they prevent reviewer confusion. Keep deep evidence and long reasoning in the plan doc instead of the PR body. + +## CI and Merge Hygiene + +- Treat green local tests as necessary but not always sufficient; CI may catch clean-environment issues. +- If a workflow fails, inspect the failing job logs before suggesting fixes. +- When a scenario workflow PR changes the roadmap or PR boundaries, update `docs/scenario_workflow_translation_plan.md` so future sessions inherit the current truth. \ No newline at end of file diff --git a/docs/scenario_workflow_translation_plan.md b/docs/scenario_workflow_translation_plan.md new file mode 100644 index 0000000..a66418b --- /dev/null +++ b/docs/scenario_workflow_translation_plan.md @@ -0,0 +1,309 @@ +# Scenario Workflow Translation Plan + +This document is the guiding plan for adding BuildingSync scenario workflow translation to BOSS. It is intended to be merged first, then used as the source of truth for a sequence of focused implementation PRs. + +## Goal + +Add first-class support for translating BuildingSync `PackageOfMeasures` scenarios into OpenStudio workflows and running those workflows alongside the baseline workflow generated by BOSS today. + +The first functional release will: + +- Discover BuildingSync package scenarios from `Reports/Report/Scenarios/Scenario/ScenarioType/PackageOfMeasures`. +- Resolve each package's `MeasureID` references against `Facilities/Facility/Measures/Measure`. +- Map supported BuildingSync measures to OpenStudio measure steps through a BOSS-owned JSON mapping file. +- Write the existing baseline workflow plus one scenario workflow for each package scenario with at least one mapped measure. +- Run baseline and scenario workflows through `OpenStudio::Extension::Runner`. +- Warn and skip unmapped measures without failing the whole translation. + +## Non-Goals + +The first release will not: + +- Write simulated OpenStudio results back into BuildingSync `ResourceUses`, `AllResourceTotals`, or final XML. +- Add full multi-facility or multi-site selection beyond the current BOSS assumptions. +- Dynamically introspect every OpenStudio measure argument. +- Treat reported BuildingSync savings or costs as simulation inputs unless a mapping explicitly defines that behavior. +- Add user-supplied mapping files as a required feature. + +## Current BOSS Anchors + +- Baseline OSW writing starts in `BOSS::Boss.write_baseline_osw` in `lib/BOSS/boss.rb`. +- Baseline execution starts in `BOSS::Boss.run_baseline_osw` in `lib/BOSS/boss.rb`. +- BuildingSync XML parsing is centralized in `BOSS::BuildingSyncReader` in `lib/BOSS/buildingsync_reader/buildingsync_reader.rb`. +- Baseline OpenStudio steps are assembled in `lib/BOSS/osw_arg_populator.rb`. +- The CLI is Thor-based in `lib/boss_cli.rb`. +- Existing baseline integration coverage is in `spec/tests/integration/write_and_run_osws_spec.rb`. + +## BuildingSync Data Model + +Scenario translation should use the first facility supported by BOSS today. + +Scenario source: + +```text +BuildingSync/Facilities/Facility/Reports/Report/Scenarios/Scenario +``` + +Package source inside a scenario: + +```text +Scenario/ScenarioType/PackageOfMeasures +``` + +Measure source: + +```text +BuildingSync/Facilities/Facility/Measures/Measure +``` + +Each package scenario should capture at least: + +- Scenario ID +- Scenario name +- Temporal status +- Source report ID +- Package ID +- Reference case ID, when present +- Package `MeasureID` IDrefs +- Linked premises IDrefs, when present + +Each measure should capture at least: + +- Measure ID +- `SystemCategoryAffected` +- Technology category element name +- `MeasureName` +- `CustomMeasureName` +- Linked premises IDrefs +- Useful cost and savings metadata +- Implementation status + +## Workflow Output Layout + +The existing baseline workflow output must remain unchanged: + +```text +/baseline/in.osw +``` + +Scenario workflows should be written under a dedicated scenario directory: + +```text +/scenarios//in.osw +``` + +Scenario workflows should deep-copy the baseline-generation workflow and append mapped retrofit measure steps. This makes every scenario OSW independently runnable and keeps scenario runs parallel-friendly. + +## Mapping Policy + +Mappings live in a new BOSS-owned JSON file, planned as: + +```text +lib/BOSS/scenario_measure_map.json +``` + +The mapper should prefer this lookup order: + +1. `SystemCategoryAffected` plus `MeasureName` +2. Technology category element plus `MeasureName`, when `SystemCategoryAffected` is absent or unusable + +Initial mappings may use the legacy BuildingSync-gem `workflow_maker.json` as background information, but every included measure directory and argument must be verified against BOSS's current OpenStudio measure dependencies before merge. + +Conditional arguments should be data-driven. The first supported conditions should come from reader-derived context such as standards building type and principal HVAC/system type. Avoid per-measure Ruby `if`/`elsif` chains. + +## Using the Legacy BuildingSync-gem Work + +The legacy `../BuildingSync-gem` repository is an important reference, but it should not become a runtime dependency of BOSS. Use it to understand proven workflow concepts, then re-implement the needed behavior in BOSS using current dependencies and BuildingSync v2.7 fixtures. + +Legacy references to inspect: + +- `../BuildingSync-gem/lib/buildingsync/report.rb` — how reports group current-building, measured, and package-of-measures scenarios. +- `../BuildingSync-gem/lib/buildingsync/scenario.rb` — how a scenario exposes `MeasureID` references, owns an OSW, and later gathers results. +- `../BuildingSync-gem/lib/buildingsync/model_articulation/measure.rb` — simple measure wrapper concept. +- `../BuildingSync-gem/lib/buildingsync/makers/workflow_maker.rb` — scenario workflow sequence: deep-copy workflow, resolve package measures, configure steps, write OSWs, run OSWs. +- `../BuildingSync-gem/lib/buildingsync/makers/workflow_maker.json` — seed ideas for category/name to OpenStudio measure mappings. + +How to leverage it by PR: + +- PRs 2-4 may borrow the report/scenario/measure concepts, but should keep parsing in `BOSS::BuildingSyncReader` and keep namespace handling compatible with current BOSS fixtures. +- PRs 5-9 may use `workflow_maker.json` as a mapping seed, but every measure directory and argument must be verified against the OpenStudio gem versions declared in `BOSS.gemspec` before it is added to BOSS. +- PRs 8-9 may borrow the idea of conditional arguments, but should express conditions through mapping data and context values rather than a per-measure Ruby `if`/`elsif` chain. +- PRs 11-14 may borrow the deep-copy/write/run workflow shape, but should preserve BOSS's current baseline OSW generation and output layout. +- Result writeback ideas from the legacy gem are intentionally future work; do not include them in this first release unless this plan is explicitly updated. + +Do not copy blindly from the legacy gem. It targets older BuildingSync schema versions and older OpenStudio gems, so stale measure names, arguments, assumptions, and failure checks are expected. Any BOSS mapping or workflow behavior inspired by the legacy gem needs a current fixture test or a clear manual verification note. + +## Warning Policy + +The implementation should return structured warnings and log them consistently. These conditions are warnings, not fatal translation errors: + +- Package has no `MeasureIDs`. +- Package references an unknown measure ID. +- Measure is missing `SystemCategoryAffected`. +- Measure is missing a usable technology category. +- Measure is missing `MeasureName`. +- Measure category/name cannot be mapped to OpenStudio steps. + +If a scenario has at least one mapped measure, write its OSW and include warnings for skipped measures. If a scenario has zero mapped measures, skip that scenario OSW and report the reason clearly. + +## PR Sequence + +Each PR should be independently reviewable and should not implement later PR scope early unless it is necessary to keep the current PR coherent. + +| PR | Status | Delivered By | Title | Scope | Depends On | Green Light | Red Light | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | In Progress | Current docs PR | Guiding plan doc | Add this repository plan document. | None | The doc directs PRs 2-18, names acceptance gates, and keeps first-release scope clear. | The doc leaves scenario selection, mapping ownership, warning behavior, or result writeback scope ambiguous. | +| 2 | Done | Current PR-2 branch | Scenario data model and discovery | Add reader-returned structures for first-facility report-level package scenarios. Extract scenario ID, name, temporal status, report ID, package ID, reference case ID, measure IDrefs, and linked premises. | PR 1 | Unit tests prove `building_151.xml` discovers baseline plus all package scenarios and existing reader behavior remains unchanged. | Parser assumes a hardcoded namespace, reads the wrong scenario path, or breaks existing reader tests. | +| 3 | Done | Current PR-3 branch | Measure index | Index first-facility measures by ID and extract category/name metadata plus linked premises, cost/savings fields, and implementation status. | PR 2 | Tests resolve package `MeasureID` references in `building_151.xml` to parsed measure metadata. | Unresolved refs are silently dropped or measures without `TechnologyCategories` crash parsing. | +| 4 | Done | Current PR-4 branch | Parser warning contract | Add structured warnings for missing IDs, unresolved refs, empty packages, missing names, missing categories, and packages with no usable measures. | PR 3 | Tests cover warning cases using `BuildingEQ-1.0.0.xml`, `Golden Test File.xml`, and no-measure fixtures. | Warnings only print to stdout or malformed package data aborts all discovery. | +| 5 | Planned | - | Initial mapping JSON | Add `lib/BOSS/scenario_measure_map.json` with verified mappings for an initial supported set from `building_151.xml`. | PR 1 | Mapping JSON is valid, and every included entry has source category/name, target `measure_dir_name`, and arguments. | Legacy mappings are copied blindly without checking current OpenStudio measure dirs/args. | +| 6 | Planned | - | Basic `ScenarioMeasureMapper` | Load JSON, normalize lookup keys, and map one parsed BuildingSync measure to OpenStudio step specs using `SystemCategoryAffected` plus `MeasureName`. | PRs 3, 5 | Mapper unit tests return expected steps and structured unmapped warnings. | Mapper mutates reader data, raises on unmapped measures, or hardcodes rules outside JSON. | +| 7 | Planned | - | Technology category fallback | Add fallback lookup by technology category plus `MeasureName`. | PR 6 | Tests show fallback mapping works when `SystemCategoryAffected` is absent, while normal lookup priority is preserved. | Fallback changes normal category/name lookup behavior. | +| 8 | Planned | - | Conditional mapping rules | Add data-driven conditional argument support for building type and principal HVAC/system context. | PR 6 | Tests prove conditions include and exclude arguments predictably. | Implementation becomes a per-measure Ruby condition chain. | +| 9 | Planned | - | Structured mapping results | Return mapped steps, skipped measure IDs, warnings, and scenario-level write/skip status. | PRs 6-8 | Scenarios with mapped measures are writable; zero-mapped scenarios are skipped with clear warnings. | Callers must infer status from logs or empty arrays. | +| 10 | Planned | - | Baseline OSW builder refactor | Refactor baseline writing into an internal baseline OSW builder while preserving public behavior. | PR 1 | Existing baseline integration tests pass and generated baseline OSW steps are unchanged. | Existing API, CLI, or baseline output changes unexpectedly. | +| 11 | Planned | - | Scenario OSW writer API | Add an API that writes baseline plus scenario OSWs for package scenarios with at least one mapped measure. | PRs 9, 10 | Generation tests show expected directories and skipped-scenario reporting. | API writes OSWs for zero-mapped scenarios or disturbs baseline output. | +| 12 | Planned | - | Scenario workflow step assembly | Deep-copy baseline OSW and append mapped retrofit steps in deterministic order. | PR 11 | OSW tests assert baseline steps plus expected mapped measure steps and arguments. | Scenario workflows omit baseline creation steps, mutate baseline OSW, or use nondeterministic step order. | +| 13 | Planned | - | Generic OSW step helpers | Centralize mapped step appending and `OpenStudio::Extension.set_measure_argument` use. | PR 12 | Tests prove boolean, numeric, and string arguments serialize correctly. | Baseline populator methods are broadly rewritten or baseline JSON changes unintentionally. | +| 14 | Planned | - | Generalized runner | Run `baseline/in.osw` and `scenarios/**/in.osw`; keep `run_baseline_osw` intact. | PR 11 | Tests or smoke run show the correct OSW list and baseline-only compatibility. | Runner includes skipped or missing OSWs, or removes baseline-only behavior. | +| 15 | Planned | - | CLI command | Add a Thor command for baseline plus all package scenarios using existing output, weather, standard, and run options. | PRs 11, 14 | CLI help documents the command, write-only mode produces OSWs, and run mode invokes the generalized runner. | Existing `write_baseline_osw` or `run_osw` behavior changes unexpectedly. | +| 16 | Planned | - | User documentation | Update README or user docs with scenario path, output layout, mapping policy, warning behavior, and CLI examples. | PR 15 | Docs match implemented command names/options and include one known fixture example. | Docs promise result writeback or unsupported mapping coverage. | +| 17 | Planned | - | Mapper unit test expansion | Add focused tests for mapped, conditional, fallback, and unmapped cases. | PRs 5-9 | Mapper behavior is covered without relying only on slow integration tests. | Important mapper paths only have integration coverage. | +| 18 | Planned | - | Scenario generation and integration coverage | Add OSW generation tests and focused write/run integration for a small known-translatable scenario subset. | PRs 11-15, 17 | Focused commands pass; supported scenario `out.osw` files show `completed_status: Success`. | Tests run every package scenario by default, are too slow for CI, or hide failures behind broad skips. | + +## Progress Tracking Protocol + +The PR sequence table is the tracker. Keep it current so future sessions can trust it without reconstructing history from memory. + +Allowed status values: + +- `Planned` — not started. +- `In Progress` — actively being implemented on a branch. +- `Done` — merged or otherwise delivered; green-light criteria satisfied. +- `Blocked` — cannot proceed without a decision, dependency, or external fix. +- `Closed` — intentionally no longer needed; keep the reason in the row or nearby note. + +When a PR step is completed or merged: + +1. Change its `Status` in the table. +2. Fill `Delivered By` with the PR number, commit SHA, or short note that identifies what delivered it. +3. Confirm the row's green-light criteria were met. +4. If scope or order changed, update the affected rows and explain the change in this document. +5. Leave future `Planned` rows in place so later sessions can find the next unfinished step. + +When starting a future session, check this table first, then verify against `git log` and the current file tree. If the table is stale, update it before doing implementation work. + +## PR Handoff Notes + +Use this section for short durable notes that help the next PR start without reconstructing context from chat history. Keep entries concise and tied to a PR number. + +Each completed or blocked implementation PR should add an entry with: + +- Scope delivered or intentionally deferred. +- Reader, mapper, OSW, CLI, or warning contracts that later PRs can rely on. +- Verification commands run and their result. +- Known blockers, local-environment issues, or follow-up decisions. + +Suggested entry format: + +```md +### PR : + +- Status: <Done | Blocked | In Progress> +- Delivered by: <PR number, commit SHA, or branch note> +- Handoff: <one or two bullets with stable contracts for later PRs> +- Verification: `<command>` - <result> +- Follow-up: <only items needed by later PRs> +``` + +Keep detailed rationale in the relevant PR description or code review thread. Keep this section focused on what future implementation work needs to know. + +### PR 2: Scenario data model and discovery + +- Status: Done +- Delivered by: Current PR-2 branch +- Handoff: `BOSS::BuildingSyncReader#get_report_scenarios` returns first-facility report scenarios as symbol-keyed hashes with `scenario_id`, `scenario_name`, `temporal_status`, `report_id`, `scenario_type`, `package_id`, `reference_case_id`, `measure_ids`, and `linked_premises_idrefs`. +- Handoff: `BOSS::BuildingSyncReader#get_package_measure_scenarios` filters those records to package-of-measures scenarios. PR 3 should use `measure_ids` to resolve package references against facility measures; it should not expect warnings or parsed measure metadata yet. +- Verification: `bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb` - 22 examples, 0 failures. +- Verification: `bundle exec rspec spec/tests/integration/write_and_run_osws_spec.rb` - completed in 79 minutes 27 seconds with 19 examples and 1 failure. The failure is an unrelated Windows command quoting issue for `Golden Test File` output paths with spaces; 18 final baseline `out.osw` files had `completed_status: Success`. +- Follow-up: PR 3 starts facility measure indexing and should keep warning contracts deferred to PR 4. + +### PR 3: Measure index + +- Status: Done +- Delivered by: Current PR-3 branch +- Handoff: `BOSS::BuildingSyncReader#get_measures` returns first-facility measures as a hash keyed by `Measure/@ID`, with symbol-keyed metadata for `measure_id`, `system_category_affected`, `technology_category_element_name`, `measure_name`, `custom_measure_name`, `linked_premises_idrefs`, useful cost/savings fields, and `implementation_status`. +- Handoff: Missing optional measure fields return `nil` or `[]`, missing `Measures` returns `{}`, and measures without `TechnologyCategories` remain parseable; PR 4 should add structured warnings for missing IDs, unresolved refs, missing names, and missing categories. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb` - 29 examples, 0 failures. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rubocop --only Lint/UnreachableLoop lib/BOSS/buildingsync_reader/buildingsync_reader.rb` - 1 file inspected, no offenses. Full changed-file RuboCop still reports existing reader/spec style debt and new-cop configuration warnings. +- Follow-up: PR 4 should consume `get_package_measure_scenarios` plus `get_measures` to report unresolved `MeasureID` references and incomplete measure metadata without changing the parser return shapes unless the warning contract requires it. + +### PR 4: Parser warning contract + +- Status: Done +- Delivered by: Current PR-4 branch +- Handoff: `BOSS::BuildingSyncReader#get_parser_warnings` returns structured symbol-keyed warning hashes with `code`, `severity`, `message`, and relevant scenario/package/measure context. Existing scenario and measure reader return shapes remain unchanged. +- Handoff: Parser warning codes cover missing scenario/package/measure IDs, missing `MeasureID` IDrefs, unresolved measure references, packages without `MeasureIDs`, packages with no parser-usable measures, and resolved measures missing `SystemCategoryAffected`, technology category, or `MeasureName`. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb` - 33 examples, 0 failures. +- Verification: `C:\Ruby32-x64\bin\ruby.exe -S bundle exec rubocop lib/BOSS/buildingsync_reader/buildingsync_reader.rb spec/tests/unit/buildingsync_reader_spec.rb` - fails on existing reader/spec style debt and new-cop configuration warnings; after PR-4 cleanup, the focused run reports 132 existing offenses, 117 autocorrectable. +- Follow-up: PR 5 can add mapping JSON without expecting parser warnings to identify unmapped OpenStudio measures; mapping-specific warnings remain PR 6/PR 9 scope. + +## Verification Commands + +Use the narrowest command that verifies the current PR. Broader commands should be run before merging larger integration PRs. + +```bash +bundle exec rspec spec/tests/unit/buildingsync_reader_spec.rb +bundle exec rspec spec/tests/unit/scenario_measure_mapper_spec.rb +bundle exec rspec spec/tests/integration/write_and_run_osws_spec.rb +bundle exec rake +``` + +After the CLI PR, manually smoke-test the new command against these fixtures: + +- `spec/files/v2.7.0/building_151.xml` for package scenarios with known translatable measures. +- `spec/files/v2.7.0/BuildingEQ-1.0.0.xml` for unmapped warning behavior. +- `spec/files/v2.7.0/Golden Test File.xml` for package/reference edge cases. + +## Planned Files + +New files expected during the PR sequence: + +- `docs/scenario_workflow_translation_plan.md` +- `lib/BOSS/scenario_measure_mapper.rb` +- `lib/BOSS/scenario_measure_map.json` +- `spec/tests/unit/scenario_measure_mapper_spec.rb` + +Existing files likely to change in later PRs: + +- `lib/BOSS/boss.rb` +- `lib/BOSS/osw_arg_populator.rb` +- `lib/BOSS/buildingsync_reader/buildingsync_reader.rb` +- `lib/BOSS/constants.rb` +- `lib/boss_cli.rb` +- `spec/tests/unit/buildingsync_reader_spec.rb` +- `spec/tests/integration/write_and_run_osws_spec.rb` +- `README.md` + +## How Future Sessions Should Resume + +At the start of any future implementation session: + +1. Read this document first. +2. Check the git state and recent commits to determine which PR steps have already landed. +3. Start with the first PR in the sequence whose green-light criteria are not yet satisfied. +4. Keep the change scoped to that PR unless a small supporting edit is necessary to make the PR coherent. +5. Use the green-light criteria as the definition of done and the red-light criteria as merge blockers. +6. Run the narrowest verification command that proves the current PR, then run broader compatibility checks before larger integration PRs. +7. If implementation reveals a better order or a changed decision, update this plan in the same PR so later sessions inherit the new truth. + +## Review Checklist + +Before merging any implementation PR, confirm: + +- The PR implements only its planned scope or explicitly explains why a small dependency is included. +- Existing baseline behavior remains compatible unless the PR explicitly updates it. +- New behavior has focused tests or a documented manual verification path. +- Warnings are structured and visible to callers. +- Unmapped BuildingSync measures do not crash the whole translation. +- The plan document remains accurate; update it in the same PR if a later implementation decision changes the roadmap. \ No newline at end of file diff --git a/lib/BOSS/buildingsync_reader/buildingsync_reader.rb b/lib/BOSS/buildingsync_reader/buildingsync_reader.rb index 028a0b6..bf0021f 100644 --- a/lib/BOSS/buildingsync_reader/buildingsync_reader.rb +++ b/lib/BOSS/buildingsync_reader/buildingsync_reader.rb @@ -28,6 +28,48 @@ def initialize(bsync_doc, epw_file_path, standard_to_be_used) end + def get_report_scenarios + scenarios = [] + @facility_xml&.elements&.each("#{@ns}Reports/#{@ns}Report") do |report_xml| + report_xml.elements.each("#{@ns}Scenarios/#{@ns}Scenario") do |scenario_xml| + scenarios << _scenario_hash(report_xml, scenario_xml) + end + end + + return scenarios + end + + def get_package_measure_scenarios + return get_report_scenarios.select { |scenario| scenario[:scenario_type] == :package_of_measures } + end + + def get_measures + measures = {} + _measure_xmls.each do |measure_xml| + measure = _measure_hash(measure_xml) + measures[measure[:measure_id]] = measure if !measure[:measure_id].nil? + end + + return measures + end + + def get_parser_warnings + warnings = [] + measures = get_measures + + _measure_xmls.each do |measure_xml| + measure = _measure_hash(measure_xml) + warnings << _parser_warning(:missing_measure_id, 'Measure is missing ID.') if _blank?(measure[:measure_id]) + end + + _package_measure_scenario_xmls.each do |report_xml, scenario_xml, package_xml| + scenario = _scenario_hash(report_xml, scenario_xml) + warnings.concat(_package_measure_scenario_warnings(scenario, package_xml, measures)) + end + + return warnings + end + # tries to get weather file from: # 1. given weather file # 2. city state from either building or site @@ -310,5 +352,235 @@ def get_total_weighted_average_load return nil if !all_weighted_average_loads.all? return all_weighted_average_loads.map {|s| s.to_f}.sum end + + private + + def _measure_xmls + measure_xmls = [] + @facility_xml&.elements&.each("#{@ns}Measures/#{@ns}Measure") do |measure_xml| + measure_xmls << measure_xml + end + + return measure_xmls + end + + def _package_measure_scenario_xmls + scenario_xmls = [] + @facility_xml&.elements&.each("#{@ns}Reports/#{@ns}Report") do |report_xml| + report_xml.elements.each("#{@ns}Scenarios/#{@ns}Scenario") do |scenario_xml| + package_xml = _package_of_measures_xml(scenario_xml) + scenario_xmls << [report_xml, scenario_xml, package_xml] if !package_xml.nil? + end + end + + return scenario_xmls + end + + def _measure_hash(measure_xml) + technology_category_xml = _technology_category_xml(measure_xml) + + return { + measure_id: measure_xml.attributes['ID'], + system_category_affected: _element_text(measure_xml, "#{@ns}SystemCategoryAffected"), + technology_category_element_name: _technology_category_element_name(technology_category_xml), + measure_name: _technology_measure_name(technology_category_xml), + custom_measure_name: _element_text(measure_xml, "#{@ns}CustomMeasureName"), + linked_premises_idrefs: _linked_premises_idrefs(measure_xml), + mv_cost: _numeric_element_text(measure_xml, "#{@ns}MVCost"), + useful_life: _numeric_element_text(measure_xml, "#{@ns}UsefulLife"), + measure_total_first_cost: _numeric_element_text(measure_xml, "#{@ns}MeasureTotalFirstCost"), + measure_installation_cost: _numeric_element_text(measure_xml, "#{@ns}MeasureInstallationCost"), + measure_material_cost: _numeric_element_text(measure_xml, "#{@ns}MeasureMaterialCost"), + om_cost_annual_savings: _numeric_element_text(measure_xml, "#{@ns}MeasureSavingsAnalysis/#{@ns}OMCostAnnualSavings"), + implementation_status: _element_text(measure_xml, "#{@ns}ImplementationStatus") + } + end + + def _technology_category_xml(measure_xml) + technology_category_xml = measure_xml.elements["#{@ns}TechnologyCategories/#{@ns}TechnologyCategory"] + + return technology_category_xml&.elements&.[](1) + end + + def _technology_category_element_name(technology_category_xml) + return nil if technology_category_xml.nil? + + return technology_category_xml.name.to_s.split(':').last + end + + def _technology_measure_name(technology_category_xml) + return nil if technology_category_xml.nil? + + return _element_text(technology_category_xml, "#{@ns}MeasureName") + end + + def _numeric_element_text(xml, path) + value = _element_text(xml, path) + return nil if value.nil? || value.strip.empty? + + return Float(value) + rescue ArgumentError + return nil + end + + def _package_measure_scenario_warnings(scenario, package_xml, measures) + warnings = [] + context = _scenario_warning_context(scenario) + measure_idref_xmls = _measure_idref_xmls(package_xml) + + if _blank?(scenario[:scenario_id]) + warnings << _parser_warning(:missing_scenario_id, 'Package scenario is missing ID.', context) + end + if _blank?(scenario[:package_id]) + warnings << _parser_warning(:missing_package_id, 'PackageOfMeasures is missing ID.', context) + end + if measure_idref_xmls.empty? + warnings << _parser_warning(:package_missing_measure_ids, 'Package has no MeasureIDs.', context) + end + + measure_idref_xmls.each do |measure_id_xml| + if _blank?(measure_id_xml.attributes['IDref']) + warnings << _parser_warning(:missing_measure_idref, 'MeasureID is missing IDref.', context) + end + end + + resolved_measures = [] + scenario[:measure_ids].each do |measure_idref| + measure = measures[measure_idref] + if measure.nil? + warnings << _parser_warning( + :unresolved_measure_idref, + 'Package references an unknown measure ID.', + context.merge(measure_idref:) + ) + else + resolved_measures << measure + warnings.concat(_measure_parser_warnings(measure, context)) + end + end + + if resolved_measures.none? { |measure| _usable_package_measure?(measure) } + warnings << _parser_warning(:package_has_no_usable_measures, 'Package has no usable measures.', context) + end + + return warnings + end + + def _measure_parser_warnings(measure, scenario_context) + warnings = [] + context = scenario_context.merge(measure_id: measure[:measure_id]) + + if _blank?(measure[:system_category_affected]) + warnings << _parser_warning(:missing_system_category_affected, 'Measure is missing SystemCategoryAffected.', context) + end + if _blank?(measure[:technology_category_element_name]) + warnings << _parser_warning(:missing_technology_category, 'Measure is missing a usable technology category.', context) + end + if _blank?(measure[:measure_name]) + warnings << _parser_warning(:missing_measure_name, 'Measure is missing MeasureName.', context) + end + + return warnings + end + + def _scenario_warning_context(scenario) + return { + report_id: scenario[:report_id], + scenario_id: scenario[:scenario_id], + package_id: scenario[:package_id] + } + end + + def _parser_warning(code, message, context = {}) + return { + code:, + severity: :warning, + message: + }.merge(context) + end + + def _blank?(value) + return true if value.nil? + + return value.to_s.strip.empty? + end + + def _usable_package_measure?(measure) + return false if _blank?(measure[:measure_name]) + + return !_blank?(measure[:system_category_affected]) || !_blank?(measure[:technology_category_element_name]) + end + + def _scenario_hash(report_xml, scenario_xml) + package_xml = _package_of_measures_xml(scenario_xml) + + return { + scenario_id: scenario_xml.attributes['ID'], + scenario_name: _element_text(scenario_xml, "#{@ns}ScenarioName"), + temporal_status: _element_text(scenario_xml, "#{@ns}TemporalStatus"), + report_id: report_xml.attributes['ID'], + scenario_type: _scenario_type(scenario_xml, package_xml), + package_id: package_xml&.attributes&.[]('ID'), + reference_case_id: package_xml&.elements&.[]("#{@ns}ReferenceCase")&.attributes&.[]('IDref'), + measure_ids: _measure_idrefs(package_xml), + linked_premises_idrefs: _linked_premises_idrefs(scenario_xml) + } + end + + def _scenario_type(scenario_xml, package_xml) + return :package_of_measures if !package_xml.nil? + + current_building_xml = scenario_xml.elements["#{@ns}ScenarioType/#{@ns}CurrentBuilding"] + return :current_building if !current_building_xml.nil? + + return :other + end + + def _package_of_measures_xml(scenario_xml) + return scenario_xml.elements["#{@ns}ScenarioType/#{@ns}PackageOfMeasures"] + end + + def _element_text(xml, path) + return xml.elements[path]&.text + end + + def _measure_idrefs(package_xml) + return [] if package_xml.nil? + + measure_ids = [] + _measure_idref_xmls(package_xml).each do |measure_id_xml| + measure_id = measure_id_xml.attributes['IDref'] + measure_ids << measure_id if !measure_id.nil? + end + return measure_ids + end + + def _measure_idref_xmls(package_xml) + return [] if package_xml.nil? + + measure_idref_xmls = [] + package_xml.elements.each("#{@ns}MeasureIDs/#{@ns}MeasureID") do |measure_id_xml| + measure_idref_xmls << measure_id_xml + end + return measure_idref_xmls + end + + def _linked_premises_idrefs(xml) + linked_premises_xml = xml.elements["#{@ns}LinkedPremises"] + return [] if linked_premises_xml.nil? + + return _descendant_idrefs(linked_premises_xml) + end + + def _descendant_idrefs(xml) + idrefs = [] + xml.each_element do |child_xml| + idref = child_xml.attributes['IDref'] + idrefs << idref if !idref.nil? + idrefs.concat(_descendant_idrefs(child_xml)) + end + + return idrefs + end end end diff --git a/spec/files/v2.7.0/expected/building_151_scenario1_resolved_measures.json b/spec/files/v2.7.0/expected/building_151_scenario1_resolved_measures.json new file mode 100644 index 0000000..f5a8488 --- /dev/null +++ b/spec/files/v2.7.0/expected/building_151_scenario1_resolved_measures.json @@ -0,0 +1,38 @@ +{ + "fixture": "spec/files/v2.7.0/building_151.xml", + "measure_count": 16, + "scenario": { + "scenario_id": "Scenario1", + "scenario_name": "LED Only", + "temporal_status": null, + "report_id": "Report1", + "scenario_type": "package_of_measures", + "package_id": "PackageOfMeasures1", + "reference_case_id": "Baseline", + "measure_ids": [ + "Measure1" + ], + "linked_premises_idrefs": [ + "Building151" + ] + }, + "resolved_measures": [ + { + "measure_id": "Measure1", + "system_category_affected": "Lighting", + "technology_category_element_name": "LightingImprovements", + "measure_name": "Retrofit with light emitting diode technologies", + "custom_measure_name": "TBD", + "linked_premises_idrefs": [ + "Building151" + ], + "mv_cost": 0.0, + "useful_life": 12.0, + "measure_total_first_cost": 267390.2, + "measure_installation_cost": 0.0, + "measure_material_cost": 0.0, + "om_cost_annual_savings": null, + "implementation_status": "Proposed" + } + ] +} diff --git a/spec/tests/unit/buildingsync_reader_spec.rb b/spec/tests/unit/buildingsync_reader_spec.rb index d7317d0..8fcb3f8 100644 --- a/spec/tests/unit/buildingsync_reader_spec.rb +++ b/spec/tests/unit/buildingsync_reader_spec.rb @@ -5,8 +5,21 @@ # ******************************************************************************* require 'tempfile' +require 'json' require 'BOSS/buildingsync_reader/buildingsync_reader' +UNIT_SPEC_FILES_DIR = File.expand_path('../../files', __dir__) + +def load_fixture_doc(schema_version, file_name) + xml_path = File.join(UNIT_SPEC_FILES_DIR, schema_version, file_name) + REXML::Document.new(File.read(xml_path), ignore_whitespace_nodes: :all) +end + +def load_fixture_json(schema_version, file_name) + json_path = File.join(UNIT_SPEC_FILES_DIR, schema_version, file_name) + JSON.parse(File.read(json_path)) +end + def wrap_in_site(xml) <<~XML <BuildingSync> @@ -23,7 +36,434 @@ def wrap_in_site(xml) XML end +def wrap_in_facility(xml) + <<~XML + <BuildingSync> + <Facilities> + <Facility> + <Sites> + <Site> + <Buildings> + <Building> + </Building> + </Buildings> + </Site> + </Sites> + #{xml} + </Facility> + </Facilities> + </BuildingSync> + XML +end + RSpec.describe 'BuildingSyncReader' do + describe 'get_report_scenarios should' do + it 'discover baseline and package scenarios from building_151' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + scenarios = buidingsync_reader.get_report_scenarios + package_scenarios = buidingsync_reader.get_package_measure_scenarios + + # Assert + expected_package_ids = [ + 'Scenario1', + 'Scenario3', + 'Scenario4', + 'Scenario5', + 'Scenario6', + 'Scenario7', + 'Scenario8', + 'Scenario9', + 'Scenario10', + 'Scenario11', + 'Scenario12', + 'Scenario14', + 'Scenario16', + 'Scenario18', + 'Scenario24', + 'Scenario25' + ] + + expect(scenarios.length).to eq 17 + expect(package_scenarios.map { |scenario| scenario[:scenario_id] }).to eq expected_package_ids + expect(scenarios.count { |scenario| scenario[:scenario_type] == :current_building }).to eq 1 + expect(scenarios.count { |scenario| scenario[:scenario_type] == :package_of_measures }).to eq 16 + end + + it 'extract scenario fields from package and current-building scenarios' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + scenarios = buidingsync_reader.get_report_scenarios + baseline = scenarios.find { |scenario| scenario[:scenario_id] == 'Baseline' } + scenario1 = scenarios.find { |scenario| scenario[:scenario_id] == 'Scenario1' } + + # Assert + expect(baseline).to include( + scenario_id: 'Baseline', + scenario_name: 'Baseline', + scenario_type: :current_building, + report_id: 'Report1', + temporal_status: nil, + package_id: nil, + reference_case_id: nil, + measure_ids: [], + linked_premises_idrefs: [] + ) + + expect(scenario1).to include( + scenario_id: 'Scenario1', + scenario_name: 'LED Only', + scenario_type: :package_of_measures, + report_id: 'Report1', + temporal_status: nil, + package_id: 'PackageOfMeasures1', + reference_case_id: 'Baseline', + measure_ids: ['Measure1'], + linked_premises_idrefs: ['Building151'] + ) + end + + it 'extract temporal status and multiple measure IDrefs' do + # Set Up + doc = REXML::Document.new wrap_in_facility(<<~XML) + <Reports> + <Report ID="ReportA"> + <Scenarios> + <Scenario ID="ScenarioA"> + <ScenarioName>Package A</ScenarioName> + <TemporalStatus>Post retrofit</TemporalStatus> + <ScenarioType> + <PackageOfMeasures ID="PackageA"> + <ReferenceCase IDref="BaselineA"/> + <MeasureIDs> + <MeasureID IDref="MeasureA"/> + <MeasureID IDref="MeasureB"/> + </MeasureIDs> + </PackageOfMeasures> + </ScenarioType> + </Scenario> + </Scenarios> + </Report> + </Reports> + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + scenarios = buidingsync_reader.get_package_measure_scenarios + + # Assert + expect(scenarios.first).to include( + scenario_id: 'ScenarioA', + temporal_status: 'Post retrofit', + package_id: 'PackageA', + reference_case_id: 'BaselineA', + measure_ids: ['MeasureA', 'MeasureB'] + ) + end + + it 'uses the document namespace prefix when discovering scenarios' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_n1.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + scenarios = buidingsync_reader.get_report_scenarios + + # Assert + expect(scenarios.find { |scenario| scenario[:scenario_id] == 'Baseline' }[:scenario_type]).to eq :current_building + expect(buidingsync_reader.get_package_measure_scenarios.map { |scenario| scenario[:package_id] }).to include('PackageOfMeasures1') + end + + it 'returns empty arrays when no report scenarios exist' do + # Set Up + doc = REXML::Document.new wrap_in_facility('') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_report_scenarios).to eq [] + expect(buidingsync_reader.get_package_measure_scenarios).to eq [] + end + end + + describe 'get_measures should' do + it 'index measures from building_151 by measure ID' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measures = buidingsync_reader.get_measures + + # Assert + expected_measure_ids = [ + 'Measure1', + 'Measure3', + 'Measure4', + 'Measure5', + 'Measure6', + 'Measure7', + 'Measure8', + 'Measure9', + 'Measure10', + 'Measure11', + 'Measure12', + 'Measure14', + 'Measure16', + 'Measure18', + 'Measure24', + 'Measure25' + ] + + expect(measures.keys).to eq expected_measure_ids + end + + it 'extract measure category, name, linked premises, cost, savings, and status metadata' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['Measure1'] + + # Assert + expect(measure).to include( + measure_id: 'Measure1', + system_category_affected: 'Lighting', + technology_category_element_name: 'LightingImprovements', + measure_name: 'Retrofit with light emitting diode technologies', + custom_measure_name: 'TBD', + linked_premises_idrefs: ['Building151'], + mv_cost: 0.0, + useful_life: 12.0, + measure_total_first_cost: 267390.2, + measure_installation_cost: 0.0, + measure_material_cost: 0.0, + om_cost_annual_savings: nil, + implementation_status: 'Proposed' + ) + end + + it 'resolves package MeasureID references against parsed measure metadata' do + # Set Up + fixture = File.join('spec', 'files', 'v2.7.0', 'building_151.xml') + doc = load_fixture_doc('v2.7.0', 'building_151.xml') + expected_output = load_fixture_json('v2.7.0', File.join('expected', 'building_151_scenario1_resolved_measures.json')) + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measures = buidingsync_reader.get_measures + scenario = buidingsync_reader.get_package_measure_scenarios.find { |package_scenario| package_scenario[:scenario_id] == 'Scenario1' } + parsed_output = { + fixture: fixture, + measure_count: measures.length, + scenario: scenario, + resolved_measures: scenario[:measure_ids].map { |measure_id| measures[measure_id] } + } + + # Assert + expect(JSON.parse(JSON.generate(parsed_output))).to eq expected_output + end + + it 'uses the document namespace prefix when indexing measures' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_n1.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['Measure1'] + + # Assert + expect(measure).to include( + technology_category_element_name: 'LightingImprovements', + measure_name: 'Retrofit with light emitting diode technologies' + ) + end + + it 'returns an empty hash when no measures exist' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_no_measures.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_measures).to eq({}) + end + + it 'extracts measure-owned savings metadata when present' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'BuildingEQ-1.0.0.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['MeasureType-70023838998340'] + + # Assert + expect(measure).to include( + measure_id: 'MeasureType-70023838998340', + system_category_affected: 'Cooking', + technology_category_element_name: 'FutureOtherECMs', + measure_name: 'Other', + linked_premises_idrefs: ['BuildingType-70023826271140'], + useful_life: 50.0, + measure_total_first_cost: 75242.0, + om_cost_annual_savings: 260.0 + ) + end + + it 'keeps incomplete measure metadata nil-safe for later warning handling' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'Golden Test File.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + measure = buidingsync_reader.get_measures['Building1RemovePV'] + + # Assert + expect(measure).to include( + measure_id: 'Building1RemovePV', + system_category_affected: nil, + technology_category_element_name: nil, + measure_name: nil, + custom_measure_name: nil, + linked_premises_idrefs: ['Building1'], + mv_cost: nil, + useful_life: nil, + measure_total_first_cost: nil, + measure_installation_cost: nil, + measure_material_cost: nil, + om_cost_annual_savings: nil, + implementation_status: nil + ) + end + end + + describe 'get_parser_warnings should' do + it 'return no warnings for complete package scenarios' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'BuildingEQ-1.0.0.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_parser_warnings).to eq [] + end + + it 'return no warnings when no package scenarios exist' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'building_151_no_measures.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + + # Assert + expect(buidingsync_reader.get_parser_warnings).to eq [] + end + + it 'warn for incomplete package measure metadata' do + # Set Up + doc = load_fixture_doc('v2.7.0', 'Golden Test File.xml') + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + warnings = buidingsync_reader.get_parser_warnings + + # Assert + expect(warnings.map { |warning| warning[:severity] }.uniq).to eq [:warning] + expect(warnings.map { |warning| warning[:code] }).to contain_exactly( + :missing_system_category_affected, + :missing_technology_category, + :missing_measure_name, + :package_has_no_usable_measures, + :missing_system_category_affected, + :missing_technology_category, + :missing_measure_name, + :package_has_no_usable_measures + ) + + building_measure_warning = warnings.find do |warning| + warning[:measure_id] == 'Building1RemovePV' && warning[:code] == :missing_measure_name + end + expect(building_measure_warning).to include( + report_id: 'Report-c1857e54-836b-4674-95f4-2e6e9c8510b4', + scenario_id: 'Scenario1', + package_id: 'PackageOfMeasures-b9ca1b63-acd6-4d8a-9d8f-f39a96fd8dac' + ) + end + + it 'warn for malformed package scenario references' do + # Set Up + doc = REXML::Document.new wrap_in_facility(<<~XML) + <Measures> + <Measure> + <SystemCategoryAffected>Lighting</SystemCategoryAffected> + <TechnologyCategories> + <TechnologyCategory> + <LightingImprovements> + <MeasureName>Install lighting controls</MeasureName> + </LightingImprovements> + </TechnologyCategory> + </TechnologyCategories> + </Measure> + </Measures> + <Reports> + <Report ID="ReportA"> + <Scenarios> + <Scenario> + <ScenarioType> + <PackageOfMeasures> + <MeasureIDs> + <MeasureID/> + <MeasureID IDref="UnknownMeasure"/> + </MeasureIDs> + </PackageOfMeasures> + </ScenarioType> + </Scenario> + <Scenario ID="EmptyScenario"> + <ScenarioType> + <PackageOfMeasures ID="EmptyPackage"/> + </ScenarioType> + </Scenario> + </Scenarios> + </Report> + </Reports> + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + warnings = buidingsync_reader.get_parser_warnings + + # Assert + expect(warnings.map { |warning| warning[:code] }).to contain_exactly( + :missing_measure_id, + :missing_scenario_id, + :missing_package_id, + :missing_measure_idref, + :unresolved_measure_idref, + :package_has_no_usable_measures, + :package_missing_measure_ids, + :package_has_no_usable_measures + ) + + unresolved_warning = warnings.find { |warning| warning[:code] == :unresolved_measure_idref } + expect(unresolved_warning).to include( + report_id: 'ReportA', + scenario_id: nil, + package_id: nil, + measure_idref: 'UnknownMeasure' + ) + end + end + describe 'get_climate_zone should' do it "get from site" do # Set Up