Skip to content

Commit c005529

Browse files
committed
Publish Ashley Leon Martinez officer profile
1 parent 92dd62c commit c005529

10 files changed

Lines changed: 76 additions & 22 deletions

File tree

‎.agents/skills/pre-production-checklist/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ This gate passes only when production routes are directly crawlable with consist
5454
Run this gate whenever the officer roster, a form-response snapshot, profile links, bios, or officer-data build logic changes.
5555

5656
1. Run `npm test` and `npm run validate:officers`.
57-
2. Confirm `src/data/officersFa26Roster.json` is the only authority for cohort membership, roles, order, stable IDs, and fallback portraits.
57+
2. Confirm `src/data/officersFa26Roster.json` is the only authority for cohort membership, roles, order, stable IDs, and reviewed fallback public content. For manual submissions received outside Notion, publish only the same allowlisted public content fields and never fabricate a Notion provenance ID.
5858
3. Treat Notion as private intake. Use `officers:notion:dry-run` or `officers:notion:check` before `officers:notion:write`, then inspect the resulting Git diff. Confirm the sync did not commit, push, deploy, or write back to Notion.
5959
4. Confirm `src/data/officerProfilesFa26.json` contains the real Notion response page ID for provenance and only the allowlisted public fields: source ID, submission time, full and preferred names, headshot path, bio, personal website, LinkedIn, GitHub, and ORCID. Confirm upstream database/data-source IDs and the intake window are absent from this browser-facing snapshot.
6060
5. Search the diff for email addresses, phone numbers, birthdays, scheduling information, internal notes, and unknown form properties. None may enter Git, logs, or workflow artifacts.

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Run `npm run build` for the same credential-free production build used by GitHub
1616

1717
Fall 2026 deliberately separates approved roster decisions from submitted public profile content:
1818

19-
- `src/data/officersFa26Roster.json` owns cohort membership, roles, order, stable IDs, and fallback portraits.
19+
- `src/data/officersFa26Roster.json` owns cohort membership, roles, order, stable IDs, and reviewed fallback public content, including submissions received outside Notion.
2020
- `src/data/officerProfilesFa26.json` is a reviewed, public-only snapshot of in-window officer form responses.
2121
- `scripts/lib/officer-profile-cohorts.js` owns operational file paths and the explicit intake window; upstream database identifiers stay in protected environment secrets and out of the client bundle.
2222
- `src/data/compileOfficerDirectory.js` matches responses to the roster, keeps the newest nonblank public fields, normalizes links, and ignores unmatched people.
@@ -34,7 +34,9 @@ Each profile record in the public snapshot is limited to provenance ID,
3434
submission time, full and preferred names, headshot path, bio, personal
3535
website, LinkedIn, GitHub, and ORCID. Never copy email, phone number, birthday,
3636
scheduling data, another private form field, or an upstream database identifier
37-
into the snapshot. Run:
37+
into the snapshot or roster. A manually reviewed submission received outside
38+
Notion may populate only those same publishable content fields on the matching
39+
approved roster entry; it must not use a fabricated Notion provenance ID. Run:
3840

