Skip to main content

TDD: Project Metadata Management

📋 Implementation Issue: Issue #227 - Project Metadata Management - Phase 1 (MVP)

Related PRD: Project Metadata Management

Overview​

This document provides the technical design and implementation details for making project metadata editable in the CodeProof application. This includes new gRPC APIs, backend services, frontend components, and CLI tools for managing project-metadata.json files.

What Already Exists vs. What's New​

✅ Already Exists (Current State):

  • CreateArchitecturalPlanRequest RPC - Creates entire project structure including project-metadata.json
  • GetProjectMetadata RPC - Reads project-metadata.json file
  • GetProjectMetadataResponse message - Returns project metadata (but duplicates fields)
  • ArchitecturalPlanReviewer.getProjectMetadata() method - Reads metadata using manual JSON parsing
  • ArchitecturalPlanWriteServiceImpl.createProjectMetadata() method - Creates metadata using string formatting

🆕 What This TDD Creates:

  • ProjectMetadata proto message - Dedicated message for metadata file structure (reusable)
  • ProjectAddress proto message - Structured address information
  • CreateProjectMetadata RPC - Creates metadata file independently (for backfilling)
  • CreateProjectMetadataRequest/Response messages
  • UpdateProjectMetadata RPC - Updates existing metadata (for editing)
  • UpdateProjectMetadataRequest/Response messages
  • ProjectMetadataService class - Service layer for metadata operations
  • Frontend UI for editing metadata with inline editing
  • Legacy project detection and backfilling workflow
  • CLI tool for bulk upgrades

Implementation Phases​

This TDD covers all three phases, but implementation will be incremental:

  • Phase 1 (MVP): Core metadata editing with universal fields only
  • Phase 2: Pluggable code metadata architecture with IBC support
  • Phase 3: Extended code support (IRC, NFPA, state codes)

Each section below is marked with the phase it belongs to.

Proto File Organization (Phase 2)​

For Phase 2, organize code-specific definitions by supplier (ICC, NFPA, etc.) with granular packages:

src/main/proto/
├── api.proto # Main API (Phase 1 ProjectMetadata)
├── code/ # Building code definitions
│ ├── common.proto # Shared custom options
│ ├── icc/ # ICC (International Code Council)
│ │ ├── ibc/ # IBC (International Building Code)
│ │ │ ├── ibc_common.proto # IBC-specific options
│ │ │ ├── ibc_occupancy.proto # Occupancy enums and messages
│ │ │ ├── ibc_construction.proto # Construction type enums
│ │ │ ├── ibc_fire_protection.proto # Fire protection enums
│ │ │ └── ibc_height_area.proto # Height/area messages
│ │ └── irc/ # IRC (International Residential Code)
│ │ ├── irc_common.proto
│ │ └── irc_dwelling.proto
│ └── nfpa/ # NFPA (National Fire Protection Association)
│ ├── nfpa_common.proto
│ └── nfpa_101.proto # Life Safety Code
└── state/ # State-specific codes
├── california/
│ └── cbc.proto # California Building Code
└── florida/
└── fbc.proto # Florida Building Code

Package Structure:

// code/common.proto
syntax = "proto3";
package org.codetricks.construction.code;
// Define shared custom options here

// code/icc/ibc/ibc_common.proto
syntax = "proto3";
package org.codetricks.construction.code.icc.ibc;
import "code/common.proto";
// IBC-specific custom options

// code/icc/ibc/ibc_occupancy.proto
syntax = "proto3";
package org.codetricks.construction.code.icc.ibc.occupancy;
import "code/icc/ibc/ibc_common.proto";
// IbcOccupancyGroup enum, IbcOccupancyClassification message

// code/icc/ibc/ibc_construction.proto
syntax = "proto3";
package org.codetricks.construction.code.icc.ibc.construction;
import "code/icc/ibc/ibc_common.proto";
// IbcConstructionTypeEnum, IbcConstructionType message

// code/icc/ibc/ibc_fire_protection.proto
syntax = "proto3";
package org.codetricks.construction.code.icc.ibc.fire;
import "code/icc/ibc/ibc_common.proto";
// SprinklerSystemType enum, IbcFireProtection message

// code/icc/irc/irc_dwelling.proto
syntax = "proto3";
package org.codetricks.construction.code.icc.irc.dwelling;
// IrcDwellingType enum, IrcCodeMetadata message

// code/nfpa/nfpa_101.proto
syntax = "proto3";
package org.codetricks.construction.code.nfpa.lifesafety;
// NFPA 101 Life Safety Code definitions

Benefits:

  • ✅ Supplier hierarchy: ICC → IBC/IRC, NFPA → 101/5000, State → CA/FL
  • ✅ Granular packages: Prevent enum value collisions across all codes
  • ✅ Scalable: Easy to add new suppliers and codes
  • ✅ Clear organization: Mirrors industry structure (ICC publishes IBC/IRC)
  • ✅ Smaller files: Each proto file focused on one aspect
  • ✅ Clean Java packages: Matches proto package structure

Generated Java Classes:

// ICC IBC Occupancy
package org.codetricks.construction.code.icc.ibc.occupancy;
public enum IbcOccupancyGroup { R_2, B, M, ... }

// ICC IBC Construction
package org.codetricks.construction.code.icc.ibc.construction;
public enum IbcConstructionTypeEnum { I_A, II_A, ... }

// ICC IRC Dwelling
package org.codetricks.construction.code.icc.irc.dwelling;
public enum IrcDwellingType { SINGLE_FAMILY, DUPLEX, ... }

// NFPA Life Safety
package org.codetricks.construction.code.nfpa.lifesafety;
public enum NfpaOccupancyType { ASSEMBLY, EDUCATIONAL, ... }

// No collisions across any codes - all in separate packages!

Import Example in api.proto:

// api.proto
syntax = "proto3";
package org.codetricks.construction.code.assistant.service;

import "code/icc/ibc/ibc_occupancy.proto";
import "code/icc/ibc/ibc_construction.proto";
import "code/icc/irc/irc_dwelling.proto";
import "code/nfpa/nfpa_101.proto";

// Use the imported types
message IbcCodeMetadata {
org.codetricks.construction.code.icc.ibc.occupancy.IbcOccupancyClassification occupancy_classification = 1;
// ... or use fully qualified names
}

Proto Message Definitions​

Phase 1: ProjectMetadata Message (Core Fields Only)​

For Phase 1 MVP, implement only the core universal fields:

// Message representing project metadata stored in project-metadata.json
// Phase 1: Core universal fields only
message ProjectMetadata {
// ========== CORE FIELDS (Universal) ==========

// Required: The unique identifier of the project
string project_id = 1;

// Required: The display name of the project
string project_name = 2;

// Optional: The description of the project
string description = 3;

// Required: The email of the user who created the project
string created_by = 4;

// Required: The creation timestamp in ISO 8601 format
string created_at = 5;

// Required: The current status of the project (active, archived)
string status = 6;

// Optional: Last modified timestamp in ISO 8601 format
string updated_at = 7;

// Optional: Email of the user who last modified the metadata
string updated_by = 8;

// ========== UNIVERSAL PROJECT DETAILS ==========

// Project physical address (universal)
ProjectAddress address = 9;

// Primary building code for this project (e.g., "IBC", "IRC", "NFPA")
string primary_building_code = 10;

// Jurisdiction name (e.g., "San Jose, CA", "State of California")
string jurisdiction = 11;

// Project type (universal across all codes)
// Values: new_construction, renovation, addition, alteration
string project_type = 12;

// ========== PHASE 2: CODE-SPECIFIC METADATA (Future) ==========
// RESERVED for Phase 2 - not implemented in Phase 1
// repeated CodeMetadata applicable_codes = 100;
}

// Message representing project address
// Required for jurisdiction determination and code applicability
message ProjectAddress {
string street_address = 1;
string city = 2;
string state = 3;
string zip_code = 4;
string country = 5; // Default: "USA"
}

Phase 2: Complete ProjectMetadata with Pluggable Code Metadata​

For Phase 2, add the pluggable code metadata architecture. This schema uses a pluggable, extensible design that supports multiple building codes simultaneously while maintaining type safety.

// Message representing project metadata stored in project-metadata.json
// This is the canonical representation of project metadata that can be
// used across multiple RPCs (Get, Create, Update)
//
// Design Philosophy:
// - Core fields are universal (work for any code/jurisdiction)
// - Code-specific metadata is pluggable via repeated CodeMetadata
// - Type-safe via oneof while supporting multiple codes
// - Extensible for future codes without breaking changes
message ProjectMetadata {
// ========== CORE FIELDS (Universal) ==========

// Required: The unique identifier of the project
string project_id = 1;

// Required: The display name of the project
string project_name = 2;

// Optional: The description of the project
string description = 3;

// Required: The email of the user who created the project
string created_by = 4;

// Required: The creation timestamp in ISO 8601 format
string created_at = 5;

// Required: The current status of the project (active, archived)
string status = 6;

// Optional: Last modified timestamp in ISO 8601 format
string updated_at = 7;

// Optional: Email of the user who last modified the metadata
string updated_by = 8;

// ========== UNIVERSAL PROJECT DETAILS ==========

// Project physical address (universal)
ProjectAddress address = 9;

// Primary building code for this project (e.g., "IBC", "IRC", "NFPA")
string primary_building_code = 10;

// Jurisdiction name (e.g., "San Jose, CA", "State of California")
string jurisdiction = 11;

// Project type (universal across all codes)
// Values: new_construction, renovation, addition, alteration
string project_type = 12;

// ========== CODE-SPECIFIC METADATA (Pluggable) ==========
// Multiple building codes can be applicable to a single project
// Each code has its own typed metadata message
// Examples: IBC + California Building Code amendments, IBC + NFPA 101, etc.
repeated CodeMetadata applicable_codes = 100;
}

// Message representing project address
// Required for jurisdiction determination and code applicability
message ProjectAddress {
string street_address = 1;
string city = 2;
string state = 3;
string zip_code = 4;
string country = 5; // Default: "USA"
}

