"""公司事实的来源、日期精度、填空与撤回。""" import json import sqlite3 import tempfile import unittest from contextlib import closing from pathlib import Path from types import SimpleNamespace from peach import company_profiles from peach.entities import upsert_asset_entity from peach import web_contract from support.ledger import fresh_ledger class CompanyProfileTests(unittest.TestCase): def test_dates_keep_precision_and_reject_copyright_or_invalid_dates(self): for raw, expected in [('2002年5月', '2002-05'), ('1999年3月1日', '1999-03-01'), ('2016', '2016'), ('平成16年7月21日', '2004-07-21')]: self.assertEqual(company_profiles.date_text(raw), expected) for raw in ['© 2002', '20周年', '2025年2月30日', '2002年(ブランド発売)']: self.assertIsNone(company_profiles.date_text(raw)) def test_official_tables_separate_company_and_brand_and_detect_conflicts(self): html = '
設立2002年5月
ブランド設立2003年
主要取引先WILL
所在地
東京都中野区
会社概要 Company' parsed = company_profiles.extract(html, 'https://brand.example/') self.assertEqual(parsed['facts']['founded']['value'], '2002-05') self.assertEqual(parsed['facts']['launched']['value'], '2003') self.assertNotIn('parent', parsed['facts']) self.assertNotIn('country', parsed['facts']) self.assertEqual(parsed['company_pages'], ['https://brand.example/company/']) conflict = company_profiles.extract(html + '
設立
2001
', 'https://brand.example/') self.assertNotIn('founded', conflict['facts']) self.assertEqual(conflict['conflicts'][0]['field'], 'founded') def test_fill_protects_existing_facts_and_revert_preserves_metadata(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) connection = sqlite3.connect(fresh_ledger(Path(temporary.name).resolve())) self.addCleanup(connection.close) connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,metadata_json,created_at,updated_at) VALUES(1,'studio','Brand','brand','{\"provider\":\"catalog\"}','2026-10-07','2026-10-07')") fact = {'value': '2002', 'source_url': 'https://brand.example/company', 'evidence': '設立:2002年'} self.assertEqual(company_profiles.fill(connection, 1, {'founded': fact}, source='auto:company-profile', batch='batch1')['written'], ['founded']) conflict = company_profiles.fill(connection, 1, {'founded': {**fact, 'value': '2001'}}, source='auto:company-profile', batch='batch2') self.assertEqual(conflict, {'written': [], 'conflicts': ['founded']}) self.assertEqual(company_profiles.planned_revert(connection, 'auto:company-profile', 'batch2'), []) rows = company_profiles.planned_revert(connection, 'auto:company-profile', 'batch1') self.assertEqual(company_profiles.revert(connection, rows), 1) self.assertEqual(json.loads(connection.execute('SELECT metadata_json FROM entity').fetchone()[0]), {'provider': 'catalog'}) def test_group_company_tables_are_scoped_by_the_entity_names(self): html = '
社名ソフト・オン・デマンド株式会社
設立1995年11月
社名SODクリエイト株式会社
設立1999年3月
' result = company_profiles.extract_for_entity(html, 'https://group.example/about_us/info/', ['SOD Create', 'SODクリエイト']) self.assertEqual(result['facts']['founded']['value'], '1999-03') self.assertEqual(result['facts']['legal_name']['value'], 'SODクリエイト株式会社') unnamed = company_profiles.extract_for_entity(html, 'https://group.example/about_us/info/', ['SOD Create']) self.assertEqual(unnamed['facts'], {}) def test_terms_operator_comes_from_the_page_sentence_and_listings_give_no_group(self): html = '

Vixen Media Group

OUR BRANDS Each studio is a world. Explore Vixen, Blacked, Blacked Raw, Tushy, and Deeper, all available on Vixen Plus

' self.assertEqual(company_profiles.page_facts(html, 'https://www.vixengroup.com/', ['Tushy'])['facts'], {}) terms = '

These terms of service are entered into between you and General Media Systems, LLC (the Company).

