Ask Daniel's CODEX · index

SPEC

SPEC

— SynNA Publish API (Create-Only Commit)

For: Grok_Build (implementation executor) Authored by: Claude — Axial Refinement Sherpa, MA5 Council Amended by: Grok Build — v2 incorporates code review against live pipeline (2026-06-22) Ratified by: Daniel Comp — Carbon Gnosis Steward Status: v2 · ready to build · Tier 1 (fractome hub-owner laptops) Supersedes: handoffs/SPEC_SynNA_Publish_API_v1.md Anchored to: app.py (aiohttp, laptop), scripts/syn_pipeline.py, scripts/syn_config.yaml, Ventrian News Articles module 546


v2 changelog (from v1)

| Change | Why |

1§9: categories → DnnForge_NewsArticles_ArticleCategories (not CustomValue)v1 step 4 was wrong; matches syn_sql_build.py
2Add images_published state; SFTP + record-publish both set it_finalize_publish today only sets publish.sftp_at, not state
3Simplify publish bundle: no pages[]; body_html = extract_dnn_page_body() outputSingle Page row, SortOrder = 0 only
4Server rebuilds references_html from Template8 + authoritative article_idLaptop uses predicted na_id; drift would break syndication evidence
5image_manifest[].bytes required; server may verify via os.stat as fallbackVentrian Image.Size column
6Idempotency index/SQL non-atomicity documented + retry rulesJSON file ≠ SQL transaction
7build-sql becomes optional SSMS/debug artifact via syn_ventrianCommit path does not require {slug}_insert.sql
8VPS deployment path + Windows service account for SQL authtrusted_connection needs explicit identity
9Pin custom_field_badge_id=1, custom_field_references_id=2 on serverSame as module_id: 546
10packet_sha256 canonical JSON rulesStable hash across retries
11Commit success archives packet to published_rootSpec mentioned; not in pipeline yet
12PortalID: 0 documented as site context only — not an INSERT columnVentrian Article uses ModuleID

0. What this builds, in one sentence

The missing terminal step of the SynNA pipeline: a server-side, create-only commit endpoint that runs on each fractome's hub VPS, executes the Ventrian insert into that hub's DNN database over a localhost connection, and returns the authoritative ArticleID — replacing the manual SSMS COMMIT, with no SQL credentials ever leaving the laptop.

Everything before this step already exists (publish-images, record-publish, optional build-sql). This spec adds the COMMIT and the trust boundary around it. Ventrian schema semantics are factored into syn_ventrian.py, not re-written.


1. Topology and trust boundary

`` FRACTOME-OWNER LAPTOP HUB VPS (fractal-server) ─────────────────── ─────────────────────── app.py / syn_pipeline.py syn_publish_api.py - assemble, gate 1–3 ┌──────▶ - validates fractome key - build publish bundle from packet │ - verifies image manifest on disk - SFTP images ──────────(:22 Tailscale)┤ - rebuilds references from Template8 - POST publish bundle ───(:8443 Tailscale) - executes txn → 127.0.0.1 SQL - writes LEDGER locally ◀─────────────┘ - returns authoritative ArticleID SQL Server: 127.0.0.1 ONLY ``

The two-hop rule (load-bearing):

One server = one fractome. Each hub VPS runs one syn_publish_api.py bound to one DNN database. Multi-fractome routing from a single laptop is Tier 2 (§13).

Fractome model: Hub managers publish into portal 0 module 546 on the syndication hub DB. Sub-let portal operators never receive publish credentials.


2. New components

ComponentLocationPurpose
scripts/syn_ventrian.pyLattice repo (laptop + copied to VPS)Single Ventrian insert contract. Returns parameterized ops for Article, Page, Image, ArticleCategories, CustomValue. Used by syn_sql_build.py, server API, and optional operator tools.
scripts/syn_publish_client.pyLattice repo (laptop)Build publish bundle from packet; POST to hub API; ledger + registry reconcile.
scripts/syn_publish_api.pyhub VPS (W:/_LatticePublish/shared/)aiohttp service. Tailscale bind, key auth, preconditions, transaction, idempotency.
syn_publish_server.yamlhub VPSServer identity + secrets. Never on laptop. (§9)
syn_publish_index.jsonhub VPSIdempotency: slug → ArticleID. Not audit ledger. (§7)
scripts/dnn_publish_ledger.yamllaptopAppend-only provenance ledger. (§8)
POST /api/pipeline/{slug}/commitlaptop app.pyUI route → syn_pipeline.py --commit or direct client call.

VPS deployment: Ship syn_ventrian.py + syn_publish_api.py + syn_publish_server.yaml + server copy of assets/Templates/_Template8_References_Block_v1.html to a fixed folder on the hub VPS. Version-pin to Lattice release tag or commit hash recorded in ledger server_build field.


3. Request contract — the publish bundle

POST https://<tailscale-host>:8443/api/syn/publish

Headers: `` Authorization: Bearer <fractome_publish_key> Content-Type: application/json ``

