# crawler-lib-php
AI Summary
Purpose:
- Durable note for the Packagist/Composer package crawler at
~/labrador/crawler/crawler-lib-php. - Records the 2026-06-08 missing-data investigation (root-cause analysis only; no crawler code changed yet) and the standalone missing-product finder tool built at
~/labrador/tool/composer_missing_products.
Key points:
- The crawler is Python (despite the
phpname) and collects PHP/Composer packages from Packagist.LANGUAGE='php',REPOSITORY='COMPOSER'. - Package intake:
packageListCrawlerdiffshttps://packagist.org/packages/list.jsonagainst the LIST table;updatePackageListCrawler/newPackageListCrawleradd from thereleases.rss/packages.rssfeeds. Per-package metadata comes fromhttps://packagist.org/packages/{product}.json. - Work-list table is
gatheringdb.TB_COMP_LIB_PHP_LIST(PROCESSED flag); product table isTB_COMP_LIB_PRODUCT(php/COMPOSER); version tableTB_COMP_LIB_VERSION_PHP; license recordTB_COMP_LIB_LICENSE_PHP. - 2026-06-08 finding (measured): Packagist lists 449,777 packages; DB has 509,737 php/COMPOSER products; intersection 436,674; 13,103 packages are on Packagist but missing from the product table; 73,063 are DB-only (Packagist-deleted/stale). Sampled missing packages return HTTP 200 on Packagist and are absent from DB → genuine omissions, not false positives.
- Cause breakdown of the 13,103: 13,058 (99.7%) have NO version rows either = never made it into the product table at all (work-list non-entry or whole-package crawl failure); only 45 (0.3%) have version rows but no product = product-creation-step omission (latest version was
dev-*or the last version errored). In a 40-package sample of the no-version group: ~57% latest-released in 2025+ (intake not keeping up), ~42% older (errored and never retried), 25%dev-only. - 2026-06-08 follow-up: crawler-side fixes WERE applied (retry/transient handling, list.json timeout, dev-only rescue, libraries.io removal, resync ORDER BY, malformed-authors hardening). Data row format is unchanged. See "Crawler Fixes Applied (2026-06-08)" below.
- zero-row 자가 백필 + changes 커서 하드닝 + ladder 30~39 확장 (2026-07-06,
d968c5c+f210373, master 푸시 완료):hakanispirli/parasut-laravel실측 누락(버전 게시 전 크롤 → PROCESSED=1 굳음 → 삭제→재등록 changes 이벤트 유실 → 영구 누락)을 계기로, ①zeroRowReconcile()— done 마킹(1/3/39)인데 VERSION 행 0개인 LIST 를 하루 1회processed=0재큐잉(안티조인, 500청크 UPDATE). 피해 2,419건(1=1,417/3=1,002)이 배포 첫 실행에서 자동 백필. ② changesCrawler enqueue 전부 실패 시 커서 미저장(이벤트 유실 차단),insertPackageList/setInsert*가 성공 행 수 반환. ③ zero-version ladder 를 golang 컨벤션(30..38=재시도 9회 + 39=포기, 1h/5h/10h/16h/24h/2d/5d/7d/14d ≈24일)으로 확장. 테스트 72건 green. See "Round 5" below. - 로그레벨 정책 정렬 (2026-06-23, master 푸시
0fdf683..883e37d): 누락 위험 아닌error→warning강등(ruby 기준). 21건 전부 강등, error 유지 0. ⚠️ 이 수정이feature/php-version-incremental위에 있어 feature째로 master 푸시 → 미머지였던b3f317fversion-incremental collection(incremental=신규버전만/resync=full)도 이때 master로 배포됨. 6파일(phpCrawler/cdbvdb/base/baseCralwer/component/licenseeUtil), 순수.error(→.warning(스왑(동작 불변), py_compile OK.transient/keep retryable·batch insert fail→processed=0 유지등 메시지가 이미 재시도를 명시한 곳들이 error로 찍히던 것을 교정. (1곳 주의:phpCrawler.py:97run() catch는 resync 모드에서 그 1000-batch만 스킵될 edge — 증분 모드는 processed=0 유지 재시도라 warning 처리.)app/check.py·tests 미수정. W26 worklog 참조.
Relevant when:
- Diagnosing missing Composer packages or versions.
- Working on
packageListCrawler/updatePackageListCrawler/dataCrawling/ LIST processing / Packagist metadata mapping. - Running or extending the
composer_missing_productsfinder.
Do not read full document unless:
- You need the exact omission root causes, the finder tool usage, or verification numbers.
Linked documents:
ai/workspace/repos.mdai/worklog/2026/2026-W24.mdai/repo-notes/crawler-lib-dotnet.md(sibling NuGet crawler; same code lineage and known gaps)- Tool:
~/labrador/tool/composer_missing_products/(finder)
Open Questions
- [RESOLVED 2026-06-08] transient failures no longer terminal —
TransientFetchErrorkeepsPROCESSED=0. See fix #2. - [RESOLVED 2026-06-08]
list.jsontimeout raised to 30s with retry. See fix #1. (Monitor whether 30s is enough for the multi-MB response in prod.) - [RESOLVED 2026-06-08] dev-only packages now collected via
all_devrescue. See fix #3. (Packages whose latest sorted version is dev but that ALSO have real versions still build product from the latest real version, since sort-gen orders dev to the front.) - Dependency table writes are still commented out (
798c626 "디펜던시 테이블 수집 정지"); re-enable, or is version-rowDEPENDENCIESJSON enough? (Same open decision as the dotnet crawler.) executeSqlstill swallows insert errors (logs, no re-raise) — NOT changed; see "Recommended but NOT applied". Should a failed version/product batch block the LIST row fromPROCESSED=1?- Tests run on host via stubs + the composer tool venv; no live Docker run or live re-crawl was executed this session. Run
python3 -m unittest discover -s testsin the Docker image before deploy.
Details
Repo Info
- Repo ID:
crawler-lib-php - Local path:
~/labrador/crawler/crawler-lib-php - Language: Python 3 crawler (Docker), Java
sort-genjar via JPype for version sort - Ecosystem: Packagist / Composer (
LANGUAGE='php',REPOSITORY='COMPOSER') - Branch:
master
Pipeline
run() → intake (list.json + 2 RSS feeds, incremental only) → paginate LIST → dataCrawling() per package → insert version / product / license.
- Incremental:
where='processed = 0', offset pinned at 0 each loop (theprocessed=0set shrinks as packages are marked done). - Resync:
where=None, offset climbs (LIMIT :start, 100), resumes fromCRAWLING_PACKAGE_NUMinTB_CRAWLER_STATUS.
Missing-Data Root Causes (2026-06-08 analysis — NOT yet fixed)
- Transient HTTP failure → permanent incremental exclusion (dominant).
getJsonRequests (src/ai/labradorlabs/sw/common.py) uses timeout=5, no retry, no try/except. In dataCrawling, a non-200 (404/429/5xx) → getPackageListVo(..., 5, 1) (PROCESSED=5); an exception or bad shape → status 3 or 10. All of these are non-zero, so the package is never re-selected by incremental WHERE processed = 0. Only a full resync recovers it. This matches the ~42% "older, still missing" sample slice.
list.jsonfull-sync likely times out.
packageListCrawler fetches the multi-MB list.json with the same timeout=5 → almost always times out → caught and logged, 0 new packages added that run. New packages then enter only via the small RSS feeds, so packages outside the feed window never enter the LIST. Matches the ~57% "recently released, missing" slice.
dev-*skip can drop whole packages / product rows.
In the version loop: if len(versions)>1 and version[:3]=='dev': continue. A package whose versions are all dev-* (and >1) has every version skipped → no version rows AND no product. A package whose latest sorted version (versions[-1]) is dev-* reaches continue before the if version == versions[-1] product block → version rows exist but no product (the 45-package group). Depends on the Java sort-gen ordering of dev branches.
- Per-version exception silently drops that version (per-version try/except) while the package can still look processed.
- Insert failures are swallowed.
cdbvdb.executeSqlcatches and logs but does not re-raise; version/product/list are separate inserts, and the LIST row (processed=1) is its own insert — so a failed version batch can be lost while the package is marked done.
- Resync offset pagination has no
ORDER BY(SELECT ... LIMIT :start, 100) → risk of skipped/duplicated rows across pages during a resync.
- Dependency table writes are intentionally disabled (commented out; commit
798c626).DEPENDENCIESJSON in the version row is still populated.
Crawler Fixes Applied (2026-06-08)
All changes preserve the version/product/license row format (columns + JSON encodings). They change fetch robustness, LIST PROCESSED semantics, version selection, and remove libraries.io. Verified: tests/test_missing_regressions.py 12/12 + existing tests/test_check.py 10/10; all changed modules byte-compile. Files: sw/common.py, crawler/phpCrawler.py, crawler/component.py, db/gatheringDBVo.py, util/licenseeUtil.py, input/crawlerInfo.py.
getJsonRequestsrobustness (sw/common.py) — wastimeout=5, no retry, no try/except. Now:timeout=30, retry ladder (backoff), 429Retry-After, retries on timeout/connection/5xx. ReturnsNoneonly on 404/410 (permanent); raises newTransientFetchErrorwhen transient failures persist. Fixes the multi-MBlist.jsonalways-timing-out and transient per-package failures.- Transient ≠ terminal (
crawler/phpCrawler.dataCrawling) — addedexcept TransientFetchErrorthat records the LIST row asPROCESSED=0(retryable) instead of3/5. This is the #1 missing cause: a one-off 429/timeout no longer parks a package out of the incrementalWHERE processed = 0set forever. - dev-only rescue (
phpCrawler.dataCrawling) —all_dev = all(v[:3]=='dev' for v in versions); the dev-skip (len>1 and v[:3]=='dev') is now applied only when NOT all-dev. All-devpackages (which the crawler previously dropped entirely → no version/product) are now collected. Mixed packages keep prior behavior. (Matches the backfill tool's--include-dev-only.) - malformed-authors / require hardening (
crawler/component.py) —getOrganizationnow skips non-dict author entries (e.g.authors: [[]]no longer raisesAttributeErrorand drops the version);getDependencyFirst/Secondguardisinstance(dict). This was ~10% of backfill fetch failures. Output format identical. - libraries.io removed (user-requested) — deleted
component.getLibrariesGitUrl,gatheringDBVo.getVersionLicenseWithLibrariesVo,licenseeUtil.getLibrariesRepoUrl(which contained a hardcoded libraries.io API key — now gone), and the two libraries.io blocks inphpCrawler.dataCrawling. License now comes from GitHub clone + packagist only. TheLIBRARIEScolumn inTB_COMP_LIB_LICENSE_PHPsimply stays unpopulated (schema/format unchanged). - resync pagination
ORDER BY(input/crawlerInfo.py) —LIMIT_SELECT_QUERYnow... ORDER BY PATH LIMIT :start, 100. Without a stable order, offset pagination during resync could skip/duplicate rows. PATH is the LIST PK so the order uses the index.
Round 2 (2026-06-08, user: "packageList 있으면 github 굳이 하지마", "비판적인 것 다 개선", "누락 0", data format MUST NOT change)
- License priority flipped to packagist-first + lazy GitHub (
phpCrawler.dataCrawling,gatheringDBVo.getVersionLicenseVo,component.getOriginalDataAndGitLicense) — was GitHub-first (clone+licensee always run). Now: if packagistversion.licensestandardizes (version_dict['STANDARD_LICENSE']not None) the package uses it and GitHub is skipped entirely (skip_github=True); the git clone is done lazily only on the first version that lacks a packagist license.getVersionLicenseVonow picks PACKAGE_REPOSITORY (packagist) first, GITHUB only as fallback. AI-license input is likewise packagist-first (clone license-file text only when packagist absent). Big perf win (no clone for the common case) and matches the user's request. Row/JSON format unchanged:version_license_dictkeys andORIGINAL_DATA={PR,GITHUB:{URL,TAG,LICENSEE_TEXT}}structure identical; only the STANDARD_LICENSE value source changes. - Insert atomicity (
cdbvdb.executeSql,baseCralwer.setInsertInto/IgnoreDB,phpCrawler.dataCrawling) —executeSqland the twosetInsert*now re-raise instead of swallowing.dataCrawlingwraps version/product/license inserts in try/except and inserts the LISTpackage_list(thePROCESSEDmarkers) only after they succeed; on failure it returns without marking, so the batch staysPROCESSED=0and is retried (idempotent upsert). Removes the silent "data lost but LIST=PROCESSED=1" omission. Trade-off: a systematic insert failure now wedges progress (loud) instead of silently dropping — correct for a no-omission goal; the row format is DB-compatible (the backfill tool upserts the same rows fine). getReleaseDatecorrected (component.py) —time[:time.find('+')-1](could drop the seconds digit) →time[:19]. Same stored format'YYYY-MM-DD HH:MM:SS', correct value, handles+00:00/Z/no-tz.- Scheduler every 3h (
app/main.py) — daily 00:00 →schedule.every(3).hours, so intake/retry backlog clears faster (with fix #1 this keeps the catalog current). insertErrorLogsignature hardened (baseCralwer.py) — now acceptstrx_infoand usesisinstance(list)(wastype[list], a bug). Prevents aTypeErrornow that the insertexceptpaths are live (fix #8). DB persistence toTB_CRAWLER_ERROR_LOGstill intentionally off (schema unconfirmed).
Round-2 verified: tests/test_missing_regressions.py 15/15 (now also: packagist-first license skips github, getOriginalDataAndGitLicense(skip_github) doesn't call licensee, getReleaseDate keeps full seconds) + test_check.py 10/10; full src byte-compiles; no dangling refs.
Round 3 (2026-06-08, intake cadence — user: "하루 1회는 안 됨, 배치 단위?")
Measured: releases.rss covers only ~8 minutes (41 items @ ~293 releases/hr ≈ 7,000/day); packages.rss ~9h. Daily/3h runs missed ~99% of new versions — a major omission source. One incremental run already drains the entire processed=0 set, so cadence + feed-window (not batch size) drive omission.
- changes.json cursor intake (
phpCrawler.changesCrawler,crawlerInfo.CHANGES_URL,common.getJsonAllowStatus) — replaced RSS feeds (updatePackageListCrawler/newPackageListCrawlerremoved) with Packagist's cursor-basedmetadata/changes.json?since=<cursor>: everyupdate/deletesince the cursor + a new cursor, no window loss regardless of run gaps. Cursor inTB_CRAWLER_STATUS(TYPEPhpCralwer_CHANGES).update→LISTprocessed=0,delete→processed=5,deprecated=1; name =action.package.rsplit('~',1)[0]. Bootstrap stores only the current cursor (history via list.json/resync).sincetoo old → 400 → cursor reset + log to resync. Transient → cursor not advanced (next run retries samesince, lossless).getJsonAllowStatusreads the 400 body that carries the cursor. - list.json reconcile gated ~daily (
packageListCrawler+LIST_SYNC_INTERVAL_SEC) — the 450k-name reconcile no longer runs every incremental; gated by aPhpCralwer_LISTSYNCepoch (default 24h). Stays the new-package safety net. - Scheduler every 5 min (
app/main.py) — daily/3h → 5 min; changes.json is cheap and list.json is gated, so no 450k re-download per run. - SELECT batch 100 → 300 (
crawlerInfo.LIMIT_SELECT_QUERY) — fewer round-trips (chunk size only; one run still drains allprocessed=0).
Round-3 verified: tests/test_missing_regressions.py 21/21 (adds changesCrawler bootstrap / update+delete dedup + ~-strip / since-too-old reset / transient-no-advance, and getJsonAllowStatus 200·400·transient) + test_check.py 10/10; full src byte-compiles. Tests stub versionSort (JVM//workspace jar) to import phpCrawler on host; changesCrawler doesn't use it.
Round 4 (2026-06-08, product-only omission)
- *product created from last processed version, not strictly
versions[-1](phpCrawler.dataCrawling, committedcbd6019)* —sort-gencan sort adev-*branch ABOVE real versions (e.g.superconductor/tools:20241105.* < dev-main). The oldif version == versions[-1]then landed on the dev version, which dev-skipcontinues → version rows existed but NO product (the "45 version-only" omission class). NowdataCrawlingtracks the last processed version and builds the product from it after the loop.getProductVounchanged → product row format/fields identical;LATEST_VERSION= highest real version. (Same bug + fix was found in the backfill tool while completing the run.)
Round 5 (2026-07-06, zero-row 자가 백필 — hakanispirli/parasut-laravel 실측)
사건: hakanispirli/parasut-laravel 이 LIST 에는 있으나(PROCESSED=1, 6/29 16:20 KST 크롤) TB_COMP_LIB_LICENSE_PHP/TB_COMP_LIB_VERSION_PHP 에 행 0개. Packagist 의 현재 패키지 생성시각은 같은 날 23:44 KST(14:44 UTC), 전 버전 published-time 이 23:46 동일 — 즉 아침에 등록된 첫 incarnation 을 버전 게시 전에 크롤해 0개 수집→성공(1) 마킹, 이후 삭제→재등록의 changes.json update/delete 이벤트가 유실돼 영구 누락. 이벤트 유실의 정확한 지점은 확정 불가(에러로그 DB 영구화 off) — 후보: enqueue insert 조용한 실패(커서는 전진), 다운타임+커서 리셋, changesCrawler 내 예외. 7/1 ladder(9d5cf76)는 이미 1로 굳은 행을 복구하지 못한다(커밋 메시지에도 "별도 백필" 필요로 명시돼 있었음).
측정(2026-07-06): done 마킹인데 VERSION 행 0개 = 2,419건 (PROCESSED=1: 1,417건, 최신이 hakanispirli / PROCESSED=3: 1,002건, 전부 2026-04 이전). 대조군 northlab/parasut-laravel(7/5 등록)은 정상 수집 → 파이프라인 자체는 정상.
zeroRowReconcile()(신규) — 하루 1회(PhpCralwer_ZEROROWepoch 게이트,
ZERO_ROW_SYNC_INTERVAL_SEC=86400): DEPRECATED=0 AND PROCESSED IN (1,3,39) 이고 VERSION 테이블에 행이 없는 LIST 를 안티조인(NOT EXISTS, IDX_PRODUCT_KEY 사용)으로 찾아 PROCESSED=0, LAST_UPDATED=NOW() 재큐잉. SELECT(무락) 후 PATH IN 500 청크 UPDATE 로 긴 갱신 락 회피. 같은 run() 의 페이지네이션이 곧바로 재수집(AI 라이선스 포함 풀 수집 — 버전-증분은 DB 에 행이 없으므로 전량 수집). 여전히 0개면 ladder 30..38→39, 404면 (5,1) → 발산 없음. 39 도 sweep 대상이라 ladder 포기 후 버전이 늦게 게시돼도 최대 하루 안에 복구. 실패 시 epoch 미갱신 → 다음 실행 재시도. → 과거 피해 2,419건이 배포 첫 실행에서 자동 백필(수동 백필 불필요). 첫 실행은 2,419×~1.3s ≈ 1시간 정도 추가 소요 예상.
- changes enqueue 전부 실패 시 커서 미전진 (
changesCrawler) — enqueue upsert 가
전부 실패(예: DB 순단)했는데 커서만 전진하면 그 윈도우의 이벤트가 조용히 유실된다 (이번 누락의 유력 경로). insertPackageList/setInsertIntoDB/setInsertIgnoreDB/ _insertChunked 가 적재 성공 행 수(int)를 반환하도록 변경, 0 이면 커서 미저장 → 다음 실행이 같은 since 로 재시도. 부분 실패는 행별 에러 로그 후 전진(불량 행 1건이 커서를 영구히 물지 않도록).
- zero-version ladder 30~39 확장 (
f210373) — golang 크롤러의
PACKAGE_STATUS_RETRY_1..9(30..38)/EXHAUSTED(39) 컨벤션과 정렬. 앞 5-rung 백오프(1h/5h/10h/16h/24h)는 유지(피드 앞섬은 대부분 수 시간 내 해소 + 운영 중 30..34 행 의미 보존), 뒤 4-rung 은 golang 장주기 정렬 35=2d/36=5d/37=7d/38=14d (누적 ≈24일). sweep 대상은 (1,3,LADDER_EXHAUSTED) 상수 참조. 마이그레이션: 운영 DB 에 35 이상 행 없음(31/32 각 1건) → 35 재정의(구 포기→rung 6) 무영향; 배포 전 old 코드로 35 도달한 행도 새 코드에선 재시도 계속(의도된 방향). 참고: maven(crawler-lib-java)은 90..97/98 카운터 방식이라 별개, npm(모노레포)은 30..33/34=gone — "30~39" 범위 컨벤션의 원본은 golang.
검증: tests/test_zero_row_reconcile.py 11건(sweep 게이트/대상 where/청크/epoch/실패, 커서 미전진) + test_ladder 갱신 포함 전체 suite 72건 green (composer tool venv python 으로 실행 — host 에 requests 없음). 안티조인 SQL 실 DB 검증(38s, hakanispirli 포함 확인). 커밋 d968c5c+f210373 — master 푸시 완료(5a6792d..f210373, 2026-07-06). 운영 배포 완료(2026-07-06 10:42 KST=01:42 UTC; 11:06 KST 컨테이너 소실 인시던트 후 11:13 재생성·런타임 검증 — W28 참조): 크롤러는 labCrawl1(data-crawl-01, 211.115.125.171) /product/crawler/crawler-lib/crawler-lib-php 에서 docker 컨테이너 crawler-lib-php 로 구동 (src bind-mount → 서버 git pull --ff-only + docker restart 로 반영, 이미지 재빌드 불필요. 앱 로그는 stdout 아닌 /data/logs/crawler-lib-php/phpcrawler.log). 재시작 첫 실행 실측: changes intake 3,520건 + [zero-row reconcile] 재큐잉 2,408건 → 백로그 드레인 시작.
Backfill outcome (2026-06-08)
The 13,103 missing products were backfilled into TB_COMP_LIB_PRODUCT/TB_COMP_LIB_VERSION_PHP via composer_missing_products/collect_composer_missing.py (LICENSE_AI filled through an SSH tunnel to the AI API). Final: 13,033 / 13,103 (99.5%) now have product rows; the remaining 70 are unbackfillable — 66 are registered on Packagist's list but have zero published versions, 4 are deleted (404). Two tool bugs were found and fixed during completion: (a) a package whose every version errored was wrongly marked done (conflated with dev-skip-empty) → permanent skip; (b) the same versions[-1]/dev product-omission as crawler fix #16.
Still NOT applied
- Dependency table writes remain commented out (
798c626) — version-rowDEPENDENCIESJSON still populated. Open decision (same as dotnet). TB_CRAWLER_ERROR_LOGpersistence still off (schema unconfirmed).releases.rss/packages.rssconfig keys remain incrawlerInfo(unused now) — harmless.
Performance (2026-06-23 분석 + 버전-증분 적용)
dataCrawling 은 패키지를 순차 처리하고, 패키지마다 Packagist fetch 1회 + 버전마다 작업.
측정값:
- Packagist
/packages/{id}.jsonfetch 1.0~1.8초/패키지(guzzle 1.4s, symfony/console 1.5s,
laravel 1.8s). 순차라 풀 리싱크면 514K×~1.3s ≈ ~185h 가 fetch 대기. getJsonRequests 는 timeout=30·retries=4·backoff → 429/timeout 최악 ~90s/패키지.
- AI License API(
211.115.125.171) warm ~16ms, 그러나 버전당 1회·캐시 없음. - 버전 분포(labradordb
TB_COMP_LIB_VERSION_PHP): 513,993 패키지 / 5,910,874 버전행 /
avg 11.5 / max 11,092 / ≥100버전 7,232개. → 무캐시 AI 호출 총 ~5.9M회.
시간 소모 순위: ① 순차 + Packagist fetch(패키지당 1~1.8s) ② 버전당 AI 호출 무캐시(swift 크롤러엔 _ai_cache 있으나 php엔 없음) ③ AI 호출마다 요청 body(≤5000자)+응답 INFO 로깅(5.9M줄급) ④ packagist 라이선스 없을 때 full git clone ⑤ 버전마다 SPDX(~1000행) Levenshtein 매칭.
후속 권장(미적용): 패키지 병렬화(최대 wall-clock 개선), AI 입력 캐싱(B, resync 에 유효), per-version 로깅 축소(C), 라이선스 매칭 캐싱(D).
버전-증분 수집 적용 (브랜치 feature/php-version-incremental, 커밋 b3f317f)
- 증분 실행이 패키지 재수집마다 전체 버전 재처리(버전당 AI/매칭/insert)하던 것을 **DB에 없는 신규
버전만 수집**하도록 변경. 데이터 포맷 불변.
run():start_set != 'resync'→dataCrawling(version_incremental=True), resync 는 전체.dataCrawling: 배치당 1회_getExistingVersions(IN 조회)로{path: set(versions)}→ 루프에서
기존 버전은 AI/clone/insert 생략. getVersionVo·sort_order·last_version_dict 는 유지해 product 는 최신 버전 기준으로 항상 정확(최신이 기존 버전이어도). batch summary 에 skip 카운트.
- 효과: 기존 N버전+신규1 → N작업이 1작업. 트레이드오프: 기존 버전 행은 증분에서 갱신 안 됨
(정정/리프레시는 resync 전체 재수집 — SPM 증분과 동일 철학).
- 검증:
tests/test_version_incremental.py6건 green, 기존test_missing_regressions21건 green,
byte-compile OK. 미푸시/미머지.
Missing-Product Finder Tool
~/labrador/tool/composer_missing_products/ (sibling of nuget_missing_products / maven_missing_products).
extract_composer_missing_products.py— Packagist is one-shot:list.json(1 request, ~450k names) vsSELECT LOWER(product_key) FROM TB_COMP_LIB_PRODUCT WHERE language='php' AND repository='COMPOSER'(1 query) → in-memory set diff. No catalog page walk needed (unlike NuGet). ~48s.
- Out: composer_missing_products.txt (Packagist-only = crawler omissions), composer_db_only.txt (DB-only = deleted/stale candidates).
analyze_missing.py— classifies the missing list: batches the missing keys againstTB_COMP_LIB_VERSION_PHPto split "version rows exist / product missing" vs "nothing at all", then samples the no-version group against Packagist for release recency +dev-only.collect_composer_missing.py— backfill collector. Packagist gives all version detail in ONE request (packages/{id}.json), so there is no NuGet-style phase2 leaf fetch — single phase: per package → product(latest) + all version rows.--sink file|db|both,--no-ai, resumable viaout_done.txt(only written AFTER DB commit), 404→composer_gone_404.txt, errors→composer_errors.txt.- The collector is format/behavior-identical to the crawler (per user "포맷팅 전혀 바꾸지말고"): same
sort-gen-0.5.0-lib.jarvia jpype for version sort (envPHP_SORTGEN_JAR), same dev-skip rule (len(versions)>1 and version[:3]=='dev'), samegetVersionVo/getProductVorow shape and license pipeline (TB_LICENSE_V2match + same AI API). Verified against an existing DB row (monolog:STANDARD_LICENSE [{"name":"MIT License",...}],LICENSE_IDS [247],LATEST_VERSION 3.10.0). Packages whose versions are alldev-*produce 0 rows exactly like the crawler and are logged tocomposer_devskip_empty.txt. - Unavoidable (same result): RELEASE_DATE uses robust ISO parse (the crawler's
time[:find('+')-1]slice can truncate seconds; robust parse matches stored values); license input is packagistversion.licenseonly (crawler's packagist fallback path — no git-clone text extraction / libraries.io). - DB config reuses
~/labrador/tool/data_check_script/.env(this connection cannot seegatheringdb, only the product/version DB — so the finder compares at the product grain, like the NuGet tool).LICENSE_AIonly populates when the internal AI API (211.115.125.171) is reachable. - A notable share of the 13,103 are all-
dev-version packages the crawler deliberately drops via the dev-skip rule (e.g.04l3x4ndr3/sdk-omie= onlydev-main/dev-development) — these surface incomposer_devskip_empty.txt. Whether to backfill them (i.e. relax dev-skip) is a product decision, not a crawler bug.