export const meta = { name: 'phase-6-site-extractor', description: 'Phase 6 of the procurement-ingest refactor (docs/refactor-evaluation.md): site_extractor reconciliation — end the three-way site_code contradiction. extract_site_code (lambdas/po/site_extractor/handler.py:34) already prefers record["site_code"] but validates it with the digit-requiring, prefix-anchored SITE_CODE_PATTERN (line 23) which rejects all-letter codes like KLAL (permanent pending-review rows) and accepts overlong junk like DLI6X/SNY55. Fix: validate the direct field with the canonical derived_fields shape + skip-list semantics (fullmatch), import derive_site_code for the fallback path, ship derived_fields.py into the site_extractor asset via the Phase 2/3 bundling mechanism (Phase 3 left site_extractor from_asset UNTOUCHED on purpose), delete the dead [A-Z]{4,5} regex ladder (line 62). derived_fields.py itself stays byte-untouched. Characterization tests pin current behavior first, then flip to fixed in the same PR. Gated on the shadow bake concluding. Validated field can be ai_fallback-sourced, so push is gated on /sh-security-review. Committed locally, never pushed.', phases: [ { title: 'Setup', detail: 'HARD-GATE on shadow-bake conclusion (args {bakeConcluded:true}) + Phase 3 on base; branch feature/phase-6-site-extractor', model: 'haiku' }, { title: 'Recon', detail: '3 mappers: site_extractor handler + the digit-pattern defect, derived_fields canonical shape/skip-list/derive_site_code, post-Phase-3 bundling sites + bundle-consistency test + backfill script (read-only)' }, { title: 'Spec', detail: 'serial fable spec: pin the handler fix (canonical fullmatch + skip-list), the site_extractor bundling change, backfill repoint-or-delete, the characterization-first-then-flip test plan, bundle/parity rules' }, { title: 'Implement', detail: 'opus: site_extractor handler + new characterization tests; sonnet: cdk site_extractor from_asset + test_bundle_consistency; opus: backfill_sites.py + README — disjoint files', model: 'opus' }, { title: 'Verify', detail: 'mechanical gates (suite green, ruff, synth, asset CONTAINS derived_fields.py, tests flipped to fixed, zero derived_fields diff) + 3 fable lenses (shape-parity, bundling, regression)' }, { title: 'Fix', detail: 'opus fixer, full re-verify, max 3 rounds', model: 'opus' }, { title: 'Package', detail: 'single commit via -F (no push)', model: 'sonnet' }, ], } // ---------------------------------------------------------------- constants const REPO = '/Users/adammoussa/Documents/repositories/seahaven/procurement-ingest' const BRANCH = 'feature/phase-6-site-extractor' let _args = args if (typeof _args === 'string') { try { _args = JSON.parse(_args) } catch (e) { _args = null } } const BASE = (_args && _args.base) || 'main' const CONSTRAINTS = ` PINNED BEHAVIORAL CONSTRAINTS (docs/refactor-evaluation.md Phase 6 + §4 item 6 — violating any is a build failure): 1. THE DEFECT is in lambdas/po/site_extractor/handler.py: extract_site_code (line 34) already prefers record["site_code"] (line 36), but validates it with SITE_CODE_PATTERN = re.compile(r"[A-Z]{2,4}\\d{1,2}") (line 23) applied via .match (prefix-anchored, digit-REQUIRING). That pattern REJECTS all-letter codes like KLAL — so a KLAL-class site can NEVER self-register and becomes a permanent pending-review row — and ACCEPTS overlong junk like DLI6X / SNY55 (prefix match, no end anchor). THE FIX: validate the direct record["site_code"] field with the CANONICAL derived_fields shape + skip-list semantics using FULLMATCH (derived_fields.py: _STRICT_CODE_RE at line 50 = re.compile(r"[A-Z][A-Z0-9]{2,4}") + _is_valid_code at line 97 = "not in _SKIP and >=2 alphabetic chars", _SKIP at line 69). Import derive_site_code (derived_fields.py:259) for the FALLBACK path (ship_to / line_items). DELETE the bespoke regex ladder — the line 62 alternation r"^([A-Z]{1,4}[0-9]{1,2}|[A-Z]{4,5})\\b" whose [A-Z]{4,5} arm is dead code (it can never survive the digit-requiring SITE_CODE_PATTERN.match on the very next line) — and every other SITE_CODE_PATTERN reference. NET EXPECTED BEHAVIOR: KLAL is ACCEPTED, DLI6X and SNY55 are REJECTED. 2. SHIP derived_fields.py INTO the site_extractor Lambda asset. Add the cp/root bundling change for site_extractor's from_asset in cdk/po_stack.py (currently lines 684-686: Code.from_asset("../lambdas/po/site_extractor", exclude=["**/__pycache__/**"]) — Phase 3 left this UNTOUCHED on purpose). THIS phase makes lambdas/po/email_processor/derived_fields.py reachable there, MIRRORING the Phase 2/3 mechanism the two email processors use (widened asset root + a scoped cp so the module lands FLAT in /asset-output so the bare-name import 'from derived_fields import derive_site_code' resolves). KEEP the __pycache__ / tests excludes intact; do NOT ship tests/ or a stale package/ into the zip. The spec agent pins the exact root + cp form (Docker bundling vs a no-Docker local staging — ONE mechanism, exact code). 3. Keep shape/skip validation ON the record["site_code"] field even though it is ai_fallback-SOURCED (the LLM value is authoritative during/after the bake) — this is validated authority, NOT blind trust and NOT a regex bypass. Keep parse_address (line 69) AND the pending-review write flow (write_pending_review line 223 + the handler else-branch lines 283-290) BYTE-UNCHANGED. 4. Repoint scripts/backfill_sites.py at derive_site_code (it currently does 'from handler import extract_site_code, parse_address, upsert_site' via a sys.path hack) — OR DELETE it entirely (one-time backfill script; deletion is acceptable and PREFERRED if simpler). Pick one and state why. 5. CHARACTERIZATION TESTS FIRST. site_extractor is 0% covered today (§2). Write tests that PIN CURRENT behavior first — INCLUDING the KLAL rejection divergence and the overlong-junk (DLI6X / SNY55) acceptance, parse_address variants, and the pending-review fallback — then FLIP the assertions to the FIXED behavior in the SAME PR, so the change is provably intentional. The final committed suite asserts FIXED behavior (KLAL accepted, DLI6X/SNY55 rejected); the characterization-of-old-behavior stage is a build step, not a red suite left behind. 6. derived_fields.py itself stays UNTOUCHED — import it, do NOT edit it. git diff ${BASE}...HEAD -- lambdas/po/email_processor/derived_fields.py MUST be empty. The shadow-bake authority model must not shift in this phase. 7. AWS access is READ-ONLY (get-function, downloading the deployed site_extractor zip via presigned URL to confirm the asset contents). NEVER cdk deploy, never invoke, never mutate. No push, no PR, no cdk deploy in this workflow. ` const PREAMBLE = ` You are one of several agents building refactor Phase 6 in the git repo at ${REPO} on branch ${BRANCH} (already checked out — do NOT switch branches, do NOT create branches, do NOT commit, NEVER push, do NOT run cdk deploy or touch AWS resources beyond read-only calls). Authoritative spec: docs/refactor-evaluation.md, section "Phase 6 — site_extractor reconciliation" and section "§4" item 6 (site_extractor characterization). The doc wins on any conflict. Work ONLY in the files you are told you own; other agents are concurrently editing other files in this same working tree. ${CONSTRAINTS} Your final message is consumed by an orchestrator script, not a human — return only the structured data requested. ` // ------------------------------------------------------------------ schemas const RECON = { type: 'object', required: ['summary', 'facts'], properties: { summary: { type: 'string' }, facts: { type: 'array', items: { type: 'string' } }, blockers: { type: 'array', items: { type: 'string' } }, }, } const SPEC = { type: 'object', required: ['handlerFix', 'bundlingEdit', 'backfillDecision', 'characterizationTests', 'parityRules', 'notes'], properties: { handlerFix: { type: 'string', description: 'the exact edit to lambdas/po/site_extractor/handler.py: how the direct record["site_code"] field is validated with the canonical derived_fields fullmatch shape + skip-list, the derive_site_code import + fallback wiring, every line deleted (SITE_CODE_PATTERN def, the line-62 alternation, all SITE_CODE_PATTERN references), and an explicit statement that parse_address + write_pending_review + the pending-review else-branch are byte-unchanged. Prove KLAL accepted, DLI6X/SNY55 rejected under the new logic.' }, bundlingEdit: { type: 'string', description: 'the complete replacement from_asset block for site_extractor in cdk/po_stack.py: widened asset root, the scoped cp form staging both the site_extractor *.py AND lambdas/po/email_processor/derived_fields.py flat, the __pycache__/tests/package excludes, and whether it uses Docker bundling or a no-Docker local staging — ONE mechanism, exact code, mirroring the Phase 2/3 email-processor form.' }, backfillDecision: { type: 'string', description: 'repoint scripts/backfill_sites.py at derive_site_code OR delete it — which, the exact resulting file (or git rm), and why.' }, characterizationTests: { type: 'string', description: 'the new test file(s) under lambdas/po/site_extractor/tests/: how they load the handler (loader idiom), the CURRENT-behavior pins (KLAL rejected, DLI6X/SNY55 accepted, parse_address variants, pending-review fallback) and the FIXED-behavior assertions they flip to, plus any test_bundle_consistency edit if it now pins the site_extractor asset.' }, parityRules: { type: 'string', description: 'how Verify judges the change: the staged site_extractor asset must CONTAIN derived_fields.py (+ the handler and its own siblings, flat) and nothing stray (no tests/, no __pycache__, no package/), asset hash deterministic, and the derived_fields.py diff must be empty vs base.' }, notes: { type: 'string' }, }, } const IMPL = { type: 'object', required: ['filesChanged', 'summary', 'checksRun'], properties: { filesChanged: { type: 'array', items: { type: 'string' } }, summary: { type: 'string' }, checksRun: { type: 'string' }, blockers: { type: 'array', items: { type: 'string' } }, }, } const CHECKS = { type: 'object', required: ['passed', 'details'], properties: { passed: { type: 'boolean' }, details: { type: 'string' }, scopeViolations: { type: 'array', items: { type: 'string' } }, }, } const FINDINGS = { type: 'object', required: ['findings'], properties: { findings: { type: 'array', items: { type: 'object', required: ['title', 'severity', 'confirmed', 'evidence', 'fix'], properties: { title: { type: 'string' }, severity: { enum: ['critical', 'high', 'medium', 'low'] }, confirmed: { type: 'boolean' }, evidence: { type: 'string' }, fix: { type: 'string' }, }, }, }, }, } // ------------------------------------------------------------------- setup phase('Setup') // HARD-GATE (a): the shadow bake must have concluded. This workflow cannot // auto-verify the bake ended, so require an explicit opt-in BEFORE any git work. if (!_args || _args.bakeConcluded !== true) { return { aborted: 'Phase 6 gated on shadow-bake conclusion', blockers: [ 'Phase 6 gated on shadow-bake conclusion — re-run with args {bakeConcluded:true} once the DerivedFieldAgreement bake is signed off and the switchover PR (Python authoritative on ai_fallback) has landed.', ], } } const setup = await agent(` In ${REPO}: 1. SEQUENCING GATE — Phase 3 (lambdas/shared/ extraction with the widened ../lambdas asset roots) must be on ${BASE}: derived_fields.py must be shippable into the site_extractor asset via the Phase 2/3 bundling mechanism, and Phase 3 DELIBERATELY left site_extractor's from_asset untouched — THIS phase adds the bundling change for site_extractor. git fetch origin, then pick the base ref: origin/${BASE} if that remote ref exists, otherwise the local branch ${BASE} (a stacked local-only base is expected and fine). Verify on the base ref: (a) Phase 2/3 mechanism present: cdk/po_stack.py on the base ref contains Code.from_asset("../lambdas") for the PO email processor (git show :cdk/po_stack.py | grep -n '\\.\\./lambdas'); (b) Phase 3 landed: lambdas/shared/ exists on the base ref with a cp shared/*.py in the email-processor bundling command (git show :cdk/po_stack.py | grep -n 'shared'), and git show :lambdas/shared/ses_auth.py | head -3 succeeds; (c) site_extractor's from_asset is STILL the untouched narrow form on the base ref (git show :cdk/po_stack.py | grep -n '../lambdas/po/site_extractor') — this phase is the one that widens it. If (a) or (b) is missing, STOP with a blocker naming the unmet phase and do nothing else. If (c) is already widened, note it (someone beat us to it) but do not treat as a hard blocker. 2. Verify clean working tree (untracked .coverage / .claude/ / the local 44 MB lambdas/po/email_processor/package/ dir are fine; any OTHER dirt = blocker, never stash or discard). 3. git checkout ${BASE}; then git pull --ff-only ONLY if the branch has an upstream (a local-only base skips the pull — not a blocker); then git checkout -b ${BRANCH} 4. gh pr list --state open --json number,title,headRefName (overlap check). Return facts: HEAD sha, per-gate evidence, open PRs, blockers. `, { label: 'setup:branch', model: 'haiku', schema: RECON }) if (!setup || (setup.blockers && setup.blockers.length)) { return { aborted: 'setup blockers', blockers: setup ? setup.blockers : ['setup agent died'], facts: setup ? setup.facts : [] } } log(`Branch ${BRANCH} ready off ${BASE}. ${setup.summary}`) // ------------------------------------------------------------------- recon phase('Recon') const recon = await parallel([ () => agent(`${PREAMBLE} Read-only recon of the site_extractor DEFECT surface (lambdas/po/site_extractor/handler.py): 1. Quote SITE_CODE_PATTERN verbatim with file:line and EVERY place it is used (.match / .search). Note that .match is prefix-anchored and the pattern requires 1-2 trailing digits. 2. Quote extract_site_code in full with line numbers: the direct-field branch (record["site_code"]), the ship_to name branches, the line_items branch and its r"^([A-Z]{1,4}[0-9]{1,2}|[A-Z]{4,5})\\b" alternation — prove the [A-Z]{4,5} arm is DEAD (it can never survive the very-next-line SITE_CODE_PATTERN.match, which requires a digit). 3. Concretely trace KLAL (all letters), DLI6, DLI6X, SNY55 through the CURRENT code — which return, which fall through — to pin the exact current behavior the characterization tests must first reproduce. 4. Quote the EXACT boundaries (file:line) of parse_address, write_pending_review and the handler else-branch that calls write_pending_review — these must stay byte-unchanged; note everything that must NOT move. 5. Confirm there is NO tests/ dir under lambdas/po/site_extractor today and note how the email-processor test loaders resolve a bare 'import handler' (the idiom the new characterization tests will reuse). 20-30 precise facts.`, { label: 'recon:site-extractor', model: 'sonnet', phase: 'Recon', schema: RECON }), () => agent(`${PREAMBLE} Read-only recon of the CANONICAL shape in lambdas/po/email_processor/derived_fields.py that the fix must adopt: 1. Quote _STRICT_CODE_RE (line ~50), _CODE_TOKEN_RE, _SKIP (line ~69) and _is_valid_code (line ~97) verbatim with file:line. State exactly what shape counts as a valid site code (3-5 chars, letter-first, all-letters allowed, >=2 alphabetic chars, not skip-listed) using FULLMATCH. 2. Quote derive_site_code (line ~259) signature + docstring + return contract (str | None, NEVER raises) and describe what INPUT it takes (the parsed PO dict) and the priority-ordered shapes it tries — so the fallback wiring in the handler passes the right shape of dict. 3. Determine precisely: under the canonical fullmatch shape + _is_valid_code, is KLAL valid? is DLI6X valid? is SNY55 valid? Show the reasoning token by token. If the canonical shape does NOT by itself reject DLI6X/SNY55, say so explicitly and identify what additional check (standalone-token lookaround, length, _is_valid_code, or applying derive-style resolution to the field) is required to hit the doc's pinned outcome (KLAL accepted; DLI6X/SNY55 rejected) — this is load-bearing for the spec. 4. List every import derived_fields.py needs at module load (must be stdlib-only so it can be dropped flat into the site_extractor asset with no new dependency). 15-25 facts. Flag as a blocker any way the pinned KLAL/DLI6X/SNY55 outcome cannot be met with derived_fields semantics alone.`, { label: 'recon:derived-fields-shape', model: 'sonnet', phase: 'Recon', schema: RECON }), () => agent(`${PREAMBLE} Read-only recon of the bundling + test + script surface this phase touches: 1. cdk/po_stack.py — quote the site_extractor Function block verbatim with file:line (currently ~677-694): function_name, runtime, architecture, handler property, memory/timeout, env, and the from_asset call (root + exclude) that this phase must widen. Also quote the PO email-processor from_asset (root ../lambdas + the cp glob + exclude list) as the Phase 2/3 template to mirror. Confirm no requirements.txt exists for site_extractor. List every function property that must NOT change. 2. tests/test_bundle_consistency.py — quote PO_EXPECTED_TOP_LEVEL_MODULES and every test; determine whether ANY assertion currently pins the site_extractor asset (if none does, note that a new site_extractor pin — asserting derived_fields.py now ships and a commented-out cp fails — is the clean way to keep teeth; if one does, quote it). 3. scripts/backfill_sites.py — quote the sys.path hack + the 'from handler import extract_site_code, parse_address, upsert_site' line and how it uses extract_site_code, so the spec can choose repoint vs delete. 4. Read-only AWS (us-east-1): aws lambda get-function for po-ingest-site-extractor; download its Code.Location zip to a scratch dir and produce the COMPLETE unzip -l file list (the baseline the new bundled asset must cover PLUS derived_fields.py); record CodeSha256. 15-25 facts.`, { label: 'recon:bundling-tests-script', model: 'sonnet', phase: 'Recon', schema: RECON }), ]) const reconOk = recon.filter(Boolean) const pack = reconOk.map(r => `## ${r.summary}\n${r.facts.join('\n')}`).join('\n\n') const reconBlockers = reconOk.flatMap(r => r.blockers || []) log(`Recon complete: ${reconOk.length}/3 mappers, ${reconBlockers.length} blockers`) if (reconBlockers.length) { return { aborted: 'recon blockers (likely a shape-parity gap — resolve before implementing)', blockers: reconBlockers, reconPack: pack } } // -------------------------------------------------------------------- spec phase('Spec') const spec = await agent(`${PREAMBLE} You are the SPEC agent — the single authority that pins every contested decision BEFORE parallel implementation (parallel leaves cannot see each other's choices). Using the recon pack below plus your own reads of the actual files, produce the binding implementation spec: - handlerFix: the exact edit to lambdas/po/site_extractor/handler.py. Validate record["site_code"] with the CANONICAL derived_fields fullmatch shape + skip-list (constraint 1) so KLAL is ACCEPTED and DLI6X/SNY55 are REJECTED — if recon found the bare fullmatch shape does not reject DLI6X/SNY55, pin the exact additional check that does (do NOT reintroduce a digit requirement). Import derive_site_code from derived_fields for the fallback path; wire it with the correct input shape. DELETE the SITE_CODE_PATTERN def, the line-62 alternation, and every SITE_CODE_PATTERN reference. State EXPLICITLY that parse_address, write_pending_review and the pending-review else-branch are byte-unchanged. - bundlingEdit: the complete replacement from_asset block for site_extractor — widened root + scoped cp staging both site_extractor *.py and lambdas/po/email_processor/derived_fields.py FLAT into /asset-output, with the __pycache__/tests/package excludes, mirroring the Phase 2/3 email-processor form. Pick ONE mechanism (Docker bundling vs no-Docker local staging) and give exact code. Mind that the module must land flat so 'from derived_fields import derive_site_code' resolves at runtime. - backfillDecision: repoint scripts/backfill_sites.py at derive_site_code OR git rm it — pick one, give the exact resulting file or the rm, and say why. - characterizationTests: the new tests under lambdas/po/site_extractor/tests/ — loader idiom, the CURRENT-behavior pins (KLAL rejected, DLI6X/SNY55 accepted, parse_address variants, pending-review fallback) that get FLIPPED to FIXED-behavior assertions in the same change, and any test_bundle_consistency edit that now pins the site_extractor asset (derived_fields ships; commented-out cp fails). - parityRules: exactly how Verify judges the staged asset vs the deployed baseline — staged site_extractor asset CONTAINS derived_fields.py + handler + own siblings (flat), nothing stray, asset hash deterministic, and derived_fields.py diff empty vs ${BASE}. Recon pack:\n${pack}`, { label: 'spec:pin-site-extractor', phase: 'Spec', schema: SPEC }) if (!spec) return { aborted: 'spec agent died — rerun workflow', reconBlockers } const specBlock = `BINDING SPEC (from the spec agent — implement EXACTLY this):\n${JSON.stringify(spec, null, 2)}` log('Spec pinned: handler fix, site_extractor bundling, backfill decision, characterization tests, parity rules') // --------------------------------------------------------------- implement phase('Implement') const impl = await parallel([ () => agent(`${PREAMBLE} YOU OWN: lambdas/po/site_extractor/ ONLY (the handler AND a new tests/ subdir). Do not touch cdk/, scripts/, tests/ at repo root, or README. Task: execute spec.handlerFix + spec.characterizationTests. 1. Edit handler.py per spec.handlerFix: validate record["site_code"] with the canonical derived_fields fullmatch shape + skip-list, import derive_site_code for the fallback path, DELETE SITE_CODE_PATTERN and the line-62 [A-Z]{4,5} alternation and every SITE_CODE_PATTERN reference. Keep parse_address, write_pending_review and the pending-review else-branch BYTE-UNCHANGED (constraint 3). Do NOT edit derived_fields.py (constraint 6). 2. Write the characterization tests under lambdas/po/site_extractor/tests/: FIRST pin CURRENT behavior (KLAL rejected, DLI6X/SNY55 accepted, parse_address variants, pending-review fallback), THEN flip the assertions to the FIXED behavior (KLAL accepted, DLI6X/SNY55 rejected) in this same change — the COMMITTED suite asserts FIXED behavior (constraint 5). The tests must import derived_fields via the same flat/loader idiom the asset uses so they exercise the real derive_site_code. Run before returning: ruff check lambdas/po/site_extractor && ruff format lambdas/po/site_extractor --check, and pytest -q --no-cov on your new suite (with derived_fields.py resolvable on sys.path the way the asset ships it — stub AWS/boto env as needed). git diff ${BASE}...HEAD -- lambdas/po/email_processor/derived_fields.py MUST be empty. ${specBlock}`, { label: 'impl:site-extractor', model: 'opus', phase: 'Implement', schema: IMPL }), () => agent(`${PREAMBLE} YOU OWN: cdk/po_stack.py (the site_extractor from_asset region ONLY — alarms, IAM, tables, env, and the email-processor/web_ui assets are all off-limits) and tests/test_bundle_consistency.py. Task: apply spec.bundlingEdit (widen the site_extractor asset root + scoped cp staging derived_fields.py flat, mirroring the Phase 2/3 email-processor form, keeping the __pycache__/tests/package excludes) and, per spec.characterizationTests / spec.parityRules, add or update the site_extractor pin in test_bundle_consistency.py so a commented-out or narrowed derived_fields cp FAILS the test (keep existing tests green, don't lose teeth). Touch nothing else; every other function property and every other asset stays byte-identical. Run before returning: ruff check cdk tests/test_bundle_consistency.py && cd cdk && npx cdk synth po-ingest -q -o /tmp/phase6-synth (artifact-id selector, NOT stack_name). Confirm the staged site_extractor asset CONTAINS derived_fields.py flat alongside handler.py and NO tests/__pycache__/package; note the asset hash. If the handler edits have not landed yet the synth still works (asset is just files) — but if the new tests/ dir would be staged, prove the exclude drops it. ${specBlock}`, { label: 'impl:cdk-bundle-test', model: 'sonnet', phase: 'Implement', schema: IMPL }), () => agent(`${PREAMBLE} YOU OWN: scripts/backfill_sites.py and README.md ONLY. Task A: apply spec.backfillDecision — either repoint scripts/backfill_sites.py at derive_site_code (importing from derived_fields, not the handler's deleted extractor) OR git rm it. Do exactly what the spec pinned. Task B: README — update the site_code / site_extractor section to reflect the single reconciled definition (derived_fields canonical shape is now authoritative for site_extractor too; KLAL-class all-letter codes self-register; derived_fields.py ships into the site_extractor asset via the same bundling cp). Fix any stale "three inconsistent site_code definitions" wording (finding X2). Match existing README style. Run before returning: ruff check on any script you kept, and pytest -q --no-cov at repo root (must be green against the OTHER agents' edits — they land in parallel; poll by re-running up to ~10 min before reporting a blocker). ${specBlock}`, { label: 'impl:backfill-readme', model: 'opus', phase: 'Implement', schema: IMPL }), ]) const implOk = impl.filter(Boolean) const implBlockers = implOk.flatMap(r => r.blockers || []) log(`Implement complete: ${implOk.length}/3 agents, blockers: ${implBlockers.length}`) // ---------------------------------------------------- verify + fix loop const EXPECTED_SCOPE = [ 'lambdas/po/site_extractor/', 'cdk/po_stack.py', 'scripts/backfill_sites.py', 'tests/test_bundle_consistency.py', 'README.md', ] const mechanicalPrompt = `${PREAMBLE} Independent re-verification — trust nothing self-reported. Run ALL gates, quoting failures verbatim: 1. pytest -q --no-cov (repo root, all roots green — including the NEW site_extractor suite). Confirm the new suite actually exists and is collected (it did not exist before this phase). 2. ruff check . && ruff format --check . 3. cd cdk && npx cdk synth po-ingest -q (artifact-id selector, NOT stack_name). 4. ASSET CONTENTS: locate the staged site_extractor asset under the synth -o dir and prove it CONTAINS derived_fields.py flat alongside handler.py and the site_extractor's own siblings, and contains NO tests/, NO __pycache__, NO package/ (constraint 2). List the asset's top-level files. 5. CHARACTERIZATION: confirm the committed site_extractor tests now assert the FIXED behavior — KLAL ACCEPTED, DLI6X and SNY55 REJECTED — and that a current-behavior-of-old-code pin was not left red (constraint 5). 6. UNTOUCHABLE: git diff ${BASE}...HEAD -- lambdas/po/email_processor/derived_fields.py MUST output NOTHING (constraint 6). Also confirm parse_address + write_pending_review + the pending-review else-branch in site_extractor/handler.py are byte-unchanged vs ${BASE} (git diff the file and check only extract_site_code / SITE_CODE_PATTERN / imports changed). 7. DELETION: SITE_CODE_PATTERN is gone from site_extractor/handler.py (grep returns nothing) and no path still uses the deleted digit-requiring regex or the line-62 [A-Z]{4,5} alternation. 8. git status --porcelain scope check: every modified/added/deleted path under ${EXPECTED_SCOPE.join(', ')} (untracked .coverage/.claude/package/ tolerated). If backfill_sites.py was deleted, the deletion is staged/tracked. passed=true only if all green. YOU MAY NOT edit files.` const lenses = [ { key: 'shape-parity', prompt: `${PREAMBLE} ADVERSARIAL REVIEW — shape-parity lens. Prove extract_site_code now matches derive_site_code's canonical shape + skip-list. (1) Byte-read the fixed extract_site_code and derived_fields' _STRICT_CODE_RE / _is_valid_code / _SKIP; construct the token set {KLAL, DLI6, DLI6X, SNY55, RME (skip-listed), B187 (<2 alpha)} and trace each through the fixed direct-field validation AND the derive_site_code fallback — KLAL MUST be accepted, DLI6X and SNY55 MUST be rejected, skip-listed and sub-2-alpha tokens rejected. (2) Prove NO path still uses the deleted digit-requiring SITE_CODE_PATTERN or the [A-Z]{4,5} dead arm (grep the whole handler). (3) Attack the fallback wiring: does derive_site_code receive the dict shape it expects, and does it NEVER raise (a stream record with missing/empty fields must not throw)? confirmed=true only with a concrete token-trace or file:line proof.` }, { key: 'bundling', prompt: `${PREAMBLE} ADVERSARIAL REVIEW — bundling lens. Prove derived_fields.py actually ships in the site_extractor zip and the excludes stay intact. (1) cd cdk && npx cdk synth po-ingest -q -o /tmp/phase6-bundle-a, then again into /tmp/phase6-bundle-b; locate the staged site_extractor asset in each and prove derived_fields.py is present flat next to handler.py, that the runtime import 'from derived_fields import derive_site_code' resolves with that dir alone on sys.path (python3 -c with env stubs), and that the two synths produce IDENTICAL asset hashes (determinism). (2) Drop a throwaway __pycache__/junk.pyc and a tests/scratch_test.py under the widened root region (delete them afterwards), re-synth, and confirm the excludes keep the asset hash unchanged and neither file appears. (3) Confirm NO other asset (email processors, web_ui) changed hash as a side effect of the root widening. confirmed=true only with evidence.` }, { key: 'regression', prompt: `${PREAMBLE} ADVERSARIAL REVIEW — regression lens. The fix must be surgical. (1) git diff ${BASE}...HEAD -- lambdas/po/site_extractor/handler.py — prove parse_address, write_pending_review, upsert_site, load_address_cache, lookup_by_address, deserialize_image and the handler() event loop / pending-review else-branch are BYTE-UNCHANGED; the ONLY deltas are inside extract_site_code + its imports + the deleted SITE_CODE_PATTERN. (2) Prove derived_fields.py is byte-unchanged vs ${BASE} (constraint 6) — the shadow-bake authority model did not shift. (3) Confirm the ai_fallback-sourced record["site_code"] is still VALIDATED (shape + skip-list), not blindly trusted and not regex-bypassed (constraint 3). (4) If backfill_sites.py was kept, prove it no longer imports the deleted extractor; if deleted, prove nothing else imports it. confirmed=true only with file:line or diff evidence.` }, ] let round = 0 let checks = null let confirmed = [] while (round < 3) { phase('Verify') const results = await parallel([ () => agent(mechanicalPrompt, { label: `verify:mechanical-r${round}`, model: 'sonnet', phase: 'Verify', schema: CHECKS }), ...lenses.map(l => () => agent(l.prompt, { label: `verify:${l.key}-r${round}`, phase: 'Verify', schema: FINDINGS })), ]) checks = results[0] confirmed = results.slice(1).filter(Boolean) .flatMap(r => r.findings || []) .filter(f => f.confirmed && f.severity !== 'low') const green = checks && checks.passed log(`Verify round ${round}: mechanical ${checks && checks.passed ? 'GREEN' : 'RED'}, confirmed findings: ${confirmed.length}`) if (green && confirmed.length === 0) break round += 1 if (round >= 3) break phase('Fix') await agent(`${PREAMBLE} You are the fix agent — you may edit files under: ${EXPECTED_SCOPE.join(', ')}. Fix EVERY item below minimally; the binding spec and 7 pinned constraints still hold (a finding that conflicts with a constraint is reported, not "fixed" — the constraint wins, esp. constraint 6's derived_fields untouchability and constraint 3's validated-not-blind-trust rule). Re-run the specific failing gate/test per fix. MECHANICAL:\n${checks ? checks.details : '(agent died — rerun all gates)'} CONFIRMED FINDINGS:\n${JSON.stringify(confirmed, null, 2)} ${specBlock}`, { label: `fix:round-${round}`, model: 'opus', phase: 'Fix', schema: IMPL }) } const verifyClean = checks && checks.passed && confirmed.length === 0 if (!verifyClean) { return { status: 'NEEDS ATTENTION — verify not clean after 3 rounds; branch left uncommitted', branch: BRANCH, mechanical: checks, unresolvedFindings: confirmed, implBlockers, reconBlockers, spec, } } // ----------------------------------------------------------------- package phase('Package') const commit = await agent(`${PREAMBLE.replace('do NOT commit, ', '')} YOU are the commit agent: 1. Read ~/Documents/repositories/seahaven/engineering-handbook/commit-messages.md and follow it exactly. 2. git add only paths under: ${EXPECTED_SCOPE.join(', ')} and .claude/workflows/phase-6-site-extractor.js. NOT .coverage, NOT package/. Verify the staged set with git status — if backfill_sites.py was deleted, its deletion MUST be staged too. 3. ONE commit; write the message to /tmp/phase6-commit-msg.txt and use git commit -F /tmp/phase6-commit-msg.txt (backticks in -m get eaten by zsh). Suggested subject: "feat: reconcile site_extractor site_code with derived_fields canonical shape (refactor phase 6)" Body: the digit-pattern defect (KLAL never self-registered, DLI6X/SNY55 false-accepted), the canonical fullmatch + skip-list fix + derive_site_code fallback, the derived_fields.py bundling cp into the site_extractor asset (Phase 2/3 mechanism), the characterization-first-then-flip test approach, the backfill repoint/delete decision, and that derived_fields.py is byte-unchanged. NO AI attribution / Co-Authored-By lines. 4. Do NOT push. Return commit sha + shortstat in summary.`, { label: 'package:commit', model: 'sonnet', phase: 'Package', schema: IMPL }) return { status: 'BUILT — committed locally, NOT pushed', branch: BRANCH, base: BASE, commit: commit ? commit.summary : 'commit agent died — commit manually', spec: { handlerFix: spec.handlerFix, bundlingEdit: spec.bundlingEdit, backfillDecision: spec.backfillDecision, notes: spec.notes }, implementation: implOk.map(r => r.summary), filesChanged: implOk.flatMap(r => r.filesChanged), verifyRounds: round + 1, blockers: implBlockers.concat(reconBlockers), outstandingGates: [ '/sh-security-review RECOMMENDED before push — the validated record["site_code"] field can be ai_fallback-sourced (attacker-influenced via a DKIM-passing injected body), so the new shape/skip validation is untrusted-input handling; run it on the committed diff and flag it.', 'cross-family cross_review.py NOT required (no IAM/policy change, no Lambda handler signature/event/return-contract change — internal validation logic + a bundling cp only).', 'push + PR + gh pr checks green.', 'deploy-then-merge: deploy from branch, smoke green, and confirm the LIVE success signal = the pending-site-review write rate DROPS (KLAL-class all-letter codes now self-register instead of falling to pending review), plus one real DynamoDB-stream event processed clean, THEN merge.', 'GOTCHA: npx cdk synth po-ingest uses the ARTIFACT-ID selector (po-ingest), not the stack_name — using the wrong selector silently synths nothing or the wrong stack.', ], }