This directory is one frozen, synthetic-only custom API-response capture from a fresh Directus 11.17.4 sandbox. It is evidence for one exact adapter profile, not a general Directus export claim. The custom capture bundle has not been hand-normalized into an ExitDrill export; a profile-specific normalizer must do that outside the evaluator.
The native/ path is an internal source-side label for bytes before ExitDrill
normalization; it does not identify a vendor-native Directus export format.
The lab used the official image pinned by digest:
directus/directus@sha256:eb326f679ae847c0a776f93b972761dc2ebe84980e0b9d274a6bc31cd62809f7
The resolved application version was 11.17.4. The container used SQLite and
local file storage inside an ephemeral Docker lab. Its Docker network was
internal (no external route), port 8055 was bound only to 127.0.0.1, and the
capture program rejects every HTTP origin except http://127.0.0.1:8055 and
rejects redirects. Only invented people, cases, text attachments, a policy, and
two case-creation events were present. Production data is prohibited.
The capture used documented first-party REST surfaces:
POST /auth/loginGETandPOST /collections,POST /fields/:collection, andPOST /relationsGETandPOST /items/:collectionGETandPOST /files, plusGET /assets/:idGETandPOST /policiesand/permissionsGET /activityGET /schema/snapshot
The capture program is ../../scripts/directus_canary_lab.mjs.
It intentionally refuses a non-fresh database or an existing fixed capture
directory. Supply admin credentials through ADMIN_EMAIL and ADMIN_PASSWORD;
the program never prints them or response bodies. Run it only against a
disposable, no-egress sandbox.
Official references:
- Directus API overview
- Global API query parameters
- Files API and asset access
- Policies API
- Permissions API
- Schema snapshot API
- Official self-hosting guide
- Directus v11.17.4 release
- Directus v11.17.4 license text
native/capture-manifest.json records each captured response or attachment by
relative path, exact byte count, and SHA-256. bundle_sha256 is the SHA-256 of
the fixture program's recursively canonical JSON encoding of that sorted file
metadata array. The manifest itself is outside that digest.
The bundle contains:
| Surface | Captured evidence |
|---|---|
| People items | 3 |
| Case items | 2 |
| Case-person junction items | 2 |
| Attachment metadata and bytes | 2 each |
| Policy records | 1 |
| Permission records | 2 |
| Case-create activity records | 2 |
| Schema snapshots | 1 |
The adjacent baseline.json is a separately authored assertion for this
controlled scenario. It declares complete coverage across all five ExitDrill
dimensions. Its synthetic captured_at is after both captured audit timestamps
and before the capture manifest's exported_at; none of those timestamps are
authenticated.
The accepted profile is directus-11.17.4-civic-case/v0.1:
- the exact captured
schema.jsonbytes are pinned by SHA-256 in the adapter, in addition to the manifest inventory and structural schema checks; - integer source IDs become decimal strings;
exitdrill_peoplerows becomepersonentities and SQLite0/1boolean values become JSON booleans;exitdrill_casesrows becomecaseentities, retainingstatus,priority, and the document UUID as baseline-declared critical values;- permission collection names become
directus_collection_scopetechnical entities whose only required field is the exactcollectionname; - junction rows become
assigned_torelationships fromcasetoperson; - each document UUID becomes an attachment owned by its case, with bytes read
only from the corresponding
native/assets/<uuid>.txtpath; - policy UUID
33333333-3333-4333-8333-333333333333becomes principal IDpolicy:33333333-3333-4333-8333-333333333333; and - activity IDs become event IDs
directus_activity:<decimal-id>, collectionexitdrill_casesmaps to object typecase, and source action and timestamp are retained exactly.
Permission roles preserve the action plus a digest of the semantics not representable in ExitDrill's four-string permission tuple. Canonical JSON sorts object keys recursively, emits no insignificant whitespace, and preserves array order. The digest input has exactly these keys:
{action,fields,permissions,presets,validation}
The role is <action>:<sha256(canonical-input)>. For this bundle:
| Scope | Canonical semantic SHA-256 | Resulting role |
|---|---|---|
exitdrill_cases |
0fcaa4ece823393f4e5ccfc2426e26859693cf80052aaf4b8b78c7ceb3e45a8c |
read:0fcaa4ece823393f4e5ccfc2426e26859693cf80052aaf4b8b78c7ceb3e45a8c |
exitdrill_people |
ce05c47ea42741b5272eccf39dd56d55f7e8d373dbff4c9d6da220266215a38b |
read:ce05c47ea42741b5272eccf39dd56d55f7e8d373dbff4c9d6da220266215a38b |
Keeping permission semantics out of the collection-scope entities ensures that a permission-only mutation fails the permissions dimension without also creating an entity-value failure.
../../scripts/build_directus_lossy_canary.py builds one deterministic,
equal-count adversarial derivative of this profile. It is not committed:
../../scripts/check_directus_canary_demo.py generates it fresh into a
TemporaryDirectory on every run, verifies it against this directory's
committed capture-manifest.json bundle digest first, and discards it when
the process exits. Nothing under this directory holds lossy bytes.
The derivative applies exactly six mutations, one per source file, and each
is required to leave every row and file count unchanged --
build_lossy_canary raises adversarial derivative changed row or file counts if it does not:
| Mutation | What it changes |
|---|---|
critical_field_value |
The second case's status flips from open to closed. |
unreferenced_identity_churn |
The third person's id changes from 3 to 4, orphaning any reference to the old id. |
relationship_rewire |
The first case-person link's person_id changes from 1 to 2. |
permission_field_collapse |
The case permission's fields list drops document. |
audit_action_substitution |
The first activity record's action changes from create to update. |
attachment_same_length_bytes |
One attachment's bytes change (alpha → omega) without changing its length. |
The mutation labels above are not a separately maintained list: the builder
returns the label for each mutation it actually applies, and that returned
list is what the derivative's adversarial-derivative.json statement
declares. A future mutation that is added, removed, or changed cannot leave
a stale label behind.
Running the unchanged ExitDrill evaluator against this derivative, with the same baseline used for the clean bundle, produces six observed loss signals -- the evaluator counts them; the six mutations above are what produced them, not a target the evaluator was tuned to hit:
| Dimension | Expected | Exported | Missing | Extra | Invalid | Status | Signals |
|---|---|---|---|---|---|---|---|
| Entities | 7 | 7 | 1 | 1 | 1 | fail | 2 |
| Relationships | 2 | 2 | 1 | 1 | 0 | fail | 1 |
| Attachments | 2 | 2 | 0 | 0 | 1 | fail | 1 |
| Permissions | 2 | 2 | 1 | 1 | 0 | fail | 1 |
| Audit events | 2 | 2 | 1 | 1 | 0 | fail | 1 |
overall_status is not_structurally_restorable and
observed_remediation_signals is 6. The row is a coincidence worth being
explicit about: the entities dimension absorbs two of the six mutations
(critical_field_value and unreferenced_identity_churn) and reports two
signals, so six mutations producing six signals holds by arithmetic on this
specific set, not by a one-to-one correspondence the evaluator enforces. A
future mutation that changed two dimensions at once, or none, would not
break anything -- but it would change which number is six, and this table
would need updating to match. check_directus_canary_demo.py asserts the
exact per-dimension counts above and the exact six mutation labels on every
run, so that drift is caught immediately rather than discovered by a reader.
--fail-on-loss-signal-increase exits 3 when comparing the clean receipt
against the lossy one, since a comparable result directly observes a
missing/invalid increase in every dimension above.
Directus 11.17.4 is vendor software distributed under the Business Source License 1.1 with an additional use grant; it is source-available and should not be described as OSI-approved open source. Consult the version-pinned license text for applicable terms. This repository does not redistribute Directus source code or its container image; it contains invented fixture data, first-party API responses from the synthetic lab, and an independent capture program.
This fixture can support a bounded claim that one pinned, synthetic Directus profile was captured through documented APIs and has a declared normalization contract. It does not prove:
- that the API-response capture or separately authored baseline is authentic or complete;
- support for another Directus release, database, storage adapter, schema, permission shape, or activity shape;
- a successful load into a real target, workflow preservation, identity or permission-principal equivalence, or operational restoration;
- production readiness, customer use, vendor deletion, or legal compliance; or
- that Directus licensing permits any particular deployment.
The bundle and manifest are unsigned. Hashes detect byte changes but do not authenticate the source, operator, or capture time.
The adversarial derivative adds one further, narrower claim and does not extend the above: it shows that six specific, hand-chosen mutations to this one pinned profile each survive normalization (row and file counts are unchanged) and each still produce a fail-closed structural result. It does not claim these are the only loss-producing mutations possible, that six is a meaningful score, or that a different source profile would show the same count.