| class | archive | |
|---|---|---|
| category |
|
|
| affiliations | ||
| created | 2026-08-21 | |
| modified | 2026-11-07 | |
| context | ||
| tags |
|
|
| sorting-spec | core creative library controls resolution frontmatter-helpers utils Apparatus Module Taxonomy Apparatus Status Taxonomy Apparatus Affiliations Taxonomy Apparatus Code Module Specification Accession Specification Code Style Guide % apparatus-codex package .git .github .gitignore .DS_Store |
Shared core for every module-specific script library in this vault — creative, library, and whatever gets added as this system grows. Anything that every module needs (the class/modal foundation, general-purpose UI controls, logging) lives here exactly once instead of being copied into each module's own tree.
This directory ships as a submodule, maintaining its own git repository, yet baked into the Obsidian config so it's resources are shipped with the Apparatus template vault. .codex and .commands/ are vault-specific glue: baked into the template so every new vault instance gets them automatically, with each module submodule built to assume they'll be present rather than bundling its own copy.
- Obsidian, with CodeScript Toolkit (the module system this whole tree is built for) enabled.
- No Desktop-only assumptions anywhere under
.codex— this vault is used on iPadOS, and everything here is written to work identically on Desktop and Mobile. Module submodules may add their own requirements on top of this (Templater, Dataview, etc.) — see each module's own README.
Always requireAsync() where necessary, never require().
CodeScript Toolkit's vault-root-relative path syntax (/path/from/root.js, single leading slash) is supported by require() on Desktop only. requireAsync() supports it on both Desktop and Mobile. Since cross-platform support is a standing requirement rather than a mobile-only exception, requireAsync() is used everywhere under .codex/, in every module, with no Desktop-only shortcuts.
import { BaseClass } = await requireAsync("/.codex/objects/BaseClass.js");Native ES module import/export is used for everything else (module-to-module imports within a single script tree) — requireAsync() specifically is for the CodeScript Toolkit-managed cross-module loading shown above.
.codex/
├── core/
│ ├── modals/
│ │ ├── BaseModal.js
│ │ └── ImageModal.js
│ ├── objects/
│ │ ├── Archive.js
| | ├── Archive.js Specification.md
│ │ ├── BaseClass.js
│ │ ├── BaseClass.js Specification.md
│ │ ├── Image.js
│ │ └── Image.js Specification.md
│ ├── Entity.js Remediation.md
│ ├── Entity.js Schema.md
│ └── Entity.js Specification.md
├── creative/
│ ├── controls/
│ │ ├── manuscript-pipelines.js
│ │ └── scene-template-helpers.js
│ ├── modals/
│ │ ├── SceneModal.js
│ │ └── StoryModal.js
│ ├── objects/
│ │ ├── Manuscript.js
│ │ ├── Scene.js
│ │ ├── Story.js
│ │ ├── StoryArchive.js
| | ├── StoryArchive.js Specification.md
│ │ └── index.js
│ └── creative.md
├── library/
│ ├── base-builders/
│ │ ├── build-bibliography.js
│ │ ├── build-glossary-index.js
│ │ └── index.js
│ ├── citation/
│ │ ├── apa.js
│ │ ├── chicago.js
│ │ ├── citation-engine.js
│ │ ├── citation.md
│ │ ├── mla.js
│ │ └── name-format.js
│ ├── controls/
│ │ └── library-vocab.js
│ ├── modals/
│ │ ├── AggregateModal.js
│ │ ├── ArticleModal.js
│ │ ├── AuthorModal.js
│ │ ├── ChapterModal.js
│ │ ├── CollectionModal.js
│ │ ├── DefinitionModal.js
│ │ ├── EntryModal.js
│ │ ├── EssayModal.js
│ │ ├── InterviewModal.js
│ │ ├── LectureModal.js
│ │ ├── LibraryModal.js
│ │ ├── MonographModal.js
│ │ ├── PeriodicalModal.js
│ │ ├── RecordModal.js
│ │ ├── ReportModal.js
│ │ ├── ReviewModal.js
│ │ ├── ThesisModal.js
│ │ └── WorksModal.js
│ ├── objects/
│ │ ├── Aggregate.js
│ │ ├── Aggregate.js Specification.md
│ │ ├── Article.js
│ │ ├── Author.js
│ │ ├── Author.js Specification.md
│ │ ├── Chapter.js
│ │ ├── Collection.js
│ │ ├── Collection.js Specification.md
│ │ ├── Definition.js
│ │ ├── Definition.js Specification.md
│ │ ├── Entry.js
│ │ ├── Essay.js
│ │ ├── Glossary.js
│ │ ├── Glossary.js Specification.md
│ │ ├── Interview.js
│ │ ├── Lecture.js
│ │ ├── Library.js
│ │ ├── Library.js Specification.md
│ │ ├── Monograph.js
│ │ ├── Periodical.js
│ │ ├── Periodical.js Specification.md
│ │ ├── Record.js
│ │ ├── Report.js
│ │ ├── Review.js
│ │ ├── Thesis.js
│ │ ├── Works.js
│ │ └── Works.js Specification.md
│ ├── Apparatus Library Property Taxonomy.md
│ ├── Documentation Sync Command Specification.md
│ ├── Library Accession Trigger Map.md
│ └── library.md
├── controls/
│ ├── FileSuggest.js
│ ├── FolderSuggest.js
│ ├── NoteSuggest.js
│ ├── ValueSuggest.js
│ ├── index.js
│ └── validate-expected-class.js
├── frontmatter/
│ ├── append-to-frontmatter-array.js
│ ├── build-frontmatter-block.js
│ └── index.js
├── resolution/
│ ├── index.js
│ ├── resolve-authors.js
│ ├── resolve-collection-fields.js
│ ├── resolve-library.js
│ ├── resolve-note-file.js
│ └── resolve-titles.js
├── utils/
│ ├── inputValidation.js
│ ├── links.js
│ ├── logger.js
│ └── romanNumeralConversion.js
├── Accession Specification.md
├── Apparatus Affiliations Taxonomy.md
├── Apparatus Code Module Specification Template.md
├── Apparatus Module Taxonomy.md
├── Apparatus Retrofit Work Checklist.md
├── Apparatus Spec Workflow Handoff.md
├── Apparatus Status Taxonomy.md
├── Code Style Guide.md
├── Existing Note Migration Worksheet.md
├── Module Maintenance Checklist.md
├── Publication Module Implementation.md
├── apparatus-codex.code-workspace
├── package.json
├── .gitignore
└── .codex.md
.commands/ — the only invocable, command-palette-visible layer, across every module
├── creative-create-scene.js — creative
├── creative-create-story.js — creative
├── library-create-library.js — library
├── library-create-author.js — library
├── library-create-article.js - library
└── library-create-*.js - additional library commands;
full list not printed here
ENVOY/.scripts/ - external projects which draw from the
Pulp Fiction/.scripts/ codex lib. BaseClass and BaseModal are
Victorian Mayhem/.scripts/ used in general. Library module classes
are extended for context library modules
Root of every taxonomy-class hierarchy in the Apparatus. Owns the properties every object shares — class, category, affiliations, created, modified, tags — plus:
create()— writes the file viaapp.vault.create(). Throws on path collision rather than overwriting; no silent failures anywhere in this system. Never logs a failure; the error propagates to the invoking command (see Logging flow). Logs the success deliberately, viautils/logger.js— see that section below for why success gets logged.resolveFolder(app)(static, overridable) — default strategy returns the active file's parent folder, on the assumption that commands are run from within the relevant module folder. A module needing something else — a fixed target folder, a dynamic search, a folder-suggest field in its own modal — overrides this.categoryOptions()(static, overridable, defaults tonull) — a subclass with a fixed category vocabulary returns it here;BaseModal.buildCategorySetting()reads it and renders a multi-select. A subclass that never overrides it still gets a working comma-separated input instead of nothing — the point of thenulldefault is that forgetting to declare a vocabulary degrades gracefully rather than silently losing the field.findIdentityAffiliation(app, file, prefixes)(static) — finds the entry in a note's ownaffiliationsfrontmatter matching one of the given prefixes, so a referencing note can copy that value verbatim instead of someone re-typing it and risking a mismatch that silently breaks the graph connection. No default prefixes here — there's no Apparatus-wide affiliation namespace, only per-module conventions. A module wraps this with its own defaults rather than duplicating the lookup.
One class per file, no exceptions — this is what keeps every module's objects/ navigable as the number of taxonomy classes grows, in contrast to the old Templater scaffold scripts where everything was piled into a single file because Templater doesn't support real import/export.
Root of every modal hierarchy. Field-builder helpers, called by every subclass modal to render its own fields:
| Helper | Renders |
|---|---|
buildTextSetting() |
plain text field |
buildDropdownSetting() |
single-select from a fixed list |
buildMultiSelectSetting() |
toggle-per-option from a fixed list, collected into an array |
buildListSetting() |
comma-separated text, parsed into an array — for open-ended list fields with no fixed vocabulary |
buildCategorySetting() |
reads the paired object class's categoryOptions() and picks multi-select or plain list accordingly |
buildFolderSuggestSetting() |
text field with live folder autocomplete (FolderSuggest) |
buildNoteSuggestSetting() |
text field with live note autocomplete, filtered by frontmatter class (NoteSuggest) |
buildTextSuggestSetting() / buildListSuggestSetting() |
text/list field with autocomplete sourced from values already used vault-wide for a given frontmatter key (ValueSuggest), rather than a fixed vocabulary |
buildSubmitButton() |
the shared Create button every modal ends with |
Some frontmatter fields aren't left blank for a person to fill in by hand — data is collected through modals at creation time, using whichever of the above fits the field. Data may not exist for every field at creation time regardless (open-ended fields like genres are commonly left for manual fill-in later), but the modal captures as much as it reasonably can up front. Note: BaseModal has no built-in required-field validation — a modal marking a field as required (e.g. Creative's StoryModal.titleAbbv) is documentation only; the actual block on an empty submission has to live in the calling command instead.
AbstractInputSuggest-based UI classes, attached to a modal's text inputs — not object classes, not modals, a third kind of thing, which is why they get their own folder rather than living inside objects/ or modals/. Import from the barrel (controls/index.js), not individual files.
Standalone frontmatter functions shared by every module — not object classes, not modals, not controls, which is why they get their own folder rather than living inside BaseClass.js (one class per file, and these aren't the class). Import from the barrel (frontmatter-helpers/index.js), which exports every module in the directory, not from individual files.
buildFrontmatterBlock(fields)(build-frontmatter-block.js, exported standalone function) — builds the----delimited frontmatter block from an arbitrary fields object, without requiring a livecreate()-bound instance.BaseClass.buildFrontmatter()uses it for normal note creation; it's also the entry point for anything that needs frontmatter-shaped output outside the normal create pipeline: Creative'sStory.buildSceneTemplates()is the working example — it needsScene's own field set and formatting, but with values a realScene.create()wouldn't produce (a Templater<% tp.date.now() %>placeholder instead of a realcreateddate, most fields deliberately left blank). Each value renders by type:- Array — a YAML list, one
- itemper entry; an empty array renders as a bare key. - Plain nested object — an indented YAML mapping, one level deeper per nesting level.
- String containing a newline — a
|-block scalar, since raw newlines inside a plain scalar would be invalid YAML. undefined,null, or empty string — a bare key with no value.- Anything else — written as-is.
- Array — a YAML list, one
- Frontmatter value quoting — a scalar string starting with
[(most notably a wikilink like[[note-name]]) is double-quoted in the rendered frontmatter, with any embedded double quotes backslash-escaped, because a bare leading[is YAML's own flow-sequence indicator: left unquoted,[[note-name]]parses as a one-element array containing a one-element array rather than as literal text. This surfaced via Story'smanuscriptLinkswikilinks rendering as nested list items instead of link strings — fixed once, at this shared formatting level, so it holds for any future wikilink-valued field in any module. formatFrontmatterLine()(the per-key formatter) andformatScalarValue()(the quoting rule above) are module-private tobuild-frontmatter-block.js— not exported.appendToFrontmatterArray(app, file, key, value)(append-to-frontmatter-array.js, async, exported) — appendsvalueto the frontmatter arraykeyonfilethroughapp.fileManager.processFrontMatter(). A missingkeyis treated as an empty array, and avaluealready in the array (exact string match) is skipped rather than duplicated. On failure it rethrows the error fromprocessFrontMatter()unchanged and does not log — a failed append is never safe for a caller to ignore.
Wikilink and library resolvers shared by every module — not object classes, not modals, not controls. Every wikilink resolver takes app and sourcePath (the path of the note containing the link) first, because Obsidian resolves a link relative to the note it appears in. Import from the barrel (resolution/index.js), which exports every module in the directory, not from individual files.
Strict versus lenient. A strict resolver throws on an unresolvable link and does not log; the invoking command's catch logs it (see Logging flow). A lenient resolver never throws and never logs: an unresolved link is treated as an expected, tolerated condition (a not-yet-cataloged or since-renamed cross-reference), and the resolver returns null or the raw link text instead.
resolve-note-file.js— the shared core every other wikilink resolver is built on.resolveNoteFileLenient(app, sourcePath, rawWikilink)— returns the linkedTFile, ornullif the link is empty or doesn't resolve.resolveNoteFile(app, sourcePath, rawWikilink)— strict counterpart; throws on an unresolvable link.
resolve-titles.js— class-agnostic: resolution itself never depends on what class the linked note is.toWikilink(basename)— formats a bare title as[[basename]].resolveNoteTitle()/resolveNoteTitles()— strict; a link (or array of links) to the linked note's title (its basename). The array form treats a missing input as an empty array.resolveOptionalNoteTitle()— for a field that may hold either a wikilink or plain text: resolves only when the value is bracketed, and passes anything else (including an empty value) through unchanged. A bracketed link that doesn't resolve still throws.resolveNoteLinkLenient()/resolveNoteLinksLenient()— lenient; a link (or array of links) to a normalized wikilink pointing at the same note, or the raw link text unchanged if it doesn't resolve.
resolve-authors.js—resolveAuthor()resolves a wikilink against its own Author note and readsprefix,first-name,last-name, andsuffixoff that note's frontmatter, returning{ prefix, first, last, suffix }with''for any missing field.resolveAuthors()does the same for an array. Strict.resolve-collection-fields.js—resolveCollectionFields()resolves a wikilink to a Collection note and returns{ periodical, volume, issue }for any leaf whose citation depends on the periodical issue it belongs to.periodicalis that note'speriodicallink resolved to a title;volumeandissueare read as-is. Missing fields come back as''. Strict.resolve-library.js— resolves the library enclosing a folder, rather than resolving a wikilink.resolveLibraryFolder(app, startFolderPath)— walks upward fromstartFolderPathto the nearest ancestor folder whose direct contents include a note with frontmatterclass: library, and returns that folder's path.resolveLibraryNote(app, startFolderPath)— the same resolution, returning the library's ownTFilefor callers that need to read fields off it.- Both throw if no library is found before the vault root, or if a single folder holds more than one library note.
Log.fatal(context, message, error) / .error() / .warn() / .log(context, message, item?) / .info() / .debug() / .trace() — every level fires an Obsidian Notice and the matching console method, deliberately with no verbosity/severity filtering. That's by design, not an oversight: it's meant for live tracing during active development without needing the console open constantly, with success-path calls (Log.log, typically) placed surgically and stripped once a class is performing as expected — failure-path calls (Log.error, Log.fatal) stay permanently, placed only where Logging flow puts them. Log.fatal/.error/.warn take an optional real error object as their third argument; when provided, that's the exact object that reaches the console (correct stack trace included) rather than a synthetic one built from the message string.
The only invocable, command-palette-visible layer in the entire system — everything under { module }/ and every module's own tree is imported, never invoked directly. Each command does the minimum: open its modal, take the result, resolve a folder, instantiate the class, call .create(), open the created file. All the actual logic lives in { module }/ and each module's own { module }/, not here. creative-create-story.js is the one command with real sequencing beyond that minimum (scene templates before manuscripts, so each manuscript can be wired to the right one). Each command's submit handler is also the error boundary for everything it calls; see Logging flow.
.commands/ lives as a sibling to .codex/ wherever they coexist, generally at the root of a vault.
A module with fields or behavior shared across several of its own classes — but not shared with every other module — gets its own intermediate class between BaseClass/BaseModal and its leaf classes, rather than either duplicating that behavior per leaf class or pushing module-specific concerns up into BaseClass itself.
The test for whether something belongs in BaseClass itself versus a module's own intermediate class: would every module want this, or just one? affiliations as a concept and its lookup logic (findIdentityAffiliation) is Apparatus-wide — every taxonomy class carries affiliations, so that lookup lives on BaseClass.
Every failure is logged exactly once, by the command that invoked the failing code. Class, modal, control, and helper code throws. The command's submit handler catches the error and logs it.
Class, modal, control, and helper code
- Throws an
Errorwith a specific message on failure. It does not callLog.error,Log.fatal, orLog.warnfor that failure, and it does not catch an error only to log and rethrow it. - May call
Log.logfor success-path tracing.BaseClass.create()logsCreated <path>this way.
Command
- The submit handler wraps all of its work in one
try/catch. callback()wraps modal construction and.open()in a secondtry/catch, because an error thrown while the modal is built or opened is raised there, outside the submit handler. An example isvalidateExpectedClassrejecting anexpectedClassin a control constructor duringonOpen().- A validation failure
throws. The samecatchlogs it. Nothing logs at the point of the check. - Each
catchcallsLog.error(commandId, error.message, error)once and does not rethrow. BaseModal.buildSubmitButton()callsonSubmit(this.result)without awaiting it, so a rethrown error would surface as an unhandled promise rejection.- Every
Loglevel fires a Notice, so a second log on the same failure produces a second Notice.
core-create-image.js is the reference implementation.
Not yet conforming
- Commands other than
core-create-image.jshave not been checked. A command whose submit handler does not catch and log produces no Notice for a failure thrown by class code. A command whosecallback()does not wrap modal construction and.open()produces no Notice for a failure thrown while its modal is built or opened.
- Collisions always throw. No silent overwrites, anywhere in this system.
- One class per file, one modal per file. No exceptions, in
lib/or in any module's own tree. .commands/is the only invocable layer. Everything else is imported.- Errors are logged once, at the command boundary. Class, modal, control, and helper code throws; the command's submit handler catches, logs, and does not rethrow. See Logging flow.
- A module's own intermediate classes extend
BaseClass/BaseModal— they don't fork them. If a module finds itself needing to duplicateBaseClass/BaseModallogic rather than extend it, that's a signal the shared core is missing something, not a reason to copy around it.