// Wrapper message for code-specific metadata
// Allows multiple codes while maintaining type safety via oneof
message CodeMetadata {
// Unique identifier for this code application
// Format: "{code}_{edition}" (e.g., "ibc_2021", "cbc_2022", "nfpa_101_2021")
string code_id = 1;

// Whether this is the primary code for the project
// Typically one code is primary, others are supplementary
bool is_primary = 2;

// Code-specific metadata (type-safe via oneof)
// Only one code type per CodeMetadata entry
oneof metadata {
IbcCodeMetadata ibc = 10;
IrcCodeMetadata irc = 11;
NfpaCodeMetadata nfpa = 12;
CaliforniaBuildingCodeMetadata cbc = 13;
FloridaBuildingCodeMetadata fbc = 14;
// Future: Add more codes as needed without breaking changes
}
}

// ========== IBC-SPECIFIC METADATA ==========

// IBC (International Building Code) specific metadata
// Organized by IBC chapter structure for clarity
message IbcCodeMetadata {
// Code edition year (2021, 2024, etc.)
int32 edition_year = 1;

// IBC Chapter 3: Occupancy Classification
IbcOccupancyClassification occupancy_classification = 10;

// IBC Chapter 6: Construction Type
IbcConstructionType construction_type = 11;

// IBC Chapter 5: Height and Area
IbcHeightAndArea height_and_area = 12;

// IBC Chapter 9: Fire Protection
IbcFireProtection fire_protection = 13;

// IBC Chapter 7: Fire Separation
IbcFireSeparation fire_separation = 14;
}

// IBC Chapter 3: Occupancy Classification and Use
message IbcOccupancyClassification {
// Reference: IBC 2021 Chapter 3, Section 302.1

// Primary occupancy group
IbcOccupancyGroup primary_group = 1;

// Whether building has mixed occupancies (IBC Section 508)
bool is_mixed_occupancy = 2;

// Secondary occupancy groups for mixed-use buildings
repeated IbcOccupancyGroup secondary_groups = 3;

// Design occupant load (IBC Chapter 10)
// Critical for Assembly occupancies (50 person threshold per Section 303.1.1)
int32 design_occupant_load = 4;

// Special use designations (various IBC Chapter 3 sections)
repeated IbcSpecialUse special_use_designations = 5;
}

// ========== PHASE 2: IBC ENUMS AND SUBMESSAGES ==========
// These should be defined in separate proto files for better organization
// Recommended file: src/main/proto/code/icc/ibc/ibc_occupancy.proto
// Package: org.codetricks.construction.code.icc.ibc.occupancy

// Custom options for IBC enum metadata
// Similar to step_title and step_progress_percent in task.proto
// Define once in code/common.proto or code/icc/ibc/ibc_common.proto
extend google.protobuf.EnumValueOptions {
string ibc_code = 50010; // IBC code (e.g., "A-1", "R-2", "Type I-A")
string ibc_display_name = 50011; // Human-readable name
string ibc_section = 50012; // IBC section reference (e.g., "303.2", "602")
string ibc_chapter = 50013; // IBC chapter reference (e.g., "Chapter 3", "Chapter 6")
}

// IBC Occupancy Groups (IBC Chapter 3, Section 302.1)
// File: src/main/proto/code/icc/ibc/ibc_occupancy.proto
// Package: org.codetricks.construction.code.icc.ibc.occupancy
// With custom annotations for display names and section references
enum IbcOccupancyGroup {
UNKNOWN = 0 [
(ibc_code) = "UNKNOWN",
(ibc_display_name) = "Unknown Occupancy",
(ibc_section) = "",
(ibc_chapter) = ""
];

// Assembly Groups (Section 303)
A_1 = 1 [
(ibc_code) = "A-1",
(ibc_display_name) = "Assembly - Theaters, concert halls (fixed seating)",
(ibc_section) = "303.2",
(ibc_chapter) = "Chapter 3"
];
A_2 = 2 [
(ibc_code) = "A-2",
(ibc_display_name) = "Assembly - Restaurants, bars (food/drink)",
(ibc_section) = "303.3",
(ibc_chapter) = "Chapter 3"
];
A_3 = 3 [
(ibc_code) = "A-3",
(ibc_display_name) = "Assembly - Churches, recreation, libraries",
(ibc_section) = "303.4",
(ibc_chapter) = "Chapter 3"
];
A_4 = 4 [
(ibc_code) = "A-4",
(ibc_display_name) = "Assembly - Arenas, skating rinks (indoor sporting)",
(ibc_section) = "303.5",
(ibc_chapter) = "Chapter 3"
];
A_5 = 5 [
(ibc_code) = "A-5",
(ibc_display_name) = "Assembly - Stadiums, amusement parks (outdoor)",
(ibc_section) = "303.6",
(ibc_chapter) = "Chapter 3"
];

// Business (Section 304)
B = 10 [
(ibc_code) = "B",
(ibc_display_name) = "Business - Professional offices, banks",
(ibc_section) = "304.1",
(ibc_chapter) = "Chapter 3"
];

// Educational (Section 305)
E = 20 [
(ibc_code) = "E",
(ibc_display_name) = "Educational - Schools, daycare (>5 children)",
(ibc_section) = "305.1",
(ibc_chapter) = "Chapter 3"
];

// Factory/Industrial (Section 306)
F_1 = 30 [
(ibc_code) = "F-1",
(ibc_display_name) = "Factory/Industrial - Moderate hazard",
(ibc_section) = "306.2",
(ibc_chapter) = "Chapter 3"
];
F_2 = 31 [
(ibc_code) = "F-2",
(ibc_display_name) = "Factory/Industrial - Low hazard",
(ibc_section) = "306.3",
(ibc_chapter) = "Chapter 3"
];

// High Hazard (Section 307)
H_1 = 40 [
(ibc_code) = "H-1",
(ibc_display_name) = "High Hazard - Detonation hazards",
(ibc_section) = "307.3",
(ibc_chapter) = "Chapter 3"
];
H_2 = 41 [
(ibc_code) = "H-2",
(ibc_display_name) = "High Hazard - Deflagration hazards",
(ibc_section) = "307.4",
(ibc_chapter) = "Chapter 3"
];
H_3 = 42 [
(ibc_code) = "H-3",
(ibc_display_name) = "High Hazard - Physical hazards",
(ibc_section) = "307.5",
(ibc_chapter) = "Chapter 3"
];
H_4 = 43 [
(ibc_code) = "H-4",
(ibc_display_name) = "High Hazard - Health hazards",
(ibc_section) = "307.6",
(ibc_chapter) = "Chapter 3"
];
H_5 = 44 [
(ibc_code) = "H-5",
(ibc_display_name) = "High Hazard - HPM (semiconductor fabrication)",
(ibc_section) = "307.7",
(ibc_chapter) = "Chapter 3"
];

// Institutional (Section 308)
I_1 = 50 [
(ibc_code) = "I-1",
(ibc_display_name) = "Institutional - Assisted living (>16 persons)",
(ibc_section) = "308.2",
(ibc_chapter) = "Chapter 3"
];
I_2 = 51 [
(ibc_code) = "I-2",
(ibc_display_name) = "Institutional - Hospitals, nursing homes",
(ibc_section) = "308.3",
(ibc_chapter) = "Chapter 3"
];
I_3 = 52 [
(ibc_code) = "I-3",
(ibc_display_name) = "Institutional - Prisons, jails",
(ibc_section) = "308.4",
(ibc_chapter) = "Chapter 3"
];
I_4 = 53 [
(ibc_code) = "I-4",
(ibc_display_name) = "Institutional - Daycare (>5 persons, <24hr)",
(ibc_section) = "308.5",
(ibc_chapter) = "Chapter 3"
];

// Mercantile (Section 309)
M = 60 [
(ibc_code) = "M",
(ibc_display_name) = "Mercantile - Retail stores, markets",
(ibc_section) = "309.1",
(ibc_chapter) = "Chapter 3"
];

// Residential (Section 310)
R_1 = 70 [
(ibc_code) = "R-1",
(ibc_display_name) = "Residential - Hotels, motels (transient)",
(ibc_section) = "310.2",
(ibc_chapter) = "Chapter 3"
];
R_2 = 71 [
(ibc_code) = "R-2",
(ibc_display_name) = "Residential - Apartments, dormitories (>2 units)",
(ibc_section) = "310.3",
(ibc_chapter) = "Chapter 3"
];
R_3 = 72 [
(ibc_code) = "R-3",
(ibc_display_name) = "Residential - Single-family, duplexes (1-2 units)",
(ibc_section) = "310.4",
(ibc_chapter) = "Chapter 3"
];
R_4 = 73 [
(ibc_code) = "R-4",
(ibc_display_name) = "Residential - Assisted living (6-16 persons)",
(ibc_section) = "310.5",
(ibc_chapter) = "Chapter 3"
];

// Storage (Section 311)
S_1 = 80 [
(ibc_code) = "S-1",
(ibc_display_name) = "Storage - Moderate hazard",
(ibc_section) = "311.2",
(ibc_chapter) = "Chapter 3"
];
S_2 = 81 [
(ibc_code) = "S-2",
(ibc_display_name) = "Storage - Low hazard",
(ibc_section) = "311.3",
(ibc_chapter) = "Chapter 3"
];

// Utility/Miscellaneous (Section 312)
U = 90 [
(ibc_code) = "U",
(ibc_display_name) = "Utility - Carports, sheds, towers",
(ibc_section) = "312.1",
(ibc_chapter) = "Chapter 3"
];
}

// IBC Special Use Designations
enum IbcSpecialUse {
SPECIAL_USE_NONE = 0;
SPECIAL_USE_DAY_CARE = 1; // Section 305.2, 308.5
SPECIAL_USE_RELIGIOUS_WORSHIP = 2; // Section 303.1.4
SPECIAL_USE_HIGH_HAZARD = 3; // Section 307
SPECIAL_USE_SPECIAL_AMUSEMENT = 4; // Section 303.1.5
SPECIAL_USE_COVERED_MALL = 5; // Section 402
SPECIAL_USE_AMBULATORY_CARE = 6; // Section 422
SPECIAL_USE_AIRCRAFT_HANGAR = 7; // Section 412
}

