Skip to content

Latest commit

 

History

History
333 lines (282 loc) · 22.6 KB

.codex.md

File metadata and controls

333 lines (282 loc) · 22.6 KB
class archive
category
specification
affiliations
created 2026-08-21
modified 2026-11-07
context
tags
apparatus
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.

contents


requirements


  • 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.

loading convention


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.

module map


.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

core/objects/BaseClass.js

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 via app.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, via utils/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 to null) — 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 the null default 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 own affiliations frontmatter 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.

core/modals/BaseModal.js

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.

controls/

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.

frontmatter-helpers/

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 live create()-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's Story.buildSceneTemplates() is the working example — it needs Scene's own field set and formatting, but with values a real Scene.create() wouldn't produce (a Templater <% tp.date.now() %> placeholder instead of a real created date, most fields deliberately left blank). Each value renders by type:
    • Array — a YAML list, one - item per 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.
  • 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's manuscriptLinks wikilinks 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) and formatScalarValue() (the quoting rule above) are module-private to build-frontmatter-block.js — not exported.
  • appendToFrontmatterArray(app, file, key, value) (append-to-frontmatter-array.js, async, exported) — appends value to the frontmatter array key on file through app.fileManager.processFrontMatter(). A missing key is treated as an empty array, and a value already in the array (exact string match) is skipped rather than duplicated. On failure it rethrows the error from processFrontMatter() unchanged and does not log — a failed append is never safe for a caller to ignore.

resolution/

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 linked TFile, or null if 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 reads prefix, first-name, last-name, and suffix off 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. periodical is that note's periodical link resolved to a title; volume and issue are 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 from startFolderPath to the nearest ancestor folder whose direct contents include a note with frontmatter class: library, and returns that folder's path.
    • resolveLibraryNote(app, startFolderPath) — the same resolution, returning the library's own TFile for 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.

utils/logger.js

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.

.commands/

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.

Extension pattern


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.

Logging flow


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 Error with a specific message on failure. It does not call Log.error, Log.fatal, or Log.warn for that failure, and it does not catch an error only to log and rethrow it.
  • May call Log.log for success-path tracing. BaseClass.create() logs Created <path> this way.

Command

  • The submit handler wraps all of its work in one try/catch.
  • callback() wraps modal construction and .open() in a second try/catch, because an error thrown while the modal is built or opened is raised there, outside the submit handler. An example is validateExpectedClass rejecting an expectedClass in a control constructor during onOpen().
  • A validation failure throws. The same catch logs it. Nothing logs at the point of the check.
  • Each catch calls Log.error(commandId, error.message, error) once and does not rethrow.
  • BaseModal.buildSubmitButton() calls onSubmit(this.result) without awaiting it, so a rethrown error would surface as an unhandled promise rejection.
  • Every Log level 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.js have 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 whose callback() does not wrap modal construction and .open() produces no Notice for a failure thrown while its modal is built or opened.

Governing constraints


  • 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 duplicate BaseClass/BaseModal logic rather than extend it, that's a signal the shared core is missing something, not a reason to copy around it.