PoNIFs — Powerful, Non-Intuitive Features
PoNIFs — Powerful, Non-Intuitive Features
In one sentence. A PoNIF is a WIP behaviour that violates conventional platform-development expectations by design — not a bug, but a trap that catches every new developer who assumes conventional semantics.
Why it exists. Each PoNIF buys a capability a conventionally-behaved design cannot provide — history that never breaks, schema evolution without migration, identity that doesn't need a caller-tracked ID, batch writes that don't abort on the first bad record. The cost is that assuming the conventional pattern instead produces a silent failure, not an error: the call succeeds, returns 200, and does the wrong thing. The PoNIFs resource exists to name each trap before a caller meets it live.
The model
WIP currently documents eight PoNIFs, each a fixed (number, name) pair with a description, a Trap (the conventional assumption that breaks) and a Rule (what to do instead). A closing section, "The Compactheimer's Warning," is not a numbered PoNIF but a self-check list for AI assistants specifically.
| # | PoNIF | Conventional assumption it breaks |
|---|---|---|
| 1 | Nothing Ever Dies | Deactivating an entity makes it fail for existing references too, not just new ones. |
| 2 | Template Versioning — Update Does NOT Replace | Updating a schema replaces the old one; there is one active schema. |
| 3 | Document Identity — The Hash Decides | POST creates and PUT updates; the caller/URL controls identity, not the data. |
| 4 | Bulk-First — 200 OK Always | An HTTP 200 means the whole request succeeded. |
| 5 | Registry Synonyms — Multiple IDs Are Normal | One real-world entity has exactly one platform ID; two IDs means corrupt data. |
| 6 | Template Cache — Changes Aren't Instant | A template update is visible on the very next call that resolves "latest". |
| 7 | Edge Types Are Stored as Templates | A template with two document-reference fields is just an entity template with foreign keys. |
| 8 | versioned: false — Updates Overwrite In Place | Every update creates a new version — the rule PoNIF #2 itself just established. |
How it works
The list is served as a single static MCP resource, wip://ponifs, defined
by get_ponifs() in components/mcp-server/src/wip_mcp/server.py
(@mcp.resource("wip://ponifs")). It returns one Markdown document — the
eight numbered entries plus the closing warning — with no per-call
computation; unlike the adjacent wip://query-assistant-prompt resource in
the same file, it does not query live platform state to build itself.
The platform points callers at it explicitly rather than leaving discovery
to chance: the wip://development-guide resource (same file) instructs, in
its own words, "Read wip://ponifs before Phase 3. WIP has several powerful
but non-intuitive behaviours that will cause silent failures if you rely on
conventional assumptions" — i.e. before an agent starts designing a data
model or writing code against WIP.
Each numbered entry follows the same shape: a short description of the actual behaviour, a Trap naming the conventional assumption that fails, and a Rule stating the corrective action. Several entries carry an additional named subsection where the behaviour itself has a further nuance — PoNIF #1's "Default, not absolute", PoNIF #2's "Corollary" and "v2 caveat", PoNIF #3's "What's NOT in the identity hash".
Rules & invariants
1. Nothing Ever Dies. Deactivation (soft delete) removes an entity from
availability for new data but always still resolves for existing
references — "inactive" means "retired," not "deleted." This is the
platform default (a namespace's deletion_mode: "retain"), not an
absolute: a namespace explicitly switched to deletion_mode: "full"
(guarded — the wip namespace refuses the switch; retain → full requires
confirm_enable_deletion=true) accepts hard_delete=true on delete
operations, and the record is then permanently removed — existing
references will NOT resolve. Mutable-terminology terms and binary files are
hard-deletable regardless of deletion_mode.
2. Template Versioning — Update Does NOT Replace. Updating a template
creates a new version; the old version stays active, and new documents may
still be created against it. Rule: deactivate the old version
explicitly with deactivate_template() unless multi-version operation is
intended, and always pass template_version when creating documents.
Corollary: identity_hash scopes to template_id (PoNIF #3), and
template_id is stable across a template's versions — the one exception to
"new version → new ID". Adding a non-identity field and re-submitting an
existing document's identity values therefore yields an UPDATE (new
document version, new field populated), not a CREATE. To actively re-pin
existing documents onto a newer template version, migrate_documents
(POST /documents/migrate) is the validated, identity-preserving bulk
operation: dry-run validates each document against the target version and
reports per-document readiness; apply then writes a new document version
pinned to the target, keeping the same document_id and identity_hash.
The source version may be inactive — migrate validates against the target,
not the source. An identity-changing move is rejected as a fork, not a
migrate. Migration is never automatic.
3. Document Identity — The Hash Decides. A template's identity_fields
are hashed on write; the same hash versions an existing document (update),
a different hash creates a new one (create) — one call, a natural-upsert.
Zero identity_fields makes every submission a new document — append-only,
no update path — and PATCH by document_id is rejected with error_code
append_only (a document_id is a surrogate row handle; PATCH operates
on logical identity, which an identity-less document doesn't have).
template_version and namespace are not part of the identity hash —
namespace scoping happens externally via the composite key (namespace, identity_hash, template_id), so the same identity_hash in two namespaces
is two distinct entities. Adding a field to identity_fields is
breaking: every existing document hashes differently on the next write,
producing parallel orphan documents (the resource cites a real incident: an
external loader computed identity from undeclared fields, every hash
collided to empty, and 213 of 214 records were silently dropped). Adding a
field to data.* outside identity_fields is non-breaking (PoNIF #2's
corollary applies).
4. Bulk-First — 200 OK Always. Every WIP write API returns HTTP 200
even when individual items fail; per-item status lives in
results[i].status (created, updated, error, skipped). The MCP
server's single-item tools unwrap and surface errors automatically, but
bulk tools (create_documents_bulk, etc.) require checking per-item
results explicitly.
5. Registry Synonyms — Multiple IDs Are Normal. Any entity can carry
multiple identifiers (synonyms); two WIP IDs resolving to the same
real-world entity is a normal, expected state, not corruption.
merge_entries() reconciles duplicates — synonyms let a bank ID, an ERP
code, and a WIP UUID all resolve to the same entity.
6. Template Cache — Changes Aren't Instant. "Latest active" template
resolution carries a 5-second cache TTL; after a template update, the old
definition may still be served for up to 5 seconds. Lookups by an explicit
version are cached permanently, since an explicit version is immutable.
Rule: pass an explicit template_version, or wait out the 5-second
window after a template change.
7. Edge Types Are Stored as Templates. WIP stores two conceptually
distinct schemas in one representation: entity templates (usage: 'entity', the default) and edge types (usage: 'relationship'). Both live
in the templates collection and share the same APIs, but document-store
applies edge-type-specific handling to writes: extra cross-namespace and
not-archived validation, lazy Mongo indexes on data.source_ref /
data.target_ref, two dedicated query endpoints (/relationships,
/traverse), and reporting-sync columns (source_ref_id /
target_ref_id). The MCP tool create_edge_type exists specifically to
surface this distinction at the agent-facing ingress. usage is immutable
after creation. The allowed-endpoint set (source_templates /
target_templates) is append-only, not frozen — widen it with
add_edge_type_endpoints; removal stays unsupported.
8. versioned: false — Updates Overwrite In Place. A direct, named
exception to PoNIF #2: edge types may declare versioned: false at
creation, and documents under such an edge type stay at version=1
forever — updates overwrite the existing payload in place, and the previous
data is gone. versioned: false requires non-empty identity_fields
(overwrite-in-place re-addresses an entity, which requires an identity to
re-address); this combination is rejected at both template create and
update. versioned is immutable after creation, and (per the resource) is
currently available only on edge types, always true for append-only
(identity-less) templates.
Gotchas
- The "full" deletion mode is a real trap inside the "nothing ever dies"
trap. Code and habits built on PoNIF #1's default (references always
resolve) will silently misbehave in a namespace that has been switched to
deletion_mode: "full"— in that namespace, a hard-deleted record's references genuinely stop resolving. Don't assume the default holds everywhere. - A dropped-record incident, not a hypothetical: the resource's own example for PoNIF #3 — adding an identity field after the fact silently collapsed 213 of 214 records to one hashed identity — shows this isn't a theoretical edge case; it is the shape a real identity-field change took.
- PoNIFs #7 and #8 compound. A reader who has internalized "every
document update creates a new version" (PoNIFs #1 + #2) will apply that
rule universally and be blindsided by a
versioned: falseedge type overwriting silently. Code that loadsversion=N-1to diff againstversion=N, or that assumesget_document_versions(id)returns more than one row, fails silently — not loudly — on such an edge type. - The Compactheimer's Warning (the resource's own name for this): an AI
assistant whose context has been compacted may lose these rules and
revert to conventional assumptions without noticing. The resource lists
its own drift signs to self-check against: assuming a template update
replaces the old version; adding timestamps or run-specific data to
documents; treating an inactive entity as deleted; skipping per-item
result checks on bulk operations; treating a template with two
reference_type: documentfields as a plain entity template; assuming every document update creates a new version. If any of these feel natural, the resource's own instruction is to re-read it.