// IBC Chapter 6: Types of Construction
message IbcConstructionType {
// Reference: IBC 2021 Chapter 6, Section 602

// Construction type classification
IbcConstructionTypeEnum type = 1;
}

// IBC Construction Types (IBC Chapter 6, Section 602)
// File: src/main/proto/code/icc/ibc/ibc_construction.proto
// Package: org.codetricks.construction.code.icc.ibc.construction
// With custom annotations for display names and section references
enum IbcConstructionTypeEnum {
UNKNOWN = 0 [
(ibc_code) = "UNKNOWN",
(ibc_display_name) = "Unknown Construction Type",
(ibc_section) = "",
(ibc_chapter) = ""
];

// Type I - Noncombustible (highest fire resistance)
I_A = 1 [
(ibc_code) = "I-A",
(ibc_display_name) = "Noncombustible (highest fire resistance)",
(ibc_section) = "602.2",
(ibc_chapter) = "Chapter 6"
];
I_B = 2 [
(ibc_code) = "I-B",
(ibc_display_name) = "Noncombustible (high fire resistance)",
(ibc_section) = "602.2",
(ibc_chapter) = "Chapter 6"
];

// Type II - Noncombustible
II_A = 10 [
(ibc_code) = "II-A",
(ibc_display_name) = "Noncombustible (protected)",
(ibc_section) = "602.3",
(ibc_chapter) = "Chapter 6"
];
II_B = 11 [
(ibc_code) = "II-B",
(ibc_display_name) = "Noncombustible (unprotected)",
(ibc_section) = "602.3",
(ibc_chapter) = "Chapter 6"
];

// Type III - Exterior masonry, interior any
III_A = 20 [
(ibc_code) = "III-A",
(ibc_display_name) = "Exterior masonry (protected)",
(ibc_section) = "602.4",
(ibc_chapter) = "Chapter 6"
];
III_B = 21 [
(ibc_code) = "III-B",
(ibc_display_name) = "Exterior masonry (unprotected)",
(ibc_section) = "602.4",
(ibc_chapter) = "Chapter 6"
];

// Type IV - Heavy Timber
IV_A = 30 [
(ibc_code) = "IV-A",
(ibc_display_name) = "Heavy timber (protected)",
(ibc_section) = "602.5",
(ibc_chapter) = "Chapter 6"
];
IV_B = 31 [
(ibc_code) = "IV-B",
(ibc_display_name) = "Heavy timber (protected)",
(ibc_section) = "602.5",
(ibc_chapter) = "Chapter 6"
];
IV_C = 32 [
(ibc_code) = "IV-C",
(ibc_display_name) = "Heavy timber (protected)",
(ibc_section) = "602.5",
(ibc_chapter) = "Chapter 6"
];
IV_HT = 33 [
(ibc_code) = "IV-HT",
(ibc_display_name) = "Heavy timber (traditional)",
(ibc_section) = "602.5",
(ibc_chapter) = "Chapter 6"
];

// Type V - Wood frame
V_A = 40 [
(ibc_code) = "V-A",
(ibc_display_name) = "Wood frame (protected)",
(ibc_section) = "602.6",
(ibc_chapter) = "Chapter 6"
];
V_B = 41 [
(ibc_code) = "V-B",
(ibc_display_name) = "Wood frame (unprotected)",
(ibc_section) = "602.6",
(ibc_chapter) = "Chapter 6"
];
}

// IBC Chapter 5: General Building Heights and Areas
message IbcHeightAndArea {
// Reference: IBC 2021 Chapter 5
// Table 504.3 - Allowable Building Height in Feet Above Grade Plane
// Table 504.4 - Allowable Number of Stories Above Grade Plane
// Table 506.2 - Allowable Building Area Per Floor

// ===== HEIGHT AND STORIES (Section 504) =====

// Building height in feet above grade plane (Section 502.1 definition)
double building_height_feet = 1;

// Number of stories above grade plane
// Note: IBC limits this separately from height (Table 504.4)
int32 stories_above_grade = 2;

// Number of stories below grade (basements)
// Basements have different egress requirements
int32 stories_below_grade = 3;

// Whether building has basement(s)
bool has_basement = 4;

// Whether building has mezzanines (Section 505)
// Mezzanines may not count toward floor area under certain conditions
bool has_mezzanines = 5;

// ===== BUILDING AREA (Section 506) =====

// Total building area (sum of all floors)
double total_building_area_sqft = 10;

// Typical floor area (for multi-story buildings)
// IBC limits area per floor, not just total area
double per_floor_area_sqft = 11;

// Largest single floor area
// Used to verify compliance with Table 506.2
double largest_floor_area_sqft = 12;

// Basement area (may have different requirements)
double basement_area_sqft = 13;

// Mezzanine area (Section 505)
// Aggregate mezzanine area cannot exceed 1/3 of room area
double mezzanine_area_sqft = 14;

// ===== FRONTAGE AND OPEN SPACE (Section 506.2, 506.3) =====

// Percentage of building perimeter with public way or open space frontage
// More frontage allows area increases per Section 506.2
double frontage_percentage = 20;

// Whether building qualifies for area increase due to frontage
bool has_open_space_increase = 21;
}

// IBC Chapter 9: Fire Protection and Life Safety Systems
message IbcFireProtection {
// Reference: IBC 2021 Chapter 9

// Whether building has automatic sprinkler system (Section 903)
// CRITICAL: Sprinklers can double allowable area and add stories
bool has_sprinkler_system = 1;

// Type of sprinkler system
SprinklerSystemType sprinkler_type = 2;

// Whether building has fire alarm system (Section 907)
bool has_fire_alarm = 3;

// Whether building has standpipe system (Section 905)
bool has_standpipe = 4;
}

// Sprinkler system types per NFPA standards
// File: src/main/proto/code/icc/ibc/ibc_fire_protection.proto
// Package: org.codetricks.construction.code.icc.ibc.fire
enum SprinklerSystemType {
NONE = 0 [
(ibc_code) = "NONE",
(ibc_display_name) = "No sprinkler system",
(ibc_section) = "",
(ibc_chapter) = ""
];
NFPA_13 = 1 [
(ibc_code) = "NFPA 13",
(ibc_display_name) = "Commercial sprinkler system",
(ibc_section) = "903.3.1.1",
(ibc_chapter) = "Chapter 9"
];
NFPA_13R = 2 [
(ibc_code) = "NFPA 13R",
(ibc_display_name) = "Residential sprinkler system (≤4 stories)",
(ibc_section) = "903.3.1.2",
(ibc_chapter) = "Chapter 9"
];
NFPA_13D = 3 [
(ibc_code) = "NFPA 13D",
(ibc_display_name) = "Residential sprinkler system (1-2 family)",
(ibc_section) = "903.3.1.3",
(ibc_chapter) = "Chapter 9"
];
}

// IBC Chapter 7: Fire and Smoke Protection Features
message IbcFireSeparation {
// Reference: IBC 2021 Chapter 7, Section 706

// Whether building has fire walls
// Fire walls allow treating one building as multiple buildings for height/area calculations
bool has_fire_walls = 1;

// Number of fire-separated buildings (if fire walls present)
// Each separated portion can independently meet height/area limits
int32 number_of_separated_buildings = 2;
}

// ========== IRC-SPECIFIC METADATA (Future) ==========

// IRC (International Residential Code) specific metadata
// For residential projects (1-2 family dwellings, townhouses)
message IrcCodeMetadata {
// Code edition year
int32 edition_year = 1;

// Dwelling type per IRC scope
// Values: single_family, two_family, townhouse
string dwelling_type = 2;

// Whether dwelling has basement
bool has_basement = 3;

// Number of stories (IRC has simpler height/area rules than IBC)
int32 number_of_stories = 4;

// Total floor area (IRC uses simpler area calculations)
double total_floor_area_sqft = 5;

// Whether sprinkler system is installed (IRC Section P2904)
bool has_sprinkler_system = 6;
}

// ========== NFPA-SPECIFIC METADATA (Future) ==========

// NFPA (National Fire Protection Association) code metadata
// For projects primarily governed by NFPA codes
message NfpaCodeMetadata {
// NFPA code number (e.g., "101" for Life Safety Code, "5000" for Building Code)
string nfpa_code_number = 1;

// Code edition year
int32 edition_year = 2;

// NFPA-specific requirements
// (To be defined based on specific NFPA code)
}

// ========== STATE-SPECIFIC CODE METADATA (Future) ==========

// California Building Code (CBC) amendments
message CaliforniaBuildingCodeMetadata {
// CBC edition year
int32 edition_year = 1;

// Title 24 energy compliance
bool title_24_compliant = 2;

// California-specific amendments
string cbc_amendments = 3;
}

// Florida Building Code (FBC) amendments
message FloridaBuildingCodeMetadata {
// FBC edition year
int32 edition_year = 1;

// High-velocity hurricane zone designation
bool in_hvhz = 2;

// Wind speed design (mph)
int32 design_wind_speed = 3;

// Florida-specific amendments
string fbc_amendments = 4;
}

Updated: GetProjectMetadataResponse​

Update the response to use the new ProjectMetadata message:

// Response message containing project metadata.
message GetProjectMetadataResponse {
// The project metadata
ProjectMetadata metadata = 1;
}

Phase 1: UpdateProjectMetadataRequest (Core Fields Only)​

// Request message for updating project metadata.
// Uses PATCH semantics - only updates the fields that are explicitly set.
// All fields are optional except project_id.
// Phase 1: Core fields only
message UpdateProjectMetadataRequest {
// Required: The unique identifier of the project
string project_id = 1;

// ========== CORE FIELDS ==========
string project_name = 2;
string description = 3;

// ========== UNIVERSAL PROJECT DETAILS ==========
ProjectAddress address = 9;
string primary_building_code = 10;
string jurisdiction = 11;
string project_type = 12;

// ========== PHASE 2: CODE-SPECIFIC METADATA (Future) ==========
// RESERVED for Phase 2 - not implemented in Phase 1
// repeated CodeMetadata applicable_codes = 100;
}

Phase 2: UpdateProjectMetadataRequest (With Code Metadata)​