' facts = company_profiles.page_facts(terms, 'https://www.anybrand.example/terms', ['Any'])['facts'] self.assertEqual(facts['operator']['value'], 'General Media Systems, LLC') self.assertEqual(facts['operator']['evidence'], 'These terms of service are entered into between you and General Media Systems, LLC (') self.assertEqual(company_profiles.page_facts(terms, 'https://www.anybrand.example/news', ['Any'])['facts'], {}) def test_group_statements_without_labels_are_not_observed(self): cases = [ ('

SODグループ

SODクリエイト株式会社

', 'https://corporate.sod.co.jp/business/softondemand/', ['SOD Create']), ('

メーカー一覧

FALENO GROUP

素人CLOVER PROFILE

', 'https://falenogroup.com/makers/', ['素人CLOVER']), ('
2006
組織拡大の為、ティーパワーズ株式会社を設立
', 'https://www.t-powers.co.jp/company/', ['T-POWERS']), ] for html, url, names in cases: with self.subTest(url=url): self.assertEqual(company_profiles.page_facts(html, url, names)['facts'], {}) def test_replay_reads_saved_pages_and_drops_cross_page_conflicts(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) root = Path(temporary.name) (root / 'a.html').write_text('
設立2002年5月
所在地東京都
', encoding='utf-8') (root / 'b.html').write_text('
設立
2001年
', encoding='utf-8') (root / 'c.html').write_text('
商号
株式会社ブランド
', encoding='utf-8') pages = [{'url': 'https://brand.example/', 'final_url': 'https://www.brand.example/company', 'http_status': 200, 'cache': str(root / 'a.html')}, {'url': 'https://brand.example/about', 'http_status': 404}, {'url': 'https://brand.example/x', 'final_url': 'https://brand.example/x', 'http_status': 200, 'cache': str(root / 'b.html')}, {'url': 'https://registry.example/brand', 'final_url': 'https://registry.example/brand', 'http_status': 200, 'cache': str(root / 'c.html')}, {'url': 'https://brand.example/y', 'final_url': 'https://brand.example/y', 'cache': str(root / 'c.html')}] result = company_profiles.replay(pages, ['Brand'], {'brand.example'}) self.assertEqual(list(result['facts']), ['location']) self.assertEqual(result['facts']['location']['source_url'], 'https://www.brand.example/company') def test_observed_fact_replaces_a_candidate_and_revert_lists_candidates(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) connection = sqlite3.connect(fresh_ledger(Path(temporary.name).resolve())) self.addCleanup(connection.close) held = {'company_profile': { 'group': {'value': 'Group', 'source_url': 'https://brand.example/', 'status': 'candidate', 'source': 'auto:company-profile', 'batch': 'old'}, 'location': {'value': '東京都', 'source_url': 'https://brand.example/', 'status': 'candidate', 'source': 'auto:company-profile', 'batch': 'old'}}} connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,metadata_json,created_at,updated_at) VALUES(1,'studio','Brand','brand',?,'2026-10-07','2026-10-07')", (json.dumps(held),)) self.assertEqual(company_profiles.planned_revert(connection, 'auto:company-profile', 'old'), [{'entity_id': 1, 'entity': 'Brand', 'fields': ['group', 'location']}]) fact = {'value': '大阪府', 'source_url': 'https://brand.example/company', 'evidence': '所在地:大阪府', 'status': 'observed'} self.assertEqual(company_profiles.fill(connection, 1, {'location': fact}, source='auto:company-profile', batch='new')['written'], ['location']) profile = json.loads(connection.execute('SELECT metadata_json FROM entity').fetchone()[0])['company_profile'] self.assertEqual(profile['location']['value'], '大阪府') self.assertEqual(list(company_profiles.public_profile({'company_profile': profile})), ['location']) def test_landing_writes_only_replayed_facts_and_ignores_listed_aliases_and_links(self): from scripts import harvest_company_profiles as harvest temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) root = Path(temporary.name).resolve() path = fresh_ledger(root) with sqlite3.connect(path) as connection: connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at) VALUES(1,'studio','Brand','brand','2026-10-07','2026-10-07')") connection.execute("INSERT INTO entity_link(entity_id,link_kind,label,url,hostname,created_at,updated_at) VALUES(1,'official','Brand','https://brand.example/','brand.example','2026-10-07','2026-10-07')") connection.close() (root / 'page.html').write_text('
設立2002年5月
', encoding='utf-8') listed = {'value': 'Brand Group', 'source_url': 'https://brand.example/', 'evidence': 'Brand Group', 'status': 'observed'} plan = {'entities': [{'entity_id': 1, 'name': 'Brand', 'status': '已核查', 'facts': {'founded': {**listed, 'value': '2002-05'}, 'group': listed}, 'pages': [{'url': 'https://brand.example/company', 'final_url': 'https://brand.example/company', 'http_status': 200, 'cache': str(root / 'page.html')}], 'aliases': [{'value': 'ブランド', 'source_url': 'https://brand.example/', 'status': 'observed'}], 'links': [{'entity_id': 1, 'link_kind': 'catalog', 'label': 'Brand', 'url': 'https://catalog.example/brand', 'status': 'observed'}]}]} (root / 'plan.json').write_text(json.dumps(plan, ensure_ascii=False), encoding='utf-8') args = SimpleNamespace(input=root / 'plan.json', db=path, apply=True, backup=root / 'backup.db', batch='auto:company-profile@test', output=root / 'receipt.json') self.assertEqual(harvest.land(args), 0) with closing(sqlite3.connect(path)) as connection: profile = json.loads(connection.execute('SELECT metadata_json FROM entity WHERE id=1').fetchone()[0])['company_profile'] self.assertEqual(list(profile), ['founded']) self.assertEqual(profile['founded']['evidence'], '設立:2002年5月') self.assertEqual(connection.execute('SELECT count(*) FROM entity_alias').fetchone()[0], 0) self.assertEqual(connection.execute('SELECT url FROM entity_link').fetchall(), [('https://brand.example/',)]) change = json.loads((root / 'receipt.json').read_text(encoding='utf-8'))['changes'][0] self.assertEqual(change['not_replayed'], ['group']) self.assertEqual(change['ignored'], {'aliases': 1, 'links': 1}) def test_repair_demotes_unreplayable_fields_and_drops_batch_aliases_and_links(self): from scripts import repair_company_profile_landing as repair temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) root = Path(temporary.name).resolve() path = fresh_ledger(root) batch = 'auto:company-profile@old' stamp = {'source': 'auto:company-profile', 'batch': batch, 'status': 'observed', 'source_url': 'https://brand.example/company'} profile = {'founded': {**stamp, 'value': '2002-05', 'evidence': '設立:2002年5月'}, 'group': {**stamp, 'value': 'Brand Group', 'evidence': 'Brand Group:Brand'}, 'location': {'source': 'auto:company-profile', 'batch': 'other', 'status': 'observed', 'value': '東京都', 'source_url': 'https://brand.example/company'}} with closing(sqlite3.connect(path)) as connection, connection: connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,metadata_json,created_at,updated_at) VALUES(1,'studio','Brand','brand',?,'2026-10-07','2026-10-07')", (json.dumps({'company_profile': profile}, ensure_ascii=False),)) connection.execute("INSERT INTO entity_link(entity_id,link_kind,label,url,hostname,created_at,updated_at) VALUES(1,'official','Brand','https://brand.example/','brand.example','2026-10-07','2026-10-07')") connection.execute("INSERT INTO entity_link(entity_id,link_kind,label,url,hostname,metadata_json,created_at,updated_at) VALUES(1,'catalog','Brand','https://catalog.example/brand','catalog.example',?,'2026-10-07','2026-10-07')", (json.dumps({'source': 'auto:company-profile', 'batch': batch}),)) connection.execute("INSERT INTO entity_alias(entity_id,alias,normalized_alias,source) VALUES(1,'ブランド','ブランド',?)", (batch,)) (root / 'page.html').write_text('
設立2002年5月
', encoding='utf-8') plan = {'entities': [{'entity_id': 1, 'pages': [{'url': 'https://brand.example/company', 'final_url': 'https://brand.example/company', 'http_status': 200, 'cache': str(root / 'page.html')}]}]} (root / 'plan.json').write_text(json.dumps(plan), encoding='utf-8') common = ['--db', str(path), '--batch', batch, '--plan', str(root / 'plan.json'), '--output', str(root / 'receipt.json')] self.assertEqual(repair.main(common), 0) with closing(sqlite3.connect(path)) as connection: self.assertEqual(connection.execute('SELECT count(*) FROM entity_alias').fetchone()[0], 1) self.assertEqual(repair.main([*common, '--apply', '--backup', str(root / 'backup.db')]), 0) with closing(sqlite3.connect(path)) as connection: held = json.loads(connection.execute('SELECT metadata_json FROM entity').fetchone()[0])['company_profile'] self.assertEqual({key: fact['status'] for key, fact in held.items()}, {'founded': 'observed', 'group': 'candidate', 'location': 'observed'}) self.assertEqual(connection.execute('SELECT count(*) FROM entity_alias').fetchone()[0], 0) self.assertEqual(connection.execute('SELECT url FROM entity_link').fetchall(), [('https://brand.example/',)]) self.assertEqual(json.loads((root / 'receipt.json').read_text(encoding='utf-8'))['summary'], {'keep': 1, 'evidence': 0, 'candidate': 1, 'aliases': 1, 'links': 1}) def test_resume_retries_rate_limited_entities(self): from scripts import harvest_company_profiles as harvest self.assertTrue(harvest.settled({'status': '已核查', 'pages': [{'http_status': 200}]})) self.assertTrue(harvest.settled({'status': '未取得', 'reason': '没有可访问的已确认官网', 'pages': []})) self.assertFalse(harvest.settled({'status': '已核查', 'pages': [{'http_status': 200}, {'http_status': 429}]})) self.assertFalse(harvest.settled({'status': '未取得', 'pages': [{'http_status': 403}]})) self.assertFalse(harvest.settled({'status': '未取得', 'pages': [{'reason': 'ConnectTimeout'}]})) def test_public_profile_requires_public_source_and_known_nonempty_values(self): facts = {key: {'value': value, 'source_url': url} for key, value, url in [('country','日本','https://brand.example/company'), ('operator','secret','https://localhost/admin'), ('founded','','https://brand.example/'), ('telephone','123','https://brand.example/')]} self.assertEqual(list(company_profiles.public_profile({'company_profile': facts})), ['country']) def test_company_profile_survives_catalog_ingest(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) root = Path(temporary.name).resolve() path = fresh_ledger(root) connection = sqlite3.connect(path) self.addCleanup(connection.close) connection.execute("INSERT INTO asset(id,location,path,name,medium) VALUES(1,'local','C:/test/a.mp4','a.mp4','video')") entity_id = upsert_asset_entity(connection, kind='studio', name='Brand', asset_id=1, role='studio', source='scan:filename') fact = {'value': '2002', 'source_url': 'https://brand.example/company'} company_profiles.fill(connection, entity_id, {'founded': fact}, source='auto:company-profile', batch='batch1') upsert_asset_entity(connection, kind='studio', name='Brand', asset_id=1, role='studio', source='scan:filename', metadata={'provider': 'fresh'}) metadata = json.loads(connection.execute('SELECT metadata_json FROM entity WHERE id=?', (entity_id,)).fetchone()[0]) self.assertEqual(metadata['provider'], 'fresh') self.assertEqual(metadata['company_profile']['founded']['value'], '2002') connection.commit() contract = web_contract.WebContract(path, avatar_root=root / 'avatars', logo_root=root / 'logos', cover_root=root / 'covers') response = web_contract.q_entity(contract, {'kind': 'studio', 'name': 'Brand'}) self.assertEqual(response['company_profile']['founded']['value'], '2002') if __name__ == '__main__': unittest.main()