Imported from WarrenResorts/report-builder (
AGENTS.md). Install upstream withnpx skills add WarrenResorts/report-builder. Copyright stays with the author.
Agent Rules & Project Context
This file is the single source of truth for any AI agent working on this codebase. Read it in full at the start of every session. Also read PROJECT_PLAN.md for the feature roadmap and docs/ for pipeline-specific reference.
🚨 Hard Rules — Never Break These
Git & Commits
- Never run
git add,git commit, orgit pushwithout explicit instruction from the user. - After completing changes, state what is ready and stop. Wait for the user to say "commit" and/or "push".
- This applies even when changes are fully verified and tests pass.
- Never push directly to
main. All changes go through a PR, no exceptions. The repo has branch protection but admin bypass is enabled — do not use it. - Always create a feature/chore branch, push it, and open a PR via
gh pr create.
Making Changes
- Never make code changes without explicit approval. If the user asks you to investigate or assess something, do that and present findings. Wait for a "go ahead" before touching any files.
- When asked to research or analyze, present your findings and stop.
- Never run speculative or exploratory AWS commands that create, modify, or delete resources.
AWS Operations
- Always run
aws sts get-caller-identitybefore any AWS create/update/delete operation to confirm you are in the correct account. - Two AWS accounts exist:
- Dev: profile
dev-account→ account237124340260 - Prod: profile
prod-account→ account400534944857
- Dev: profile
- SSM parameters follow the pattern
/report-builder/{environment}/... - Never assume which profile is active — always verify first.
Code Quality — Before Declaring Changes Ready
Always run all four checks in sequence:
npm run build
npm run lint
npm run format:check
npm test
- Never lower coverage thresholds.
- Fix any errors (not warnings) before declaring ready.
- Run
npx better-npm-audit audit --level highto verify the security audit passes locally before pushing — do not let CI be the first to catch audit failures.
📋 PR Requirements
Every PR must pass these checks or CI will fail:
Title
Must follow Conventional Commits: type: description where type is one of feat, fix, chore, docs, build, ci, refactor, test. Subject must not start with uppercase.
Description
Must contain all three sections exactly as written:
## What
## Why
## Testing
Missing any one of these causes PR Validation to fail.
Security Scan
Runs npx better-npm-audit audit --level high. Only high and critical vulnerabilities block the build. The .nsprc file contains exclusions for high-severity issues that are genuinely unexploitable in this pipeline (currently: tmp via exceljs). Run the audit locally before pushing — if the advisory ID for a known exclusion has changed (npm re-issues advisories), update .nsprc before committing.
📦 Dependency Update Process
When Dependabot PRs are open:
- Research first — check the changelog for every package before updating. Look for breaking API changes.
- Consolidate — close all individual Dependabot PRs and do all updates in one branch (
chore/update-dependencies-{month-year}). - Update peer dependencies together — packages like
vitestand@vitest/coverage-v8are a matched pair and must be updated to the same version simultaneously. Using--legacy-peer-depsto work around conflicts creates an inconsistent lock file that breaksnpm ciin CI. - Verify the lock file — always run
npm cilocally after updating to confirm the lock file is clean. - Run the full audit check locally (
npx better-npm-audit audit --level high) before pushing. pdf-parse— do NOT update to v2. v2 is a breaking migration (class-based API, nopagerendercallback). Tracked in PROJECT_PLAN.md Phase 13. Stay on v1 until a proper migration plan is ready.npm overrides/resolutionsdon't mix with Lambda bundling — avoid them.email-processorandfile-processoruse CDK'sNodejsFunctionwithbundling.nodeModules: [...]+depsLockFilePath(seeinfrastructure/lib/constructs/lambda-construct.ts). At deploy time, CDK generates its own trimmedpackage.jsoncontaining only those externalized packages (currentlymailparser,adm-zipforemail-processor;exceljs,pdf-parseforfile-processor), copies the realpackage-lock.jsonnext to it, and runsnpm ciinside Docker. That trimmedpackage.jsondoes not carry root-leveloverrides/resolutionsfields — so if a transitive-dependency security fix needs one,npm cisees the lockfile's overridden version as inconsistent with what the package's own manifest declares and fails withFailedToBundleAsset/ "package.json and package-lock.json ... not in sync" duringdeploy-development/deploy-production. Critically,npm run build/lint/test/format:check/better-npm-auditall pass fine in this scenario — this failure mode is invisible until the actual CDK deploy runs. This happened for real on the August 2026 dependency batch (deepmerge-ts advisory fix via anhtml-to-textoverride brokedeploy-development).- Default to a documented
.nsprcexclusion instead for any advisory onmailparser,adm-zip,exceljs, orpdf-parse(the current bundled Lambda dependencies) unless you've verified an override is compatible (next bullet). - Before pushing any
overrides/resolutionschange, verify it against Lambda bundling locally — no Docker needed: write a scratchpackage.jsonwith only the affected Lambda'sbundling.nodeModuleslist (e.g.{"dependencies":{"mailparser":"^X","adm-zip":"^Y"}}) in an empty scratch directory, copy the repo's realpackage-lock.jsonnext to it, and runnpm cithere. If that fails, the real CDK deploy will fail the same way — do this for everynodeModuleslist inlambda-construct.ts, not just the one you think is affected.
- Default to a documented
🏗️ System Architecture
What It Does
Receives daily hotel report emails with file attachments, parses them, transforms the data using GL account mappings, and emails JE (Journal Entry) and StatJE (Statistical Journal Entry) CSV reports to accounting.
Two Pipelines
- Visual Matrix (PDF) — 11 properties. Emails come in as PDF attachments. Parsed using
pdf-parsewith a custompagerendercallback that inserts pipe|delimiters for column detection. - Opera / IHG (TXT) — Currently 1 property (
holiday-inn-express-clover-lane). IHG sends two.txtfiles per day:trial_balance_*.txtandstat_dmy_seg_*.txt. Seedocs/opera-ihg-pipeline.md.
Key AWS Resources
- Lambda functions:
report-builder-email-processor-{env}andreport-builder-file-processor-{env} - S3 buckets (both environments):
report-builder-incoming-files-{env}-v2— raw email attachmentsreport-builder-processed-files-{env}-v2— generated JE/StatJE reportsreport-builder-mapping-files-{env}-v2— Excel mapping files; Opera files go inopera/prefix; Choice files go inchoice/prefix
- SSM Parameters (production account
400534944857):/report-builder/production/properties/email-mapping— JSON mapping sender email → property slug/report-builder/production/properties/override-email— if set, all reports route here instead of real recipients; NOT set in production (reports go to real recipients)
- SES: Receives inbound email on
aws.warrenresorthotels.comsubdomain; sends outbound reports - EventBridge: Triggers
file-processorLambda daily at 1 PM MST
Property Configuration
All properties are configured in src/config/property-config.ts. Each has:
propertyId(slug, matches email-mapping value)subsidiaryId/subsidiaryFullName(NetSuite)locationId,accountingPeriod,recipientEmailsroomsAvailable(Opera properties only — used for ADR/Occupancy/RevPAR)choiceMappingName(Choice Hotels properties only — display name as it appears in the Choice mapping workbook)
Opera Mapping
- Loaded from the latest
.xlsxfile underopera/in the mapping bucket - Supports dual mapping:
Map<string, OperaMappingEntry[]>— oneTRX_CODEcan produce multiple JE lines - The mapping file is uploaded manually to S3 when the hotel provides an updated version
Choice Mapping
- Loaded from the latest
.xlsxfile underchoice/in the mapping bucket - 7-column
Choicesheet:Src Data Code,Src Desc,Multiplier,Property Name,Glacct Code,Glacct Name,Acct Type - Property-specific rows (non-blank
Property Name) override global rows for that property - See
docs/choice-hotels-pipeline.mdfor full format reference
🌿 Git Workflow
main— production-ready code, protected, requires PR- Feature branches:
feature/,fix/,chore/,docs/prefixes - The CI/CD guard in
.github/workflows/ci-cd.ymlprevents non-feature Dependabot/chore PRs from deploying to dev when afeature/PR is open
Onboarding a New Property (Visual Matrix)
See docs/adding-a-new-property.md.
Onboarding a New Opera Property
- Add sender email to SSM
email-mappingin both dev and prod accounts - Add property config to
src/config/property-config.tswithroomsAvailable - Upload the Opera mapping XLSX to
opera/prefix in both mapping buckets
Onboarding a New Choice Hotels Property
See docs/choice-hotels-pipeline.md — Operations section.
- Add
auto_mail_delivery_system@choicehotels.com→__choice__to SSMemail-mapping(if not already present) - Add
choice:{code}→ property slug to SSMemail-mappingin both accounts - Add property config to
src/config/property-config.tswithchoiceMappingName - Upload or update the Choice mapping XLSX to
choice/prefix in both mapping buckets
📍 Current State (as of July 2026)
What's Live in Production
- All 11 Visual Matrix (PDF) properties — fully operational
holiday-inn-express-clover-lane(IHG/Opera) — live since June 2026- Choice Hotels pipeline (3 properties) — not yet deployed to production; verified working in dev with hotel sign-off (see below)
Open Branches / PRs
- PR #194 (
feature/choice-hotels-pipeline) — Choice Hotels pipeline (Phase 14). Implementation complete, CI green, verified working in dev including hotel-requested fixes (Deferred Revenue multiplier sign, duplicate Occy/ADR/RevPAR StatJE rows, combined Visa/MC/Discover JE row, commonSub Namevalues). Blocked on PR review/approval before merge — not yet reviewed as of this writing. - Dependabot PRs #185–#193, #195 — a new batch of routine dependency bumps; consolidate into a single
chore/update-dependencies-{month-year}branch per the Dependency Update Process above before merging any individually.
Known Technical Debt
pdf-parsev2 migration blocked — see PROJECT_PLAN.md Phase 13tmp(viaexceljs) has a recurring high-severity advisory that keeps getting new IDs; exclusion in.nsprcneeds to be updated each time (check the current ID before assuming1120654is still current)
Next Feature Work
Phase 14 — Choice Hotels pipeline (3 properties) — implementation complete and verified in dev on feature/choice-hotels-pipeline (PR #194). Remaining steps to reach production:
- Get PR #194 reviewed and merged to
main - Add SSM entries (
__choice__sentinel +choice:{code}lookups) to the productionemail-mappingparameter (already present in dev) - Upload the current Choice mapping XLSX to the
choice/prefix of the production mapping bucket (already present in dev) - Manually trigger the
deploy-productionGitHub Actions workflow (workflow_dispatch,mainbranch) — production deploys never run automatically on merge - Verify with live data in production the same way it was verified in dev
Phase 7 — Day-to-Day Comparison Engine — the next development phase after Phase 14 is deployed.
📝 Documentation Maintenance
When making changes that affect:
- Visual Matrix PDF pipeline (email routing, PDF parsing, property config, SSM, SES): update
docs/adding-a-new-property.md - Opera / IHG pipeline (parsers, mapping format, S3 prefix, slug config): update
docs/opera-ihg-pipeline.mdandPROJECT_PLAN.mdPhase 12 - Choice Hotels pipeline (parsers, mapping format, transformation rules, S3 prefix, ZIP/sentinel routing): update
docs/choice-hotels-pipeline.md - Project roadmap or completed phases: update
PROJECT_PLAN.md - These working rules: update this file (
AGENTS.md)