Extract ourdigital-estimate-engine; presales-seo now calls it

New skill 96-ourdigital-estimate-engine: method-aware quoting engine
(effort / coaching / procurement) with universal rate_card + per-service
catalog. Real catalogs: seo (effort), education (coaching); stubs:
digital_ads, digital_branding. Validated to reproduce real quotes —
SEO basic ₩10.5M / treatment ₩25.0M, SHR chain ₩29.5M, L'Escape basic
₩10.5M, GA4/GTM coaching ₩1,570,000, procurement +15%.

Refactor 95-ourdigital-presales-seo: remove rate_card.yaml, sow_templates.yaml,
estimate.py (migrated to engine); add findings_to_scope.py; Stage 5 now maps
findings→scope.json and calls the engine CLI. build_deck/kg_query unchanged;
end-to-end validated on SHR (29.5M) + deck renders engine estimate.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 01:54:11 +09:00
parent 34c3a1df4f
commit c9bdbb57f7
20 changed files with 822 additions and 349 deletions

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Map an SEO findings.json → estimate-engine scope.json (service=seo, effort).
Stage 5 of ourdigital-presales-seo: keeps the SEO-specific mapping here; the engine
(96-ourdigital-estimate-engine) owns the costing. Then call:
python <engine>/scripts/estimate.py --rate-card <engine>/references/rate_card.yaml \
--catalog-dir <engine>/catalog --scope scope.json --out-dir <engagement>
Usage:
python findings_to_scope.py --findings data/findings.json --out data/scope.json [--tier auto] [--seq N]
"""
import argparse
import json
def main():
ap = argparse.ArgumentParser(description="SEO findings.json -> engine scope.json")
ap.add_argument("--findings", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--tier", default="auto", help="auto|smb|basic|treatment")
ap.add_argument("--billing", type=float, default=None)
ap.add_argument("--seq", type=int, default=1)
args = ap.parse_args()
with open(args.findings, encoding="utf-8") as fh:
f = json.load(fh)
e = f.get("entity", {})
p = f.get("prospect", {})
severity = sorted({x.get("severity") for x in f.get("findings", []) if x.get("severity")})
scope = {
"service": "seo",
"method": "effort",
"tier": args.tier,
"billing_rate": args.billing,
"signals": {
"properties_total": e.get("properties_total", 0),
"subbrands_total": e.get("subbrands_total", 0),
"vertical": p.get("vertical", ""),
"severity": severity,
},
"prospect": {
"name": p.get("name", ""), "domain": p.get("domain", ""),
"audit_date": p.get("audit_date", ""), "account_code": p.get("account_code", ""),
},
"seq": args.seq,
}
with open(args.out, "w", encoding="utf-8") as fh:
json.dump(scope, fh, ensure_ascii=False, indent=2)
print(f"scope -> {args.out} | service=seo tier={args.tier} "
f"properties={scope['signals']['properties_total']} subbrands={scope['signals']['subbrands_total']} "
f"vertical={scope['signals']['vertical']!r}")
if __name__ == "__main__":
main()