Skip to content

Commit 3c1e882

Browse files
adereisclaude
andcommitted
Adopt bracketed format for Workday metadata fields
format_notes_field() now outputs bracketed format matching app.py's export and the module's own documentation: - [Performance Rating: X%] - [Strengths: ...] - [Improvements: ...] - [Mentor: ...] - [Mentees: ...] - Section header Justification: (allows multi-line text) Parser simplified to only accept the canonical bracketed format, removing unnecessary dual-pattern matching that added complexity for backward compatibility we don't need. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c1409e5 commit 3c1e882

5 files changed

Lines changed: 95 additions & 90 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ Manager name parsed from: `"Supervisory Organization (Manager Name)"` or `"Direc
9393

9494
**Overwritten on re-import** (from Workday): salary, bonus targets, org structure, management_level, country, tenure fields
9595

96+
**Notes Field Format** (canonical bracketed format for Workday round-tripping):
97+
- `[Performance Rating: X%]`
98+
- `[Override: X%, reason]` (special cases like pro-rata leave)
99+
- `[Strengths: ...]` / `[Improvements: ...]`
100+
- `[Mentor: ...]` / `[Mentees: ...]`
101+
- `Justification:` (section header, allows multi-line text)
102+
103+
Parser: `notes_parser.py``parse_notes_field()` extracts, `format_notes_field()` serializes. Exports use this format in the Notes column; the separate Description column is human-readable (no brackets).
104+
96105
### Talent Calibration
97106

98107
**Routes**: `/calibrate` (UI), `/api/calibrate` (POST), `/api/calibrate/status` (GET), `/export/talent` (GET)

notes_parser.py

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -74,51 +74,41 @@ def parse_notes_field(notes_text: Optional[str]) -> dict:
7474
# Normalize line endings
7575
text = notes_text.replace('\r\n', '\n').replace('\r', '\n')
7676

77-
# Parse Performance Rating
78-
rating_match = re.search(r'Performance\s+Rating:\s*([\d.]+)\s*%', text, re.IGNORECASE)
77+
# Parse Performance Rating: [Performance Rating: X%]
78+
rating_match = re.search(r'\[Performance\s+Rating:\s*([\d.]+)\s*%\]', text, re.IGNORECASE)
7979
if rating_match:
8080
try:
8181
result['performance_rating'] = float(rating_match.group(1))
8282
except ValueError:
8383
pass
8484

85-
# Parse Mentor (single person who mentored this employee)
86-
# Supports both bracketed [Mentor: X] and non-bracketed Mentor: X formats
87-
mentor_match = re.search(r'^\[Mentor:\s*([^\]]+)\]', text, re.MULTILINE | re.IGNORECASE)
88-
if not mentor_match:
89-
mentor_match = re.search(r'^Mentor:\s*(.+?)$', text, re.MULTILINE | re.IGNORECASE)
85+
# Parse Mentor: [Mentor: X]
86+
mentor_match = re.search(r'\[Mentor:\s*([^\]]+)\]', text, re.IGNORECASE)
9087
if mentor_match:
9188
mentor_value = mentor_match.group(1).strip()
9289
if mentor_value:
9390
result['mentors'] = mentor_value
9491

95-
# Parse Mentees (people this employee mentored)
96-
mentees_match = re.search(r'^Mentees?:\s*(.+?)$', text, re.MULTILINE | re.IGNORECASE)
92+
# Parse Mentees: [Mentees: X; Y]
93+
mentees_match = re.search(r'\[Mentees?:\s*([^\]]+)\]', text, re.IGNORECASE)
9794
if mentees_match:
9895
mentees_value = mentees_match.group(1).strip()
9996
if mentees_value:
10097
result['mentees'] = mentees_value
10198

102-
# Parse Strengths
103-
strengths_match = re.search(r'^Strengths?:\s*(.+?)$', text, re.MULTILINE | re.IGNORECASE)
99+
# Parse Strengths: [Strengths: X; Y]
100+
strengths_match = re.search(r'\[Strengths?:\s*([^\]]+)\]', text, re.IGNORECASE)
104101
if strengths_match:
105102
strengths_value = strengths_match.group(1).strip()
106103
if strengths_value:
107104
result['tenets_strengths'] = strengths_value
108105

109-
# Parse Areas for Improvement (various phrasings)
110-
improvements_patterns = [
111-
r'^Areas?\s+for\s+Improvement:\s*(.+?)$',
112-
r'^Improvements?:\s*(.+?)$',
113-
r'^Areas?\s+to\s+Improve:\s*(.+?)$',
114-
]
115-
for pattern in improvements_patterns:
116-
improvements_match = re.search(pattern, text, re.MULTILINE | re.IGNORECASE)
117-
if improvements_match:
118-
improvements_value = improvements_match.group(1).strip()
119-
if improvements_value:
120-
result['tenets_improvements'] = improvements_value
121-
break
106+
# Parse Improvements: [Improvements: X; Y]
107+
improvements_match = re.search(r'\[Improvements?:\s*([^\]]+)\]', text, re.IGNORECASE)
108+
if improvements_match:
109+
improvements_value = improvements_match.group(1).strip()
110+
if improvements_value:
111+
result['tenets_improvements'] = improvements_value
122112

123113
# [Override: 50%] or [Override: 50%, Paternity leave Apr-Sep]
124114
# Combined format: percentage required, reason optional after comma
@@ -182,11 +172,9 @@ def format_notes_field(
182172
"""
183173
lines = []
184174

175+
# Bracketed fields (tool additions) - order matches app.py export
185176
if performance_rating is not None:
186-
lines.append(f"Performance Rating: {performance_rating}%")
187-
188-
if justification:
189-
lines.append(f"Justification: {justification}")
177+
lines.append(f"[Performance Rating: {performance_rating}%]")
190178

191179
# Special case override (pro-rata leave, retention, etc.)
192180
if bonus_override_percent is not None:
@@ -195,18 +183,24 @@ def format_notes_field(
195183
else:
196184
lines.append(f"[Override: {bonus_override_percent}%]")
197185

186+
if tenets_strengths:
187+
lines.append(f"[Strengths: {tenets_strengths}]")
188+
189+
if tenets_improvements:
190+
lines.append(f"[Improvements: {tenets_improvements}]")
191+
198192
if mentor:
199-
lines.append(f"Mentor: {mentor}")
193+
lines.append(f"[Mentor: {mentor}]")
200194

201195
if mentees:
202196
# Normalize to semicolon-separated for consistency with tenets
203197
normalized_mentees = '; '.join(m.strip() for m in mentees.replace(';', ',').split(',') if m.strip())
204-
lines.append(f"Mentees: {normalized_mentees}")
205-
206-
if tenets_strengths:
207-
lines.append(f"Strengths: {tenets_strengths}")
198+
lines.append(f"[Mentees: {normalized_mentees}]")
208199

209-
if tenets_improvements:
210-
lines.append(f"Areas for Improvement: {tenets_improvements}")
200+
# Justification uses section header format (allows multi-line, any characters)
201+
if justification:
202+
lines.append('') # Blank line before section
203+
lines.append('Justification:')
204+
lines.append(justification)
211205

212206
return '\n'.join(lines)

scripts/generate-sample-xlsx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ def generate_historical_notes(rating, include_full_details=True):
630630
if choice == 'empty':
631631
return ''
632632
elif choice == 'rating_only':
633-
return f"Performance Rating: {rating}%"
633+
return f"[Performance Rating: {rating}%]"
634634
else:
635635
return format_notes_field(
636636
performance_rating=rating,

tests/test_import_api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def test_analyze_valid_xlsx(self, client, db_session):
157157
'associate': 'John Doe',
158158
'supervisory_organization': 'Engineering',
159159
'current_job_profile': 'Senior Engineer',
160-
'notes': 'Performance Rating: 125%\nJustification: Great work',
160+
'notes': '[Performance Rating: 125%]\n\nJustification:\nGreat work',
161161
'proposed_percent_of_target_bonus': 118.5
162162
},
163163
{
@@ -196,7 +196,7 @@ def test_analyze_historical_period_exists(self, client, db_session):
196196
{
197197
'associate_id': 'EMP001',
198198
'associate': 'John Doe',
199-
'notes': 'Performance Rating: 125%'
199+
'notes': '[Performance Rating: 125%]'
200200
}
201201
]
202202

@@ -587,7 +587,7 @@ def test_import_historical_creates_period_and_snapshots(self, client, db_session
587587
'current_job_profile': 'Senior Engineer',
588588
'bonus_target_manager_currency': 15000,
589589
'proposed_percent_of_target_bonus': 118.5,
590-
'notes': 'Performance Rating: 125%\nJustification: Excellent work\nMentor: Alice\nStrengths: Leadership'
590+
'notes': '[Performance Rating: 125%]\n[Mentor: Alice]\n[Strengths: Leadership]\n\nJustification:\nExcellent work'
591591
},
592592
{
593593
'associate_id': 'EMP002',
@@ -675,7 +675,7 @@ def test_import_historical_updates_existing_snapshots(self, client, db_session):
675675
'associate': 'New Name',
676676
'supervisory_organization': 'New Org',
677677
'proposed_percent_of_target_bonus': 125.0,
678-
'notes': 'Performance Rating: 130%\nJustification: Updated review'
678+
'notes': '[Performance Rating: 130%]\n\nJustification:\nUpdated review'
679679
}
680680
]
681681

@@ -781,7 +781,7 @@ def test_parse_xlsx_employees(self, db_session):
781781
'current_base_pay_manager_currency': 150000,
782782
'bonus_target_manager_currency': 22500,
783783
'proposed_percent_of_target_bonus': 118.5,
784-
'notes': 'Performance Rating: 125%'
784+
'notes': '[Performance Rating: 125%]'
785785
}
786786
]
787787

@@ -809,7 +809,7 @@ def test_parse_xlsx_employees(self, db_session):
809809
assert emp['current_base_pay_manager_currency'] == 150000
810810
assert emp['bonus_target_manager_currency'] == 22500
811811
assert emp['proposed_percent_of_target_bonus'] == 118.5
812-
assert 'Performance Rating: 125%' in emp['notes']
812+
assert '[Performance Rating: 125%]' in emp['notes']
813813
# Check that currency was extracted from headers
814814
assert metadata.get('currency') == 'USD'
815815
finally:

tests/test_notes_parser.py

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ class TestParseNotesField:
1111

1212
def test_parse_complete_notes(self):
1313
"""Test parsing a complete notes field with all fields."""
14-
notes = """Performance Rating: 125.5%
15-
Justification: Great performance, delivered feature X, Y and Z. Role model to the team.
16-
Mentor: Alice Chen
17-
Mentees: Bob Jones, Carol White
18-
Strengths: Customer Obsession, Ownership, Bias for Action
19-
Areas for Improvement: Earn Trust, Dive Deep"""
14+
notes = """[Performance Rating: 125.5%]
15+
[Strengths: Customer Obsession, Ownership, Bias for Action]
16+
[Improvements: Earn Trust, Dive Deep]
17+
[Mentor: Alice Chen]
18+
[Mentees: Bob Jones, Carol White]
19+
20+
Justification:
21+
Great performance, delivered feature X, Y and Z. Role model to the team."""
2022

2123
result = parse_notes_field(notes)
2224

@@ -29,7 +31,7 @@ def test_parse_complete_notes(self):
2931

3032
def test_parse_rating_only(self):
3133
"""Test parsing notes with only performance rating."""
32-
notes = "Performance Rating: 100%"
34+
notes = "[Performance Rating: 100%]"
3335

3436
result = parse_notes_field(notes)
3537

@@ -42,11 +44,13 @@ def test_parse_rating_only(self):
4244

4345
def test_parse_multiline_justification(self):
4446
"""Test parsing multi-line justification text."""
45-
notes = """Performance Rating: 130%
46-
Justification: This employee demonstrated exceptional performance.
47+
notes = """[Performance Rating: 130%]
48+
[Mentor: Senior Dev]
49+
50+
Justification:
51+
This employee demonstrated exceptional performance.
4752
They led the API redesign project which reduced latency by 40%.
48-
Additionally, they mentored two junior engineers.
49-
Mentor: Senior Dev"""
53+
Additionally, they mentored two junior engineers."""
5054

5155
result = parse_notes_field(notes)
5256

@@ -76,10 +80,12 @@ def test_parse_none_notes(self):
7680

7781
def test_parse_case_insensitive(self):
7882
"""Test that field names are case-insensitive."""
79-
notes = """PERFORMANCE RATING: 110%
80-
justification: good work
81-
MENTOR: Boss
82-
strengths: Leadership"""
83+
notes = """[PERFORMANCE RATING: 110%]
84+
[MENTOR: Boss]
85+
[STRENGTHS: Leadership]
86+
87+
Justification:
88+
good work"""
8389

8490
result = parse_notes_field(notes)
8591

@@ -90,37 +96,28 @@ def test_parse_case_insensitive(self):
9096

9197
def test_parse_rating_with_decimal(self):
9298
"""Test parsing rating with decimal places."""
93-
notes = "Performance Rating: 115.75%"
99+
notes = "[Performance Rating: 115.75%]"
94100

95101
result = parse_notes_field(notes)
96102

97103
assert result['performance_rating'] == 115.75
98104

99105
def test_parse_without_rating(self):
100106
"""Test parsing notes without performance rating."""
101-
notes = """Justification: Solid performance this period.
102-
Strengths: Teamwork, Communication"""
107+
notes = """[Strengths: Teamwork, Communication]
108+
109+
Justification:
110+
Solid performance this period."""
103111

104112
result = parse_notes_field(notes)
105113

106114
assert result['performance_rating'] is None
107115
assert result['justification'] == "Solid performance this period."
108116
assert result['tenets_strengths'] == "Teamwork, Communication"
109117

110-
def test_parse_alternate_improvement_phrasings(self):
111-
"""Test parsing various phrasings for improvements."""
112-
notes1 = "Improvements: Time Management"
113-
notes2 = "Areas to Improve: Focus"
114-
115-
result1 = parse_notes_field(notes1)
116-
result2 = parse_notes_field(notes2)
117-
118-
assert result1['tenets_improvements'] == "Time Management"
119-
assert result2['tenets_improvements'] == "Focus"
120-
121118
def test_parse_windows_line_endings(self):
122119
"""Test parsing notes with Windows line endings."""
123-
notes = "Performance Rating: 100%\r\nJustification: Test\r\nMentor: Alice"
120+
notes = "[Performance Rating: 100%]\r\n[Mentor: Alice]\r\n\r\nJustification:\r\nTest"
124121

125122
result = parse_notes_field(notes)
126123

@@ -130,12 +127,14 @@ def test_parse_windows_line_endings(self):
130127

131128
def test_parse_real_world_example(self):
132129
"""Test parsing a real-world example from the export page."""
133-
notes = """Performance Rating: 155.0%
134-
Justification: Great performance, delivered feature X, Y and Z. Role model to the team.
135-
Mentor: Rhoda Map
136-
Mentees: Mai Stone
137-
Strengths: We Serve Our Customers, We Champion Ownership, We Start with Trust
138-
Areas for Improvement: We Embrace Transparency, We Navigate Change with Resilience"""
130+
notes = """[Performance Rating: 155.0%]
131+
[Strengths: We Serve Our Customers, We Champion Ownership, We Start with Trust]
132+
[Improvements: We Embrace Transparency, We Navigate Change with Resilience]
133+
[Mentor: Rhoda Map]
134+
[Mentees: Mai Stone]
135+
136+
Justification:
137+
Great performance, delivered feature X, Y and Z. Role model to the team."""
139138

140139
result = parse_notes_field(notes)
141140

@@ -161,12 +160,14 @@ def test_format_complete_notes(self):
161160
tenets_improvements="Communication"
162161
)
163162

164-
assert "Performance Rating: 125.0%" in result
165-
assert "Justification: Great work this quarter." in result
166-
assert "Mentor: Alice" in result
167-
assert "Mentees: Bob; Carol" in result # Normalized to semicolons
168-
assert "Strengths: Leadership, Teamwork" in result
169-
assert "Areas for Improvement: Communication" in result
163+
assert "[Performance Rating: 125.0%]" in result
164+
assert "[Mentor: Alice]" in result
165+
assert "[Mentees: Bob; Carol]" in result # Normalized to semicolons
166+
assert "[Strengths: Leadership, Teamwork]" in result
167+
assert "[Improvements: Communication]" in result
168+
# Justification uses section header format
169+
assert "Justification:" in result
170+
assert "Great work this quarter." in result
170171

171172
def test_format_partial_notes(self):
172173
"""Test formatting with only some fields."""
@@ -175,10 +176,11 @@ def test_format_partial_notes(self):
175176
justification="Met expectations."
176177
)
177178

178-
assert "Performance Rating: 100.0%" in result
179-
assert "Justification: Met expectations." in result
180-
assert "Mentor:" not in result
181-
assert "Strengths:" not in result
179+
assert "[Performance Rating: 100.0%]" in result
180+
assert "Justification:" in result
181+
assert "Met expectations." in result
182+
assert "[Mentor:" not in result
183+
assert "[Strengths:" not in result
182184

183185
def test_format_empty_notes(self):
184186
"""Test formatting with no fields returns empty string."""
@@ -279,7 +281,7 @@ def test_format_with_override(self):
279281
justification="Pro-rata bonus"
280282
)
281283

282-
assert "Performance Rating: 100.0%" in result
284+
assert "[Performance Rating: 100.0%]" in result
283285
assert "[Override: 50.0%, Paternity leave Apr-Sep]" in result
284286
assert "Justification:" in result
285287
assert "Pro-rata bonus" in result
@@ -303,7 +305,7 @@ def test_format_no_override(self):
303305
)
304306

305307
assert "[Override:" not in result
306-
assert "Performance Rating: 100.0%" in result
308+
assert "[Performance Rating: 100.0%]" in result
307309

308310
def test_override_roundtrip(self):
309311
"""Test that formatting then parsing returns original override values."""

0 commit comments

Comments
 (0)