A photo appearing in different case folders does not tell us which folder describes it correctly. Reviewing the selection and publication code led me to revisit the boundary between detecting duplicates and confirming their attribution.

Finding a duplicate is different from assigning its case

The photo workflow contains a deliberate boundary: when the same original appears in different case folders, hold it from publication rather than choosing which case must be correct. The instructions distinguish matching byte or upright-pixel fingerprints from evidence about the actual installation location. A duplicate signal and a case attribution answer different questions.

It is tempting to tidy two folders by deleting one copy. Choosing the first folder found, the newer folder name or the larger collection would make the inventory simpler without establishing the location. The decision I want to preserve is the boundary against presenting an image as evidence for the wrong project.

I did not open original photographs to resolve an attribution for this article. I reviewed bounded historical final answers and current instructions, selection logic, rendering code and test source. The old records distinguish completed frames, frames held for matters such as reflections, and retouch drafts not yet activated. They are not evidence that the current entire review is finished.

Do not add information that the equality signal lacks

A matching byte digest can group declared copies of the same stored material. The source instructions also use upright-pixel equality as a signal when reviewing duplicate storage. Neither output identifies which folder correctly describes the real location. Assigning a location from the digest alone would add a fact that the comparison did not establish.

Attribution requires a separate basis, such as collection provenance, an actual source record or a user’s confirmation about that photograph. Without that basis, the distinct case inventories can remain intact while publication is held. Declaring every frame incorrect or merging the cases would be additional decisions, not consequences of one duplicate signal.

The shared example keeps this boundary explicit. Its digest strings are supplied observations. The new function does not read files or compare pixels. Different case IDs using an equal declared byte digest create a review signal, while the function deliberately declines to select the correct case.

A hold does not rewrite phase or attribution

The selection code retains the selected source inventory and reads per-photo publication_holds. A supported hold status and recorded evidence reference are required before applying the hold. It does not turn an after photo into a before photo or move its case identifier. Construction phase and an unresolved publication condition are separate records.

Cover selection, the gallery, before/after comparisons and public photo data read this hold. If the preferred cover is held, another usable after frame can be selected. When every photo is held, the individual case entry can remain without public images. The original test source includes preservation of the inventory, phase and case while excluding public references. I did not execute those original tests in this run.

Excluding an image reference is also different from blocking access to the underlying file. The reused gallery module computes public IDs; it does not enforce remote file permissions or recall previously distributed caches. If an integration requires file withdrawal, that responsibility needs its own operational verification.

Resolving attribution must not clear unrelated holds

The inspected selection code supports holds for source identity as well as reflections, retouching, room classification, cleanup and installation-completion review. Setting one aggregate hold flag to false after confirming a case could accidentally erase another unresolved condition. The resolved reason and the remaining reasons need separate representation.

The new example computes only the source-identity hold. A supplied resolution identifies a photo, a confirmed case and an evidence ID. If the declared confirmed case differs from the photo’s stored case, the photo remains held and is not moved automatically. A matching resolution does not release other observations in the digest group. Nor does the presence of these strings authenticate the actual confirmation.

Before calling the existing gallery, combine the current other holds with the source hold. In the integration example, held on each base photo represents current non-source holds, not an old aggregate projection being fed back as if it were the original record. A real integration needs to derive those values from its current per-reason review ledger.

Explore the implementation

Minimal synthetic composition with the existing module

from dataclasses import replace
from projection import Photo, project
from source_gate import source_gate
import hashlib

same = hashlib.sha256(b'invented duplicate bytes').hexdigest()
observations = [
    {'id': 'a-after', 'case_id': 'case-a', 'byte_sha256': same},
    {'id': 'b-after', 'case_id': 'case-b', 'byte_sha256': same},
]
base = [Photo(r['id'], r['case_id'], 'room-one', 'living',
              'after', 1600, 900, True, False) for r in observations]
# base.held represents CURRENT non-source holds, not a previous aggregate result.
decisions = {r['id']: r for r in source_gate(observations, [])}
adjusted = [replace(p, held=p.held or decisions[p.id]['source_identity_held'])
            for p in base]
result = project(case_id='case-a', photos=adjusted, pairs=[],
                 gallery_order=['a-after'], preferred_cover='a-after')
assert result['case_id'] == 'case-a'
assert result['public_photo_ids'] == []
print(result)

Put a small review step before the existing gallery projection

The function accepts at most 64 observations and 64 resolution records. It validates exact fields, duplicate IDs and digest format. Its opaque identifiers contain no real path or business name. Absence of a cross-case digest conflict is not reported as verified attribution or permission to publish. authenticates_source and grants_publication always remain false.

The complete code below is an independently authored synthetic implementation, not a patch installed into the original photo selector. Save it as source_gate.py beside projection.py from the existing public ZIP when trying the integration. The new helper is inline in this article. It has not been added to that ZIP or published as a new version of the existing package.

Visual review and same-view comparison review remain conditions of the downstream gallery module. Confirming a case does not set reviewed or same_view_reviewed to true. Evidence established at one step must not silently substitute for review at another.

Explore the implementation

