Skip to content
getnextpdf.com

Enterprise edition

AST audit trail — Deep Reference

The Enterprise AST module records document mutations and prepares documents for retrieval pipelines.

  • AstAuditTrailInterface defines an append-only, per-document audit trail over the Pro AST MutationLog.
  • AstAuditEntry is an immutable record of one mutation: node identity, mutation kind, page, before/after snapshots, UTC timestamp.
  • InMemoryAstAuditTrail is the per-process reference implementation of the trail contract.
  • AstAwareChunker walks the AST depth-first and emits citation-anchored AstChunk values for RAG ingestion.

This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.

The AST audit-trail surface is licensed by the enterprise.compliance.evidence capability. A denied entitlement denies the feature.

TierProvides
CoreAST document model (AstDocument, AstNode, NodeId)
ProAST mutation flow and MutationLog
EnterpriseAppend-only per-document audit trail; citation-anchored chunker

The Enterprise surface consumes the Pro mutation log. It does not replace the AST model.

Terminal window
composer require nextpdf/enterprise:^3
SymbolParametersDefault behaviorReturnsThrows or fails withNotes
AstAuditTrailInterface::record()string $documentSourceHash, MutationLog $logConverts each mutation entry in the log to an AstAuditEntry and appends itvoidNothing in the reference implementationRepeated calls with the same hash accumulate entries
AstAuditTrailInterface::findByDocument()string $documentSourceHashReturns the entries recorded for one document, in insertion orderlist<AstAuditEntry>Nothing in the reference implementationEmpty list when no entries match the hash
AstAuditTrailInterface::count()noneCounts audit entriesint<0, max>Nothing in the reference implementationTotal across all documents, not per-document
InMemoryAstAuditTrailnoneArray-backed trail scoped to the current processimplements AstAuditTrailInterfaceNothingNot durable; suited to single-request lifecycles
AstAuditEntry::__construct()promotes all fieldsImmutable audit recordvalue objectNothingfinal readonly; see signature fence below
AstAwareChunker::__construct()int $maxChunkChars = 1500, int $overlapChars = 150Validates chunking bounds at constructioninstanceInvalidArgumentException on out-of-range configurationBounds: 16 <= maxChunkChars <= 1048576; 0 <= overlapChars < maxChunkChars
AstAwareChunker::chunk()AstDocument $documentDepth-first walk; headings delimit chunks; leaf text accumulateslist<AstChunk>NothingEmpty list for a document without accumulable text
AstChunk::__construct()promotes all fieldsCitation-anchored chunk recordvalue objectNothingfinal readonly; see signature fence below
namespace NextPDF\Enterprise\Ast;
use NextPDF\Pro\Ast\Mutation\MutationLog;
interface AstAuditTrailInterface
{
public function record(string $documentSourceHash, MutationLog $log): void;
/** @return list<AstAuditEntry> */
public function findByDocument(string $documentSourceHash): array;
/** @return int<0, max> */
public function count(): int;
}
final readonly class AstAuditEntry
{
public function __construct(
public readonly string $documentSourceHash,
public readonly string $nodeId,
public readonly string $mutationType,
public readonly int $pageIndex,
public readonly array $before,
public readonly array $after,
public readonly DateTimeImmutable $occurredAt,
) {}
}
final class AstAwareChunker
{
public function __construct(
private readonly int $maxChunkChars = 1500,
private readonly int $overlapChars = 150,
) {}
/** @return list<AstChunk> */
public function chunk(AstDocument $document): array {}
}
final readonly class AstChunk
{
public function __construct(
public readonly string $text,
public readonly string $nodeId,
public readonly int $pageIndex,
public readonly ?array $bbox,
public readonly string $nodeType,
public readonly string $documentSourceHash,
public readonly int $chunkIndex,
) {}
}
  • Append-only. Implementations must be append-only: a recorded entry cannot be modified or removed through this API. Repeated record() calls with the same hash accumulate entries.
  • Conversion. record() converts each entry of the Pro MutationLog (via MutationLog::all()) into an AstAuditEntry and appends it. All entries produced by one record() call share one UTC occurredAt timestamp.
  • Per-document isolation. findByDocument() filters on the exact document source hash and preserves insertion order. count() is the total across all documents.
  • Snapshots. before and after are attribute maps keyed by text_content. An updated mutation fills both sides; inserted leaves before empty; deleted leaves after empty. mutationType is the string value of the Pro MutationType enum: updated, inserted, or deleted.
  • Page derivation. pageIndex is extracted from the canonical node ID (ast:{hash}:{page}:{seq}). A malformed node ID yields pageIndex 0; the entry is still recorded.

Append-only is a contract of the configured store, not a cryptographic property. Tamper-evidence and non-repudiation come from how the trail is persisted and timestamped (Evidence module), not from this module alone.

  • Traversal. chunk() walks the AST depth-first from the document root.
  • Text accumulation. Leaf text of type Paragraph, ListItem, TableCell, Code, or Annotation accumulates into the current buffer. Container types (Document, Section, Artifact, FormField, Figure, Table, List, TableRow) are traversed without emitting text.
  • Delimiters. A Heading node flushes the current buffer as a chunk and seeds the next buffer with the heading text.
  • Splitting. When accumulated text would exceed maxChunkChars, the chunker fills the remaining space, flushes the chunk, and continues with the last overlapChars characters plus the overflow. Length accounting is UTF-8 character based.
  • Citation anchor. Each AstChunk carries the nodeId, pageIndex, bbox, and nodeType of its first contributing node, plus the document source hash and a sequential 0-based chunkIndex.
  • Finalization. A trailing buffer with non-whitespace content is flushed as the final chunk; whitespace-only remainders are discarded, and chunk text is trimmed.
  • Recording the same MutationLog twice accumulates duplicate entries; idempotency must be enforced upstream.
  • A fresh, unshared InMemoryAstAuditTrail is always empty. The integration contract requires one shared AstAuditTrailInterface instance handed to both the mutation-producing flow and the audit-reading consumer, with record() called after each successful write. Until then, findByDocument() returns an empty list and count() returns 0.
  • The in-memory trail is per-process and not durable; entries do not survive the request that created them. Production supplies a persistent implementation.
  • A node ID that fails canonical parsing does not abort recording; the affected entry falls back to pageIndex 0.
  • AstAwareChunker::__construct() rejects degenerate configuration (overlapChars >= maxChunkChars, or maxChunkChars outside [16, 1048576]) with InvalidArgumentException. This prevents unbounded buffer growth during chunking.
  • AstChunk::$bbox is null when the first contributing node carries no bounding box.
  • A document with no accumulable text yields an empty chunk list.
  • This module performs no cryptographic operations. Hashing, signing, and timestamping for tamper-evidence are handled by the Evidence, Security, and Signature modules; FIPS-mode policy lives there.
BehaviorReference
Incremental-update / signature-integrity contextISO 32000-2:2020 §12.8

The audit trail is a record-keeping aid. It supports audit-style evidence workflows.

  • Supply a durable AstAuditTrailInterface implementation for cross-request retention. Persist it in a WORM-capable store where compliance requires immutability; the append-only guarantee is only as strong as the backing store.
  • Mutation snapshots can carry personal data; data residency follows the operator’s store.
  • The trail consumes the Pro mutation log as produced; it does not re-derive mutations from document state.
  • Chunker defaults (maxChunkChars 1500, overlapChars 150) suit typical RAG ingestion; tune within the documented bounds for embedding models with different context budgets.
  • Internal mechanism detail stays in the source repository’s internal documentation and is out of scope for this manual.

This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.