Texts Pre-Work Audit: shanti_texts/shanti_texts_features/shanti_footnotes D7 Content Model
Audience: Developers (Sprint 2 Workstream C3 — audit only, no migration code)
Date: 2026-09-01
Source: Legacy D7 custom modules shanti_texts, shanti_texts_features,
shanti_texts_splitter, shanti_texts_search_settings, shanti_footnotes
(mandala-drupal/docroot/sites/all/modules/custom/{shanti_texts,shanti_texts_features,shanti_texts_splitter,shanti_texts_search_settings,shanti_footnotes})
Relates to: Migration Complexity Comparison
(scores this site against AV/Sources), ADR 009 (Texts/Sources
fork after Images), Sprint 2 backlog
(C3), [[project-spike-4b-ckeditor-footnotes]] (CLOSED — the footnotes cross-page
transform is already fully spiked; this audit does not re-investigate it), Images
Content-Model Audit, AV Content-Model
Audit, Sources Content-Model
Audit (methodology template and points of comparison
throughout)
Scope. This is a data/field/entity-graph inventory only. No migration code, no
mandala_migrationsscaffolding, and no D11 content-type is created as part of this audit — per the Sprint 2 backlog's explicit constraint on Workstream C. The footnotes cross-page reference pattern is out of scope here — it was fully investigated and closed by Spike 4b (docs/deferred/texts-footnotes-production-transform.md); this audit only confirmsshanti_footnotesexists and its role, not its mechanics.
Purpose
Inventory the real D7 field model behind the Texts site — the one audit of the three where the primary content type turns out to be D7 core's own Book module, not a site-invented bundle, which changes the shape of the D11 remodeling question compared to Images/AV/Sources. Extends Spike 4b's already-closed footnotes investigation (which only looked at the body/footnote fields) into the full field/entity-graph inventory the sprint backlog calls for.
Key finding: book is D7 core's Book content type, not a custom bundle — the outline is core mechanism, not a shanti_texts invention
Unlike every other site audited this sprint, book is not a site-defined content
type. Confirmed in shanti_texts_features.features.inc:
function shanti_texts_features_node_info() {
$items = array(
'book' => array(
'name' => t('Book page'),
'base' => 'node_content',
'description' => t('<em>Books</em> have a built-in hierarchical navigation. Use for handbooks or tutorials.'),
base => 'node_content' and that exact description string are Drupal core's own
stock defaults for the Book module's content type — the Features export here
re-asserts the core bundle plus attaches custom fields to it, it does not invent a new
type. field_book_mlid (a computed, non-stored field — module => computed_field,
store => 0) confirms this further: it derives its value live from core's
menu_links table (db_select('menu_links','ml')->condition('link_path', 'node/'.$nid))
rather than storing anything itself — the outline position lives entirely in core's
{book}/{menu_links} tables (bid/plid/mlid/weight), not in any
shanti_texts-owned schema.
Migration implication: the page-outline hierarchy is a known, core-supported
migration path (D11's Book module uses the same bid/plid/weight shape), not a
bespoke data model to reverse-engineer the way Images' satellite graph or Sources'
flat-column table were. This narrows what would otherwise be the single biggest
open question for Texts.
A second, unrelated source of the multi-page-per-book pattern:
shanti_texts_splitter is an editorial convenience tool, not an import or migration
mechanism — see the Splitter section below. It means many books' page trees were
machine-generated from one pasted document rather than hand-authored, but produces
ordinary core Book-outline nodes either way; D11 does not need to distinguish
splitter-generated pages from hand-authored ones structurally (though no field
currently flags which is which — see Open questions).
The entity graph
book (D7 core Book content type + custom field instances)
├── field_book_content → text_long (body — the field the footnotes spike already covers)
├── field_book_mlid → COMPUTED, not stored — derived live from core menu_links
├── field_book_date → date
├── field_split_headings / field_split_text → consumed by shanti_texts_splitter (editorial tool, not migration-relevant)
├── field_book_author / _editor / _translator → text (255), free-text
├── field_admin_status → text, internal status tag
├── field_dc_description → text_with_summary
├── field_dc_lang_code → list (ISO-639), the PRIMARY language field in practice (62.8% filled)
├── field_dc_language_original → free text, sparse (3.2% filled)
├── field_language_kmap → KMaps taxonomy, sparse (2.3% filled) — a THIRD, largely-unreconciled language field
├── field_dc_date_orginial_year / _publication_year → datetime (year granularity; note "orginial" typo is in the real machine name)
├── field_dc_rights_creativecommons → list (CC license picklist)
├── field_dc_rights_general → text_long
├── field_kmap_term (label "Subjects") / field_kmap_places / field_kmap_terms → shanti_kmaps_fields_default (same KMaps pattern as Images/AV/Sources)
├── field_general_featured_image → image (shared cross-site field, same as Images/AV/Sources)
├── field_pdf_version → file_generic, sparse (2.8% filled)
├── field_og_collection_ref → entityreference (og_subgroups handler) — field-storage table EMPTY in
│ production; real membership lives in `og_membership`, same bug as AV/Sources (see Data profile)
└── group_content_access (required) → OG Visibility, 100% filled
collection / subcollection / asset_link — NOT defined by any shanti_texts* module;
all three come from the shared `shanti_collections`/`asset_link` modules, the SAME
cross-site bundles already confirmed reused by Images, AV, and Sources.
Field inventory
Identity / title metadata
| Field | Type | Notes |
|---|---|---|
field_book_author |
text (255) | free-text |
field_book_editor |
text (255) | free-text |
field_book_translator |
text (255) | free-text |
field_admin_status |
text | internal status tag, not a workflow/moderation field |
Body / outline mechanics
| Field | Type | Notes |
|---|---|---|
field_book_content |
text_long | the body (covers footnote markup, already spiked) |
field_book_mlid |
computed, not stored | derived live from core menu_links |
field_book_date |
date | cardinality 1 |
field_split_headings |
list (options_buttons) | which <h1>–<h6> levels the splitter breaks on |
field_split_text |
boolean | "split on save" toggle for shanti_texts_splitter |
Descriptive / Dublin Core metadata
| Field | Type | Notes |
|---|---|---|
field_dc_description |
text_with_summary | |
field_dc_lang_code |
list, ISO-639 | primary language field in practice — see Data profile |
field_dc_language_original |
text (255) | free-text, sparse |
field_dc_date_orginial_year |
datetime (year) | machine name has the typo verbatim |
field_dc_date_publication_year |
datetime (year) | |
field_dc_rights_creativecommons |
list | CC license picklist |
field_dc_rights_general |
text_long | free-text rights statement |
KMaps taxonomy (same pattern as Images/AV/Sources)
| Field | Label | Cardinality |
|---|---|---|
field_kmap_term |
Subjects | -1 |
field_kmap_places |
Places | -1 |
field_kmap_terms |
Terms | -1 |
field_language_kmap |
Language (KMaps taxonomy — third language field, see Open questions) | -1 |
A vestigial plural-named pair (field_language_kmaps/field_terms_kmaps) exists in
the same field-base file but is not instantiated on book — noted, not chased
further; likely dead or used on a bundle outside this audit's scope.
Access / collections
| Field | Type | Notes |
|---|---|---|
field_og_collection_ref |
entityreference (og_subgroups handler), cardinality 1 |
field-storage table empty in production, see Data profile |
group_content_access |
options_select, required | 100% filled |
Media / attachments
| Field | Type | Notes |
|---|---|---|
field_general_featured_image |
image | shared cross-site field (same base as Images/AV/Sources) |
field_pdf_version |
file_generic | sparse |
shanti_texts_splitter — an editorial tool, not a migration mechanism
Reading the code directly rather than guessing from the name: this is a node-save-time
convenience feature, not an import or runtime rendering mechanism. When
field_split_text = 1 on save, shanti_texts_splitter_node_presave() regex-splits
field_book_content's HTML on the heading levels chosen in field_split_headings
(also extracting a trailing <section class="footnotes">/<div class="endnotes">
block if present), then shanti_texts_splitter_node_postinsert() clones the parent
node once per fragment via shanti_texts_splitter_make_page() — assigning each new
page's book['bid']/['plid']/['weight'] to slot it into the outline, stripping
inherited KMap terms, and explicitly resetting field_og_collection_ref to empty on
each generated child (the code comment reads: "Without reseting collection for
pages, site crashes").
In short: paste a Word-exported document into one book node, flip a toggle, and the
module auto-generates the full multi-page outline tree from heading boundaries. This
explains why many books have deep trees of many small pages — a second, independent
source of the shared-bid page-cluster pattern beyond ordinary hand-authoring — but is
irrelevant to D11 migration logic itself (it produces ordinary Book-outline nodes
either way). Notably, the splitter's own code distrusts field_og_collection_ref's
field storage enough to explicitly clear it on every generated page — a strong hint,
independent of the data profile below, that this field was never treated as reliable
storage even by the D7 codebase itself.
shanti_footnotes and the cross-page footnote pattern — out of scope here (already spiked)
Confirmed present at docroot/sites/all/modules/custom/shanti_footnotes — a custom
CKEditor 4 plugin producing paired inline-citation/definition markup, frequently split
across different pages of the same book (definitions collected on a dedicated "Notes"
page). This was already fully investigated and closed by Spike 4b
([[project-spike-4b-ckeditor-footnotes]]) — direction chosen (per-citation transform +
Notes-list aggregation), team sign-off merged (PR #76), production-build details
tracked separately at
texts-footnotes-production-transform.md.
This audit does not re-derive any of that; it only confirms the module's existence and
role for completeness of the field/module inventory.
shanti_texts_search_settings
Solr/search indexing configuration for Texts, analogous to AV's mb_solr and Sources'
sources_biblio_search — not separately inventoried here (out of scope: it configures
discovery/facets, not content fields).
Data profile (production dump, loaded 2026-09-01)
Verified against d7_texts, already loaded in DDEV from a prior spike session
(mandala-prod-texts-db_20260710.sql.gz, 90MB, previously verified as complete — see
[[project-spike-4b-ckeditor-footnotes]]'s own note about a prior silently-truncated
download of this same dump, caught by checking table/line counts before use).
Node counts: 7,633 book · 65 collection · 57 subcollection · 8 asset_link —
matches the counts already established by Spike 4b exactly.
The og_membership bug found on both AV and Sources also affects Texts — now
confirmed on all three sites checked this sprint. field_data_field_og_collection_ref
is empty (0 rows) for book/asset_link, despite the field being a real,
field-storage-backed entityreference. The actual membership data lives in
og_membership: 7,419 node memberships tagged field_og_collection_ref (7,411 book +
8 asset_link — 97.1% of book nodes), plus 57 subcollection→collection memberships
tagged field_og_parent_collection_ref (matching the subcollection count exactly).
This is now a cross-site pattern, not a per-site quirk — worth treating as a single
platform-wide migration-tooling fix rather than three separate site-specific notes (see
Recommended next step).
Required field: clean. group_content_access is filled on all 7,633 book nodes
(100%) — no gap, matching AV's clean result and unlike Sources' 91.2%.
The three-language-fields question, resolved with real numbers:
| Field | Books filled | % |
|---|---:|---:|
| field_dc_lang_code (ISO list) | 4,791 | 62.8% |
| field_dc_language_original (free text) | 246 | 3.2% |
| field_language_kmap (KMaps taxonomy) | 176 | 2.3% |
field_dc_lang_code is clearly the primary/authoritative language field in practice;
the other two are minority-use. Its value distribution: en 3,650 (76.2% of filled
rows), bo 1,138 (23.7% — matches Spike 4b's known Tibetan count exactly), dz 2,
id 1.
Other field coverage: field_kmap_term/Subjects on 593 books (7.8%); field_pdf_version
on 211 books (2.8%); field_book_author on 3,316 books (43.5%). field_split_text has
2,887 rows, all currently value 0 — no books are mid-flagged for splitting in
this snapshot, consistent with it being a one-time transient authoring toggle rather
than persistent state.
What this audit establishes
bookis D7 core's Book content type, not a custom bundle — its outline hierarchy is core Drupal mechanism (bid/plid/mlid/weight), a known-supported migration path rather than a bespoke shanti_texts data model.- The full field inventory for
book, including one computed/non-stored field (field_book_mlid) that any migration tooling correctly won't find a storage table for. shanti_texts_splitteris an editorial convenience tool (auto-generates a page tree from one pasted document at save time), not a migration-relevant mechanism — but its own code independently distrustsfield_og_collection_refstorage, foreshadowing the data-profile finding below.- The footnotes cross-page pattern is confirmed present but is out of scope here — fully resolved by the already-closed Spike 4b.
- Data-verified, cross-site pattern (now confirmed on Texts, AV, and Sources
alike): collection membership is not readable from
field_data_field_og_collection_ref— it lives inog_membership. Required fields (group_content_access) are 100% clean. The three language fields are resolved:field_dc_lang_codeis primary (62.8% filled, matches known Tibetan counts exactly), the other two are minority-use.
What this audit does NOT establish (still open)
- Whether
field_dc_language_originalandfield_language_kmapshould be preserved in D11 given their sparse (2–3%) use, or folded away in favor offield_dc_lang_code— a recommendation candidate, not a decision made here. - Whether splitter-generated vs. hand-authored page provenance matters for D11. No field currently flags which pages were machine-split; not decided whether that distinction needs preserving.
- The vestigial
field_language_kmaps/field_terms_kmapsfield-base pair (plural forms, not instantiated onbook) — not chased down to confirm what bundle, if any, actually uses them; flagged only for completeness. - OG → D11 Group mapping for the collection-membership
og_membershipfinding — same open item as AV/Sources; needs to inform whatever proxy-auth/access design work covers all three sites' equivalent gap, ideally as one shared fix rather than three separate migration-plugin patches. - Whether the core-Book-migration path is actually sufficient as-is, or whether D11's book outline needs any Texts-specific adjustment (e.g. interaction with Collections/OG membership at the page level) — this audit confirms the mechanism is core-supported, not that zero migration work remains.
Recommended next step
No modeling decision is made here — per the Sprint 2 Workstream C scope, this audit
stops at data/field/entity-graph facts. When Texts' migration turn comes up (ADR 009
sequences Texts/Sources after Images, before AV):
- Confirm the core Book-module migration path handles bid/plid/weight as
expected for D11's own Book module — expected to be the most standard modeling step
of any site audited this sprint.
- This is the third and final site this sprint to independently hit the
og_membership-vs-field_data_field_og_collection_ref gap (after AV and Sources).
Recommend treating this as a single shared migration-source-plugin fix — read
og_membership for collection membership across all OG-collection-participating
bundles — rather than reimplementing the same workaround three times.
- Resolve the three-language-field question (open question #1) alongside whatever
faceting/search design work touches field_dc_lang_code, since it's already the
clear practical winner.
- No action needed on footnotes — Spike 4b already closed that path; production build
work is tracked separately.
- Any new AJAX/JSON/download endpoint Texts needs should follow the
_entity_access: 'node.view' convention documented in
entity-access-endpoint-convention.md —
Images hit and fixed the same D7 blanket-permission gap three separate times this
sprint; don't reopen it here.