// Phase 2: Add code-specific metadata support
message UpdateProjectMetadataRequest {
// Required: The unique identifier of the project
string project_id = 1;

// ========== CORE FIELDS ==========
string project_name = 2;
string description = 3;

// ========== UNIVERSAL PROJECT DETAILS ==========
ProjectAddress address = 9;
string primary_building_code = 10;
string jurisdiction = 11;
string project_type = 12;

// ========== CODE-SPECIFIC METADATA ==========
// To update code metadata:
// - Include the CodeMetadata entry with updated fields
// - Backend will merge/replace based on code_id
repeated CodeMetadata applicable_codes = 100;
}

New: UpdateProjectMetadataResponse​

// Response message for updating project metadata.
message UpdateProjectMetadataResponse {
// Whether the update was successful
bool success = 1;

// The updated project metadata
ProjectMetadata metadata = 2;

// Optional error message if update failed
string error_message = 3;
}

Phase 1: CreateProjectMetadataRequest (Core Fields Only)​

// Request message for creating project metadata file.
// This is separate from CreateArchitecturalPlanRequest which creates
// the entire project structure (folders + metadata).
// Used primarily for backfilling legacy projects.
// Phase 1: Core fields only
message CreateProjectMetadataRequest {
// Required: The unique identifier of the project
string project_id = 1;

// Required: The display name of the project
string project_name = 2;

// Optional: The description of the project
string description = 3;

// ========== OPTIONAL: UNIVERSAL PROJECT DETAILS ==========
// All fields below are optional and can be populated later via UpdateProjectMetadata

ProjectAddress address = 9;
string primary_building_code = 10;
string jurisdiction = 11;
string project_type = 12;

// ========== PHASE 2: CODE-SPECIFIC METADATA (Future) ==========
// RESERVED for Phase 2 - not implemented in Phase 1
// repeated CodeMetadata applicable_codes = 100;
}

New: CreateProjectMetadataResponse​

// Response message for creating project metadata.
message CreateProjectMetadataResponse {
// Whether the creation was successful
bool success = 1;

// The created project metadata
ProjectMetadata metadata = 2;

// Optional error message if creation failed
string error_message = 3;
}

Architecture Overview​

Extensible Metadata Structure​

ProjectMetadata
├── Core Fields (Universal)
│ ├── project_id
│ ├── project_name
│ ├── description
│ ├── created_by/at
│ ├── updated_by/at
│ └── status
│
├── Universal Details
│ ├── address (ProjectAddress)
│ ├── primary_building_code
│ ├── jurisdiction
│ └── project_type
│
└── applicable_codes[] (Pluggable)
├── CodeMetadata #1 (IBC 2021)
│ ├── code_id: "ibc_2021"
│ ├── is_primary: true
│ └── ibc (IbcCodeMetadata)
│ ├── occupancy_classification
│ ├── construction_type
│ ├── height_and_area
│ ├── fire_protection
│ └── fire_separation
│
├── CodeMetadata #2 (California CBC 2022)
│ ├── code_id: "cbc_2022"
│ ├── is_primary: false
│ └── cbc (CaliforniaBuildingCodeMetadata)
│ ├── edition_year
│ ├── title_24_compliant
│ └── cbc_amendments
│
└── CodeMetadata #3 (Future: NFPA, IRC, etc.)
└── ...

Design Benefits​

Why This Extensible Design?​

1. Multiple Codes Support

  • Real projects often comply with multiple codes simultaneously
  • Example: IBC 2021 + California Building Code 2022 + Local amendments
  • Each code can have its own metadata without conflicts

2. Type Safety

  • oneof ensures each CodeMetadata entry has exactly one code type
  • Compile-time checking prevents invalid combinations
  • IDE autocomplete works perfectly

3. Future-Proof

  • Adding new codes (e.g., NfpaCodeMetadata) doesn't break existing projects
  • Old projects without code metadata continue working
  • Can add new code types without schema migration

4. Clean Organization

  • Core fields stay simple and universal
  • Code-specific complexity is isolated in submessages
  • Each code's metadata is organized by its own chapter structure

5. Queryable

  • Easy to find specific code: metadata.applicable_codes.find(c => c.code_id === "ibc_2021")
  • Can check if code exists: hasCodeMetadata(metadata, "ibc_2021")
  • Can iterate over all applicable codes

6. Versioned

  • Each code can have its own edition year
  • Same project can reference IBC 2021 and CBC 2022 simultaneously
  • Clear tracking of which code version applies

7. Gradual Adoption

  • Start with minimal metadata (just name and description)
  • Add code metadata incrementally as information becomes available
  • No forced fields except core project information

Usage Examples​

Phase 1 Examples (Core Fields Only)​

Example 1: Simple Project (Phase 1 MVP)​

{
"project_id": "office-building-123",
"project_name": "Downtown Office Building",
"description": "5-story office building",
"created_by": "user@example.com",
"created_at": "2025-10-05T12:00:00Z",
"status": "active",
"primary_building_code": "IBC",
"jurisdiction": "San Jose, CA",
"project_type": "new_construction",
"address": {
"street_address": "123 Main Street",
"city": "San Jose",
"state": "CA",
"zip_code": "95110",
"country": "USA"
}
}

Example 2: Legacy Project Backfill (Phase 1 MVP - Minimal)​

{
"project_id": "legacy-project-old",
"project_name": "legacy-project-old",
"description": "",
"created_by": "system@codeproof.app",
"created_at": "2025-10-05T12:00:00Z",
"status": "active",
"primary_building_code": "IBC"
}

Phase 2 Examples (With Code Metadata)​

Example 3: IBC Project with Full Metadata (Phase 2)​

{
"project_id": "office-building-123",
"project_name": "Downtown Office Building",
"description": "5-story office building",
"primary_building_code": "IBC",
"jurisdiction": "San Jose, CA",
"project_type": "new_construction",
"address": {
"street_address": "123 Main Street",
"city": "San Jose",
"state": "CA",
"zip_code": "95110",
"country": "USA"
},
"applicable_codes": [
{
"code_id": "ibc_2021",
"is_primary": true,
"ibc": {
"edition_year": 2021,
"occupancy_classification": {
"primary_group": "B"
},
"construction_type": {
"type": "II_A"
},
"height_and_area": {
"building_height_feet": 65.0,
"stories_above_grade": 5,
"stories_below_grade": 1,
"has_basement": true,
"total_building_area_sqft": 50000.0,
"per_floor_area_sqft": 10000.0
},
"fire_protection": {
"has_sprinkler_system": true,
"sprinkler_type": "NFPA_13",
"has_fire_alarm": true
}
}
}
]
}

Example 4: Mixed-Use Building with Multiple Codes (Phase 2)​

{
"project_id": "mixed-use-sf-456",
"project_name": "Bay Area Mixed-Use Development",
"description": "Residential over retail with California Title 24 compliance",
"primary_building_code": "IBC",
"jurisdiction": "San Francisco, CA",
"project_type": "new_construction",
"applicable_codes": [
{
"code_id": "ibc_2021",
"is_primary": true,
"ibc": {
"edition_year": 2021,
"occupancy_classification": {
"primary_group": "R_2",
"is_mixed_occupancy": true,
"secondary_groups": ["B", "M"]
},
"construction_type": {
"type": "V_A"
},
"height_and_area": {
"building_height_feet": 55.0,
"stories_above_grade": 4,
"total_building_area_sqft": 80000.0,
"per_floor_area_sqft": 20000.0
},
"fire_protection": {
"has_sprinkler_system": true,
"sprinkler_type": "NFPA_13R"
}
}
},
{
"code_id": "cbc_2022",
"is_primary": false,
"cbc": {
"edition_year": 2022,
"title_24_compliant": true,
"cbc_amendments": "California Title 24 energy efficiency requirements"
}
}
]
}

Phase 3 Examples (Extended Code Support)​

Example 5: Residential Project with IRC (Phase 3)​

{
"project_id": "single-family-789",
"project_name": "Single Family Residence",
"primary_building_code": "IRC",
"jurisdiction": "Austin, TX",
"project_type": "new_construction",
"applicable_codes": [
{
"code_id": "irc_2021",
"is_primary": true,
"irc": {
"edition_year": 2021,
"dwelling_type": "single_family",
"has_basement": true,
"number_of_stories": 2,
"total_floor_area_sqft": 3500.0,
"has_sprinkler_system": true
}
}
]
}

RPC Service Definitions​

Add to ArchitecturalPlanService in src/main/proto/api.proto:

service ArchitecturalPlanService {
// ... existing RPCs ...

// Creates project metadata file (project-metadata.json) for an existing project.
// This is a lightweight operation separate from CreateArchitecturalPlan which
// creates the entire project structure.
rpc CreateProjectMetadata(CreateProjectMetadataRequest) returns (CreateProjectMetadataResponse) {
option (google.api.http) = {
post: "/v1/architectural-plans/{project_id}/metadata"
body: "*"
};
}

// Updates project metadata file (project-metadata.json).
// Only updates the fields that are explicitly provided in the request.
// This is a lightweight, synchronous operation.
rpc UpdateProjectMetadata(UpdateProjectMetadataRequest) returns (UpdateProjectMetadataResponse) {
option (google.api.http) = {
patch: "/v1/architectural-plans/{project_id}/metadata"
body: "*"
};
}
}

Enum Utility Classes (Phase 2)​

IbcOccupancyGroupUtils​

Location: src/main/java/org/codetricks/construction/code/icc/ibc/occupancy/IbcOccupancyGroupUtils.java

Purpose: Provides helper methods to access IBC enum annotations, similar to PlanIngestionStepUtils.

package org.codetricks.construction.code.icc.ibc.occupancy;

import com.google.protobuf.Descriptors;
import org.codetricks.construction.code.icc.ibc.occupancy.IbcOccupancyGroup;
import org.codetricks.construction.code.icc.ibc.IbcCommonProto;