Complete independent source_gate.py implementation

"""Review declared fingerprints and source decisions; never infer actual case attribution."""
from collections import defaultdict
from copy import deepcopy
import re


def source_gate(observations, resolutions):
    def rows(value):
        if type(value) is not list or len(value) > 64:
            raise ValueError('BOUNDED_RECORD_LIST')
        return value

    def token(value):
        if type(value) is not str or not re.fullmatch(r'[a-z][a-z0-9_-]{0,47}', value):
            raise ValueError('OPAQUE_IDENTIFIER')

    source, groups = {}, defaultdict(set)
    for row in rows(observations):
        if type(row) is not dict or set(row) != {'id', 'case_id', 'byte_sha256'}:
            raise ValueError('EXACT_OBSERVATION_FIELDS')
        token(row['id']); token(row['case_id'])
        if row['id'] in source:
            raise ValueError('DUPLICATE_OBSERVATION')
        if type(row['byte_sha256']) is not str or not re.fullmatch('[0-9a-f]{64}', row['byte_sha256']):
            raise ValueError('DECLARED_SHA256')
        source[row['id']] = row
        groups[row['byte_sha256']].add(row['case_id'])

    decisions = {}
    for row in rows(resolutions):
        if type(row) is not dict or set(row) != {'id', 'confirmed_case_id', 'evidence_id'}:
            raise ValueError('EXACT_RESOLUTION_FIELDS')
        for value in row.values():
            token(value)
        if row['id'] not in source:
            raise ValueError('UNKNOWN_OBSERVATION')
        if row['id'] in decisions:
            raise ValueError('DUPLICATE_RESOLUTION')
        decisions[row['id']] = row

    result = []
    for row in source.values():
        decision = decisions.get(row['id'])
        cross_case = len(groups[row['byte_sha256']]) > 1
        if decision and decision['confirmed_case_id'] != row['case_id']:
            reason, held = 'different_case_declared_no_auto_move', True
        elif decision:
            reason, held = 'source_resolution_declared', False
        else:
            reason, held = ('cross_case_digest_needs_review', True) if cross_case else ('no_cross_case_digest_conflict', False)
        result.append({
            'id': row['id'], 'case_id': row['case_id'],
            'source_identity_held': held, 'reason': reason,
            'cross_case_digest': cross_case,
            'evidence_id': decision['evidence_id'] if decision else None,
            'authenticates_source': False, 'reads_or_moves_files': False,
            'grants_publication': False,
        })
    return deepcopy(result)

The executed evidence uses synthetic metadata

I checked the existing public ZIP’s 6,435 bytes and SHA, extracted it and ran its 19 packaged tests. I separately ran 24 tests for the new source gate and its composition with the gallery. The cases include equal digests across two cases, a resolution for one observation, a different declared case and an unrelated hold that remains. These are not counts of reviewed original photos or successful original-program checks.

Five synthetic execution records show a pending photo leaving the cover and comparison candidates while an independently usable photo remains in the gallery. A case with all photos held retains its case ID and pending state. Other examples keep a photo excluded after attribution resolution because another hold or missing review still applies. The excerpt below is the actual synthetic output for the unresolved case.

The knowledge I want to share is a way to connect what an automatic comparison establishes with what still needs review. Before turning an equality signal into a claim about a specific project, preserve the originals and the distinct case records. A maintainable portfolio keeps that boundary inspectable as its material grows.

Explore the implementation

Actual synthetic execution record: unresolved case

{
  "source_decisions": [
    {
      "id": "a-before",
      "case_id": "case-a",
      "source_identity_held": false,
      "reason": "no_cross_case_digest_conflict",
      "cross_case_digest": false,
      "evidence_id": null,
      "authenticates_source": false,
      "reads_or_moves_files": false,
      "grants_publication": false
    },
    {
      "id": "a-after",
      "case_id": "case-a",
      "source_identity_held": true,
      "reason": "cross_case_digest_needs_review",
      "cross_case_digest": true,
      "evidence_id": null,
      "authenticates_source": false,
      "reads_or_moves_files": false,
      "grants_publication": false
    },
    {
      "id": "a-safe",
      "case_id": "case-a",
      "source_identity_held": false,
      "reason": "no_cross_case_digest_conflict",
      "cross_case_digest": false,
      "evidence_id": null,
      "authenticates_source": false,
      "reads_or_moves_files": false,
      "grants_publication": false
    },
    {
      "id": "b-after",
      "case_id": "case-b",
      "source_identity_held": true,
      "reason": "cross_case_digest_needs_review",
      "cross_case_digest": true,
      "evidence_id": null,
      "authenticates_source": false,
      "reads_or_moves_files": false,
      "grants_publication": false
    }
  ],
  "projection": {
    "case_id": "case-a",
    "gallery_ids": [
      "a-safe"
    ],
    "pair_ids": [],
    "comparison_ids": [],
    "public_photo_ids": [
      "a-safe"
    ],
    "cover_id": "a-safe",
    "empty_state": null
  }
}
Existing gallery ZIP; source gate is inline above
Existing 6,435-byte package; source_gate.py not included