Body (JSON) — content only, never identity: ``json { "slug": "first-tracks-reversal", "title": "Intium PRIME 523 [Portal:Keywords] First Tracks Reversal", "summary": "Full hook paragraph…", "body_html": "…prose + Template7 bottom only — output of extract_dnn_page_body()…", "categories": [ 12, 27 ], "badge": "maker", "author_key": "arnie", "publish_at": "2026-06-22T00:00:00Z", "expire_at": null, "image_manifest": [ { "filename": "first-tracks-reversal_200.jpg", "role": "thumb", "w": 200, "h": 200, "sort": 0, "bytes": 8421 }, { "filename": "first-tracks-reversal_1920x420.jpg", "role": "banner", "w": 1920, "h": 420, "sort": 1, "bytes": 198432 }, { "filename": "first-tracks-reversal_768x420.jpg", "role": "tablet", "w": 768, "h": 420, "sort": 2, "bytes": 61204 }, { "filename": "first-tracks-reversal_480x420.jpg", "role": "mobile", "w": 480, "h": 420, "sort": 3, "bytes": 38901 } ] } ``

Not sent by client (server derives):

Bundle rules:

packet_sha256 (laptop, for ledger): SHA-256 of canonical JSON: json.dumps(bundle, sort_keys=True, separators=(',', ':'), ensure_ascii=False) before POST.


4. Field ownership — the security spine

FieldSourceNotes
slug, title, summary, body_html, categories, badge, publish_at, expire_atClient (packet)Parameter-bound at insert.
author_keyUserIDClient names; server resolvesUnknown key → 422. Never accept raw UserID.
image_manifestClientPrecondition check on disk; builds Image rows.
references_htmlServerTemplate8 + authoritative article_id. Client value ignored.
ArticleIDServer / DBSCOPE_IDENTITY(). Authoritative.
dnn_database, ModuleID (546), image folder DB pathServer config
CustomFieldID 1 (badge), 2 (references)Server constants
ImageUrl = NULL, URL = NULL, FileCount = 0, ImageCount = 4Server (Ventrian constants)
Title apostropheNormalized to U+2019 in syn_ventrianSame as syn_sql_build.py
StartDateServerpublish_at if future, else NULL (existing CASE logic)
CreatedDate / LastUpdateServerGETDATE() on insert

5. References rebuild (server-side, load-bearing)

Laptop _merge_na_record() fills references using predicted next_article_id. Pre-commit validation requires syn-us evidence matching prediction. If identity drifts, stored references would be wrong.

Server rule: Before CustomValue insert for references (CustomFieldID = 2):

  1. Load Template8 from server path (mirror of assets/Templates/_Template8_References_Block_v1.html).
  2. Replace {{ArticleID}}, {{ARTICLEID}}, {{NA_ID}}, {{na_id}} with returned article_id.
  3. If facilitated attribution applies (server config flag mirroring attribution.facilitated_author_key), append steward <li> from server config.

Laptop may still send predicted refs for Gate 3 proof display; server ignores them at commit.


6. Preconditions (fail closed, before any write)

  1. Auth. Valid Bearer key. Else 401.
  2. Bundle validation. Required fields, types, 1–5 categories, four image manifest entries. Mirror _na_record_validation() blockers (title, hook, categories, no 127.0.0.1 in body if server re-checks). Else 422 with blockers[].
  3. Author allowlist. author_key ∈ server operators. Else 422.
  4. Image manifest precondition. Every file exists under server ventrian.image_folder. Missing → 422 with missing_files[]. Optionally verify bytes matches os.stat (warn or 422 on mismatch — prefer 422).

Only after all four pass does the transaction open.


7. Create-only + idempotency

Create-only. No update/delete. Corrections via DNN site editor.

Idempotency index (syn_publish_index.json on VPS): ``json { "first-tracks-reversal": { "article_id": 240, "committed_at": "2026-06-22T18:30:04Z", "title_hash": "sha256:…" } } ``

Index/SQL non-atomicity (explicit):

Three artifacts:


8. Lattice audit ledger (laptop)

Path: scripts/dnn_publish_ledger.yaml (create on first commit).

```yaml

fractome: "scotomaville" slug: "first-tracks-reversal" article_id: 240 syndication_url: "https://scotomaville.com/syn-us?na=240" author_key: "arnie" author_userid: 5 packet_sha256: "…" image_manifest: [ first-tracks-reversal_200.jpg, … ] server_host: "100.69.160.85" key_id: "scotomaville-2026" server_build: "lattice@abc123" result: "created" ```


9. Server config — `syn_publish_server.yaml` (VPS only)