/**
* Utility class for working with IbcOccupancyGroup enum.
* Provides access to proto annotations for display names and IBC section references.
*
* Usage:
* IbcOccupancyGroup occupancy = IbcOccupancyGroup.OCCUPANCY_R_2;
* String code = IbcOccupancyGroupUtils.getIbcCode(occupancy); // "R-2"
* String name = IbcOccupancyGroupUtils.getDisplayName(occupancy); // "R-2: Residential - Apartments..."
* String section = IbcOccupancyGroupUtils.getIbcSection(occupancy); // "310.3"
*/
public class IbcOccupancyGroupUtils {

/**
* Gets the IBC code (e.g., "A-1", "R-2") from proto annotations.
*/
public static String getIbcCode(IbcOccupancyGroup occupancy) {
try {
Descriptors.EnumValueDescriptor descriptor = occupancy.getValueDescriptor();
if (descriptor.getOptions().hasExtension(IbcCommonProto.ibcCode)) {
return descriptor.getOptions().getExtension(IbcCommonProto.ibcCode);
}
} catch (Exception e) {
System.err.println("Warning: Could not read ibc_code annotation: " + e.getMessage());
}
return occupancy.name();
}

/**
* Gets the human-readable display name from proto annotations.
* Returns: "Assembly - Theaters, concert halls (fixed seating)"
*/
public static String getDisplayName(IbcOccupancyGroup occupancy) {
try {
Descriptors.EnumValueDescriptor descriptor = occupancy.getValueDescriptor();
if (descriptor.getOptions().hasExtension(IbcCommonProto.ibcDisplayName)) {
return descriptor.getOptions().getExtension(IbcCommonProto.ibcDisplayName);
}
} catch (Exception e) {
System.err.println("Warning: Could not read ibc_display_name annotation: " + e.getMessage());
}
return getIbcCode(occupancy);
}

/**
* Gets the IBC section reference (e.g., "303.2") from proto annotations.
*/
public static String getIbcSection(IbcOccupancyGroup occupancy) {
try {
Descriptors.EnumValueDescriptor descriptor = occupancy.getValueDescriptor();
if (descriptor.getOptions().hasExtension(IbcCommonProto.ibcSection)) {
return descriptor.getOptions().getExtension(IbcCommonProto.ibcSection);
}
} catch (Exception e) {
System.err.println("Warning: Could not read ibc_section annotation: " + e.getMessage());
}
return "";
}

/**
* Gets the IBC chapter reference (e.g., "Chapter 3") from proto annotations.
*/
public static String getIbcChapter(IbcOccupancyGroup occupancy) {
try {
Descriptors.EnumValueDescriptor descriptor = occupancy.getValueDescriptor();
if (descriptor.getOptions().hasExtension(IbcCommonProto.ibcChapter)) {
return descriptor.getOptions().getExtension(IbcCommonProto.ibcChapter);
}
} catch (Exception e) {
System.err.println("Warning: Could not read ibc_chapter annotation: " + e.getMessage());
}
return "";
}

/**
* Gets the full IBC reference (e.g., "IBC Chapter 3, Section 303.2").
*/
public static String getFullReference(IbcOccupancyGroup occupancy) {
String chapter = getIbcChapter(occupancy);
String section = getIbcSection(occupancy);
if (!chapter.isEmpty() && !section.isEmpty()) {
return "IBC " + chapter + ", Section " + section;
}
return "";
}

/**
* Parses IBC code string (e.g., "R-2") to enum value.
*/
public static IbcOccupancyGroup fromIbcCode(String ibcCode) {
for (IbcOccupancyGroup occupancy : IbcOccupancyGroup.values()) {
if (getIbcCode(occupancy).equals(ibcCode)) {
return occupancy;
}
}
return IbcOccupancyGroup.OCCUPANCY_UNKNOWN;
}
}

Similar utility classes for Phase 2:

  • IbcConstructionTypeUtils - For construction type enums
  • IbcSpecialUseUtils - For special use designations
  • SprinklerSystemTypeUtils - For sprinkler types

Usage Examples in Java (Phase 2)​

// Example 1: Using enum with annotations (Clean syntax!)
IbcOccupancyGroup occupancy = IbcOccupancyGroup.R_2; // ✅ No prefix!

// Get IBC code: "R-2"
String code = IbcOccupancyGroupUtils.getIbcCode(occupancy);

// Get display name: "Residential - Apartments, dormitories (>2 units)"
String displayName = IbcOccupancyGroupUtils.getDisplayName(occupancy);

// Get IBC section: "310.3"
String section = IbcOccupancyGroupUtils.getIbcSection(occupancy);

// Get full reference: "IBC Chapter 3, Section 310.3"
String fullRef = IbcOccupancyGroupUtils.getFullReference(occupancy);

// Example 2: Parsing from IBC code string
IbcOccupancyGroup parsed = IbcOccupancyGroupUtils.fromIbcCode("R-2");
// Returns: IbcOccupancyGroup.R_2

// Example 3: JSON serialization (via Proto.toJson)
IbcOccupancyClassification classification = IbcOccupancyClassification.newBuilder()
.setPrimaryGroup(IbcOccupancyGroup.R_2)
.setIsMixedOccupancy(true)
.addSecondaryGroups(IbcOccupancyGroup.B)
.build();

String json = Proto.toJson(classification);
// Output: { "primary_group": "R_2", "is_mixed_occupancy": true, "secondary_groups": ["B"] }

// Example 4: Display in UI
System.out.println("Occupancy: " + IbcOccupancyGroupUtils.getDisplayName(occupancy));
System.out.println("Reference: " + IbcOccupancyGroupUtils.getFullReference(occupancy));
// Output:
// Occupancy: Residential - Apartments, dormitories (>2 units)
// Reference: IBC Chapter 3, Section 310.3

// Example 5: Construction type (also clean!)
IbcConstructionTypeEnum constructionType = IbcConstructionTypeEnum.II_A; // ✅ Clean!
String typeCode = IbcConstructionTypeUtils.getIbcCode(constructionType); // "II-A"

Usage in TypeScript/Angular (Phase 2)​

// Generated TypeScript enum from proto
import { IbcOccupancyGroup } from '../../generated.commonjs/ibc_occupancy_pb';

// Example 1: Using enum in code (Clean syntax!)
const occupancy = IbcOccupancyGroup.R_2; // ✅ No prefix!

// Example 2: Display in dropdown
const occupancyOptions = [
{ value: IbcOccupancyGroup.A_1, label: 'Assembly - Theaters' },
{ value: IbcOccupancyGroup.B, label: 'Business - Offices' },
{ value: IbcOccupancyGroup.R_2, label: 'Residential - Apartments' },
// ... auto-generate from proto annotations
];

// Example 3: Create utility service (similar to Java)
export class IbcOccupancyGroupService {
// Map generated from proto annotations (exported from Java utility)
private static readonly METADATA = {
[IbcOccupancyGroup.R_2]: {
ibcCode: 'R-2',
displayName: 'Residential - Apartments, dormitories (>2 units)',
section: '310.3',
chapter: 'Chapter 3'
},
// ... other mappings
};

static getIbcCode(occupancy: IbcOccupancyGroup): string {
return this.METADATA[occupancy]?.ibcCode || occupancy.toString();
}

static getDisplayName(occupancy: IbcOccupancyGroup): string {
return this.METADATA[occupancy]?.displayName || this.getIbcCode(occupancy);
}
}

Benefits of Proto Enum Annotations​

✅ Single Source of Truth:

  • All metadata (codes, names, sections) defined once in .proto file
  • Java and TypeScript get same values automatically
  • No duplication or sync issues

✅ Type Safety:

  • Compile-time checking in both Java and TypeScript
  • IDE autocomplete works perfectly
  • Cannot use invalid occupancy codes

✅ Readable Code:

// Instead of:
String occupancy = "R-2"; // Magic string, no validation

// We have:
IbcOccupancyGroup occupancy = IbcOccupancyGroup.R_2; // Type-safe! Clean!
String display = IbcOccupancyGroupUtils.getDisplayName(occupancy); // Rich metadata!

✅ JSON Serialization:

  • Proto enums serialize as strings: "R_2" (clean, no prefix!)
  • Utility methods convert to IBC codes: "R-2"
  • Display names available for UI: "R-2: Residential - Apartments..."
  • Context is clear from parent: {"occupancy_classification": {"primary_group": "R_2"}}

✅ Namespace Management via Packages:

  • Granular packages prevent collisions: org.codetricks.construction.code.ibc.occupancy
  • No prefixes needed because Java/TypeScript scope by type
  • Clean, readable enum names: IbcOccupancyGroup.R_2
  • Different codes in different packages: ibc.occupancy vs irc.dwelling

✅ Extensibility:

  • Add new occupancy types without breaking existing code
  • Annotations can be extended (e.g., add ibc_url for documentation links)

Backend Implementation​

Phase 1: ProjectMetadataService (Core Fields Only)​

Location: src/main/java/org/codetricks/construction/code/assistant/service/ProjectMetadataService.java

Phase 1 Scope: Implement only core field handling. Code-specific metadata handling will be added in Phase 2.

package org.codetricks.construction.code.assistant.service;

import org.codetricks.construction.code.assistant.ArchitecturalPlanReviewer;
import org.codetricks.construction.code.assistant.data.FileSystemHandler;
import org.codetricks.construction.code.assistant.proto.ProjectMetadata;
import org.codetricks.construction.code.assistant.proto.UpdateProjectMetadataRequest;
import org.codetricks.construction.code.assistant.util.Proto;

