v0.17.0 — PWA layer, X draft @ restoration, planning docs
Adds installable PWA to projecthermes.tech: web manifest, 10-file icon set
generated from master artwork (wordmark and textless variants), iOS meta tags,
and manifest/favicon routes in app.py. No service worker — offline support is
deferred to v1.5. Restores @{handle} prefix on all 12 X outreach
drafts for conventional reply format. Adds tests/test_reply_drafts_integrity.py
start-with-@ assertion. Adds internal planning documents:
docs/strategy.md and docs/roadmap.md. No changes
to audit rules, evidence scoring, or methodology version semantics.
- PWA manifest. Served at
/manifest.jsonwithstart_url=/intake?src=pwa,display=standalone, three icon entries (192 any, 512 any, 512 maskable). - Icon generation. Two master variants derived from
assets/icon-master.jpg(784×741) via Pillow: square (wordmark, padded to 784×784) and textless (cropped above wordmark, re-centred). Size matrix: 120, 152, 167, 180 apple-touch; 192 and 512 Android/Chrome; 32 and 16 favicon PNG; multi-size ICO (16/32/48); 512 maskable with 80% safe-zone padding. - Meta tags. Manifest link, four apple-touch-icon sizes, two PNG
favicon sizes, shortcut icon,
apple-mobile-web-app-capable,apple-mobile-web-app-status-bar-style,apple-mobile-web-app-title, andmobile-web-app-capableadded tolanding.htmlandintake.html. - Flask routes.
/manifest.jsonand/favicon.icoadded to app.py;send_from_directoryadded to Flask import. - X draft @ fix. All 12 outreach drafts now start with
@{handle}for conventional reply format. Integrity test updated with starts-with-@ assertion (10 tests total). test_x_bot.py assertions restored to expect@alpha/@witnessX. - Planning docs.
docs/strategy.md(two-property model, flywheel, identity decisions) anddocs/roadmap.md(sprint status, parked items, locked and pending decisions) created. Internal use only; not served by the application.
v0.16.0 — X discovery and drafting bot
Adds hermes_x_bot.py, a standalone background agent that discovers
UAP-related posts on X, filters candidates, scores them, and enqueues drafts for
operator review. Introduces a 30-minute systemd timer (installed, not started).
No changes to audit rules, evidence scoring, or methodology version semantics.
Reply drafts rewritten in Hermes voice and redirected to /intake. Integrity test added at tests/test_reply_drafts_integrity.py to prevent regression.
- New module:
hermes_x_bot.py. Entry pointrun_cycle(dry_run, enqueue_test)executesoutreach_pass()andblog_pass()per cycle. Per-cycle result logged to/opt/hermes/data/x_activity/bot_YYYY-MM-DD.jsonl. CLI:python3 -m hermes_x_bot [--dry-run] [--enqueue-test]. - Outreach pass. Builds an OR-combined tweepy v2
search_recent_tweetsquery fromx_triggers.json(≤480 chars, highest-strength triggers prioritised). Five-stage filter pipeline: US/CA geo (3-signal cascade: place-tag > profile-location regex > English language fallback), video ≤5 min, exclusion-phrase list, account age ≥30 days, likes <10 000. Scoring: trigger strength + recency decay (168 h window) + low-follower bonus + video-duration bonus. Register routed from triggerregister_hint; draft variant selected viasha256(handle|date|register)[:8] % 3(deterministic, handle-stable). Callshermes_x_queue.enqueue(lane='outreach', ...). - Blog pass. Scrapes
ufoindex.tech/blogvia regex (<time datetime>+<h2 class="blog-card-title">). First-run backfill: marks oldestmax(0, total − 6)posts as done so the operator isn't buried on launch. Enqueues at most 1 post per cycle, oldest-first, with a 2-day spacing guard. State persisted at/opt/hermes/data/x_blog_state.json. Callshermes_x_queue.enqueue(lane='blog', ...). - Config files in
/opt/hermes/config/:x_triggers.json— 15 trigger phrases (strength 1–3, register_hint)x_exclusions.json— 20 exclusion patterns (fiction, spam, commercial)x_reply_drafts.json— 12 drafts: 3 per register × 4 registersx_blog_template.txt—{title} — new on UFO Index ({n_of_m}) {url}
- Systemd oneshot service
hermes-x-bot.service(same hardening ashermes.service: NoNewPrivileges, PrivateTmp, ProtectSystem=strict, etc.) and timerhermes-x-bot.timer(OnBootSec=5min,OnUnitActiveSec=30min,Persistent=true). Installed under/etc/systemd/system/; not started — enable when credentials are ready. - Path constants all env-overridable for test isolation:
HERMES_X_BOT_LOG_DIR,HERMES_X_BLOG_STATE_PATH,HERMES_X_CONFIG_DIR,HERMES_BLOG_URL. - Unit tests: 33 tests in
tests/test_x_bot.pycovering geo filter cascade (8 cases), video filter (5), exclusion (3), register routing ×4, draft determinism (4), blog oldest-first (2), blog 2-day spacing (2), first-run backfill math (3), blog tweet formatting (1), dedup/duplicate handling (1). Run:python3 -m unittest tests.test_x_botfrom/opt/hermes/. - README updated with bot entry point, config file table, timer enable/status commands.
v0.15.0 — Operator review queue (X)
Adds the operator-gated X (Twitter) post review queue and associated infrastructure. No changes to audit rules, evidence scoring, or methodology version semantics.
- New module:
hermes_x_queue.py. Exposesregister_x_queue(app),enqueue(...), andpost_to_x(...). Routes are only registered whenHERMES_OPERATOR_TOKENis present in/opt/hermes/.env; Hermes boots normally without it. - SQLite table
x_queuein/opt/hermes/hermes.db(created on first boot). Columns: id, lane, created_at, status, source_url, target_handle, register, draft_text, rationale, decided_at, decided_by, posted_tweet_id, failure_reason. Unique index on (lane, source_url) prevents double-queuing the same target. - Two posting lanes:
- Outreach — reply to a specific X post; carries target_handle and
one of four reply registers (curious, grounded, warm, technical).
Hard cap:
OUTREACH_DAILY_CAP = 9per UTC calendar day. - Blog — announce a blog post; no target handle.
Hard cap:
BLOG_WEEKLY_CAP = 3per ISO week (Mon–Sun UTC).
- Outreach — reply to a specific X post; carries target_handle and
one of four reply registers (curious, grounded, warm, technical).
Hard cap:
- Operator gate. All
/operator/x-queue/*routes requireX-Operator-Tokenheader or?t=query param matchingHERMES_OPERATOR_TOKEN(checked withhmac.compare_digest). Failures return 404 — the surface is not advertised. - Posting wrapper.
post_to_x(draft_text, lane, in_reply_to=None)uses tweepy v4create_tweet. Acquiresx_post.lock(file lock viafcntl.flock) before every post to serialize both lanes. Cap is re-checked inside the lock, before the API call. Missing credentials return(False, 'x_credentials_missing')without crashing. - Activity log.
/opt/hermes/data/x_activity/YYYY-MM-DD.jsonl— one JSONL line per operator decision and per post attempt. Fields: ts, queue_id, lane, action, target_handle, source_url, tweet_id, error, operator_fp. - Blocklist.
/opt/hermes/data/x_blocklist.txt— one lowercase handle per line. Populated via the Block-handle button; checked on everyenqueue()call. Deduped on write. - Email notification. On every
enqueue()insert, a notification is sent toRESEND_FROM_EMAILvia the existing Resend helper, throttled to at most one email per 30 minutes. State persisted in/opt/hermes/data/x_notify_state.json. - Insertion API.
enqueue(lane, source_url, target_handle, register, draft_text, rationale)validates all fields (lane, register allowlists, 280-char limit, https source URL, blocklist, uniqueness) before inserting. Designed to be called byhermes_x_bot.py(next session); treats every call as untrusted. - New dependency:
tweepy==4.16.0added torequirements.txt. - Unit tests: 17 tests in
tests/test_x_queue.pycovering insert validation, blocklist, duplicate guard, operator gate (correct/wrong/missing token), outreach daily cap, blog weekly cap, skip/block routes, and stats endpoint. Run:python3 -m unittest tests.test_x_queuefrom/opt/hermes/. - README added at
/opt/hermes/app/README.mddocumenting all operator surfaces including env vars required. - Out of scope (next session):
hermes_x_bot.py— the agent that discovers candidates and callsenqueue(). - METHODOLOGY_VERSION bumped to 0.15.0. No audit rules changed; existing case audit hashes recompute on next view with the new version stamp.
v0.14.0 — Hermes Evidence Score
Every audit record now produces a single float in [−1.0, +1.0] called the
Hermes Evidence Score. Negative means conventional explanations remain
plausible; positive means they are collectively weakened; zero is inconclusive. The score
is a pure, deterministic function of the audit record — no network requests, no rule
re-execution. It is version-stamped so it can be reproduced by any third party given the
same audit record and SCORE_VERSION.
- New module:
hermes_score.py. ExportsSCORE_VERSION = "0.14.0",SCORE_FLOOR = -1.0,SCORE_CEILING = +1.0,BUCKET_CAP = 0.40,RuleContributiondataclass,HermesEvidenceScoredataclass,compute(audit_record) -> HermesEvidenceScore(pure function), andscore_qualifier(score) -> strhelper. - Four weight policies (A–D):
- Policy A — Informational: Any rule with
confidence_effect = 0.0contributes zero and is marked "informational" in the ledger. All AI-signal rules (bucketai_signals) are informational regardless of verdict. - Policy B — Verdict gating: Rules returning
no_dataorindeterminateare suppressed to zero. Data gaps do not shift the score. - Policy C — Per-bucket cap (±0.40): If rules in a single bucket sum beyond ±0.40, all contributors are scaled proportionally so the bucket total lands exactly at the cap. Excess amounts are reported in the "excluded" ledger.
- Policy D — Final clamp: The post-cap sum is clamped to [−1.0, +1.0].
- Policy A — Informational: Any rule with
- Qualifier bands. Score < −0.15: Leans-Mundane. −0.15 to +0.15: Inconclusive. Score > +0.15: Leans-Unexplained. These bands are informational labels, not verdicts.
- Weight ledger. A
# HERMES EVIDENCE SCORE — WEIGHT LEDGERcomment block was prepended tohermes_audit.pycataloguing everyconfidence_effectvalue with a one-line justification. Four values are flagged REVIEW (SAT-LOS-01 sign convention, CEL-MOON-01 internal inconsistency, AC-DENSITY-01 live-snapshot timing, CEL-PLANET-01 asymmetry). No weights were changed; REVIEW items are policy decisions for v0.15.0. - Case detail page updated. The Evidence Score panel (score, qualifier badge, narrative, collapsible contribution ledger, collapsible excluded table) appears above the audit-trail rules table. All nine rule buckets now render (the previous template only showed six; corroboration, provenance, fingerprint, exclusion, and AI-signals were missing).
- API updated.
GET /api/audit/<case_id>now returns anevidence_scorekey alongsideaudit. - New docs page:
/docs/score. Documents what the score is and is not, the four policies, qualifier bands, three worked examples with full contribution ledgers, and known review items. - Unit tests. 19 tests in
tests/test_hermes_score.py(stdlib unittest). Run:python3 -m unittest tests.test_hermes_scorefrom/opt/hermes/. - Backfill tool.
tools/backfill_scores.pywalks all cases, computes the score, and prints a per-case summary line. The score is not stored in the case JSON (it is always recomputed on read).--summaryflag prints only aggregate stats. - No new dependencies.
hermes_score.pyuses only stdlib (dataclasses,collections). - No AI signals aggregated into score. All five AI-signal rules carry weight 0.0 and are informational only. This is a deliberate policy choice pending a more reliable detector ecosystem.
- METHODOLOGY_VERSION bumped to 0.14.0. Existing audit hashes recompute on next view; underlying rule verdicts unchanged.
Posture: the score describes the strength of evidence relative to conventional explanations. It does not identify, classify, or characterise the underlying phenomenon.
v0.13.0 — AI-provenance signal panel
Every media upload now runs a panel of five independent provenance signals designed to surface evidence relevant to AI-generation detection. No signal claims a verdict. Aggregating them into a single AI-or-not score would be dishonest — there is no reliable, generalizable AI-detection algorithm in 2026, and every signal in this panel has a known failure mode. The platform documents what it can measure; the analyst decides what it means.
- New module:
hermes_ai_signals.py. Orchestrator. Public API:run_panel(case, camera_record=None, file_path=None) -> dict. Calls all five signal modules, assembles theai_signalsdict, returns it. Idempotent within a session. Stored atcase.provenance.ai_signals.PANEL_VERSION = "1.0.0". - Five signal modules:
hermes_codec_forensics.py—analyze(provenance_dict) -> dict. Pure function over the existing v0.10.0 provenance record. No new file reads. Four sub-signals, each{value, reason, analyst_note}:software_tag_suspicious(EXIF Software field matchesKNOWN_AI_SOFTWARE_MARKERSconstant list — Sora, Runway, Pika, Stable Diffusion, Midjourney, and ~20 others);bitrate_suspicious(exact multiple of 1000 kbps ≥ 1000 kbps — tighter than the existing round-bitrate check, targeting CBR synthetic pipelines);container_codec_mismatch(container/codec pairs not produced by real cameras);gop_anomaly(intra-only, atypically tight, or closed-GOP synthetic pattern). Each sub-signal carries its own failure-mode disclosure.hermes_shadow_check.py—check(case) -> dict. Computes expected sun azimuth and elevation at the witness location and sighting time using Skyfield + DE421. Location precedence: EXIF GPS → witness geometry → camera calibration → witness-reported area. Timestamp precedence: provenance capture_timestamp → witness date/time/tz → submitted. Returns{ran, ran_reason, expected_sun_azimuth_deg, expected_sun_elevation_deg, sun_above_horizon, daytime, analyst_note}. v1 scope discipline: does not attempt automatic shadow detection from imagery — vision-model shadow extraction is unreliable enough to be dishonest at this scale. The analyst compares the computed sun position against shadows visible in the frame manually. Automatic shadow-direction extraction is deferred to a future version.hermes_prnu_check.py—extract_prnu_fingerprint(file_path) -> dictandcheck(case, camera_record, file_path=None) -> dict. PRNU (Photo Response Non-Uniformity): each image sensor has a stable, device-specific gain pattern measurable from any image it produces. Method: Gaussian residual of the green channel, resized to 256×256, normalized zero-mean unit-variance. Similarity metric: normalized cross-correlation.PRNU_MATCH_CORRELATION_THRESHOLD = 0.05(named constant; real same-sensor matches typically 0.05–0.20; different-sensor or AI-generated content near zero; conservative threshold documented in module). Only runs when the case comes from a registered camera with a reference-media PRNU fingerprint on file. Reference: Lukas, Fridrich, Goljan (2006).hermes_ai_detectors.py—run_all(file_path=None) -> dict. Stub. Returns{detectors_run: 0, detectors_available: [], note: "..."}. Integration deferred: the 2026 open-source AI-detector ecosystem is unreliable for out-of-distribution generators. When detectors are integrated in a future session, the field shape is unchanged — only the values change.
- PRNU fingerprint storage on cameras. When reference media is uploaded via
POST /api/cameras/<cam_id>/reference-media,extract_prnu_fingerprint()runs and the result is stored atcameras[].prnu_fingerprintas{prnu_b64, shape, dtype, method, sha256_prnu, extracted_at, source_sha256}.sha256_prnuis the sha256 of the raw float32 bytes — used in the audit hash to bind the fingerprint blob without embedding it in the case record. Subsequent witness uploads from that camera runprnu_check.check()against the stored reference. - Camera reference footage scrutinized equally.
run_panel()is called on camera reference media uploads as well as witness uploads. The panel result is stored atcameras[].registration_provenance.ai_signals. On reference media, PRNU is extracted and stored (not checked against itself). - New field
case.provenance.ai_signals. Nested inside the existing provenance record. Populated on first media upload, immediately after the v0.12.0 exclusion engine call. Null for cases with no uploaded media. - Score weight 0 on all AI-signal rules. None of the five AI-* rules apply an automatic penalty or bonus to the confidence score. They surface evidence; the analyst verdicts. This is a deliberate methodological choice: AI-generation signals are not yet reliable enough to be load-bearing in automated scoring. That may change in future versions as the detector ecosystem matures.
- New rules AI-01 through AI-05 in a new "AI-provenance signals" bucket in the Rule Catalog.
- AI-01 "AI-provenance signal panel ran." Informational. Records whether panel ran and which sub-signals returned data.
- AI-02 "C2PA result." Mirrors the C2PA Content Credentials result. Verdict:
passedif valid manifest,flaggedif present-but-invalid,no_dataif absent. "Absence is not evidence of AI generation — most consumer devices do not yet sign with C2PA." - AI-03 "Codec forensics findings." Triggers
flaggedwhen any of the four codec sub-signals is true. Enumerates which sub-signals fired with their analyst notes.passedwhen all four return false. - AI-04 "Sun-shadow geometry available." Verdict:
passedwhen sun position computed,no_dataotherwise. "Hermes computes where the sun was; the analyst confirms whether shadows in the frame agree. Automatic shadow detection is deferred to a future version." - AI-05 "PRNU sensor-noise match." Verdict:
passedwhen correlation ≥ threshold (sensor noise consistent with registered camera),flaggedwhen correlation < threshold (mismatch — possible different camera or generated content),no_datawhen check could not run (no calibrated reference available).
- Audit hash.
case.provenance.ai_signals(minusran_at) is included in the hash payload. PRNU correlation value andprnu_fingerprint_sha256are pinned. The PRNU blob itself lives on the camera record; its sha256 travelling with the prnu_check result means tampering with the stored fingerprint breaks the case hash. - No new heavy dependencies. PRNU uses numpy and Pillow (already present). Shadow check uses skyfield (installed in v0.12.0). Codec forensics is pure Python. Third-party detector integration is a stub.
- Backfill not run in this session. Pre-v0.13.0 cases retain
provenance.ai_signals: null. Seetools/backfill_ai_signals.py— exists, has docstring, exits cleanly with--dry-run. Operator-triggered when ready. - METHODOLOGY_VERSION bumped to 0.13.0. Existing case audit hashes recompute on next view; underlying rule verdicts unchanged except AI-01 through AI-05 (all new, all score weight 0) appearing.
Posture: footage is a sensor. The panel documents what the sensor can measure about its own provenance. Each signal stands alone. None claims a verdict. The analyst decides.
v0.12.0 — Layer 4: known-object exclusion engine
For every case with sufficient grounding (a usable timestamp AND a usable location), Hermes now queries four independent catalogs of known sky objects and checks whether any of them match the sighting envelope. Every match is surfaced as a candidate explanation. The absence of any match is surfaced as a documented clean result. The engine never sets a verdict — it shows the analyst the evidence and the math.
- New module:
hermes_exclusion.py. Orchestrator. Public API:check_preconditions(case) -> dict(fast, no network),run_exclusion(case) -> dict(runs all four catalogs). Catalogs run in parallel viaThreadPoolExecutor(max_workers=4)with a 60-second total engine timeout. Individual catalog hangs are caught per-future; the engine always returns whatever it has. - Four catalog sub-modules:
hermes_catalog_adsb.py— OpenSky Networkstates/alllive aircraft positions. Anonymous access is live-only (no historical time parameter without credentials). Cache: 5-min bucketed snapshot keyed by bbox + timestamp, 7-day TTL. Each aircraft: haversine distance, azimuth, elevation angle to observer. Timing note documents the gap between sighting time and query time.hermes_catalog_satellites.py— Celestrak TLE groups (starlink, stations, visual) + Skyfield propagation. TLE cache: 24-hour TTL per group at/opt/hermes/data/cache/tle/. Celestrak HTTP 403 "not updated" response is treated as a cache-hit signal (data hasn't changed since last download — use cached TLE). SkyfieldEarthSatellite.at(t).altaz()per satellite; above-horizon + in-FOV cone tests applied. Starlink satellites are annotated as common visual false-positives.hermes_catalog_astronomical.py— Skyfield + DE421 ephemeris (no network, no cache — purely computational). Bodies: Sun, Moon, Venus, Mercury, Mars, Jupiter, Saturn, Uranus, Neptune. Venus is the single most misidentified UAP in history — it is always computed and always annotated in the result, even when below the horizon. Sun altitude drives twilight category and lens-flare window flag (5°–30° solar altitude).hermes_catalog_weather.py— NOAA Aviation Weather METAR bbox query. Cache: 24-hour TTL per bbox+hour. Surfaces atmospheric signals that could explain a visual anomaly: reduced visibility (<3 SM), low ceiling (<1500 ft), fog, mist, haze, smoke, volcanic ash, thunderstorm, multiple cloud layers, sun-glare window. Weather does not "match" sightings the way other catalogs do — it provides context.
- Grounding precedence. Location (best to worst): EXIF GPS from provenance → witness_geometry standpoint Point → calibrated camera location (if case.camera_id set) → coarse witness-reported area (case.location.lat/lon). Timestamp (best to worst): provenance.capture_timestamp (EXIF DateTimeOriginal) → witness-reported date+time+timezone → case.submitted. If either is absent the engine does not run. That is not a failure — the case gets a clear "did not run: reason" status with no penalty.
- Sighting envelope. Spatial: FOV cone centered on observer. If witness_geometry includes an FOV polygon with a half-angle property, it is used; otherwise
DEFAULT_FOV_HALF_ANGLE_DEG = 90(full visible hemisphere). Temporal:EXCLUSION_TIME_WINDOW_SECONDS = 300(±5 minutes around sighting timestamp). Both constants are module-level, tunable in one place. - Within-FOV test. Great-circle angular separation:
cos(d) = sin(el₁)·sin(el₂) + cos(el₁)·cos(el₂)·cos(az₁−az₂). When no reported bearing is available, fov_half_angle_deg = 90 passes all above-horizon objects. - New field
case.exclusion_results. Populated on first media upload (same timing as fingerprint). Structure: engine_version, ran_at, preconditions (has_timestamp, timestamp_source, has_location, location_source, ran), envelope (center, fov_half_angle_deg, time_window_seconds, timestamp_utc, reported_az/el), catalogs (one dict per catalog with queried, match_count, matches, error, cached), summary (total_matches, best_explanation, human_readable). Null for cases with no uploaded media. - User-Agent attribution. All HTTP requests to external APIs include
User-Agent: ProjectHermes/0.12 (https://projecthermes.tech). Honest attribution to data providers. - New rules EXC-01, EXC-02, EXC-03 in a new "exclusion" bucket in the Rule Catalog. All have score weight 0 in v1.
- EXC-01 "Exclusion engine ran." Informational. Records whether engine ran, preconditions, catalog query summary.
- EXC-02 "Known-object candidate match." Triggers when
total_matches ≥ 1. Verdict:flagged. "Surfaces candidate matches from external catalogs. Does not exclude — analyst review required to confirm or reject any specific candidate. The presence of a match does not invalidate a sighting; the absence does not validate it." - EXC-03 "Clean of known objects." Triggers when all four catalogs queried successfully AND total_matches = 0. Verdict:
passed. "All four exclusion catalogs were queried and found no candidate matches within the sighting envelope. This does not prove anomaly; it documents an honest absence of mundane explanation."
- Exclusion results pinned in audit hash.
case.exclusion_results(minusran_atand allcachedfields) is included in the hash payload. Re-running the engine on the same case at a later date may produce different results if the underlying catalogs have updated — that is the audit hash correctly reflecting that the exclusion analysis has changed, not a bug. - New dependency:
skyfield 1.54+jplephem 2.24. DE421 ephemeris (17 MB BSP file) pre-downloaded to/opt/hermes/data/cache/de421.bsp. - Backfill not run in this session. Pre-v0.12.0 cases retain
exclusion_results: null. Seetools/backfill_exclusion.py— exists, has a docstring, exits cleanly with--dry-run. Operator-triggered when ready. Note: backfill makes live network requests; use--delay Nto rate-limit. - Exclusion engine runs synchronously inline with media upload for v1. Asynchronous re-runs and bulk re-evaluation are future work. The 60-second engine timeout is within the gunicorn 120-second worker timeout.
- METHODOLOGY_VERSION bumped to 0.12.0. Existing case audit hashes recompute on next view; underlying rule verdicts unchanged except EXC-01, EXC-02, EXC-03 (all new, all score weight 0) appearing.
Posture: footage is a sensor. A sensor is only as good as the data it is grounded upon. This session grounds every sighting against the catalog of known objects in the sky at that time, from that location. Excluded sightings are not failures — they are the platform working correctly.
v0.11.0 — Reused-footage detection pipeline
Every media upload now produces a perceptual fingerprint at upload time, stored on the case under case.provenance.fingerprint, and immediately checked against the existing corpus for near-matches. Near-matches are surfaced for analyst review — never auto-rejected. The goal is not to verdict. The goal is to make reuse visible.
- New module:
hermes_fingerprint.py. Public API:extract_fingerprint(path) -> dict,search_collisions(fingerprint_dict, ...) -> list,rebuild_index() -> int. No network calls; no side effects in extraction. New dependency:imagehash 4.3.2(pHash + dHash) +PyWavelets(imagehash transitive dep). Frame extraction usesffmpeg -vf fps=1into a temp directory (already present). - New field
case.provenance.fingerprint. Dict nested inside the existing provenance record. Fields:phash_frames(list of 64-bit pHash hex strings, 1fps, ≤600 frames; single-element list for images),phash_video(aggregate video hash via bit-majority voting over frame hashes; null for images),phash_dhash(dHash variant, same shape as phash_frames),frame_count,fingerprint_version("1.0.0"),fingerprint_error(null on success),match_candidates(search results, excluded from audit hash). - phash_video algorithm: bit-majority voting. For each of the 64 bit positions, the aggregate bit is 1 iff more than half the frames have that bit set. Deterministic, order-independent, robust to outlier frames. Documented in a code comment in
hermes_fingerprint._aggregate_phash.FINGERPRINT_VERSION = "1.0.0"must be bumped if the algorithm changes; old fingerprints remain searchable but are flagged as algorithm-mismatched in the audit breakdown. - Fingerprint index at
/opt/hermes/data/fingerprint_index.json. Rebuilt-able from case records viarebuild_index(); never the source of truth, only the lookup accelerator. Each entry carries: case_id (or camera_id), kind ("case" or "camera_reference"), phash_video (primary lookup hash), phash_frames (needed for partial-match search), capture_timestamp, upload_timestamp, fingerprint_version, witness_id. Updated incrementally on each upload via_add_index_entry. - Collision search thresholds.
HAMMING_THRESHOLD_NEAR_MATCH = 8bits out of 64 triggers a full match.PARTIAL_REUSE_FRAME_THRESHOLD = 0.30: if 30%+ of incoming frames near-match any frame in an existing case, avideo_partialcandidate is raised (catches stitched clips). Both are named constants, tunable without touching the algorithm. - Same-witness same-event carve-out. Matches where the indexed entry shares the same
witness_id(session user_id) AND timestamps (capture_timestamp, or upload_timestamp if capture is null) are within ±2 hours are excluded from results. This covers one witness uploading two clips of the same event — that is legitimate corroboration. Different witnesses with matching footage within 2h still surfaces (independent witnesses with identical footage is analytically significant). The same witness reusing footage for a new claimed event more than 2h away also surfaces. The carve-out is narrow and intentional; documented in a code comment. - Camera reference media fingerprinted. The
POST /api/cameras/<cam_id>/reference-mediaendpoint now extracts and stores a fingerprint atcameras[].registration_provenance.fingerprintand runs collision search against the existing corpus. - Fingerprint pinned in audit hash.
build_audit_trail()now includesprovenance_fingerprint(the fingerprint dict, excludingmatch_candidates) in the hash payload.match_candidatesis excluded because it grows as the corpus grows and must not invalidate previously computed hashes. The fingerprint itself — what was measured — is pinned; who it matched against is not. - New rules FP-01 and FP-02 in a new "fingerprint" bucket in the Rule Catalog.
- FP-01 "Fingerprint recorded." Informational (score weight 0). Surfaces whether a fingerprint is present; lists frame count and algorithm version. Mirrors PROV-01 in structure.
- FP-02 "Reused-footage candidate." Score weight 0 in v1. Triggers when
match_candidatesis non-empty. Verdict isflagged;passedwhen match list is empty. Rule description: "Surfaces near-matches against existing corpus and operator camera references. Does not penalise — analyst review required. Same-witness same-event matches within ±2h are excluded as legitimate corroboration."
- Backfill not run in this session. Pre-v0.11.0 cases retain
provenance.fingerprint: null. Seetools/backfill_fingerprints.py— exists, has a docstring, exits cleanly with--dry-run. Operator-triggered when ready. - METHODOLOGY_VERSION bumped to 0.11.0. Existing case audit hashes recompute on next view; underlying rule verdicts unchanged except FP-01 and FP-02 (both new, both informational, score weight 0) appearing.
Posture: footage is a sensor. Reused footage breaks every claim it grounds. This pipeline makes reuse visible.
v0.10.0 — Layer 1 file provenance pipeline
Every media upload through /intake now produces a cryptographic provenance record before the file enters any corpus. The record is stored on the case under case.provenance and is locked into the audit hash — once populated, the provenance cannot be silently changed without breaking the hash.
- New module:
hermes_provenance.py. Public API:extract_provenance(path) -> dict. Deterministic, no side effects, no network calls. Tools used:exiftool(EXIF, MIME type via magic bytes, GPS, capture timestamp),ffprobe(container, codec, duration, resolution, bitrate, GOP structure),c2pa-python 0.32.6(C2PA manifest presence and verification). All fields null when the extractor cannot provide them — Hermes does not invent values. - New field
case.provenance. Populated on first media upload; contains sha256, file_size_bytes, mime_type (content-based), container, codec, duration_seconds, resolution, bitrate_kbps, gop_structure, exif (full dict), exif_gps ({lat,lon,alt} or null), capture_timestamp, upload_timestamp, capture_to_upload_seconds, reencode_signals, and c2pa. Field is null for cases with no uploaded media and for all pre-v0.10.0 cases until backfill runs. - sha256 locked into audit hash. The audit hash payload now includes
provenance_sha256. Once a provenance record is attached, any change to the file hash breaks the audit hash. Same discipline as the calibration flag from v0.8.1. - New rule PROV-01 “Provenance record present.” Informational only (score weight 0). Surfaces whether a provenance record is attached and lists the extracted non-null fields. Verdict is
passedwhen a record exists,no_datawhen absent. Absent is valid — old cases are not penalised. - GEO-WITNESS-01 data source updated. The EXIF GPS trigger now reads from
case.provenance.exif_gps(new canonical location) with a backward-compat fallback tocase.media[].metadata.exif.gps_latfor pre-v0.10.0 cases. Rule verdict logic and semantics are unchanged. - Camera registration reference media. New endpoint
POST /api/cameras/<cam_id>/reference-media(token-gated multipart). Operators can attach a calibration reference image or video to a registered camera; provenance is extracted and stored atcameras[].registration_provenance. - Bug fix:
hermes_media_metadata.analyzename error. The upload handler was callinghermes_media_metadata.analyze(), which does not exist (the function is namedextract()). Every media upload silently fell into the error branch. Fixed to callhermes_media_metadata.extract(). This means media metadata tier and flags now populate correctly for the first time. - Additive schema changes only.
case.provenanceandcameras[].registration_provenanceare nullable JSON fields. Absent = null. Existing records are unaffected. No migration needed. - Backfill not run in this session. Pre-v0.10.0 cases retain
provenance: null. A separate backfill operation — walkingcases/, re-extracting provenance from stored media files in/opt/hermes/media/, and updating case records — is a planned future task. - METHODOLOGY_VERSION bumped to 0.10.0. Existing case audit hashes recompute on next view; underlying rule verdicts unchanged except PROV-01 (new, informational) appearing and GEO-WITNESS-01 data source annotation updated.
Posture: footage is a sensor. A sensor is only as good as the data it is grounded upon. This pipeline grounds the file itself.
v0.9.0 — witness geometry grounding + operator camera calibration
Two new data layers ground Hermes reports in verifiable geometry. Every submission can now carry a GeoJSON standpoint-and-FOV record from the witness; every profiled camera can carry a GeoJSON viewshed polygon from the operator. Both inputs feed two new audit rules that give the confidence engine honest geometry to reason over, rather than leaving it to work from verbal descriptions alone.
- Witness standpoint map (GEO-WITNESS-01). An optional new step in the intake wizard lets the witness drop a pin at their location, point a field-of-view cone in the direction they were looking, and set an approximate observation distance. The step is visibly optional with a prominent Skip button — no dark patterns. The resulting GeoJSON FeatureCollection (a Point for the standpoint, a Polygon for the FOV cone) is stored as
case.witness_geometry. Cases without geometry are not penalised if the case has EXIF GPS on uploaded media, is corroborated by a second independent witness (CORR-01 flagged), or is sourced from a calibrated registered camera. Cases with none of those signals receive a−0.10confidence adjustment under GEO-WITNESS-01. - Operator camera calibration (CAM-GEO-01). A new operator-only wizard at
/cameras/register(token-gated, same token as calibration intake) lets operators place a camera on a Leaflet map, point its FOV cone (azimuth, angular width, max range), and record mounting height and horizon elevation. The result is a GeoJSON FeatureCollection stored ascameras[].calibration_geometryincameras.json. When a submitted case resolves to a registered camera with calibration geometry, CAM-GEO-01 applies a+0.05confidence boost. - Shared Leaflet component. Both features use a single JS component (
hermes-fov-map.js) that wraps Leaflet 1.9.x. The component is parameterised to produce different GeoJSON property shapes for witness vs. camera contexts. One file, no duplication. - CORR-01 wired into the audit trail. The corroboration rule existed since v0.6.0 but was never called from
build_audit_trail(). This is a v0.6.0 gap now closed: CORR-01 runs on every case, and its result is passed to GEO-WITNESS-01 so that corroborated cases are not penalised for absent witness geometry. Existing case audit hashes will recompute; the rule logic is unchanged. - METHODOLOGY_VERSION bumped to 0.9.0. Existing cases will show changed audit hashes on next view; the underlying rule verdicts are unchanged except for cases newly evaluated by CAM-GEO-01 and GEO-WITNESS-01.
- New rules: CAM-GEO-01, GEO-WITNESS-01. Both are in the geometry bucket of the Rule Catalog with stable IDs.
- Additive schema changes.
case.witness_geometryandcameras[].calibration_geometryare nullable GeoJSON FeatureCollection fields. Absent = null. Existing records are unaffected. No migration needed.
Posture: footage is a sensor. A sensor that isn't grounded in honest geometry is a guess dressed as measurement.
v0.8.3 — service hardening (least-privilege)
No methodology change and no rule change. Operational hardening only: the Hermes app no longer runs as root. METHODOLOGY_VERSION stays at 0.7.0; corpus snapshot hash is unchanged.
- Service user. A dedicated unprivileged system user (
hermes, uid 999, no login shell, no home) now owns the gunicorn process and the application's writeable paths (cases/,cases_archive/,media/,uploads/,case_index.json,app/cache/, the SQLite teams DB, and the Flask.secret_key). Source code and the venv stayroot:rootand read-only to the service. - Secrets file.
/opt/hermes/.envis nowroot:hermesmode 640 (was world-readable 644). API keys are no longer visible to other local users. - Systemd hardening. Added
NoNewPrivileges,PrivateTmp,ProtectSystem=strict,ProtectHome,ProtectKernelTunables/Modules,ProtectControlGroups,RestrictSUIDSGID,LockPersonality,RestrictNamespaces,RestrictRealtime, andSystemCallArchitectures=native.ReadWritePaths=/opt/hermesbounds writes; everything else on the filesystem is read-only to the service. - Backup. Pre-change unit files preserved at
/opt/hermes/backups/20260427T035225Z-rootfix/(originalhermes.service,ufoindex.service,tracker.confdrop-in, and.env). Rollback is onecp+daemon-reload+restartaway. - Verification. Post-restart: workers run as uid 999, all public endpoints (homepage, /intake, /docs/changelog, /docs/watch-to-intake, /research, /api/corpus, /backtest, /workbench, /watch) return 200, and write tests against
cases/,media/, and the corpus index succeed under the new user.
v0.8.2 — landing-page truth pass + index reconciliation
No methodology change and no rule change. This release closes the gap between what the homepage claimed and what the system could actually show, and reconciles a stale index where the public counter had drifted from the cases on disk. METHODOLOGY_VERSION stays at 0.7.0; existing audit hashes are unchanged.
- Homepage labels rewritten for honesty over appearance. The single "corpus size: 87,460 cases" line was split into two:
Hermes cases(currently 5, the active witness submissions) andNUFORC reference rows(87,458, marked "statistical context only"). The previous label conflated audited Hermes cases with imported NUFORC reference rows, which would have led a reasonable visitor to read the larger number as audited work. It was not. - "integrity hash" relabeled to "corpus snapshot hash." The value is a sha256 of the sorted indexed case_id list, truncated to 16 hex characters. It changes when the indexed case set changes; it does not encode per-verdict cryptographic integrity. The label now describes what the hash actually covers.
- "date range" relabeled to "reference corpus span" + new field "Hermes intake live since." The 1906–present span is dominated by the NUFORC archive, not by Hermes operating history. A separate field now states the earliest native intake date (2025-08-12). Same data, less misleading framing.
- Static fallbacks replaced with honest defaults. The previous template used
—as the static value for every status field; if/api/corpuswas slow or failed, visitors saw em-dashes everywhere. The new fallbacks are the real values as of this release. A flaky API call now degrades to truth, not to a placeholder. The defaults will go stale as new native cases land; refreshing them is a maintenance task in future releases. - Index rebuild reconciled by_source.HERMES with on-disk reality.
case_index.jsonregenerated viabuild_index.py. Three native cases that were on disk undercases/but absent from the index are now indexed.by_source.HERMESmoves 2 → 5;_active_count()already reported 5, so the homepage no longer depends on index freshness for that figure to be honest. - Integrity hash transition. Corpus snapshot hash
A13E1408841A3CF7→A0271D6814576FB2. Total indexed records 87,460 → 87,463. Reference span ceiling 2026-04-12 → 2026-04-22 (driven by the latest native case now included in the index). - JS rewritten to read by_source counts. The landing page's inline script now pulls
totals.by_source.HERMESandtotals.by_source.NUFORCdirectly instead of the combinedtotals.total. Same data, more honest framing in the rendered output. - Backup. Pre-change state preserved at
/opt/hermes/backups/20260426T174514Z-phase1/(pre-changelanding.html,hermes_corpus.py,case_index.json) and/opt/hermes/backups/20260426T212953Z-phase3/(pre-changechangelog.html). Rollback is a file copy and a service restart.
Posture: a small honest number is better than a big misleading one. The corpus is small. The methodology is published. The numbers on the homepage are now what the system actually has.
v0.8.1 — calibration intake (operator mode + ground truth)
Hermes now has a separate intake path for calibration cases: submissions where the identity of the object on camera is already known. These cases are the measurement baseline for the photometric, motion, and astrometric analyzers that arrive in v0.9.x. They never enter the public corpus and they never influence witness verdicts.
- Operator intake at
/intake?mode=calibration&token=<token>. Same wizard witnesses use, same validator, same audit hash — with one extra step (ground truth) inserted betweenmediaandreview, and an amber "CALIBRATION MODE — Operator Intake" banner at the top of the page. The witness flow is unchanged; operator mode is token-gated and invisible to anyone without the URL. - New module:
hermes_calibration.py. Token verification, case-ID prefixing (HERMES-CAL-*), field stamping, corpus filtering. Standalone; imported byapp.pyandhermes_corpus.py. No effect on witness intake. - Ground truth field set. Verified identity, category, confidence, source of truth, known distance (km), known altitude (m), true azimuth (°), true elevation (°), operator notes. Stored under
case.ground_truth. Blank fields are valid and stay blank — honesty over precision. - Public corpus exclusion is structural.
hermes_corpus._active_count()filters out any case withcalibration_case: true. The/api/corpustotal, the landing-page counter, and the workbench all see only witness cases. Calibration cases still receive the full rule pipeline, audit hash, and rule catalog — they are simply partitioned. - Integrity firewall is cryptographic. The
calibration_caseflag is included inhermes_audit.build_audit_trail()'s hash payload. A calibration case cannot be silently reclassified as a witness case without changing its audit hash. Any attempt to reuse calibration data as witness data is therefore visible in the audit trail. - Teams integration deliberately skipped for calibration. Calibration submissions do not fire team notifications and do not auto-link within the 30-min / 50-mi CORR-01 window. Calibration is method work, not incident reporting.
CASE_ID_RErelaxed. Accepts bothHERMES-*(witness) andHERMES-CAL-*(calibration) formats. Pre-existing case IDs unaffected.- Operator token. 64-char hex,
/opt/hermes/app/cache/cal_token.txt(mode 0600, root:root). Not committed. Not logged. Rotation is a file replacement + service restart; no schema migration. - METHODOLOGY_VERSION. Internal constant bumped from 0.7.0 to 0.8.1. The public
/api/corpusstring continues to report 0.7.0 until the first calibration case has been recorded and reviewed. The v0.8.0 bump to 0.8.0 is skipped in public reporting because 0.8.0's measurement analyzer is not yet active on live submissions; the public version moves when the first tool produces a real output. - Not shipped yet: photometric analyzer, motion analyzer, astrometric analyzer, slate-OCR analyzer, non-vision reasoner, AI vision bias study. Those arrive in v0.9.x on top of calibration-tier data.
- Backup. Pre-change state preserved at
/opt/hermes/backups/20260424T190700Z-pre-v0.8.1/(originalapp.py,hermes_audit.py,hermes_corpus.py,intake.html,changelog.html).
Posture: we measure against a truth we know. Only then can we say what we do not know.
v0.8.0 — first measurement analyzer (media metadata + tier + consent)
This is the beginning of the video analysis workstream. Hermes now reads files and assigns a provenance tier before any deeper analysis is attempted.
- New module:
hermes_media_metadata.py. Usesexiftoolandffprobe. No AI. No network. Rule-based and traceable. - Provenance tiers A / B / C / D. Every uploaded file carries a tier that describes the FILE, not the person. Tier A = original with full metadata. Tier D = significantly degraded (screen-capture, stripped, or heavily re-encoded). Every tier comes with written reasons.
- Consent contract. New intake step ("Video or Photo") with three mandatory understanding boxes before any file can be accepted: what we measure, why provenance matters, how zoom and camera specs limit what can be recovered. An optional fourth box opts the file into Hermes' internal AI calibration research.
- Affirming feedback. After upload, the witness sees what the file gave us and a forward-looking note about what helps most next time. Hermes never grades the witness.
- Upload capacity.
MAX_UPLOAD_MBraised from 50 to 500. Accepted formats expanded to include HEIC, TIFF, MKV, WebP, M4V, 3GP. - Platform marker detection. WhatsApp, Telegram, Signal, TikTok, Instagram, Facebook, Messenger, Snapchat, Twitter/X, Discord, Reddit, YouTube, Google Photos, iCloud, FFmpeg/libavformat, HandBrake, screen recorders, QuickTime Player. Drives tier assignment even when some metadata survives.
- Missing is valid. Fields the file does not contain are reported as
null. Hermes does not invent values. This is the analytical discipline. - METHODOLOGY_VERSION bumped from 0.7.0 to 0.8.0. Existing cases' audit hashes will recompute; their rules logic is unchanged, but the version string now differs.
- Not shipped yet: photometric, motion, astrometric, and scope-restricted AI scene description analyzers. Those arrive in later methodology increments.
- Backup. Pre-change state preserved at
/opt/hermes/backups/20260424T021743Z/.
Posture: we measure. We do not determine. When we don't know, we say so.
v0.7.3 (UX) — mission statement
Voice change. Every visitor — witness or researcher — now reads the same opening statement before anything else. No methodology or rule changes. METHODOLOGY_VERSION stays at 0.7.0; audit hashes for existing cases are unchanged.
- Mission statement added to landing page. Placed between the header and the two doors, so both audiences read it before choosing a path. Text: “Hermes is here to help. We really want your data, and we bet you’re here because you had an experience that left you with some questions. No, we don’t have answers — but we really want your data, and we want to value your data. We’re here to help you make better data, so that together we can formulate better questions.”
- Mission statement added to intake welcome step. Same text, same voice, framed with a green accent bar. Witnesses see it first when they start a report.
- Posture established. Hermes does not grade the witness. Hermes helps the witness make better data. Feedback on submissions — present and future — will be affirming of the contribution and forward-looking about technique, never scolding about what was already done.
- Backup. Pre-change state preserved at
/opt/hermes/backups/20260423T225346Z/(originallanding.html,intake.html,changelog.html).
v0.7.2 (UX) — conversational intake
Release date: 2026-04-23 · interface and witness-UX change; methodology version unchanged at v0.7.0, no audit hashes affected. Two new stored fields added to the case schema (feeling, privacy.location_precision, intake_version) that do not feed the audit trail.
- New intake flow at
/intake. The old single-page form (long, technical, lat/lon-first) has been replaced by a 12-step conversational wizard that asks one question per screen in the order a human actually remembers an event: when, where, where-in-the-sky, what-you-saw-first, what-it-did, how-it-ended, duration, who-was-with-you, how-you're-feeling, privacy. Review screen lets the witness edit any answer before submission. Success screen gives the case ID with explicit "write this down" framing. - No lat/lon fields in the default flow. The witness types a city, address, landmark, or highway milepost; we forward-geocode via OpenStreetMap Nominatim (the same dependency the previous intake already used) and show a confirmation card. A "use my phone's GPS" button is the secondary option. An Expert toggle exposes raw coordinate entry for users who want it. The witness never sees the words "latitude" or "longitude" unless they ask for them.
- Draft-save on every keystroke. All answers persist to
localStorageunderhermes.intake.draft.v2. A resume banner appears at the top of the page if an unfinished draft exists on the device. A frazzled witness who loses signal or closes the tab can come back and pick up mid-flow. - Sky-direction calibration via compass rose + sliders + FOV select. Azimuth (0–359°), elevation angle (−10° to 90°), and apparent field-of-view ("the size of the moon," "a fist at arm's length," etc.) feed the existing
facing,elevation_angle, and a new FOV interpretation. A placeholder is reserved for the 3D Earth calibration surface (Google Photorealistic 3D Tiles), which will replace the sliders once the Maps API key is configured — the parameter tuple stays the same regardless. - New "how are you feeling" question. Four options: shaken, curious, unsure, fine. Stored verbatim in the case record as
feeling. This is witness-experience data, not phenomenon data; it never enters the audit trail or affects confidence scores. - Privacy posture on public display. During intake the witness chooses between coarse (nearest town) and exact public display of their location. Default is coarse. The precise coordinates are always retained internally for the rule catalog to reason over; only the public case-detail rendering is affected by this choice. Stored as
privacy.location_precision. - Backend validator relaxed for three fields.
camera,ir,naked_eye, andtimezonenow skip validation when the value is empty (previously they rejected empty strings, which prevented the new intake from submitting without collecting optional media metadata). Non-empty values are validated as before. No effect on cases that did supply those fields. - Pre-existing
EPHEM_OKNameError fixed.render_verdictreferenced a module-levelEPHEM_OKflag that was never defined, which would have caused HTTP 500s on any future submit. Added a guarded import that sets the flag at module load. - Backup. Pre-change state preserved at
/opt/hermes/backups/20260423T195930Z/(originalintake.html,app.py) for rollback.
v0.7.1 (UX) — two-door landing page
Release date: 2026-04-23 · interface-only change; methodology version unchanged at v0.7.0, no audit hashes affected.
- New landing page at
/. Replaces the intake form as the root URL. Two parallel entry points on one threshold: a witness door ("Report what you saw") and a researcher door ("Examine the system"). Both doors share the same corpus, methodology, rule catalog, and audit trail; they only differ in how the visitor is oriented in the first three seconds. - Intake form moved to
/intake. All template navigation bars rewritten to pointINTAKE → /intake. The old/intake URL is preserved behind the new landing's primary CTA, so existing bookmarks to the intake flow redirect cleanly via the visible button. - Live system state surfaced on the landing. The right door and the footer read
/api/corpusat page load and display the current methodology version, corpus size, integrity hash, and date range. If the API is unavailable the landing still renders — trust signals degrade silently, they do not block the doors. - Scope statement added. A "What Hermes is / what Hermes is not" contract appears below the doors, stating in plain language that confidence scores measure thoroughness of checking rather than likelihood of any interpretation, and that witness-reported kinematics are preserved verbatim but not treated as instrumented measurements. This is the same language used in
/api/auditdisclaimers, surfaced earlier in the visit. - Backup. Pre-UX state preserved at
/opt/hermes/backups/20260423T185451Z/(originalindex.html,app.py, and the seven template files whose nav was rewritten) for rollback.
v0.7.0 — corpus completeness upgrade
Release date: 2026-04-23
- NUFORC corpus upgraded to the "complete" CSV. Switched from
ufo-scrubbed-geocoded-time-standardized.csv(planetsig original, 79,636 rows retained) toufo-complete-geocoded-time-standardized.csv(via truthiswill/ufo-reports fork, 2019-09-17 snapshot, 87,458 rows retained after lat/lon validation). Net gain: +7,822 reports, all within the existing 1906-11-11 to 2014-05-08 date range. The "scrubbed" filter had dropped rows for quality reasons that do not apply to a reporting-behavior corpus where we treat every report as a signal about when people chose to report, not whether the report is true. - Provenance trail strengthened. Each archival case now carries
archive_provenancenaming the exact fork and file used:planetsig/ufo-reports -> truthiswill/ufo-reports fork (2019-09-17 snapshot); ufo-complete-geocoded-time-standardized.csv. The/corpusmanifestsnapshot_datereflects both the fork's commit date and the date we imported it. - Gap disclosed, not hidden. The NUFORC archive still ends at 2014-05-08. No public GitHub mirror exists past that date, and nuforc.org's live
/databank/is served via the wpDataTables plugin which gates bulk export. The/corpusnotes field now explicitly discloses the 2014-05-09 to 2026-04-04 gap rather than letting it be inferred fromdate_range. - Backup of previous state preserved. Pre-upgrade CSV,
case_index.json, and compressedcases_archivetar stored at/opt/hermes/backups/20260423T181234Z/for rollback and reproducibility of v0.6.0 analyses. - Integrity hash changed as expected:
264F1972A076011A→A13E1408841A3CF7. Any v0.6.0 audit_hash values remain valid for v0.6.0 corpus state; re-auditing against v0.7.0 requires recomputation.
v0.6.0 — SAT-LOS-01 + CORR-01
Release date: 2026-04-23
- SAT-LOS-01 (satellite line of sight) landed. TLE-propagated check against the reported bearing and elevation; a report is eliminated only when a catalogued satellite was geometrically consistent AND sunlit while the observer was in darkness. Upgrades SAT-OVER-01 from an availability count to a strict geometric test. TLEs cached daily from CelesTrak (stations + visual groups, ~170 objects) and propagated with
sgp4locally. - CORR-01 (spatio-temporal corroboration) landed. Scans the native case store for independent reports within 30 minutes and 50 km of the subject case. Verdict language emphasises that corroboration is evidence of reporting behaviour, not of anomaly; the rule never eliminates and cannot inflate anomaly confidence.
- Deterministic audit hash now exposed. Every audit response carries
audit_hashkeyed on methodology version + per-rule hashes, excluding wall-clock timestamps, so two evaluations of the same inputs yield the same 16-char identifier. - New endpoints:
/api/satlos/status,/api/satlos/check,/api/satlos/refresh,/api/corr/status,/api/corr/check. - Corpus manifest now lists CelesTrak GP feed as a first-class source.
v0.4.1 — audit trail
Release date: 2026-04-22
- Introduced structured rule-by-rule audit on every case page (
/case/<id>) showing inputs, data source, result, verdict, and plain-language explanation for each check. - Published the Rule Catalog with stable IDs so cited cases survive methodology revisions.
- Added
/api/audit/<case_id>JSON endpoint for external reproducibility. - Each audit now carries a SHA-256 audit-hash keyed on rule IDs, verdicts, and methodology version.
METHODOLOGY CHANGELOG
Every substantive change to the Hermes analysis methodology, in reverse chronological order. If you are replicating an analysis from a paper, find the version it was published under and read forward.
Versions follow semantic versioning against the methodology (not the software). MAJOR: breaking change to analysis output semantics. MINOR: new analysis module or expanded corpus. PATCH: bug fix or documentation clarification with no effect on outputs.
v0.4.0 April 2026
MINOR Corpus expansion + research console + reproducibility hashing.
- NUFORC archive imported. 79,636 public NUFORC reports ingested from the
planetsig/ufo-reportsopen-source compilation. Taggedsource: NUFORC,is_archive: true. Corpus grew from 2 active records to 79,638 total indexed records. - Research console launched at
/research. Cohort builder, spatial cluster detector, index-backed volume forecast. - Reproducibility hash added. Every cohort and cluster query now returns
COHORT-XXXXXXXXXXXXXXXX. Deterministic, order-insensitive, cite-able. - DBSCAN spatial cluster endpoint.
/api/clusterwith haversine metric, configurableeps_miandmin_samples. Returns clusters with centroid, count, date range, top shape, sample places. - Index caching. Case records loaded into compact in-memory index for sub-second cohort queries over ~80k records.
- Documentation pages published. Mission, how-it-works, for-witnesses, for-investigators, for-researchers, glossary, and this changelog.
Breaking effects on prior analyses: none; this is purely additive. Older /api/cohort endpoint remains available but is superseded by /api/cohort/v2 for index-backed queries.
v0.3.0 April 2026
MINOR Multi-network routing + workbench.
- Export for Filing added. Each submitted case generates pre-formatted MUFON, NUFORC, and Enigma filings. Hermes does not auto-submit; the witness chooses where to file.
- Workbench launched at
/workbench. Early cohort builder, volume and conditions forecasts. Superseded by the research console in v0.4.0 but remains accessible. - Share-case URL. Every case gets a permanent shareable public URL at
/case/<id>.
v0.2.0 April 2026
MINOR Intake form improvements + teams.
- Categorized camera selector. 9 equipment groups (smartphone, superzoom, mirrorless, action, IR/thermal, smart telescope, passive/stationary, vehicle, other) with ~35 specific models.
- Live stream URL field. Captures YouTube Live / Twitch URLs with embed preview.
- Teams with areas of operations. Geofenced groups; active pings, passive alerts, auto-linking within 30 min / 50 mi.
v0.1.0 Initial public release
MAJOR Initial methodology.
- Structured intake form. Captures location, time, geometry (bearing + elevation), duration, color, shape, light characteristics, behavior, equipment, IR, naked-eye, description, witnesses.
- Automated cross-reference pipeline.
- Weather: Visual Crossing historical API.
- Satellites: CelesTrak TLE + Skyfield propagation. Notable-object identification (Starlink, ISS, named reflectors).
- Aircraft: ADS-B query within 50nm, with hovering-behavior elimination.
- Celestial: Moon phase + bright planet altitude/azimuth, with bright-object flag when angularly co-located with report.
- Elimination/flag logic. Clear-sky, low-wind, low-aircraft-density, hovering, orange-color, bright-moon, bright-planet rules. See How Hermes Works for exact text of each rule.
- Confidence grade. Four tiers: LOW (insufficient data), MEDIUM (conventional plausible), MEDIUM-HIGH (anomalous characteristics noted), HIGH (investigator-reviewed with corroboration).
- Language discipline. "Report activity," not "UAP activity." "Unexplained after screening," not "anomalous." Uncertainty intervals on every quantitative output.
Planned changes under review
The following are proposed methodology changes with open review. They are not yet active.
- EXIF extraction for uploaded media. Extract camera model, timestamp, GPS, exposure settings when available. Flag when EXIF timestamp disagrees with reported time by more than 5 minutes.
- Observer-effect correction on anomaly flags. When Hermes publishes a forecast for a region, any subsequent report-volume spike in that region is down-weighted by a correction factor to account for the self-fulfilling prophecy problem.
- Post-stratification of cluster counts. Normalize cluster intensity by US Census tract population (or international equivalents) so that sparse-population clusters are surfaced above dense-population clusters of the same size.
- Public backtest dashboard. All published forecasts archived with version, inputs, outputs; dashboard tracks hit rate vs noise distribution over time.
- Hawkes process hotspot detection. Temporal-spatial self-exciting point process for identifying short-horizon report clusters that exceed rate-chance.
- Bayesian change-point detection. For longer-horizon shifts in regional report volume, with explicit priors.
How to propose a methodology change
Hermes welcomes concrete methodology contributions. A well-formed proposal includes: (a) what change, (b) what problem it solves, (c) references or prior art, (d) proposed effect on existing confidence grades or forecast outputs, (e) backward-compatibility plan. The bar for a MAJOR change is that no currently-cited analysis becomes retroactively wrong without a documented migration path.
Version-to-version hash behavior
A reproducibility hash verifies the query, not the output. If the methodology version changes between publication and replication, the hash will still match for the same query, but the numbers returned may differ. Reviewers should always note the methodology version under which an analysis was originally performed.