FLUX DOCUMENTATION SYSTEM Layer 9 — GOVERNANCE | decisions-log flux.dantesisofo.com/wiki/decisions-log/
Canonical record of protocol decisions. Each entry is an RFC-style record: problem, decision, rationale, consequences, implementation.
LOCKED decisions cannot be changed without a new DECISION entry that explicitly supersedes and DEPRECATES the prior one. LOCKED is not advisory. It is binding.
Date: 2026-05-13 Status: LOCKED Category: Protocol
The FLUX protocol required a fixed frame count per issue. Without a locked count, issues would vary in length, contact sheet grids would vary in geometry, manifest layouts would vary in structure, and PDF page counts would be unpredictable. Consistency is a protocol requirement, not a preference.
FRAMES_PER_ISSUE = 36. This value is fixed. It does not change at runtime. It is not configurable per-photographer or per-issue.
36 frames is the capacity of one roll of 35mm film. This is not coincidence. The FLUX protocol derives from film photography's material constraints. 36 frames per roll = 36 frames per issue. The constraint is historical, physical, and meaningful.
A 6×6 contact sheet grid requires exactly 36 cells. The manifest requires exactly 36 entries (2 columns × 18 rows). The PDF structure requires exactly 36 image pages (pages 5–40). These are not coincidences either — the entire protocol is built around this number.
# flux_constants.py
FRAMES_PER_ISSUE = 36 # Do not change without updating all dependent scripts
Referenced in: issue_builder_worker.py, approve_worker.py, generate_flux_issue.py, publish_submission.py, generate_wiki.py (contact sheet renderer), PDF manifest generator.
Date: 2026-05-13 Status: LOCKED Category: PDF
The PDF format for a FLUX issue required a fixed page structure. Variable page counts or layouts would make PDFs non-archival — different issues would not be interchangeable, and any system consuming FLUX PDFs would need to handle arbitrary structures.
Every FLUX PDF is exactly 44 pages. Page assignments are fixed:
Page 1: Front cover
Page 2: Blank (IFC — inside front cover)
Page 3: Protocol page
Page 4: Blank
Pages 5–40: 36 photographs (one per page, sequential)
Page 41: Parity pad
Page 42: Contact sheet (6×6 grid)
Page 43: Manifest (2 columns × 18 rows)
Page 44: Blank back cover
Fixed page structure enables: - Predictable print output: any printer handling any FLUX issue gets the same structure - Archival stability: PDF page N always means the same thing in any FLUX issue - Automated processing: any system can parse a FLUX PDF knowing exactly where photographs, manifest, and contact sheet live - Physical production: saddle-stitch binding requires paired pages; 44 pages = 22 spreads = 11 sheets, which folds and staples cleanly
The blank pages (IFC and back cover) are structural requirements for physical binding, not accidents.
# generate_flux_issue.py
PAGES_TOTAL = 44
PAGE_COVER = 1
PAGE_IFC = 2
PAGE_PROTOCOL = 3
PAGE_BLANK_4 = 4
PAGE_IMAGES = range(5, 41) # 36 pages
PAGE_PARITY = 41
PAGE_CONTACT = 42
PAGE_MANIFEST = 43
PAGE_BACK = 44
Date: 2026-05-13 Status: LOCKED Category: Naming
Photographs entering the FLUX archive from multiple photographers, multiple cameras, and multiple sessions needed a canonical filename format that: - Encoded capture timestamp (for chronological ordering) - Identified the photographer (for attribution) - Preserved the original camera filename (for provenance) - Was human-readable and machine-parseable - Was sortable by filename alone (no metadata lookup required)
Canonical format: YYYY-MM-DD_HH-MM-SS_PhotographerName_OriginalFilename.JPG
Rules:
- Date and time are separated by underscore (not ISO 8601 T)
- Time components use hyphens, not colons (filesystem compatibility)
- PhotographerName is CamelCase, no spaces, no hyphens
- OriginalFilename is the camera-generated filename, preserved exactly
- Extension is always .JPG (uppercase)
- All four components are separated by single underscores
Examples:
2023-01-15_13-22-44_DanteSisofo_R0001234.JPG
2024-07-04_09-15-00_IgorKrivokon_IMG_3421.JPG
2025-11-30_16-45-12_JamesB_DSC09812.JPG
_ with a field countsplit('_', 3) yields [date, time, photographer, original].# generate_canonical_filename() in approve_worker.py / publish_submission.py
def generate_canonical_filename(captured_at, photographer_name, original_filename):
date_str = captured_at.strftime("%Y-%m-%d")
time_str = captured_at.strftime("%H-%M-%S")
name = photographer_name.replace(" ", "").replace("-", "")
return f"{date_str}_{time_str}_{name}_{original_filename}"
Date: 2026-05-13 Status: ACTIVE Category: Infrastructure
The FLUX S3 bucket (flux-dantesisofo) needed to host multiple asset types — full-resolution photographs, thumbnails, generated PDFs, and catalog assets — without namespace collisions.
S3 prefix assignments:
photos/ — full-resolution personal photographs
thumbs/ — generated thumbnails (personal archive)
FLUX_ISSUES/ — personal archive PDF issues (FLUX_NNN/)
FLUX_CATALOG/ — public catalog assets (CAT_NNN/)
Prefix separation enables:
- Independent IAM policies per prefix
- Independent CloudFront cache behavior configurations per prefix
- Clean ListObjectsV2 calls scoped to a single asset type
- Safe bulk operations (delete all thumbs, sync all issues) without risk of cross-contamination
FLUX_ISSUES/FLUX_001/, FLUX_ISSUES/FLUX_002/, etc. — personal issues are always under FLUX_ISSUES/FLUX_CATALOG/CAT_001/, FLUX_CATALOG/CAT_002/, etc. — catalog entries are always under FLUX_CATALOG/# deploy_s3.py / publish_submission.py
PERSONAL_PREFIX = "photos/"
THUMBS_PREFIX = "thumbs/"
ISSUES_PREFIX = "FLUX_ISSUES/"
CATALOG_PREFIX = "FLUX_CATALOG/"
Date: 2026-05-14 Status: LOCKED Category: Naming
The FLUX protocol serves two distinct use cases: 1. Dante Sisofo's personal photographic archive (continuous, private, 423+ issues) 2. The public FLUX catalog — issues submitted by any photographer
These needed separate identifier namespaces. Using a single sequence (e.g., FLUX_001 for both) would create ambiguity about whether an identifier referred to a personal or public issue.
Personal archive identifiers: FLUX_NNN (FLUX_001, FLUX_002, ..., FLUX_423, ...)
Public catalog identifiers: CAT_NNN (CAT_001, CAT_002, ..., CAT_018, ...)
The namespaces are completely separate. A CAT_NNN entry is not a FLUX_NNN issue and never becomes one.
The personal archive is a private, continuous, chronological record. The public catalog is a curated collection of external submissions. These are different things. Conflating their identifiers would falsify the record. A submission from another photographer is not Dante Sisofo's personal archive.
_next_catalog_id() generates CAT_NNN identifiers only_next_issue_id() generates FLUX_NNN identifiers only# publish_submission.py
CATALOG_ID_PREFIX = "CAT_"
# issue_builder_worker.py
ISSUE_ID_PREFIX = "FLUX_"
Date: 2026-05-14 Status: ACTIVE Category: Infrastructure
The public catalog required a persistent, authoritative record of all catalog entries. This record needed to be machine-readable, human-readable, and deployable to S3 for the live website.
catalog.json is the single source of truth for all public catalog entries. Every catalog read and write goes through this file. No catalog operation is complete until catalog.json is updated and synced to S3.
A single JSON file is simple, version-controllable, and directly readable by the static site. No database dependency for the public catalog. No API dependency for catalog reads.
catalog.json must be synced to S3 on every write (see DECISION-008)catalog.json must be preferred over local disk when the two diverge (see DECISION-008)_next_catalog_id() function must NOT derive IDs from catalog.json (see DECISION-007)catalog.json plus its S3 object versions# publish_submission.py
CATALOG_JSON_LOCAL = "/path/to/catalog.json"
CATALOG_JSON_S3 = "catalog.json" # root of bucket, served by CloudFront
Date: 2026-05-19 Status: LOCKED Category: Infrastructure
_next_catalog_id() was computing the next catalog identifier by reading max(catalog.json entries) + 1. This caused a critical failure: if catalog.json was not current with S3 (due to a deferred or failed sync), the function would generate an ID that was already in use, overwriting existing catalog entries.
CAT_002 and CAT_003 were overwritten by this bug. Both were recovered from S3 object versioning.
_next_catalog_id() must scan S3 FLUX_CATALOG/ folder prefixes directly using ListObjectsV2 with Delimiter='/'. It must derive the next ID from max(existing_CAT_NNN_prefixes) + 1. It must not consult catalog.json for ID generation.
S3 is the authoritative record of what catalog entries exist. catalog.json is a derived view that can be stale. The ID space must be derived from the authoritative source, not from a potentially stale cache.
_next_catalog_id() requires a live S3 API call on every invocation. This is intentional.def _next_catalog_id(s3_client, bucket):
paginator = s3_client.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=bucket, Prefix='FLUX_CATALOG/', Delimiter='/')
existing = []
for page in pages:
for prefix in page.get('CommonPrefixes', []):
folder = prefix['Prefix'] # e.g., 'FLUX_CATALOG/CAT_018/'
match = re.search(r'CAT_(\d+)/', folder)
if match:
existing.append(int(match.group(1)))
next_n = (max(existing) + 1) if existing else 1
return f"CAT_{next_n:03d}"
Date: 2026-05-19 Status: LOCKED Category: Infrastructure
_save_catalog() was writing catalog.json to local disk and deferring the S3 upload. In some code paths, the S3 upload was skipped entirely. This caused local and S3 versions to diverge. _load_catalog() was preferring local disk over S3. The combination created a state where the local system had stale catalog data and generated duplicate CAT IDs.
_save_catalog() must upload catalog.json to S3 immediately on every write, before returning. The function is not complete until S3 is updated.
_load_catalog() must prefer the S3 version over local disk. On load, both versions are fetched; whichever has more entries is used; the local disk is updated to match.
S3 must be the authoritative source. Local disk is a cache. Caches are not authoritative. The system must treat them as such at all times, not only when convenient.
_save_catalog() are hard errors. The catalog state is undefined if S3 is not updated._load_catalog() on a system with no local disk copy still works correctly (loads from S3).def _save_catalog(catalog, s3_client, bucket, local_path, s3_key):
with open(local_path, 'w') as f:
json.dump(catalog, f, indent=2)
s3_client.put_object(
Bucket=bucket,
Key=s3_key,
Body=json.dumps(catalog, indent=2).encode('utf-8'),
ContentType='application/json'
)
# S3 upload must complete before function returns
def _load_catalog(s3_client, bucket, local_path, s3_key):
local = _load_local(local_path)
remote = _load_s3(s3_client, bucket, s3_key)
canonical = remote if len(remote) >= len(local) else local
_write_local(local_path, canonical)
return canonical
Date: 2026-05-15 Status: ACTIVE Category: Infrastructure
The FLUX portal required an automated pathway from approved photograph accumulation to issue generation. Manual triggering would create bottlenecks and require constant monitoring.
When the count of unassigned approved photographs in the queue reaches FRAMES_PER_ISSUE (exactly 36), approve_worker.py spawns issue_builder_worker.py as a detached subprocess. A file lock at /tmp/flux_issue_builder.lock prevents duplicate builds.
The threshold is exact: 36, not "more than 36." The system triggers precisely when one full issue worth of photographs is available. This is the simplest correct implementation of the protocol.
File locking prevents race conditions when multiple approve operations complete in rapid succession.
# approve_worker.py
LOCK_FILE = "/tmp/flux_issue_builder.lock"
def _maybe_trigger_build(unassigned_count):
if unassigned_count < FRAMES_PER_ISSUE:
return
if os.path.exists(LOCK_FILE):
return # build already in progress
subprocess.Popen(
["python3", "/path/to/issue_builder_worker.py"],
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
Date: 2026-05-20 Status: ACTIVE Category: Infrastructure
The FLUX archive requires three distinct capabilities: durable primary storage, compute for PDF generation and image processing, and public distribution via CDN. These are different requirements with different failure modes and cost profiles. Running them on a single machine creates single points of failure and performance conflicts.
Three-node architecture: - NAS (Synology, 2x mirrored IronWolf): canonical archive root. All photographs, all issues, all metadata, all embeddings live here. This is the source of truth for the physical archive. - Mac mini M4 Pro: compute node. Runs all processing — ingest pipeline, PDF generation, embedding generation, keeper model training, portal server. Stores no canonical data; all outputs are written to NAS or S3. - AWS S3 + CloudFront: public distribution. Serves the live website, PDFs, and photographs to the public. NAS is the source; S3 is the published mirror.
NAS mirroring protects against single-drive failure. Mac mini M4 Pro has sufficient compute for the full FLUX pipeline (PDF generation, embedding inference, model training). S3 + CloudFront provides CDN-grade public delivery without requiring the NAS to be internet-accessible.
This architecture has no single point of failure for the archive: if Mac mini fails, the archive on the NAS is intact. If S3 goes down, the archive on the NAS is intact. If the NAS loses one drive, the mirror continues.
NAS folder structure:
/FLUX_ARCHIVE/ — full corpus + keepers (~400,000 frames)
/FLUX_SYSTEM/ — scripts, configuration, virtualenvs
/FLUX_PUBLIC/ — S3 sync source (generated site assets)
/FLUX_METADATA/ — SQLite database, manifests
/FLUX_EMBEDDINGS/ — vector database files
/FLUX_ISSUES/ — generated PDF issues
/FLUX_LOGS/ — all process logs
/FLUX_INBOX/ — incoming photos for ingest
Date: 2026-05-15 Status: ACTIVE Category: Protocol
Physical FLUX issue production requires saddle-stitch binding. The question was whether stapling should be automated (via a connected electric stapler triggered by the print pipeline) or performed manually by the photographer.
Stapling is a human step. The print pipeline delivers the folded sheets. The photographer staples.
The physical act of assembling the issue is part of the protocol. FLUX issues are not produced by a machine end-to-end. The photographer shoots, selects (currently manual, eventually model-assisted), approves (portal), and assembles (stapling). The assembly step belongs to the photographer.
Automated stapling would require a network-connected stapler, additional failure modes, and additional calibration. The marginal time savings do not justify the complexity.
Operational, not code. The print pipeline sends the PDF to the network printer via system print command. Physical assembly is the photographer's responsibility.
| Document | Layer | Relationship |
|---|---|---|
| CHANGELOG | Layer 9 — Governance | Chronological version history for all decisions above |
| VERSIONS | Layer 9 — Governance | Version numbering and locked-version semantics |
| PROTOCOL | Layer 2 — Protocol | The canonical method that DECISION-001 and DECISION-002 define |
| BOOTSTRAP | Layer 4 — Infrastructure | Phased implementation plan for DECISION-010 hardware |
| ARCHIVE | Layer 2 — Protocol | Digital archive structure that DECISION-003 and DECISION-004 govern |
FLUX_WIKI_v2.0 — flux.dantesisofo.com/wiki/decisions-log/