import java.io.Reader;
import java.io.Writer;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
* Service for managing project metadata files.
* Follows the same pattern as InputFileMetadataService.
*/
public class ProjectMetadataService {
private static final Logger logger = Logger.getLogger(ProjectMetadataService.class.getName());
private final FileSystemHandler fileSystemHandler;

public ProjectMetadataService(FileSystemHandler fileSystemHandler) {
this.fileSystemHandler = fileSystemHandler;
}

/**
* Creates project metadata file.
* Similar to InputFileMetadataService pattern.
*/
public ProjectMetadata createProjectMetadata(
String projectId,
String projectName,
String description,
String createdBy
// ... additional fields can be added here
) throws Exception {
String projectPath = ArchitecturalPlanReviewer.getDefaultProjectHomeDir(projectId);
String metadataPath = projectPath + "/project-metadata.json";

// Check if metadata already exists
if (fileSystemHandler.exists(metadataPath)) {
throw new IllegalStateException("Project metadata already exists for project: " + projectId);
}

// Build ProjectMetadata proto message
ProjectMetadata metadata = ProjectMetadata.newBuilder()
.setProjectId(projectId)
.setProjectName(projectName)
.setDescription(description != null ? description : "")
.setCreatedBy(createdBy)
.setCreatedAt(Instant.now().toString())
.setStatus("active")
.build();

// Save to JSON file using Proto utility
saveMetadataToFile(metadata, metadataPath);

logger.info("Created project metadata for project: " + projectId);
return metadata;
}

/**
* Updates project metadata file.
* Only updates fields that are explicitly set in the request (PATCH semantics).
*
* Phase 1: Handles core fields only
* Phase 2: Will add code-specific metadata merging by code_id
*/
public ProjectMetadata updateProjectMetadata(
UpdateProjectMetadataRequest request,
String updatedBy
) throws Exception {
String projectPath = ArchitecturalPlanReviewer.getDefaultProjectHomeDir(request.getProjectId());
String metadataPath = projectPath + "/project-metadata.json";

// Check if metadata exists
if (!fileSystemHandler.exists(metadataPath)) {
throw new IllegalStateException("Project metadata does not exist for project: " + request.getProjectId());
}

// Load existing metadata
ProjectMetadata existing = loadMetadataFromFile(metadataPath);

// Build updated metadata (merge with existing)
ProjectMetadata.Builder builder = existing.toBuilder();

// ========== PHASE 1: CORE FIELDS ==========
if (!request.getProjectName().isEmpty()) {
builder.setProjectName(request.getProjectName());
}
if (!request.getDescription().isEmpty()) {
builder.setDescription(request.getDescription());
}

// ========== PHASE 1: UNIVERSAL PROJECT DETAILS ==========
if (request.hasAddress()) {
builder.setAddress(request.getAddress());
}
if (!request.getPrimaryBuildingCode().isEmpty()) {
builder.setPrimaryBuildingCode(request.getPrimaryBuildingCode());
}
if (!request.getJurisdiction().isEmpty()) {
builder.setJurisdiction(request.getJurisdiction());
}
if (!request.getProjectType().isEmpty()) {
builder.setProjectType(request.getProjectType());
}

// ========== PHASE 2: CODE-SPECIFIC METADATA (Future) ==========
// TODO: Implement in Phase 2
// Merge code metadata by code_id
// if (request.getApplicableCodesCount() > 0) {
// Map<String, CodeMetadata> existingCodeMap = new HashMap<>();
// for (CodeMetadata code : existing.getApplicableCodesList()) {
// existingCodeMap.put(code.getCodeId(), code);
// }
//
// // Merge or add new code metadata
// for (CodeMetadata requestCode : request.getApplicableCodesList()) {
// existingCodeMap.put(requestCode.getCodeId(), requestCode);
// }
//
// // Rebuild the list
// builder.clearApplicableCodes();
// builder.addAllApplicableCodes(existingCodeMap.values());
// }

// Set update metadata
builder.setUpdatedAt(Instant.now().toString());
builder.setUpdatedBy(updatedBy);

ProjectMetadata updated = builder.build();

// Save to JSON file
saveMetadataToFile(updated, metadataPath);

logger.info("Updated project metadata for project: " + request.getProjectId());
return updated;
}

// ========== PHASE 2: CODE METADATA HELPERS (Future) ==========

/**
* Helper method to get code metadata by code_id.
* Phase 2: To be implemented
*/
// public CodeMetadata getCodeMetadata(ProjectMetadata metadata, String codeId) {
// return metadata.getApplicableCodesList().stream()
// .filter(code -> code.getCodeId().equals(codeId))
// .findFirst()
// .orElse(null);
// }

/**
* Helper method to check if project has specific code metadata.
* Phase 2: To be implemented
*/
// public boolean hasCodeMetadata(ProjectMetadata metadata, String codeId) {
// return getCodeMetadata(metadata, codeId) != null;
// }

/**
* Lists all legacy projects that need project-metadata.json backfilling.
* A legacy project has plan-metadata.json but no project-metadata.json.
*/
public List<String> listLegacyProjects() throws Exception {
List<String> allProjects = ArchitecturalPlanReviewer.listArchitecturalPlanIds(fileSystemHandler);
List<String> legacyProjects = new ArrayList<>();

for (String projectId : allProjects) {
if (isLegacyProject(projectId)) {
legacyProjects.add(projectId);
}
}

logger.info("Found " + legacyProjects.size() + " legacy projects out of " + allProjects.size() + " total projects");
return legacyProjects;
}

/**
* Checks if a project is a legacy project (has plan-metadata.json but no project-metadata.json).
*/
public boolean isLegacyProject(String projectId) throws Exception {
String projectPath = ArchitecturalPlanReviewer.getDefaultProjectHomeDir(projectId);
String planMetadataPath = projectPath + "/plan-metadata.json";
String projectMetadataPath = projectPath + "/project-metadata.json";

return fileSystemHandler.exists(planMetadataPath) &&
!fileSystemHandler.exists(projectMetadataPath);
}

/**
* Loads metadata from JSON file using Proto utility.
*/
private ProjectMetadata loadMetadataFromFile(String metadataPath) throws Exception {
try (Reader reader = fileSystemHandler.getReader(metadataPath)) {
StringBuilder jsonContent = new StringBuilder();
char[] buffer = new char[1024];
int length;
while ((length = reader.read(buffer)) != -1) {
jsonContent.append(buffer, 0, length);
}
return Proto.loadJson(ProjectMetadata.class, jsonContent.toString());
}
}

/**
* Saves metadata to JSON file using Proto utility.
*/
private void saveMetadataToFile(ProjectMetadata metadata, String metadataPath) throws Exception {
try (Writer writer = fileSystemHandler.getWriter(metadataPath)) {
String jsonString = Proto.toJson(metadata);
writer.write(jsonString);
logger.info("Saved metadata to: " + metadataPath);
}
}
}

Service Implementation: ArchitecturalPlanServiceImpl​

Add these methods to src/main/java/org/codetricks/construction/code/assistant/service/ArchitecturalPlanServiceImpl.java:

@Override
public void createProjectMetadata(
CreateProjectMetadataRequest request,
StreamObserver<CreateProjectMetadataResponse> responseObserver
) {
try {
// Get authenticated user from gRPC context
String currentUser = getCurrentUserEmail();

// Check if this is a backfill operation for a legacy project
boolean isLegacyProject = projectMetadataService.isLegacyProject(request.getProjectId());

if (isLegacyProject) {
logger.info("Backfilling project-metadata.json for legacy project: " + request.getProjectId());
}

// Create metadata
ProjectMetadata metadata = projectMetadataService.createProjectMetadata(
request.getProjectId(),
request.getProjectName(),
request.getDescription(),
currentUser
// ... additional fields
);

CreateProjectMetadataResponse response = CreateProjectMetadataResponse.newBuilder()
.setSuccess(true)
.setMetadata(metadata)
.build();

responseObserver.onNext(response);
responseObserver.onCompleted();

} catch (Exception e) {
logger.severe("Error creating project metadata: " + e.getMessage());

CreateProjectMetadataResponse response = CreateProjectMetadataResponse.newBuilder()
.setSuccess(false)
.setErrorMessage(e.getMessage())
.build();

responseObserver.onNext(response);
responseObserver.onCompleted();
}
}

@Override
public void updateProjectMetadata(
UpdateProjectMetadataRequest request,
StreamObserver<UpdateProjectMetadataResponse> responseObserver
) {
try {
// Get authenticated user from gRPC context
String currentUser = getCurrentUserEmail();

// Check permissions (must be owner or editor)
if (!hasEditPermission(request.getProjectId(), currentUser)) {
throw new SecurityException("User does not have permission to edit project metadata");
}

// Update metadata
ProjectMetadata metadata = projectMetadataService.updateProjectMetadata(
request,
currentUser
);

UpdateProjectMetadataResponse response = UpdateProjectMetadataResponse.newBuilder()
.setSuccess(true)
.setMetadata(metadata)
.build();

responseObserver.onNext(response);
responseObserver.onCompleted();

} catch (Exception e) {
logger.severe("Error updating project metadata: " + e.getMessage());

UpdateProjectMetadataResponse response = UpdateProjectMetadataResponse.newBuilder()
.setSuccess(false)
.setErrorMessage(e.getMessage())
.build();

responseObserver.onNext(response);
responseObserver.onCompleted();
}
}

Frontend Implementation​

Phase 1: ProjectSettingsComponent (Core Fields Only)​

Location: web-ng-m3/src/app/components/project-settings/project-settings.component.ts

Phase 1 Scope: Implement inline editing for core fields only. Code-specific forms will be added in Phase 2.

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ArchitecturalPlanService } from '../../services/architectural-plan.service';
import { ProjectMetadata, UpdateProjectMetadataRequest, CreateProjectMetadataRequest } from '../../generated.commonjs/api_pb';
import { BackfillMetadataDialogComponent } from '../backfill-metadata-dialog/backfill-metadata-dialog.component';

