Extensible controlled vocabularies
Note-status disposition (2026-07-11). ADR 044 removed the global note status field rather than making it collection-expandable. This workshop now concerns only genuinely coherent controlled vocabularies such as source_type; the status analysis below is retained as the investigation that ruled note status out of this mechanism.
Field placement disposition (2026-07-12, adopted same day). ADR 045 shipped the trigger case: source genre is now one required genre field on the snapshot (the report's source_type is removed and rejected), with the known values as a severity-warn enum — direction B below, implemented. The meaning-vs-values guard is stated generally in collections never own frontmatter semantics. What this workshop still owns: whether direction A (a structured vocabulary file with documented default-list growth and a promotion guard) should sit on top of the warn floor, whether source-tier gets the same treatment, and how per-value prose guidance (the Limitations lenses) scales with an open list.
Workshop for designing how a code-enforced, closed enum field becomes open-ended per installed KB while staying validator-checkable.
Trigger
kb/sources/types/ingest-report.schema.yaml's source_type is a JSON-schema enum — currently 11 values (scientific-paper, practitioner-report, conceptual-essay, design-proposal, tool-announcement, github-issue, conversation-thread, code-repository, court-opinion, news-article, official-statement). The last three were just added because casework in the sibling epistack-casebooks repo hit legal filings and press coverage that had no honest fit and were getting forced into conceptual-essay/conversation-thread, degrading retrieval. That's a PR against this repo's schema to unblock a downstream KB — the same "argue a new category into the closed set" pattern ADR 042 retired for registers, recurring on a different field.
Why this isn't just "do the register move again"
Register was a prose taxonomy — COLLECTION.md headers and a theory note, no code enforcement, so opening it up meant relaxing a claim and adding a catalogue doc. source_type is schema-enforced: a bare JSON-schema enum can't be open-ended without either breaking validation or ceasing to validate anything. "Default profiles, open per-KB" here needs a real mechanism, not just a rename — that mechanism is what this workshop is for.
Extension points inventory (2026-07-09 survey)
A full pass over every *.schema.yaml under kb/ for enum: constraints, filtered by the membership test from first-principles are inherited constraints, not design choices: does the set classify what knowledge can be (content taxonomy — demotes) or what the machinery is (may stay closed)? Tell: can you name a rival vocabulary that would also work under the same consumer/substrate/domain/machinery commitments?
Content-taxonomy-shaped — real candidates:
kb/sources/types/ingest-report.schema.yaml→source_type(11 values) — the trigger case above.kb/agent-memory-systems/types/agent-memory-system-review.schema.yaml→source-tier(code-grounded/doc-grounded) — classifies a review's evidentiary basis, the same shape assource_type(source-kind classification) just in a different collection. It's deliberately binary today ("the only authority discriminator," pertypes/agent-memory-system-review.md) andscripts/build_systems_matrix.pybranches on its exact value when building the comparison matrix, so a third tier (e.g. trace-grounded, vendor-doc-grounded) would hit the same wallsource_typejust hit. Not under growth pressure yet — worth designing this workshop's mechanism generally enough to cover it later, not urgent to fix now.kb/types/note.schema.yaml→status(seedling/current/speculative/outdated) — already named in a universal knowledge framework demotes content taxonomies to defaults as the theory's "predicted fourth instance." A proposal already exists — assertion force separate from lifecycle status — but it answers a different half: the field fuses lifecycle with first-person assertion-force meaning, and that proposal's fix keeps lifecycle in the type with the four values unchanged ("validators keep checking the enum unchanged"). Its own adoption trigger (a non-endorsed collection needing to disambiguate whatcurrentmeans) technically exists now — the LHC casebook is live — but hasn't actually fired: every casebook note there is stillstatus: seedling(checked directly in the sibling repo), so nobody has hit the ambiguity in practice. That proposal is adjacent, not overlapping: it never asks whether the four lifecycle values themselves should ever grow, which is this workshop's question.
Machinery-shaped — probably stay closed:
kb/reference/types/adr.schema.yaml→status(accepted/superseded/deprecated) — a narrow, standard ADR-lifecycle vocabulary; hard to construct a plausible rival under this framework's own boundary commitments.kb/types/index.schema.yaml/kb/types/tag-readme.schema.yaml→index_source(directory/tag/tag-indexes) — names generation mechanisms Commonplace's own indexing code implements, not knowledge content.kb/reports/types/connect-report.schema.yaml→depth(quick/standard/deep) — an operational parameter of the connect skill, not a content classification.
Status is a collapsed cube, not a fourth open-list instance
Sharper analysis from a session working the epistack-casebooks casework directly (2026-07-09), worth carrying here in full because it changes what "fix status" even means. The four status values aren't one axis with a missing option — they're a hand-picked diagonal through at least three bundled axes: maturity (has it been through review), currency (live vs. superseded), and endorsement/assertion-force (does the KB commit to it), plus a promotion/durability flavor smeared into seedling. Off-diagonal cells are simply inexpressible — there's no way to say "reviewed but deliberately conjectural and still being drafted," because that cell doesn't exist in a four-value enum built as a diagonal.
The casebook makes the twist concrete, and it's sharper than "endorsement needs a new value": endorsement is inapplicable there, not differently valued. A casebook note asserts nothing in its own voice, so there's no belief to commit or withhold — seedling means "provisionally mapped," outdated means "no longer an accurate map of the debate," not "no longer believed." One whole axis drops out and gets replaced by a different quality entirely (fidelity-to-contestation). That means assertion-force-separate-from-lifecycle-status's frame — keep the enum, let the contract redefine what current means — may not go far enough: if an axis can be absent, not just re-valued, per-collection meaning-redefinition of a fixed enum can't express that; per-collection axis selection can.
The wider connection to this workshop's own question. "Epistemic strength" is currently smeared across at least four places: status (endorsement), source_type (genre as an evidential-weight proxy — peer-reviewed paper vs. news article vs. tweet), the casebook's citation grounding-layer marker (verbatim/paraphrase/second-hand — provenance fidelity), and independence (the shared-authorship/shared-dataset problem — whether three agreeing papers are three lines of evidence or one). That's not one hidden axis, it's a composite that splits along a line this framework already has a name for: objective/checkable (provenance fidelity, source genre) — these can be fields, gates, and open lists, which is exactly what this workshop is designing for source_type — versus judgment/contextual (evidential weight, independence-adjusted strength) — these are contestable and relational (independence is a property of the citation graph, not of any one artifact) and must never collapse into a stored scalar. The named trap: don't add an epistemic_strength field to fix this — that would smear the objective and judgment parts back together, the identical mistake status already made, one level up. Two casebook doctrines from the epistack-framework-additions design menu (workshop deleted 2026-07-23; recoverable from git history) enforce the same discipline from two directions: "confidence must be attributed, never a mark" and "adjudication as a separate, labeled, downstream layer".
Unifying read. status and source_type are the same refactor family, and it's the shape ADR-017/041 already established: push config into the collection contract, keep the core machinery generic. But they're two different mechanisms under that shared philosophy, not one fix — source_type needs an open value list within one axis (this workshop's question); status needs axis decomposition (factor into small named axes — lifecycle, currency, assertion-force — and let the collection contract declare which axes apply here and what their values mean, so a collection can drop an axis, not just re-value it). Conflating the two mechanisms would be the same category error the cube already made. Whether status belongs in this workshop's scope or needs its own is an open call — see Design Questions.
The sibling-repo session that produced this offered to write a transferable methodology note ("epistemic strength is not a storable scalar: factor into checkable provenance facts, an open genre list, attributed judgments, and a relational independence graph") plus log a backlog entry. Per the backlog-to-commonplace protocol (in the sibling epistack-casebooks repo) — build-local-first in the casework, prove it, log the need, let this repo watch and promote what survives — that write-up belongs in epistack-casebooks/backlog-to-commonplace.md first, not authored directly here. Not yet written as of this note.
Borrowing candidates for these axes (not ready)
Whether any of the axes above have real first-principles backing, or are just distinctions that felt right on contact, was raised directly in discussion (2026-07-09). None of the following are adopted or even proposed — this is a scouting list for philosophy-borrowing to evaluate later, against its existing bar: a borrowing earns adoption by changing a concrete operation, not by supplying vocabulary (see programming patterns get a fast pass but other borrowed ideas must earn first-principles support).
- Assertion-force-as-inapplicable ↔ speech-act theory's illocutionary force plus the use-mention distinction (asserting X commits the speaker to X; reporting that someone asserted X does not). A second, sharper application of the theory already sitting as a candidate in speech-acts.md, currently scoped only to document-types-as-actions.
- Independence (shared-authorship/shared-dataset discounting) ↔ Bayesian confirmation theory's correlated-evidence problem — don't multiply likelihoods from non-independent sources. Closer to the KB's "formal systems get a fast pass" bucket than a human-field analogy.
- Genre-as-evidential-weight (
source_typeas a proxy) ↔ evidence law / historical source criticism (primary vs. hearsay, admissibility vs. weight). Compelling structural match, but lands in the same "compelling but untested" bucket the fast-pass note already assigns to legal drafting generally — no concrete technique from law has been successfully applied in this KB yet. - Currency/maturity — probably doesn't need external grounding at all; more likely already "machinery" (a lifecycle existing at all is inherited from machinery coherence, per first-principles items 5–6), not a content taxonomy needing a borrowed theory.
None of this is ready to promote, and it deliberately stays here rather than in philosophy-borrowing/ until one of these changes a concrete operation.
Exception: verifiable quotes. One piece is more ready than the rest, and on reflection it isn't really an external borrowing at all — it's already covered by this KB's own inherited derived-copy rule (first-principles, item 7): "a copy of information recomputable from a ground-truth source must be machine-checked against that source or not exist." A casebook citation marked verbatim (per the citation grounding-layer marker) is exactly that — a copy of a source-snapshot substring. Right now it's a hand-maintained-and-trusted copy: an agent asserts "verbatim" and nothing checks it, which is precisely the trap a derived copy of recomputable truth must be checked or absent warns against. The machinery this needs already has a name and a deferred slot: factored dependency pairs for review freshness names source-as-gate — a (note, source) pair checking "a derived note's consistency with the source snapshot it distills" — as the next factored-pair instance after COLLECTION.md-as-gate shipped (ADR 041). A verbatim-quote check (does the quoted text still appear in the cited snapshot?) is a specific instance of exactly that. Not building this now — flagging it as the one candidate in this whole thread that's already first-principled today, not speculative, with a mechanism already designed and waiting.
Disposition (2026-07-12): the type system is already the extension point — A′ retired as overbuild
Maintainer conclusion after working A′ through its consequences: the escalating machinery it needed to stay coherent — vocabulary file pair with union semantics, rendered projections into type specs, validator staleness checks on the projections, an init carve-out for regenerating marked blocks — is the signal that the modification was mis-sized. It looked like "just a list," too small for a type; it is actually type-sized, and the framework already ships the type-sized extension mechanism: a KB whose sources collection needs a different genre vocabulary declares its own snapshot type (drop a type-spec doc + schema in the collection's types/, list it in COLLECTION.md's Types menu). The genre list and the Limitations lenses travel inside that type spec, exactly where they live in the shipped default. This is sanctioned alternative 3 of collections never own frontmatter semantics — a collection-local type owning the field outright — and, in A′'s own OO framing, subclassing at the designed boundary instead of a value-level plugin system.
The resulting layering, no new machinery anywhere:
- Default: shipped snapshot type, eleven genres, severity-warn on off-list values (ADR 045). Zero effort.
- Casual extension: use an off-list value, live with the standing warning. The warning doubles as the promotion prompt.
- Structural extension: author a collection-local snapshot type. Authoring a type is the promotion guard this workshop asked about; read cost stays type + collection, same as today; the fork is honest (a declared type stops tracking the shipped one — ordinary subclassing contract) rather than silent divergence in a scaffolded copy.
One consequential edit shipped with this disposition: cp-skill-snapshot-web now takes the snapshot type from kb/sources/COLLECTION.md's Types menu instead of hardcoding the shipped path, so a KB's local snapshot type is stamped without skill edits.
Directions A/A′/B below are retained as the worked evaluation trail. What remains open in this workshop is only source-tier — which differs because a code consumer (scripts/build_systems_matrix.py) branches on its values, so a locally-declared type doesn't relieve that coupling by itself — and whether that remainder justifies keeping the workshop open at all or closing it with the source-tier note filed as a smaller question.
Candidate directions (unevaluated)
A. Extensible vocabulary file. The list lives as a YAML file under kb/ — a symbolic artifact, not a schema literal — so an installed KB's own agents can edit it locally without a framework schema change. Rough shape, not yet designed: shipped defaults ship as one file; a consuming KB's local additions live in the same file or a sibling; the JSON schema's enum is either generated from that file at validation time or replaced by a lookup against it. Exact placement, file format, generation vs. runtime-lookup, and interaction with commonplace-init/upgrade are all open. Gives structured tracking (a real shared default list, promotion discipline) at the cost of new machinery.
A′. Per-value docs as declared extension points (favored direction, 2026-07-12). Maintainer direction after ADR 045 shipped the warn floor: don't invite users to edit framework files — a KB owning its scaffolded snapshot.schema.yaml copy is a fork, not an extension mechanism (local edits diverge, and init's never-overwrite policy means upgrades silently stop reaching customized files). The framework should behave like a base class: it declares where extension happens, everything undeclared stays framework-owned and upgradeable. Two refinements over A:
- The extension unit is compound, not a bare value. A genre is value + meaning + classification cues for the capturing agent + a Limitations lens for the ingesting agent — currently spread over three framework files (schema enum,
snapshot.mdvocabulary,ingest-report.mdlens table). The extension point carries all four together, so adding a genre and adding its guidance are one act. - Packaging: one YAML vocabulary file, not per-genre docs (refined 2026-07-12). First cut was per-genre doc files — ADR 018's move (closed
type:enum → path-referenced docs) applied to field values. Maintainer correction: the read pattern is wrong for that granularity. Per-value docs suittype:because type selection is dispatch-then-read-one (pick a type, read only its doc); genre classification is discriminate-among-all (the capturing agent needs every candidate's meaning at once to choose), so the whole vocabulary is the read unit and per-genre files would cost N reads per capture. Instead: one YAML file holding compound entries (id, meaning, recognition cues, lens) — readable as prose-bearing entries by agents and as data by the validator. To avoid reintroducing the fork this direction exists to kill, ship it as a pair with union semantics: a framework-ownedgenres.yaml(read-only, upgradeable) plus an optional KB-local additions file. Two small reads for the agent, one logical vocabulary for the code, and add-only falls out structurally — local entries live in a different file from shipped ones. Derived-copy constraint on the mechanism: if the schemaenumis generated from the YAML it is a copy of recomputable truth and must be machine-checked against it or not exist (a derived copy of recomputable truth must be checked or absent) — so either the validator reads the YAML directly (no enum in the schema) or generation ships with a staleness check. - Read cost at capture time is solved by frontloading, not by the agent reading the YAML (refined 2026-07-12). Step-by-step accounting: a writer already reads type spec + collection contract; today the genre vocabulary rides inside
snapshot.md, so capture pays zero extra reads, and a YAML pair would add two hops to every capture — with the usual failure that optional hops get skipped and the agent classifies from memory. Resolution via frontloading spares execution context: the vocabulary union is known at install/upgrade time, so the YAML pair is the source of truth (validator reads it as data, KBs extend the local file, upgrades replace the shipped file) while the agent-facing surface —snapshot.md's Genre section, and any skill instruction that carries the list — holds a rendered projection inserted at init/upgrade, machine-checked against the union by the validator per the derived-copy rule (the tag-README mark discipline: enforced or omitted). Hot path (every capture) reads exactly what it reads today; the extensibility lives entirely in the cold path (extend, upgrade, validate). In the OO framing: the extension point stays dynamic in the source, dispatch is resolved at compile time — inlining. This also resolves Design Question 3 below in favor of the type spec keeping its rendered list rather than shrinking to a pointer. - Extension points are add-only. In the OO framing, shipped values are
final: a KB addscourt-filing, it never redefinesscientific-paper— the frontmatter-semantics boundary restated as an override rule. The precedents already speak this vocabulary: text-contract profiles are "adopt, extend, or replace" (ADR 042), and ADR 021 already splits shipped-read-only from user-owned for library content — the scaffoldedkb/sources/types/copies predate that discipline and sit on the wrong side of it.
Wider question this raises (bigger than this workshop): the scaffold model itself — which scaffolded files are framework-owned-and-upgradeable vs. user-owned — is undeclared for all type specs, not just genre. A future framework-upgrade story needs that boundary drawn explicitly; this workshop's mechanism is its first worked case.
B. Per-constraint severity downgrade. ADR 024 already ships per-constraint severity (fail/warn), read generically off error.schema for any schema keyword — confirmed in src/commonplace/lib/validation.py (_schema_error_message, ~line 291), not enum-specific. So source_type: {enum: [...], severity: warn} would make out-of-list values a warning, not a validation failure, today, with a one-line schema annotation and zero new code. Materially lighter than A. Trade-off: no shared, inspectable default-list growth or promotion tracking — an agent can type any string and it just warns, which may be too loose for a controlled vocabulary that wants "these are the known categories, extend deliberately," the same tension the register profile's worked-case guard exists to manage.
A and B are not mutually exclusive — B could be the enforcement floor while A (or something like the register profile catalogue) supplies the documented default list a KB extends against.
Design Questions
- Where would an extensible-vocabulary file (direction A) live —
kb/types/next to the schema,kb/sources/(collection-local, sincesource_typeis currently sources-only), or a new global location now thatsource-tieris a second candidate? - Generated enum (a build/validate-time step regenerates the schema's
enumfrom the YAML) vs. runtime lookup (the validator reads the YAML directly, noenumin the schema at all)? Trade thecommonplace-validatearchitecture cost against keeping the schema self-describing. - Does severity downgrade (direction B) alone satisfy the need, or does an open-but-unchecked vocabulary proliferate the same way ADR 042's "guards and defaults" section warns open link/register sets would without a worked-case brake?
- Does an installed KB's local addition need the same worked-case-first promotion guard the register profiles got (ADR 019/042), or is that overkill for a controlled vocabulary that's cheaper to add and remove than a whole text contract?
- What's the shipped-defaults vs. local-additions split: does Commonplace ship a starter list and let each KB append freely, or does it ship categories plus a documented extension mechanism with no implied hierarchy?
- Do the per-
source_type"Limitations Standards" lenses iningest-report.md's authoring instructions extend the same way (each new category needs its own lens paragraph), or is that a separate, harder problem — the schema field is data, the lens is prose guidance keyed to it? - Should the mechanism be designed generally enough to also cover
source-tierfrom day one, or issource_typealone enough to prove it out first (build-local-first, one field at a time)? - Relationship to vocabulary-governance: that workshop is about definitional terms (prose vocabulary like "register", collection/type scope for word meanings); this is about enum values in a schema field. Adjacent, not the same — check whether one mechanism could serve both before designing two.
- Does
status's axis-decomposition problem belong in this workshop's scope, or is it big enough (a different mechanism — collection-selected axes, not an open value list) to need its own workshop once the backlog write-up lands? Leaning toward separate — the two share a philosophy (push config into the contract) but not an implementation. - For A′: where does the vocabulary file pair live — shipped
genres.yamlunder a read-only framework namespace (ADR 021 style, e.g. materialized underkb/commonplace/) or next to the sources types, and what is the KB-local additions file called and located? Does the entry format need a schema of its own (id + meaning + recognition cues + lens)? - For A′: validator reads the YAML union directly (no
enumin the snapshot schema — the schema loses self-description for this one field) vs. generated enum with a machine staleness check (derived-copy rule)? The warn floor from ADR 045 stays as the behavior for values present in neither file. - For A′: do
snapshot.md's Genre section andingest-report.md's Limitations Standards table then shrink to pointers at the vocabulary file, so the type specs stop carrying a second copy of the same list? Resolved by the frontloading refinement above: they keep rendered, machine-checked projections — copies are fine when enforced, and the hot path stays one read. Remaining sub-question: which surfaces get projections (type specs only, or also the capture skill's SKILL.md), and doescommonplace-init's never-overwrite policy need a carve-out for regenerating marked projection blocks? - Is verifiable-quote checking worth building before the axis-decomposition and open-list questions are settled? Settled: shipped as a validator check (ADR 046) — a narrower, Level A deterministic check, distinct from source-as-gate's Level B judgment.
Closure
Close this workshop when there's a concrete mechanism proposal that answers: where the vocabulary file lives, how it's validated against, whether local additions need a promotion guard, and how the per-category prose guidance (the Limitations Standards lenses) is meant to extend alongside it. That becomes a proposal in kb/reference/proposals/, same as the register case, then an ADR once (if) it's exercised.
Grounding
- ADR 042: register becomes a default profile under open-ended text contracts — extends: the precedent this workshop generalizes to a code-enforced field
- Text contract / Text contract profiles — extends: the prose-side open-ended-list pattern; compare its worked-case-first guard against what a controlled-vocabulary guard should look like
- A universal knowledge framework demotes content taxonomies to defaults and keeps answerability — grounds: the demote-don't-delete theory (routing/growth-brake/interoperability) and the "predicted fourth instance" (status) the inventory found
- First principles are inherited constraints, not design choices — grounds: the membership test (content taxonomy vs. machinery) the inventory above applies to every schema enum
- ADR 024: schema severity is per-constraint, fail by default — grounds: candidate direction B — existing, generic per-constraint severity machinery, not
enum-specific - Assertion force separate from lifecycle status — see-also: adjacent proposal on the same
statusfield, fixing fused semantics rather than closed values — not overlapping, worth reading together - ingest-report type and its schema — operates-on: the concrete artifact with the closed
source_typeenum - agent-memory-system-review type and its schema — operates-on: the second content-taxonomy-shaped candidate (
source-tier) - "A richer
sourcetype" (from the deletedepistack-framework-additionsmenu) — see-also: adjacent casework candidate (capture metadata as first-class fields), not the same problem but same workshop-of-origin - Vocabulary governance — see-also: adjacent workshop on definitional-term scope; check for a shared mechanism before designing two
- "Confidence must be attributed, never a mark" / "Adjudication as a separate, labeled, downstream layer" (from the deleted
epistack-framework-additionsmenu) — grounds: the two casebook doctrines the "epistemic strength isn't a scalar" argument rests on - The backlog-to-commonplace protocol (sibling
epistack-casebooksrepo; its framework-side pointer workshop was deleted 2026-07-23) — part-of: governs where the transferable methodology claim should actually get written up - Philosophy borrowing / Speech acts — see-also: the adoption-gated workshop these borrowing candidates would need to clear, and the closest existing candidate they'd extend
- Programming patterns get a fast pass but other borrowed ideas must earn first-principles support — grounds: the adoption bar the borrowing candidates are graded against
- A derived copy of recomputable truth must be checked or absent — grounds: the inherited principle that already covers verifiable quotes, no borrowing needed
- Factored dependency pairs for review freshness — grounds: names
source-as-gate, the deferred mechanism a verbatim-quote check would instantiate