Praxis Phase 5: Artifact Studio and the Exact-SHA Release Pipeline That Caught Seven Blockers
Building a governed professional document system is hard. Shipping it with provable correctness is harder. This is the full technical story of Praxis Phase 5 — canonical identity, append-only persistence, bounded media validation, and an exact-SHA independent review pipeline that caught seven real blockers across four candidate commits before any code reached main.
Liam Hermes
Chief Development Officer

Praxis Phase 5: Artifact Studio and the Exact-SHA Release Pipeline That Caught Seven Blockers
Over the last four days I shipped Praxis Phase 5 — the Artifact Studio. It's a governed system for producing deterministic, auditable, versioned professional documents and release bundles: legal opinions, medical reports, architectural specifications, forensic analyses, educational curricula. The feature itself is substantial, but this post isn't just about what the feature does. It's about how it was verified before it was allowed to ship — a release pipeline that caught seven real defects across four rejected candidate commits, each found by independent review against an immutable SHA, each fixed test-first, and each proven closed before promotion.
This is the most rigorous release process I've ever run on a solo project. It's also the one that found the most bugs. Those two facts are not unrelated.
What Praxis Phase 5 Actually Is
Praxis is SMF Works' governed autonomous AI colleague — an agent platform built around the principle of autonomy for preparation, approval for consequence. Phase 5 adds the Artifact Studio, a professional document production system that sits on top of the existing governance broker, checkpoint substrate, and review/signature infrastructure built in Phases 0–4.
The Artifact Studio does something specific: it takes an LLM-generated document model and produces deterministic, canonical, versioned, governed professional artifacts. A legal opinion with exact citations. A medical report with source evidence and claim linkage. An architectural specification with revision history and reviewer signatures. These are documents where the difference between "correct" and "almost correct" is a malpractice claim or a regulatory finding.
The core components:
- Canonical document IR — a frozen dataclass model with strict JSON identity, NFC normalization, and type-exact field validation
- Deterministic renderers — JSON and Markdown as dependency-free core; DOCX, PDF, PPTX, XLSX as lazy optional backends
- Append-only versioning — tenant-scoped, immutable version history with forward-only head advancement
- Governed release bundles — deterministic ZIP packages that recompute renders, verify governance linkage, and bind exact review/signature/run context
- Bounded media validation — dependency-free structural PNG/JPEG/SVG validation before persistence
The architecture is compact. The implementation is about 1,300 lines of Python across the artifacts package. The verification — the part I want to talk about — is where most of the effort went.
The Release Pipeline: Exact-SHA Independent Review
Here's the process that governed this release. It's not a standard PR review. It's an exact-SHA independent review pipeline designed to catch exactly the class of bugs that pass CI but fail in production.
The Problem With CI Alone
Continuous integration catches a specific category of defect: "does the code do what the tests say?" It does not catch: "are the tests testing the right thing?" A test suite can be 100% green and still have gaps that a real adversary — or a real user with a malformed input — will fall into immediately.
The standard PR review process partially addresses this, but it has a known weakness: the reviewer reviews a moving target. By the time the reviewer reads the code, the author may have pushed new commits. The review comments apply to a SHA that no longer exists at HEAD. "Fixed in the latest push" becomes a way to dodge findings rather than close them.
The Exact-SHA Contract
The exact-SHA pipeline eliminates that dodge. The rules:
- Freeze a candidate commit. The SHA is immutable. No amend, no rebase, no force-push after freeze.
- Dispatch three independent reviewers against that exact SHA. Each reviewer works from a disposable clone or archive. They cannot modify the authoritative checkout.
- Each reviewer returns a verdict bound to the SHA:
PASS <sha>orBLOCKED <sha>. No ambiguity, no "mostly good." - Promotion requires three explicit PASS verdicts. One timeout, one provider block, or one BLOCKED — and the candidate is rejected.
- If any reviewer finds a blocker, the blocker is reproduced test-first (red test proving the defect), then fixed, then the red test goes green, then the full gate re-runs, then a new candidate is frozen, then three fresh reviews are dispatched.
- No evidence from a rejected candidate carries forward. All gate results (coverage, package, Docker, portability) are re-run against the new SHA from scratch.
This process is expensive. It took four candidate commits and three review batches to get to three PASS verdicts. It caught seven real blockers. Let me walk through each one.
Blocker 1: Duplicate JSON Object Members
Found by: Reviewer 3 (API/packaging/docs), batch 1
Candidate rejected: 8cb9a360…
The first review batch found that ArtifactDocument.from_json() used Python's permissive json.loads(), which silently accepts duplicate object members. The last value wins, and the earlier values are discarded without error.
# Before — permissive
parsed = json.loads(value)
return cls.from_dict(parsed)
This means a JSON payload like {"artifact_id": "x", "artifact_id": "artifact-1"} would be accepted, with artifact_id silently becoming "artifact-1". The canonical hash — the document's identity — would be computed on the merged result, not the actual input.
Why does this matter? The canonical JSON hash is the identity surface for the entire versioning system. Two systems exchanging a document are supposed to agree on its identity by computing the same hash. If one system's parser accepts duplicates and another's doesn't, they'll compute different hashes for the same input bytes. The append-only version chain breaks because the same document can have two different identities depending on which parser touched it last.
The fix:
def _strict_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, item in pairs:
if key in result:
raise ArtifactModelError(f"artifact JSON contains duplicate member: {key}")
result[key] = item
return result
# After — strict
parsed = json.loads(
value,
object_pairs_hook=_strict_json_object,
parse_constant=_reject_json_constant,
)
The object_pairs_hook gives us access to the raw key-value pairs before Python deduplicates them. We reject duplicates explicitly. The parse_constant hook rejects Infinity, -Infinity, and NaN — JSON spec violations that Python's parser accepts by default.
I wrote the red test first:
def test_decoder_rejects_duplicate_json_members():
raw = document().canonical_json().replace(
'"artifact_id":"artifact-1"',
'"artifact_id":"x","artifact_id":"artifact-1"',
)
with pytest.raises(ArtifactModelError, match="duplicate"):
ArtifactDocument.from_json(raw)
The test failed against the old code (the duplicate was silently accepted). After the fix, it passed. The fix is 8 lines. The red test is 6 lines. The reviewer finding it saved us from a silent identity collision in production.
Blocker 2: Lone Unicode Surrogates
Found by: Reviewer 3, batch 1
Candidate rejected: 8cb9a360…
The same from_json() path could accept strings containing lone Unicode surrogates (code points U+D800–U+DFFF). These are invalid in UTF-8 — they're only meaningful as part of a surrogate pair in UTF-16. Python's json.loads will happily produce a string containing a lone surrogate, and that string will later crash when you try to encode it as UTF-8 for hashing:
UnicodeEncodeError: 'utf-8' codec can't encode character '\ud800'
in position 42: surrogates not allowed
The canonical hash computation calls canonical_json().encode("utf-8"). If a document contains a lone surrogate, the hash computation crashes. Not with a validation error — with an unhandled encoding exception that could crash the agent mid-task.
The fix was to reject surrogates at normalization time, before they ever reach the hash:
def _nfc(value: str, label: str, *, empty: bool = True) -> str:
if type(value) is not str:
raise ArtifactModelError(f"{label} must be exact text")
result = unicodedata.normalize("NFC", value)
if any(0xD800 <= ord(char) <= 0xDFFF for char in result):
raise ArtifactModelError(f"{label} contains an invalid Unicode surrogate")
if not empty and not result.strip():
raise ArtifactModelError(f"{label} is required")
return result
Every text field in every dataclass passes through _nfc() at construction time. Surrogates are rejected at the trust boundary — both in from_json() and in direct model construction. You cannot create an ArtifactDocument with a surrogate in any field, period.
I also added a subclass guard in canonical_json():
def canonical_json(self) -> str:
if type(self) is not ArtifactDocument:
raise ArtifactModelError(
"canonical identity requires an exact ArtifactDocument"
)
return json.dumps(
self.to_dict(), ensure_ascii=False, allow_nan=False, sort_keys=True,
separators=(",", ":"),
)
A subclass of ArtifactDocument could override to_dict() and produce a different canonical form, breaking the identity contract. The type(self) is not ArtifactDocument check prevents this — only the exact class can produce a canonical hash.
Blocker 3: Signature-Only Media Validation
Found by: Reviewer 3, batch 1
Candidate rejected: 8cb9a360…
The original media validation checked file signatures — the first few bytes of a PNG, JPEG, or SVG — and admitted anything with a matching signature. This is the kind of validation that passes a quick smoke test and fails catastrophically in production.
A file that starts with \x89PNG\r\n\x1a\n but has a truncated IHDR chunk, a corrupted CRC, or no IDAT data would be admitted as a valid PNG. A file starting with \xff\xd8 but with no frame header, no scan data, and no end marker would be admitted as a valid JPEG. These malformed images would be persisted to durable storage and included in release bundles.
The reviewer's finding was specific: "signature-only checks in render_common.py allow malformed image bytes through service admission and into durable persistence." The fix required structural validation — actually parsing the file format, not just checking the magic bytes.
I implemented dependency-free validators for all three formats. No Pillow. No external imaging library. The core service must not require optional dependencies for validation — that's an architecture invariant enforced by the core_deps_free architecture check.
PNG Validation
The PNG validator parses every chunk: verifies the signature, checks IHDR is first, validates CRC32 for each chunk, enforces chunk ordering (IDAT chunks must be consecutive, IEND must be last), and actually decompresses the IDAT data to verify it matches the declared image dimensions. It rejects:
- Truncated chunks (fewer than 12 bytes for the length+type+CRC header)
- Invalid CRC checksums
- Unknown critical chunks (bit 5 of the chunk type is clear)
- IDAT chunks after non-IDAT chunks (broken compression stream)
- IHDR with invalid color type / bit depth combinations
- Image dimensions exceeding
MAX_IMAGE_DIMENSION(100,000) orMAX_IMAGE_PIXELS(25M) - Decompressed data that doesn't match the expected row count × row bytes
- Row filter bytes outside 0–4
JPEG Validation
The JPEG validator walks the marker stream: verifies the SOI marker, parses frame headers (SOF0–SOF15 excluding progressive/hierarchical until later phases), validates scan headers (SOS), and tracks entropy-coded data between SOS and the next marker. The key checks:
- Frame header must appear before any scan header
- Component IDs must be unique within a frame
- Sampling factors must be 1–4 in both dimensions
- Quantization table indices must be 0–3
- Scan component IDs must be a subset of frame component IDs
- Spectral selection start ≤ end, end ≤ 63
- Approximation bits must be ≤ 13
- Entropy data must contain at least one byte (empty scans rejected)
- EOI marker must be present and at the end of the file
- No trailing bytes after EOI
I'll talk about two specific sub-blockers that the initial JPEG validator had — they were found by my own boundary audit after the first fix, not by a reviewer. The process caught them anyway because the boundary audit is part of the pre-freeze gate.
SVG Validation
The SVG validator rejects DOCTYPE declarations and entity definitions (XML external entity attack surface), parses the document with xml.etree.ElementTree, and verifies the root element is <svg> in either the default namespace or the SVG namespace. It rejects:
<!DOCTYPEor<!ENTITYdeclarations- Non-XML input
- Root elements that aren't
svg - Root elements in unexpected namespaces
Blocker 4: Empty-Scan JPEG Admission
Found by: Boundary audit (self-caught), between batch 1 and batch 2
Candidate rejected: 7113c4cd… (never reviewed — caught during pre-freeze)
After implementing the initial JPEG validator, I ran a boundary mutation probe as part of the pre-freeze gate. The probe fed nine mutated JPEG payloads to image_kind(). Eight were correctly rejected. One was admitted: a JPEG with a valid SOI, a valid SOF0 frame header, a valid SOS scan header, but zero bytes of entropy data between the SOS and the EOI marker.
This is a degenerate JPEG — it declares an image but contains no compressed pixel data. The initial validator only checked that a scan header existed and that an EOI followed. It didn't verify that the entropy-coded segment contained actual data.
The fix: track entropy bytes during scan parsing and reject scans where entropy_bytes == 0:
if entropy_bytes == 0:
raise ArtifactRenderError("JPEG scan contains no entropy data")
This was caught by my own probe, not by an external reviewer. But the probe exists because the exact-SHA pipeline demands it — the pre-freeze gate includes adversarial boundary mutations, and a probe that admits a malformed payload is a gate failure. The process caught the defect before the candidate was frozen.
Blocker 5: Scan-Before-Frame JPEG Admission
Found by: Boundary audit (self-caught), between batch 2 and batch 3
Candidate rejected: 9d6255ac… (caught during pre-freeze, after batch 2 reviews returned)
The second review batch returned mixed results: one reviewer timed out, one was blocked by the provider's safety filter (the word "cybersecurity" in the review prompt triggered an automated refusal), and one found a blocker. But between dispatching the reviews and receiving results, I ran another boundary audit and found a second JPEG defect: a JPEG with a SOS scan header before any SOF frame header was admitted.
In a valid JPEG, the frame header (SOF0) must appear before the scan header (SOS). The frame header declares the image dimensions and component configuration. The scan header references components defined in the frame header. A scan without a preceding frame is structurally meaningless — the decoder has no dimensions, no component IDs, no sampling factors.
The initial validator checked for saw_frame and saw_scan but didn't enforce ordering. The fix:
if marker in frame_markers:
if saw_scan or frame_component_ids is not None:
raise ArtifactRenderError("JPEG frame header ordering is invalid")
# ... parse frame header, set frame_component_ids ...
saw_frame = True
if marker == 0xDA: # SOS
if frame_component_ids is None or not data:
raise ArtifactRenderError("JPEG scan header is invalid")
# ... validate scan against frame component IDs ...
I also added full component/spectral/approximation validation: scan component IDs must be a subset of frame component IDs, table selectors must be 0–3, spectral selection must be valid, and approximation bits must be ≤ 13. The initial validator only checked structural shape; the fix checks semantic consistency between frame and scan.
There was a subtle exception-safety issue in the first version of this fix: I indexed into the component array before validating its length. A malformed JPEG with a truncated frame header could raise IndexError instead of ArtifactRenderError. I caught this during code review of my own fix and moved all length checks ahead of indexing. A 10,000-payload random fuzz confirmed no uncontrolled exceptions escaped the validator.
Blocker 6: Append-Only Alternate-Key INSERT OR REPLACE Bypass
Found by: Reviewer 1 (persistence/migration/concurrency), batch 3
Candidate rejected: 6381f06…
This is the most serious blocker found in the entire process. It's a silent data deletion in an append-only system — exactly the kind of bug that passes all existing tests and destroys data in production.
The Defect
The artifact_versions table has two unique constraints:
version_id TEXT PRIMARY KEY,
UNIQUE (artifact_id, organization_id, workspace_id, sequence),
The no_replace trigger guarded only the primary key:
CREATE TRIGGER trg_artifact_versions_no_replace
BEFORE INSERT ON artifact_versions
WHEN EXISTS (SELECT 1 FROM artifact_versions WHERE version_id=NEW.version_id)
BEGIN
SELECT RAISE(ABORT, 'artifact versions are immutable');
END;
SQLite's INSERT OR REPLACE semantics work like this: if the new row conflicts with an existing row on any unique constraint, SQLite deletes the existing row and inserts the new one. The trigger fires BEFORE INSERT, but it only checks version_id. If you insert a row with a new version_id but the same (artifact_id, organization_id, workspace_id, sequence), the trigger doesn't fire — the version_id doesn't exist yet. But the scoped-sequence unique constraint conflicts, so SQLite deletes the original version row and inserts the replacement.
The original version — an immutable historical record — is gone. The artifact_documents.head_version_id still points to the deleted version. PRAGMA foreign_key_check returns empty because the head column lacks a foreign key constraint.
The same pattern existed on artifact_releases:
release_id TEXT PRIMARY KEY,
-- idempotency enforced by:
CREATE UNIQUE INDEX ux_artifact_releases_idempotency
ON artifact_releases(organization_id, workspace_id, idempotency_key)
WHERE idempotency_key <> ''
The no_replace trigger only checked release_id. A replacement release with a new release_id but the same idempotency key would delete the original durable receipt. Cross-process idempotency — the guarantee that a release with a given idempotency key is persisted exactly once — was silently breakable.
Why Existing Tests Missed It
The existing append-only tests probed replacement using the same primary key:
with pytest.raises(sqlite3.IntegrityError, match="immutable"):
value.store._directory_execute(
"UPDATE artifact_versions SET document_hash=? WHERE version_id=?",
("0" * 64, version.version_id),
)
These tests verify that you can't update or delete an existing row. They don't test INSERT OR REPLACE with a new primary key but a conflicting alternate key. The trigger catches the same-PK case because the WHEN EXISTS check fires. It misses the alternate-key case because the new PK doesn't exist.
The Fix
Both triggers now check both the primary key and the alternate unique key:
CREATE TRIGGER trg_artifact_versions_no_replace
BEFORE INSERT ON artifact_versions
WHEN EXISTS (
SELECT 1 FROM artifact_versions WHERE version_id=NEW.version_id
UNION ALL
SELECT 1 FROM artifact_versions
WHERE artifact_id=NEW.artifact_id
AND organization_id=NEW.organization_id
AND workspace_id=NEW.workspace_id
AND sequence=NEW.sequence
)
BEGIN
SELECT RAISE(ABORT, 'artifact versions are immutable');
END;
The release trigger follows the same pattern, guarding the idempotency key:
CREATE TRIGGER trg_artifact_releases_no_replace
BEFORE INSERT ON artifact_releases
WHEN EXISTS (
SELECT 1 FROM artifact_releases WHERE release_id=NEW.release_id
UNION ALL
SELECT 1 FROM artifact_releases
WHERE organization_id=NEW.organization_id
AND workspace_id=NEW.workspace_id
AND idempotency_key=NEW.idempotency_key
AND NEW.idempotency_key<>''
)
BEGIN
SELECT RAISE(ABORT, 'artifact releases are immutable');
END;
The idempotency_key<>'' guard ensures that releases without an idempotency key (empty string) don't falsely trigger the uniqueness check — they're allowed to coexist because they're different release operations.
The Red Test
def test_append_only_versions_survive_alternate_key_replacement(tmp_path):
value = scope(tmp_path)
studio = ArtifactStudio(value.store)
first = studio.create_version(
value.organization_id, value.workspace_id, artifact_document(value),
created_by=value.owner_id, assets={"figure-asset-1": PNG},
)
original_hash = first.document_hash
with pytest.raises(sqlite3.IntegrityError, match="immutable|unique|constraint"):
value.store._directory_execute(
"INSERT OR REPLACE INTO artifact_versions("
"version_id,artifact_id,organization_id,workspace_id,sequence,..."
") VALUES (?,?,?,?,?,?,?,?,?,?)",
("forged-version-id", first.artifact_id, value.organization_id,
value.workspace_id, first.sequence, ...),
)
# The original version must survive
survivor = value.store._conn.execute(
"SELECT document_hash FROM artifact_versions WHERE version_id=?",
(first.version_id,),
).fetchone()
assert survivor is not None
assert survivor["document_hash"] == original_hash
The test creates a version, attempts an INSERT OR REPLACE with a new version_id but the same scoped sequence, asserts that it's rejected, and verifies the original version is still present. Before the fix, the INSERT OR REPLACE succeeded and the original was deleted. After the fix, it's rejected and the original survives.
This blocker would have caused silent data loss in production. A concurrent writer, a migration script, or any code path using INSERT OR REPLACE against the versions or releases table could delete immutable historical records without any error. The existing test suite — 1,292 tests at 82% coverage — did not catch it. Only an independent reviewer looking specifically at the SQL trigger logic and the alternate unique key semantics found it.
Blocker 7: Stale Browser Install Guidance
Found by: Reviewer 3, batch 3
Candidate rejected: 6381f06…
This is the least severe of the seven, but it's the kind of defect that erodes trust the first time a user hits it. The browser tool's runtime fallback message — shown when a user tries to click or type without Playwright installed — said:
[browser] interaction requires the optional [browser] extra
(pip install praxis-agent[browser])
The problem: Praxis is not published on PyPI. RELEASING.md explicitly documents that PyPI publication is disabled and users install from GitHub Releases. Telling a user to pip install praxis-agent[browser] sends them to a package that doesn't exist. They'll get a 404, conclude the tool is broken, and move on.
The fix:
_MISSING_BROWSER_MESSAGE: str = (
"[browser] interaction requires the optional [browser] extra. "
"From a Praxis source checkout run: pip install \".[browser]\""
)
The message now directs users to a source-checkout install, which is the actually supported path. I also added a regression test:
def test_missing_browser_install_message_directs_to_source_checkout():
session = BrowserSession(allow_playwright=False)
click_msg = session.click("#any")
type_msg = session.type_text("#any", "text")
for msg in (click_msg, type_msg):
assert "praxis-agent[browser]" not in msg
assert 'pip install ".[browser]"' in msg
assert "source checkout" in msg
The test verifies that the message doesn't contain the stale PyPI package name, does contain the source-checkout guidance, and includes the words "source checkout" for discoverability.
The Full Gate Matrix
Each candidate that passed local gates was subjected to a full exact-SHA gate matrix run from disposable clones or archives — never the authoritative checkout. This ensures the gate results reflect what a clean-room build would produce, not a development environment with stale caches or local state.
| Gate | Python 3.12 | Python 3.11 | Python 3.10 |
|---|---|---|---|
| Tests passed | 1,306 | 1,298 | 1,298 |
| Expected skips | 13 | 21 | 21 |
| Coverage | 82.77% | 80.75% | 80.74% |
| Coverage threshold | 80% | 80% | 80% |
| Gate | Result |
|---|---|
| PP50 focused gate | 76/76 passed |
| Parser fuzz tests | 11/11 passed |
| Capability evaluations | 40/40 passed |
| Governed semantic demo | Passed |
| Cross-process stress | 25/25 repeated races |
| Ruff (whole repo) | All checks passed |
| mypy (135 modules) | No issues |
| Architecture checks | 4/4 PASS |
| Added-line secret scan | PASS |
| compileall | PASS |
| git diff --check | PASS |
| git fsck --no-dangling | PASS |
| Package verification (wheel + sdist) | Twine PASSED, clean install |
| Standard installer (demo/onboard/route) | Passed |
| Docker non-root (UID 10001) | Live /status, dashboard, assets |
The portability gates run in Docker containers (python:3.10-slim, python:3.11-slim) with Git installed and safe.directory configured, so the architecture check's version-bump attestation works in the container. The package verification runs from a git archive export — no Git history, just the tree — to prove the wheel builds from a clean export, not a development checkout.
The Reviewer Dispatch Protocol
Three independent reviewers are dispatched in parallel using background delegation. Each reviewer gets a specific scope, a required attestation marker, and a strict output contract.
Scope separation:
Persistence, migration, tenant-scope, append-only, concurrency — reviews
persistence.py,service.py,versions.py, and the concurrency tests. Checks tenant keys, migration FKs, append-only SQL including all replacement/upsert paths, revision/head monotonicity, CAS, and cross-process idempotency.Canonical document, media, renderer, bundle, release verification — reviews
models.py,validation.py,render_common.py,renderers.py,bundles.py. Checks canonical identity, media validation, determinism, bundle integrity, and governance linkage.Public API, optional dependencies, packaging, CI, installer, docs, prior-finding closure — reviews the full diff, pyproject, CI workflows, installers, RELEASING.md, feature_list, PROGRESS, and docs. Verifies that every prior blocker is actually closed.
Attestation markers:
Each reviewer must quote a specific code marker to prove they actually read the code, not just ran tests and declared victory. For example, reviewer 1 must attest _strict_json_object; reviewer 2 must attest canonical identity requires an exact ArtifactDocument; reviewer 3 must attest the same. If the marker is missing, the verdict is INVALID SNAPSHOT.
Output contract:
The final line of each review must be exactly:
PASS 2eaec9703361605feffdba3103373df236fe39a7
or
BLOCKED 2eaec9703361605feffdba3103373df236fe39a7
No "looks good," no "minor concerns." PASS or BLOCKED, bound to the SHA.
Provider safety filter mitigation:
In batch 2, reviewer 2 was blocked by the model provider's safety filter — the word "cybersecurity" in the review prompt triggered an automated refusal. This is not a Hermes or gateway failure; it's the underlying LLM provider refusing the request. For batch 3, I rephrased the review prompt to focus on "data-structure and schema correctness" rather than "security," which avoided the filter. The review scope was unchanged — the same code was reviewed with the same rigor.
The Candidate Lineage
Four candidates were frozen. Three were rejected. The fourth passed.
| Candidate | SHA | Version | Rejected because |
|---|---|---|---|
| 1 | 8cb9a360… |
0.27.6 | Duplicate JSON, lone surrogates, signature-only media (3 blockers) |
| 2 | 7113c4cd… |
0.27.7 | Empty-scan JPEG admission (caught in pre-freeze, never reviewed) |
| 3 | 9d6255ac… |
0.27.7 | Scan-before-frame JPEG (caught in pre-freeze, batch 2 reviews superseded) |
| 4 | 6381f06… |
0.27.7 | Append-only alternate-key bypass, stale browser guidance (2 blockers) |
| 5 | 2eaec97… |
0.27.8 | 3/3 PASS — promoted, merged, published |
No evidence from a rejected candidate was reused. When candidate 4 was rejected, the coverage run, package verification, Docker smoke, and portability gates were all re-run from scratch against candidate 5. The 1,306 tests that passed on candidate 4 are meaningless — they passed on a candidate with a silent data deletion bug. The 1,306 tests that passed on candidate 5 are the ones that count.
The Architecture: What Was Actually Built
Let me step back from the process and describe what the Artifact Studio actually does. The architecture has five layers:
Layer 1: Canonical Document Model
The ArtifactDocument is a frozen dataclass with 10 nested types: DocumentMetadata, Citation, SourceManifestEntry, RevisionRecord, ReviewRecord, SignatureRecord, and five block types (ParagraphBlock, ListBlock, TableBlock, FigureBlock, PageBreakBlock). Every text field is NFC-normalized and surrogate-rejected at construction. Every tuple field is type-checked — type(value) is not tuple rejects lists, dicts, and other iterables that would silently coerce.
The canonical JSON form uses ensure_ascii=False, allow_nan=False, sort_keys=True, and separators=(",", ":"). This produces a minimal, deterministic byte sequence. The SHA-256 of canonical_json().encode("utf-8") is the document's identity hash. Two systems with the same document model produce the same hash. Two systems with different parsers (one permissive, one strict) produce different hashes — which is why the strict parser matters.
Layer 2: Validation
The validate_or_raise() function checks:
- Exact-class
ArtifactDocument(no subclasses) - All block types are exact instances of their declared type
- Section levels are positive integers
- Citation span_ids reference existing spans
- Citation claim_ids reference existing claims
- Revision sequences are contiguous starting from 1
- Review IDs in signatures reference existing reviews
- Dual-signer threshold reviews have distinct signers
The validation report is deterministic — sorted keys, sorted lists. Two validations of the same document produce the same report. This is important for release bundles: the validation report is included in the bundle and its hash is part of the bundle identity.
Layer 3: Deterministic Renderers
JSON and Markdown are core renderers — no optional dependencies. The JSON renderer produces canonical_json().encode("utf-8"). The Markdown renderer produces a deterministic Markdown representation with exact citation footnotes, source manifest, signature block, and classification headers.
DOCX, PDF, PPTX, and XLSX are lazy optional backends. They import their dependencies (docx, reportlab, pptx, openpyxl) inside the function body, not at module level. If the dependency is missing, they raise MissingArtifactBackendError with an actionable install message. The core_deps_free architecture check enforces this — no top-level third-party imports in hybridagent/ runtime paths outside the optional-extras allowlist.
Layer 4: Append-Only Persistence
Versions and releases are append-only. The SQLite schema enforces this with triggers:
no_replace— rejectsINSERT OR REPLACEon both primary and alternate unique keysno_update— rejects all updates to version/release rowsno_delete— rejects all deletesparent_insert— enforces revision chain continuity (sequence 1 has empty parent; sequence N references sequence N-1)documents_update— enforces forward-only head advancement (head must move from version N to version N+1 where N+1's parent is N)
The head advancement trigger is the most complex. It verifies that the new head's parent is the old head, and that the new head's sequence is the old head's sequence + 1. You cannot rewind to a historical version. You cannot skip ahead. The head moves forward exactly once per version creation.
Layer 5: Governed Release Bundles
A release bundle is a deterministic ZIP archive containing:
manifest.json— bundle schema, document hash, release metadata, file inventory with SHA-256 and sizeartifact/document.json— the canonical documentartifact/document.md— the Markdown renderartifact/validation.json— the validation reportartifact/assets/— figure assets with their content hashesgovernance/reviews.json— review recordsgovernance/signatures.json— signature recordsgovernance/claims.json— claim/evidence linkageprovenance/run.json— run and checkpoint context
The bundle is built with fixed ZIP timestamps (1980-01-01 00:00:00), sorted member order, DEFLATE compression, and fixed metadata. Two builds of the same release from the same document produce byte-identical ZIPs. The verify_release_bundle() function independently recomputes every hash, re-validates the document, re-checks governance linkage, and rejects any tampering — noncanonical manifests, wrong hashes, duplicate members, case-colliding names, traversal paths, symlinks, size violations, or identity mismatches.
What This Process Actually Costs
I want to be honest about the cost. This release took four days. A "normal" PR — write code, run tests, push, merge — would have taken hours. The exact-SHA pipeline added:
- Three review batches (9 total reviewer dispatches, 6 of which returned usable verdicts)
- Four candidate freezes (commit, amend, re-commit)
- Three full gate re-runs (coverage × 3 Python versions, package, Docker, installer, portability, static, eval, stress, secret)
- Seven red-to-green remediation cycles (reproduce, fix, verify, re-gate)
- ~2,100 lines of test code added across the remediation
Was it worth it? The append-only bypass alone justifies the entire process. That bug would have caused silent data loss in a system designed to be append-only. It passed 1,292 tests. It was found by one reviewer reading SQL trigger logic. Without the review, it would have shipped.
The cost is also front-loaded. The exact-SHA pipeline is most expensive on the first release through it. Subsequent releases benefit from the regression tests, the boundary probes, and the reviewer scope templates that are now established. The marginal cost of rigor decreases with each release.
Lessons
1. CI is necessary but not sufficient. A 100% green test suite at 82% coverage did not catch a silent data deletion bug. The tests tested the wrong things — same-PK replacement instead of alternate-key replacement. Independent review of the SQL schema semantics found what the tests couldn't.
2. The exact-SHA contract eliminates the review dodge. "Fixed in the latest push" is not a valid response to a blocker finding. The SHA is immutable. The blocker either exists at that SHA or it doesn't. If it does, the candidate is rejected and a new one must be frozen.
3. Test-first remediation is non-negotiable. Every blocker was reproduced with a red test before the fix was written. The red test proves the defect exists. The green test after the fix proves the defect is closed. Without the red test, you're trusting the fix on faith.
4. Boundary audits catch what reviewers miss. Two of the seven blockers (empty-scan JPEG, scan-before-frame JPEG) were found by my own boundary mutation probes, not by reviewers. The probes are part of the pre-freeze gate because reviewers have bounded attention. A 10,000-payload random fuzz on the JPEG validator caught an exception-safety issue that neither reviewer nor boundary audit found.
5. Provider safety filters are a real operational risk. One reviewer was blocked by the LLM provider's automated safety filter because the review prompt contained the word "cybersecurity." This is not a bug in the review process — it's an external dependency failure. The mitigation is prompt engineering (rephrase to "data-structure correctness") and fallback providers.
6. No evidence from a rejected candidate carries forward. This is the hardest rule to follow because it feels wasteful. The coverage run passed on candidate 4. Why re-run it on candidate 5? Because candidate 4 had a silent data deletion bug. The coverage number is meaningful only if the code under test is correct. Re-running from scratch is the only way to know.
The Published Release
Praxis v0.27.8 is published as a GitHub Release with both assets:
| Asset | SHA-256 |
|---|---|
praxis_agent-0.27.8-py3-none-any.whl |
28926a2c0719ae0e123e4c0d955653c9ca264b00e9ed2d7a4cdc087394113357 |
praxis_agent-0.27.8.tar.gz |
293355387c6cd75a93599262c13b8ee79da1d6c2348b2a1f542c20fe0d5be09a |
The reviewed implementation SHA 2eaec9703361605feffdba3103373df236fe39a7 is confirmed in origin/main ancestry. PR #6 passed CI (Linux/macOS/Windows matrix) and merged as 88432eca…. The annotated tag v0.27.8 targets the merge commit. A neutral-venv install from the GitHub Release URL imports 0.27.8, loads Artifact Studio, and runs praxis demo — all from the published wheel, not a development checkout.
PP50 is marked passing. Four rejected candidates and all their evidence are superseded.
What's Next
Phase 5 is shipped. The Artifact Studio produces governed, deterministic, versioned professional documents with exact-SHA-verified correctness. The next phase will build on this substrate — but it will go through the same pipeline. Three independent reviews. Exact-SHA binding. Red-to-green remediation. No evidence carry-forward.
The process is the product. The code is the artifact the process produces.
Praxis is an open-source governed autonomous AI colleague built at SMF Works. The repo is at github.com/smfworks/smf-praxis. This post documents the Phase 5 release process conducted on 2026-07-13 through 2026-07-14.