@Component({
selector: 'app-project-settings',
templateUrl: './project-settings.component.html',
styleUrls: ['./project-settings.component.scss']
})
export class ProjectSettingsComponent implements OnInit {
projectId: string;
projectMetadata: ProjectMetadata | null = null;
isLegacyProject = false;

isEditingName = false;
isEditingDescription = false;
editedProjectName = '';
editedDescription = '';

constructor(
private architecturalPlanService: ArchitecturalPlanService,
private dialog: MatDialog,
private snackBar: MatSnackBar
) {}

ngOnInit(): void {
this.loadProjectMetadata();
}

loadProjectMetadata(): void {
this.architecturalPlanService.getProjectMetadata(this.projectId)
.subscribe({
next: (response) => {
if (response.getMetadata()) {
this.projectMetadata = response.getMetadata();
this.isLegacyProject = false;
} else {
// No metadata found - legacy project
this.isLegacyProject = true;
}
},
error: (err) => {
console.error('Failed to load project metadata', err);
}
});
}

// Edit Project Name
editProjectName(): void {
this.isEditingName = true;
this.editedProjectName = this.projectMetadata?.getProjectName() || '';
}

saveProjectName(): void {
const request = new UpdateProjectMetadataRequest();
request.setProjectId(this.projectId);
request.setProjectName(this.editedProjectName);

this.architecturalPlanService.updateProjectMetadata(request)
.subscribe({
next: (response) => {
if (response.getSuccess()) {
this.projectMetadata = response.getMetadata();
this.isEditingName = false;
this.snackBar.open('Project name updated', 'Close', { duration: 3000 });
} else {
this.snackBar.open('Error: ' + response.getErrorMessage(), 'Close', { duration: 5000 });
}
},
error: (err) => {
this.snackBar.open('Failed to update project name', 'Close', { duration: 5000 });
}
});
}

cancelEditProjectName(): void {
this.isEditingName = false;
}

// Edit Project Description
editProjectDescription(): void {
this.isEditingDescription = true;
this.editedDescription = this.projectMetadata?.getDescription() || '';
}

saveProjectDescription(): void {
const request = new UpdateProjectMetadataRequest();
request.setProjectId(this.projectId);
request.setDescription(this.editedDescription);

this.architecturalPlanService.updateProjectMetadata(request)
.subscribe({
next: (response) => {
if (response.getSuccess()) {
this.projectMetadata = response.getMetadata();
this.isEditingDescription = false;
this.snackBar.open('Description updated', 'Close', { duration: 3000 });
} else {
this.snackBar.open('Error: ' + response.getErrorMessage(), 'Close', { duration: 5000 });
}
},
error: (err) => {
this.snackBar.open('Failed to update description', 'Close', { duration: 5000 });
}
});
}

cancelEditProjectDescription(): void {
this.isEditingDescription = false;
}

// Legacy Project Backfill
showBackfillDialog(): void {
const dialogRef = this.dialog.open(BackfillMetadataDialogComponent, {
width: '500px',
data: { projectId: this.projectId }
});

dialogRef.afterClosed().subscribe(result => {
if (result) {
this.createProjectMetadata(result.projectName, result.description);
}
});
}

createProjectMetadata(projectName: string, description: string): void {
const request = new CreateProjectMetadataRequest();
request.setProjectId(this.projectId);
request.setProjectName(projectName);
request.setDescription(description);

this.architecturalPlanService.createProjectMetadata(request)
.subscribe({
next: (response) => {
if (response.getSuccess()) {
this.projectMetadata = response.getMetadata();
this.isLegacyProject = false;
this.snackBar.open('Project metadata created successfully', 'Close', { duration: 3000 });
} else {
this.snackBar.open('Error: ' + response.getErrorMessage(), 'Close', { duration: 5000 });
}
},
error: (err) => {
this.snackBar.open('Failed to create project metadata', 'Close', { duration: 5000 });
}
});
}
}

Template: project-settings.component.html​

<!-- Legacy Project Banner -->
<mat-banner *ngIf="isLegacyProject" class="legacy-project-banner">
<mat-icon matBannerIcon>info</mat-icon>
<div>
This project was created before project metadata was introduced.
Please set up project metadata to enable full editing capabilities.
</div>
<button mat-button matBannerAction (click)="showBackfillDialog()">
Set Up Metadata
</button>
</mat-banner>

<!-- Project Metadata Card -->
<mat-card *ngIf="!isLegacyProject">
<mat-card-header>
<mat-card-title>Project Metadata</mat-card-title>
</mat-card-header>

<mat-card-content>
<!-- Project ID (read-only) -->
<div class="metadata-field">
<label>Project ID</label>
<span>{{ projectMetadata?.getProjectId() }}</span>
</div>

<!-- Project Name (editable) -->
<div class="metadata-field">
<label>Project Name</label>
<div *ngIf="!isEditingName" class="field-with-edit">
<span>{{ projectMetadata?.getProjectName() }}</span>
<button mat-icon-button (click)="editProjectName()">
<mat-icon>edit</mat-icon>
</button>
</div>
<div *ngIf="isEditingName" class="edit-field">
<mat-form-field appearance="outline">
<input matInput [(ngModel)]="editedProjectName" />
</mat-form-field>
<button mat-icon-button color="primary" (click)="saveProjectName()">
<mat-icon>check</mat-icon>
</button>
<button mat-icon-button (click)="cancelEditProjectName()">
<mat-icon>close</mat-icon>
</button>
</div>
</div>

<!-- Project Description (editable) -->
<div class="metadata-field">
<label>Description</label>
<div *ngIf="!isEditingDescription" class="field-with-edit">
<span>{{ projectMetadata?.getDescription() || 'No description' }}</span>
<button mat-icon-button (click)="editProjectDescription()">
<mat-icon>edit</mat-icon>
</button>
</div>
<div *ngIf="isEditingDescription" class="edit-field">
<mat-form-field appearance="outline">
<textarea matInput rows="3" [(ngModel)]="editedDescription"></textarea>
</mat-form-field>
<button mat-icon-button color="primary" (click)="saveProjectDescription()">
<mat-icon>check</mat-icon>
</button>
<button mat-icon-button (click)="cancelEditProjectDescription()">
<mat-icon>close</mat-icon>
</button>
</div>
</div>

<!-- Created By (read-only) -->
<div class="metadata-field">
<label>Created By</label>
<span>{{ projectMetadata?.getCreatedBy() }}</span>
</div>

<!-- Created At (read-only) -->
<div class="metadata-field">
<label>Created At</label>
<span>{{ projectMetadata?.getCreatedAt() | date:'medium' }}</span>
</div>

<!-- Status (read-only for now) -->
<div class="metadata-field">
<label>Status</label>
<mat-chip>{{ projectMetadata?.getStatus() }}</mat-chip>
</div>
</mat-card-content>
</mat-card>

CLI Tool for Bulk Upgrades​

Phase 1: BackfillProjectMetadataCommand (Core Fields Only)​

Phase 1 Scope: Bulk upgrade creates metadata with core fields only. Code-specific metadata can be added later via UI.

Location: src/main/java/org/codetricks/construction/code/assistant/cli/BackfillProjectMetadataCommand.java

package org.codetricks.construction.code.assistant.cli;

import org.codetricks.construction.code.assistant.data.FileSystemHandler;
import org.codetricks.construction.code.assistant.data.FileSystemHandlerFactory;
import org.codetricks.construction.code.assistant.proto.ProjectMetadata;
import org.codetricks.construction.code.assistant.service.ProjectMetadataService;

import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

/**
* CLI handler for bulk backfilling project metadata.
* Used by administrators to upgrade legacy projects.
*
* Usage:
* cli/codeproof.sh admin list-legacy-projects
* cli/codeproof.sh admin backfill-project-metadata --dry-run
* cli/codeproof.sh admin backfill-project-metadata
*/
public class BackfillProjectMetadataCommand {
private static final Logger logger = Logger.getLogger(BackfillProjectMetadataCommand.class.getName());

private final ProjectMetadataService projectMetadataService;

public BackfillProjectMetadataCommand() {
FileSystemHandler fileSystemHandler = FileSystemHandlerFactory.createDefaultFileSystemHandler();
this.projectMetadataService = new ProjectMetadataService(fileSystemHandler);
}

public void listLegacyProjects() throws Exception {
List<String> legacyProjects = projectMetadataService.listLegacyProjects();

System.out.println("=== Legacy Projects Needing Upgrade ===");
System.out.println("Total: " + legacyProjects.size());
System.out.println();

for (String projectId : legacyProjects) {
System.out.println(" - " + projectId);
}
}

public void execute(BackfillOptions options) throws Exception {
List<String> projectsToUpgrade;

if (options.hasSpecificProjects()) {
projectsToUpgrade = Arrays.asList(options.getProjects().split(","));
} else {
// Find all legacy projects
projectsToUpgrade = projectMetadataService.listLegacyProjects();
}

logger.info("Found " + projectsToUpgrade.size() + " legacy projects to upgrade");

if (options.isDryRun()) {
// Preview mode - just list what would be upgraded
System.out.println("=== DRY RUN MODE ===");
System.out.println("The following projects would be upgraded:");
System.out.println();

for (String projectId : projectsToUpgrade) {
String projectName = formatProjectName(projectId, options);
System.out.println(" - " + projectId);
System.out.println(" Name: " + projectName);
System.out.println(" Description: " + options.getDefaultDescription());
System.out.println();
}
return;
}

// Execute bulk upgrade
int successCount = 0;
int failureCount = 0;

System.out.println("=== Starting Bulk Upgrade ===");
System.out.println();

for (String projectId : projectsToUpgrade) {
try {
ProjectMetadata metadata = projectMetadataService.createProjectMetadata(
projectId,
formatProjectName(projectId, options),
options.getDefaultDescription(),
options.getCreatedBy()
);
System.out.println("✓ Successfully upgraded: " + projectId);
successCount++;
} catch (Exception e) {
System.err.println("✗ Failed to upgrade " + projectId + ": " + e.getMessage());
failureCount++;
}
}

System.out.println();
System.out.println("=== Bulk Upgrade Completed ===");
System.out.println("Succeeded: " + successCount);
System.out.println("Failed: " + failureCount);
}

private String formatProjectName(String projectId, BackfillOptions options) {
String pattern = options.getDefaultNamePattern();
return pattern.replace("{projectId}", projectId);
}

public static class BackfillOptions {
private boolean dryRun = false;
private String projects = null;
private String createdBy = "system@codeproof.app";
private String defaultNamePattern = "{projectId}";
private String defaultDescription = "Legacy project";

// Getters and setters
public boolean isDryRun() { return dryRun; }
public void setDryRun(boolean dryRun) { this.dryRun = dryRun; }

public boolean hasSpecificProjects() { return projects != null && !projects.isEmpty(); }
public String getProjects() { return projects; }
public void setProjects(String projects) { this.projects = projects; }

public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }

public String getDefaultNamePattern() { return defaultNamePattern; }
public void setDefaultNamePattern(String defaultNamePattern) { this.defaultNamePattern = defaultNamePattern; }

public String getDefaultDescription() { return defaultDescription; }
public void setDefaultDescription(String defaultDescription) { this.defaultDescription = defaultDescription; }
}
}

Migration Strategy​

