mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 06:03:14 +00:00
586 lines
35 KiB
JavaScript
586 lines
35 KiB
JavaScript
|
|
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 <baseref>: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 <baseref>:cdk/po_stack.py | grep -n 'shared'), and
|
||
|
|
git show <baseref>: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 <baseref>: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.',
|
||
|
|
],
|
||
|
|
}
|