```yaml fractome: "scotomaville" bind: host: "100.69.160.85" port: 8443 tls: enabled: true cert_file: "W:/_LatticePublish/scotomaville/certs/tailscale.pem" key_file: "W:/_LatticePublish/scotomaville/certs/tailscale-key.pem" auth: keys: - id: "scotomaville-2026" secret_hash: "<bcrypt hash>" database: driver: "ODBC Driver 18 for SQL Server" server: "127.0.0.1" name: "IN9_Scotomaville_Syn"

Option A: Windows service account with trusted connection

trusted_connection: true

Option B: localhost SQL login (never on laptop)

user: "LatticePublish"

password_env: "LATTICE_PUBLISH_SQL_PASSWORD"

encrypt: true ventrian: module_id: 546 portal_id: 0 image_folder: "W:/IN9_Scotomaville_Syn/Portals/0/images/articles/arnie" image_folder_db: "images/articles/arnie/" public_base_url: "https://syn.scotomaville.com/Portals/0/images/articles/arnie" syndication_url_pattern: "https://scotomaville.com/syn-us?na={na_id}" custom_field_badge_id: 1 custom_field_references_id: 2 references_template: "W:/_LatticePublish/scotomaville/templates/_Template8_References_Block_v1.html" attribution: facilitated_author_key: "arnie" steward_reference_html: '<li><em>Carbon steward: Daniel Comp · Lattice facilitation</em></li>' operators: daniel: { userid: 3 } angelina: { userid: 4 } arnie: { userid: 5, username: "syndicator" } index_file: "W:/_LatticePublish/scotomaville/syn_publish_index.json" deploy: lattice_scripts_version: "v2-build" ```

Service account: Run syn_publish_api.py as a dedicated Windows service identity trusted by SQL Server (local LatticePublish login with INSERT on Ventrian tables only, or Windows auth mapped to db_datawriter on IN9_*_Syn).


10. The transaction (`syn_publish_api.py` core)

Order, one SQL transaction:

  1. INSERT DnnForge_NewsArticles_ArticleSCOPE_IDENTITY()article_id.
  2. Build references_html from Template8 + article_id (§5).
  3. INSERT DnnForge_NewsArticles_Page — single row, SortOrder = 0, PageText = body_html.
  4. INSERT DnnForge_NewsArticles_Image — 4 rows (thumb sort 0, banner 1, tablet 2, mobile 3).
  5. INSERT DnnForge_NewsArticles_ArticleCategories — Cat1 always; Cat2–5 if not null (same IF pattern as syn_sql_build.py).
  6. INSERT DnnForge_NewsArticles_CustomValue — badge (field 1), references (field 2).
  7. COMMIT.
  8. Write syn_publish_index.json entry (after commit success).

On any exception before COMMIT: ROLLBACK; index untouched.

Response: ``json { "ok": true, "article_id": 240, "syndication_url": "https://scotomaville.com/syn-us?na=240", "already_published": false } ``


11. Error taxonomy

HTTPCondition
200Created or idempotent replay
400Malformed JSON / missing required fields
401Bad/missing API key
422Validation blockers, unknown author_key, missing images, bytes mismatch
500DB failure (rolled back) or index write after commit (see §7)

Client: non-2xx = not published (except replay 200). index_write_failed with article_id = manual reconcile, no retry.


12. Laptop state machine

`` proof_approved → publish-images (SFTP) or record-publish (manual W: + HTTPS verify) → images_published → commit (POST hub API) → published (+ archive to published_root) ``

StateSet by
images_published_finalize_publish() in syn_publish.py after successful upload/verify
publishedcommit success on laptop

/api/pipeline/{slug}/commit:

build-sql (optional, operator debug):

record-publish: Manual image path when SFTP times out — must also advance to images_published (same gate as SFTP).


13. Laptop config additions (`syn_config.yaml` publish section)

```yaml publish:

… existing sftp_* fields …

api_host: "100.69.160.85" api_port: 8443 api_key_file: "D:/04_WORK/Code/_APIs/scotomaville_publish_key.txt" api_key_id: "scotomaville-2026" ledger_file: "dnn_publish_ledger.yaml" ```

Key file contains raw secret only (one line). Never commit to git.


14. Explicitly out of scope (v2)


15. Tailscale ACL

``` hub-owner-laptop → hub-server:22 hub-owner-laptop → hub-server:8443

NOT

:1433 from anywhere off-box ```


16. Acceptance checklist


17. Build order (Grok_Build)

  1. scripts/syn_ventrian.py — factor from syn_sql_build.py; parameterized ops; render_sql_script() for SSMS debug; self-test against makers-exodus / next packet.
  2. Refactor syn_sql_build.py — thin wrapper calling syn_ventrian.
  3. images_published statesyn_publish.py _finalize_publish; update app.py / index.html gate buttons.
  4. scripts/syn_publish_client.py — bundle builder + POST + ledger + registry reconcile.
  5. scripts/syn_publish_api.py + syn_publish_server.yaml.example — VPS service (dev: mock SQL or Daniel's RDP test).
  6. syn_pipeline.py --commit + app.py POST /api/pipeline/{slug}/commit.
  7. Archive on publish in pipeline.
  8. Daniel: deploy API to fractal-server; generate API key; first live commit NA 240.

NA 238 proved the Ventrian write model. v2 wraps it in a fractome-scoped trust boundary safe for hub-owner laptops.

Ask Daniel's CODEX