Backward Compatibility​

  1. Existing files remain valid: Old project-metadata.json files without new fields will work fine
  2. Graceful field handling: Missing fields will use default values
  3. No data migration required: New fields are optional and added on first edit
  4. Legacy projects continue to work: Projects with only plan-metadata.json remain functional

Refactoring Existing Code​

  1. Update ArchitecturalPlanReviewer.getProjectMetadata():

    • Change return type from inner class to proto ProjectMetadata
    • Use Proto.loadJson() instead of manual JSON parsing
  2. Update ArchitecturalPlanWriteServiceImpl.createProjectMetadata():

    • Use proto message and Proto.toJson() instead of string formatting
    • Reuse ProjectMetadataService for consistency
  3. Update GetProjectMetadataResponse:

    • Wrap ProjectMetadata message instead of duplicating fields
  4. Maintain dual-file support:

    • Keep existing fallback logic in listArchitecturalPlanIds()
    • Keep existing fallback logic in getArchitecturalPlan()
    • Do not remove or modify plan-metadata.json handling
  5. Future-proof for Issue #167:

    • project-metadata.json is project-level metadata (orthogonal to file structure)
    • When Issue #167 is implemented, project-metadata.json will coexist with files/{file_id}/metadata.json
    • plan-metadata.json will be deprecated in favor of file-level metadata
    • No changes to project-metadata.json schema or location will be required

Permission Requirements​

  • View metadata: All project members (Reader, Prompter, Editor, Owner)
  • Edit metadata: Only Editor and Owner roles
  • Create metadata: Only during project creation or by Owner
  • Bulk upgrade: Only system administrators via CLI

Error Handling​

  1. Project not found: Return appropriate error message
  2. Permission denied: Check RBAC before allowing updates
  3. Invalid field values: Validate input before saving
  4. File system errors: Handle GCS failures gracefully
  5. Concurrent updates: Last write wins (no conflict resolution for MVP)
  6. Metadata already exists: Prevent duplicate creation

Testing Strategy​

Unit Tests​

  • ProjectMetadataServiceTest.java - Test create, update, list operations
  • Test legacy project detection logic
  • Test PATCH semantics for updates
  • Test error handling and validation

Integration Tests​

  • Test gRPC endpoints with real file system
  • Test permission checks with RBAC
  • Test concurrent updates
  • Test backward compatibility with existing files

Frontend Tests​

  • Component tests for inline editing
  • Dialog tests for backfill workflow
  • Service tests for gRPC calls
  • E2E tests for complete user flows

Performance Considerations​

  • RPC latency target: < 500ms for UpdateProjectMetadata
  • File I/O optimization: Use buffered readers/writers
  • Caching: Consider caching metadata in memory for frequently accessed projects
  • Bulk operations: Process in batches to avoid timeouts

Security Considerations​

  • RBAC enforcement: Check permissions in backend, never trust frontend
  • Input validation: Sanitize all user inputs
  • Audit logging: Log all metadata changes for compliance
  • Authentication: Verify user identity via gRPC context

Phase 2 & 3: Extended Implementation (Future)​

Phase 2: Code Metadata Implementation​

When Phase 2 is implemented:

  1. Uncomment the repeated CodeMetadata applicable_codes = 100 field in ProjectMetadata
  2. Implement code metadata merging logic in updateProjectMetadata()
  3. Add helper methods: getCodeMetadata(), hasCodeMetadata()
  4. Create frontend forms for IBC-specific fields
  5. Add validation for IBC enums

Key Changes:

  • Proto: Add CodeMetadata, IbcCodeMetadata, and all IBC submessages
  • Backend: Implement merge-by-code_id logic
  • Frontend: Code-specific form sections with conditional rendering
  • UI: Tabs or accordion for multiple codes

Phase 3: Additional Codes​

When Phase 3 is implemented:

  1. Add IrcCodeMetadata to CodeMetadata.oneof
  2. Add NfpaCodeMetadata to CodeMetadata.oneof
  3. Add state-specific code messages (CBC, FBC, etc.)
  4. Implement code-specific validation rules
  5. Add frontend forms for each code type

No Breaking Changes: Phase 1 projects continue working without code metadata.

IBC Taxonomy Reference (Phase 2)​

This section documents the IBC (International Building Code) 2021 classifications and taxonomies that inform the Phase 2 metadata schema design.

Occupancy Groups (IBC Chapter 3, Section 302.1)​

Source: api/icc/content/chapter-xml/2217/24199948.html - IBC 2021 Chapter 3

Enum: IbcOccupancyGroup

IBC CodeEnum ValueDescriptionIBC Section
A-1OCCUPANCY_A_1Theaters, concert halls (fixed seating)303.2
A-2OCCUPANCY_A_2Restaurants, bars (food/drink)303.3
A-3OCCUPANCY_A_3Churches, recreation, libraries303.4
A-4OCCUPANCY_A_4Arenas, skating rinks (indoor sporting)303.5
A-5OCCUPANCY_A_5Stadiums, amusement parks (outdoor)303.6
BOCCUPANCY_BProfessional offices, banks304
EOCCUPANCY_ESchools, daycare (>5 children)305
F-1OCCUPANCY_F_1Factory - moderate hazard306.2
F-2OCCUPANCY_F_2Factory - low hazard306.3
H-1OCCUPANCY_H_1High hazard - detonation307.3
H-2OCCUPANCY_H_2High hazard - deflagration307.4
H-3OCCUPANCY_H_3High hazard - physical307.5
H-4OCCUPANCY_H_4High hazard - health307.6
H-5OCCUPANCY_H_5High hazard - HPM (semiconductor)307.7
I-1OCCUPANCY_I_1Institutional - assisted living (>16)308.2
I-2OCCUPANCY_I_2Institutional - hospitals, nursing308.3
I-3OCCUPANCY_I_3Institutional - prisons, jails308.4
I-4OCCUPANCY_I_4Institutional - daycare (>5, <24hr)308.5
MOCCUPANCY_MMercantile - retail stores, markets309
R-1OCCUPANCY_R_1Residential - hotels, motels (transient)310.2
R-2OCCUPANCY_R_2Residential - apartments (>2 units)310.3
R-3OCCUPANCY_R_3Residential - single-family (1-2 units)310.4
R-4OCCUPANCY_R_4Residential - assisted living (6-16)310.5
S-1OCCUPANCY_S_1Storage - moderate hazard311.2
S-2OCCUPANCY_S_2Storage - low hazard311.3
UOCCUPANCY_UUtility - carports, sheds, towers312

Key Concepts:

  • Mixed Occupancies (IBC Section 508): Buildings with multiple occupancy types
  • Accessory Occupancies (IBC Section 508.2): Small spaces accessory to main use
  • Occupant Load Threshold: 50 persons determines Assembly vs Business classification (Section 303.1.1)

Construction Types (IBC Chapter 6, Section 602)​

Enum: IbcConstructionTypeEnum

IBC TypeEnum ValueFire ResistanceTypical Use
I-ACONSTRUCTION_TYPE_I_AHighestHigh-rise buildings, hospitals
I-BCONSTRUCTION_TYPE_I_BHighMulti-story commercial
II-ACONSTRUCTION_TYPE_II_AModerate (protected)Warehouses, retail
II-BCONSTRUCTION_TYPE_II_BModerate (unprotected)Industrial buildings
III-ACONSTRUCTION_TYPE_III_AModerate (protected masonry)Mixed-use buildings
III-BCONSTRUCTION_TYPE_III_BModerate (unprotected masonry)Small commercial
IV-ACONSTRUCTION_TYPE_IV_AHeavy timber (protected)Modern mass timber
IV-BCONSTRUCTION_TYPE_IV_BHeavy timber (protected)Modern mass timber
IV-CCONSTRUCTION_TYPE_IV_CHeavy timber (protected)Modern mass timber
IV-HTCONSTRUCTION_TYPE_IV_HTHeavy timber (traditional)Historic buildings
V-ACONSTRUCTION_TYPE_V_ALight frame (protected)Apartments, condos
V-BCONSTRUCTION_TYPE_V_BLight frame (unprotected)Single-family homes

Height and Area Limitations (IBC Chapter 5)​

Source: api/icc/content/chapter-xml/2217/24202559.html - IBC 2021 Chapter 5

Key Tables:

  • Table 504.3: Allowable Building Height in Feet Above Grade Plane
  • Table 504.4: Allowable Number of Stories Above Grade Plane
  • Table 506.2: Allowable Building Area Per Floor

Critical Concepts:

  1. Height ≠ Stories: IBC limits both independently
  2. Grade Plane: Specific definition for measuring height (IBC Section 502.1)
  3. Story: Space between floors or floor and ceiling (IBC Chapter 2)
  4. Basement: Story with floor level more than 6 feet below grade
  5. Mezzanine: Intermediate level between floor and ceiling (IBC Section 505)
    • Cannot exceed 1/3 of room area
    • May not count as a story under certain conditions

Area Increases:

  • Frontage Increase (IBC Section 506.2): Based on % of perimeter with public way access
  • Sprinkler Increase (IBC Section 506.3): Can double or triple allowable area
  • Combined: Both increases can be applied together

Fire Protection Systems (IBC Chapter 9)​

SystemIBC SectionImpact on Height/Area
NFPA 13903.3.1.1Commercial sprinklers - doubles area, adds stories
NFPA 13R903.3.1.2Residential (≤4 stories) - area increases
NFPA 13D903.3.1.31-2 family dwellings - limited increases
Fire Alarm907Required based on occupancy and size
Standpipe905Required for buildings >30 feet

Fire Separation (IBC Chapter 7, Section 706)​

Fire Walls: Allow one physical building to be treated as multiple buildings for code purposes

  • Each fire-separated portion can independently meet height/area limits
  • Critical for large buildings exceeding single-building limits

References​

  • PRD: Project Metadata Management
  • IBC 2021 Chapter 3: Occupancy Classification (api/icc/content/chapter-xml/2217/24199948.html)
  • IBC 2021 Chapter 5: Height and Area Limitations (api/icc/content/chapter-xml/2217/24202559.html)
  • InputFileMetadataService: Similar pattern for file-level metadata
  • Proto Utilities: Proto.loadJson() and Proto.toJson() for JSON serialization
  • Issue #167: Future file structure reorganization