3941
```sh
4042
npm test
86 KB
Loading

‎scripts/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Duplicate form responses are supported. `compileOfficerDirectory.js` takes the n
2323
- `write` refuses to overwrite dirty managed files, fingerprints the snapshot and portrait tree before network work, and rechecks that state immediately before committing. It validates a complete candidate in temporary storage before atomically replacing the snapshot or a portrait.
2424
- Repeating a command against unchanged Notion content is idempotent. Portraits are streamed through a 15 MB bound, decoded, metadata-stripped, and normalized before comparison; expiring attachment query strings do not create a diff, while replacing an attachment on an existing response does.
2525
- The roster remains authoritative for membership, roles, order, stable IDs, and fallback portraits. Form answers may enrich only matched officers.
26+
- A manually reviewed submission received outside Notion may populate the same public content fields directly on the matching roster entry. Do not fabricate a Notion page ID; exclude all non-public message and scheduling data.
2627

2728
The explicit cohort window and managed paths live in
2829
`scripts/lib/officer-profile-cohorts.js`. The private upstream database ID is

‎scripts/lib/compile-officer-directory.test.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ test("publishes a complete profile without replacing the canonical full name", (
9292
});
9393
});
9494

95+
test("uses reviewed public roster content when intake arrives outside Notion", () => {
96+
const roster = [
97+
{
98+
id: "manual-officer",
99+
name: "Manual Officer",
100+
role: "Professional Development Chair",
101+
image: "/officers/fa26/manual-officer.webp",
102+
bio: "A reviewed public biography.",
103+
linkedin: "https://www.linkedin.com/in/manual-officer",
104+
},
105+
];
106+
107+
const { officers, audit } = compileOfficerDirectory(roster, []);
108+
109+
assert.equal(officers[0].bio, "A reviewed public biography.");
110+
assert.equal(
111+
officers[0].linkedin,
112+
"https://www.linkedin.com/in/manual-officer",
113+
);
114+
assert.equal(officers[0].image, "/officers/fa26/manual-officer.webp");
115+
assert.deepEqual(audit.officersWithoutSubmission, []);
116+
});
117+
95118
test("uses the newest nonblank value from duplicate submissions", () => {
96119
const roster = [
97120
{

‎scripts/validate-officer-data.js‎

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
normalizePublicUrl,
1111
} from "../src/data/compileOfficerDirectory.js";
1212
import {
13+
OFFICER_PUBLISHABLE_CONTENT_FIELDS,
1314
OFFICER_PROFILE_FIELDS,
1415
validateOfficerProfileSnapshot,
1516
} from "../src/data/officerProfileContract.js";
@@ -24,7 +25,13 @@ const roster = readJson(profileCohort.roster);
2425
const profileSnapshot = readJson(profileCohort.profiles);
2526
const profiles = profileSnapshot.profiles ?? [];
2627

27-
const allowedRosterFields = new Set(["id", "name", "role", "image", "aliases"]);
28+
const allowedRosterFields = new Set([
29+
"id",
30+
"name",
31+
"role",
32+
"aliases",
33+
...OFFICER_PUBLISHABLE_CONTENT_FIELDS,
34+
]);
2835
const allowedProfileFields = new Set(OFFICER_PROFILE_FIELDS);
2936

3037
const errors = [];
@@ -122,23 +129,31 @@ profiles.forEach((profile, index) => {
122129
errors.push(`profiles[${index}] has an invalid submittedAt timestamp`);
123130
}
124131

125-
for (const field of [
126-
"personal website",
127-
"linkedin",
128-
"github",
129-
"orcid",
130-
]) {
131-
const value = String(profile[field] ?? "").trim();
132-
if (!value) continue;
133-
const normalized = normalizePublicUrl(value, field);
134-
if (!normalized) {
135-
errors.push(`profiles[${index}] has an invalid ${field}`);
136-
} else if (normalized !== value) {
137-
errors.push(`profiles[${index}] ${field} is not normalized`);
138-
}
139-
}
140132
});
141133

134+
for (const [label, records] of [
135+
["roster", roster],
136+
["profiles", profiles],
137+
]) {
138+
records.forEach((record, index) => {
139+
for (const field of [
140+
"personal website",
141+
"linkedin",
142+
"github",
143+
"orcid",
144+
]) {
145+
const value = String(record[field] ?? "").trim();
146+
if (!value) continue;
147+
const normalized = normalizePublicUrl(value, field);
148+
if (!normalized) {
149+
errors.push(`${label}[${index}] has an invalid ${field}`);
150+
} else if (normalized !== value) {
151+
errors.push(`${label}[${index}] ${field} is not normalized`);
152+
}
153+
}
154+
});
155+
}
156+
142157
let compiled;
143158
try {
144159
compiled = compileOfficerDirectory(roster, profiles);

‎src/data/compileOfficerDirectory.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,14 @@ export const compileOfficerDirectory = (roster, submissions) => {
173173
unmatchedSubmissions,
174174
invalidLinks,
175175
officersWithoutSubmission: roster
176-
.filter((entry) => !submissionsByOfficer.has(entry.id))
176+
.filter(
177+
(entry) =>
178+
!submissionsByOfficer.has(entry.id) &&
179+
!PROFILE_FIELDS.some(
180+
(field) =>
181+
field !== "image" && String(entry[field] ?? "").trim(),
182+
),
183+
)
177184
.map((entry) => entry.name),
178185
},
179186
};

‎src/data/officerPortraits.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const framingByImage = {
4848
"/officers/fa26/asmar-khasmammadli.webp": {
4949
objectPosition: "50% 40%",
5050
},
51+
"/officers/fa26/ashley-jaquelyn-leon-martinez-2026-08-27-slack.webp": {
52+
objectPosition: "50% 28%",
53+
},
5154
"/officers/fa26/anisha-s-pallikonda-2026-07-29-3ac3e3c8.webp": {
5255
objectPosition: "50% 35%",
5356
},

‎src/data/officersFa26.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import profileSnapshot from "@/data/officerProfilesFa26.json";
44
import roster from "@/data/officersFa26Roster.json";
55

66
// The official roster owns cohort membership, order, roles, stable IDs, and
7-
// fallback portraits. The form snapshot can enrich only public profile fields.
7+
// reviewed fallback public content. The form snapshot can enrich only public
8+
// profile fields.
89
const compiled = compileOfficerDirectory(roster, profileSnapshot.profiles);
910

1011
export const officersFa26 = compiled.officers as Officer[];

‎src/data/officersFa26Roster.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"id": "ashley-jaquelyn-leon-martinez",
4646
"name": "Ashley Jaquelyn Leon Martinez",
4747
"role": "Professional Development Chair",
48-
"image": ""
48+
"image": "/officers/fa26/ashley-jaquelyn-leon-martinez-2026-08-27-slack.webp",
49+
"bio": "Hiii I'm a junior studying Chemical Biology minoring in Data Science and I love exploring wet lab+dry lab research with an interest in ML for drug development/biomedical analysis and biotech applications. In my free time, I love running, exploring neighborhoods in the area, playing guitar, going to plant nurseries, and hanging out with my friends!",
50+
"linkedin": "https://www.linkedin.com/in/ashley-leon-martinez-12224029a"
4951
},
5052
{
5153
"id": "bhavna-malladi",

0 commit comments

Comments
 (0)