docs(adr-0087): rhetorical style as selection axis + writing-chain harvester spec (#72)
Pre-work for a writing-curriculum extension to CORE. Two companion
documents, both Proposed status (no code shipped).
docs/decisions/ADR-0087-rhetorical-style-axis.md
Pins rhetorical style as a third selection axis — sibling to anchor
lens (ADR-0073), orthogonal to register (ADR-0070). Substantive
axis: trace_hash DISTINCT across styles (style changes which moves
the composer requires and which frames the realizer emits, which
changes the propositional plan, which changes the trace).
Four anti-patterns explicitly named and rejected:
- style as motor (re-couples realizer to geometry; same shape as
the ADR-0085 fusion-operator rejection)
- style as register variant (conflates substantive with stylistic)
- style as identity axis (bloats identity doctrine)
- style auto-detected from user input (operator-chosen only)
Pack shape mirrors packs/anchor_lens/. default_unstyled_v1 is the
null-lift pack identical to no-style behavior. Three CI invariants
proposed: rhetorical_style_null_lift, schema validation, three-axis
orthogonality.
Substrate-only ADR — no consumer code, no genre packs. Consumer
integration is a follow-up ADR (composer + realizer extensions
that read permitted_frames + required_moves_per_claim +
forbidden_moves).
docs/curriculum/writing-chain-harvester-spec.md
Layer 0 of the writing curriculum. A deterministic tool that
extracts candidate (subject, predicate, object) triples from
reviewed expert prose and surfaces them as proposals to the
existing teaching/review pipeline.
Five stages (segment → classify → extract → propose → audit) —
pure-Python rule-based, no LLM generation, no auto-acceptance.
Trust boundary: reviewer accept/reject via the existing
core teaching propose/review path. No bypass permitted.
The harvester is a proposal PRODUCER, not a proposal CONSUMER.
Plugs into the existing pipeline without inventing a new review
mechanism. Each proposal carries source_id + source_line + the
exact source_clause it came from for reviewer verification.
First-implementation acceptance criteria deliberately tight:
Stage 0+1 with dry-run only. Stages 2-5 are follow-up PRs.
Substrate-first sequencing pattern (ADR-0084 → 0085) reused
throughout. Both documents acknowledge open questions deferred to
implementation phase rather than pre-deciding.
Why now: a writing curriculum is being scoped. Without this ADR,
every downstream PR faces the same "should style be a motor?"
question and the temptation to reach for the geometry will recur
every time the realizer produces a stilted surface. Pinning the
axis up front prevents that recurrence.
This commit is contained in:
parent
537d73e394
commit
ac75cfc659
2 changed files with 603 additions and 0 deletions
294
docs/curriculum/writing-chain-harvester-spec.md
Normal file
294
docs/curriculum/writing-chain-harvester-spec.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# Writing-Chain Harvester — Specification
|
||||
|
||||
**Status:** Proposed (specification only — no code shipped)
|
||||
**Date:** 2026-05-20
|
||||
**Author:** Shay
|
||||
**Companion to:** [ADR-0087](../decisions/ADR-0087-rhetorical-style-axis.md)
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
The writing curriculum needs *thousands* of ratified rhetorical
|
||||
chains (claim → evidence → warrant; evidence → support → conclusion;
|
||||
hedge → uncertainty → revision; etc.) to operate at PhD level.
|
||||
Today the entire `cognition_chains_v1` corpus has ~21 active chains
|
||||
for ~22 lemmas — a number chosen so each chain could be hand-authored
|
||||
and reviewed by the project author. PhD-level writing operates on
|
||||
orders of magnitude more.
|
||||
|
||||
Two paths to fill that gap:
|
||||
|
||||
| Path | Cost | Risk |
|
||||
|---|---|---|
|
||||
| Hand-author every chain | Prohibitive | None (matches existing discipline) |
|
||||
| LLM-generate then accept | Cheap | Violates the no-LLM-content rule; reintroduces drift |
|
||||
|
||||
Neither is viable. This spec defines a **third path**: a *harvester*
|
||||
that proposes candidate rhetorical chains by extracting them from
|
||||
reviewed expert prose (peer-reviewed journals, ratified technical
|
||||
documentation, the project author's own canonical writing) — one
|
||||
proposed `(subject, predicate, object)` triple at a time, each
|
||||
surfaced to a human reviewer for accept/reject via the existing
|
||||
teaching/review loop.
|
||||
|
||||
The harvester does no LLM-style generation. It *extracts* structure
|
||||
from already-reviewed text, surfaces it as a proposal, and lets the
|
||||
existing `core teaching propose / review / accept` pipeline carry the
|
||||
proposal to ratification. The reviewer's accept/reject is the load-
|
||||
bearing trust boundary, identical in shape to the one ADR-0084
|
||||
content used.
|
||||
|
||||
The harvester is the writing curriculum's **Layer 0**.
|
||||
|
||||
---
|
||||
|
||||
## Trust boundary
|
||||
|
||||
This spec touches a high-risk surface: external prose ingestion. The
|
||||
input is text the author did not write. The output is candidate
|
||||
content for a corpus that downstream surfaces draw from. Every step
|
||||
must enforce that no input prose token ends up on a user surface
|
||||
without passing through a human review gate.
|
||||
|
||||
| Boundary | Enforcement |
|
||||
|---|---|
|
||||
| Input prose | Read-only from a *manifest* of approved sources (each source pinned by checksum). No web fetch at harvest time. No automatic source admission. |
|
||||
| Candidate proposals | Land in the existing teaching-proposal queue. NEVER auto-accepted. NEVER ratified without `core teaching review --accept`. |
|
||||
| Tokens in proposals | Restricted to lemmas already in mounted packs (lexicon residency check) + a small "novel-token" candidate flag that requires explicit operator promotion before the proposal is ratifiable. |
|
||||
| Output corpus | Existing teaching-corpus path. Harvester proposals are tagged with provenance `harvester:<source_id>:<line_no>` so any accepted chain can be traced back to the prose it came from. |
|
||||
|
||||
**No bypass of the review path is permitted.** The harvester
|
||||
produces *proposals*, never *content*.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Stage 0 — Source manifest
|
||||
|
||||
`packs/writing_sources/<corpus_id>/manifest.json` declares an
|
||||
approved-source corpus:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"corpus_id": "scientific_method_canon_v1",
|
||||
"version": 1,
|
||||
"issued_at": "2026-05-21T00:00:00Z",
|
||||
"sources": [
|
||||
{
|
||||
"source_id": "popper_logic_of_scientific_discovery_1959_ch01",
|
||||
"title": "The Logic of Scientific Discovery, Ch.1",
|
||||
"author": "Karl Popper",
|
||||
"license": "fair-use:short-quotation",
|
||||
"path": "raw/popper_1959_ch01.txt",
|
||||
"checksum": "<sha256 of raw text bytes>"
|
||||
},
|
||||
...
|
||||
],
|
||||
"checksum": "<sha256 of this manifest minus checksum field>",
|
||||
"provenance": "writing-harvester:reviewed:2026-05-21"
|
||||
}
|
||||
```
|
||||
|
||||
Sources are added by *human admission only*. The harvester never
|
||||
auto-discovers a source. The manifest is checksum-pinned the same
|
||||
way every other CORE pack is.
|
||||
|
||||
### Stage 1 — Sentence segmentation + clause extraction
|
||||
|
||||
`writing_curriculum/harvester/segment.py`:
|
||||
|
||||
- Tokenize raw text into sentences (deterministic, rule-based — same
|
||||
discipline as existing pack compilers; no statistical tokenizer).
|
||||
- For each sentence, identify candidate clauses by punctuation +
|
||||
conjunction markers.
|
||||
- Output: a typed stream of `(source_id, line_no, sentence_no,
|
||||
clause_no, raw_clause_text)` records.
|
||||
|
||||
This stage produces zero CORE content. It only structures the input.
|
||||
|
||||
### Stage 2 — Rhetorical-move classification
|
||||
|
||||
`writing_curriculum/harvester/classify.py`:
|
||||
|
||||
- For each clause, apply a *deterministic* pattern matcher (rules,
|
||||
not learned) to classify the clause's likely rhetorical move:
|
||||
- `claim`: declarative assertion without explicit warrant marker.
|
||||
- `evidence`: clause introduced by `"because"`, `"as shown by"`,
|
||||
`"the data indicate"`, etc.
|
||||
- `warrant`: clause introduced by `"therefore"`, `"hence"`,
|
||||
`"this implies"`, etc.
|
||||
- `concession`: clause introduced by `"although"`, `"while"`,
|
||||
`"granted"`, etc.
|
||||
- `hedge`: clause containing `"may"`, `"suggests"`, `"appears"`,
|
||||
`"possibly"`, etc.
|
||||
- `definitional_move`: clause matching `"X is Y"` pattern with X
|
||||
being a candidate technical term.
|
||||
- Output: typed records `(source_id, ..., clause_text, move,
|
||||
confidence)`.
|
||||
|
||||
The classifier is deliberately conservative. Ambiguous clauses
|
||||
output move `unknown` and are skipped at the proposal stage. No
|
||||
classifier weight is trained — the pattern set is hand-maintained
|
||||
and reviewed like any other ratified discipline.
|
||||
|
||||
### Stage 3 — Triple extraction
|
||||
|
||||
`writing_curriculum/harvester/extract.py`:
|
||||
|
||||
For each clause classified as a known move, attempt to extract a
|
||||
`(subject, predicate, object)` triple where:
|
||||
|
||||
- `subject` and `object` resolve to lemmas in mounted packs (via
|
||||
`chat.pack_resolver.resolve_lemma`).
|
||||
- `predicate` is one of the existing relation predicates the
|
||||
cognition / relations corpora already use (`requires`,
|
||||
`supports`, `grounds`, `precedes`, `entails`, `contrasts_with`,
|
||||
`evidences`, `causes`, `implies`).
|
||||
- A new predicate is allowed ONLY if proposed as a separate
|
||||
candidate with explicit `new_predicate: true` flag — never
|
||||
silently introduced.
|
||||
|
||||
Extraction outputs candidate `TeachingProposal` records ready for
|
||||
the existing `core teaching propose` path. Each proposal carries:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"proposal_id": "harvester:scientific_method_canon_v1:popper_1959_ch01:L42",
|
||||
"chain_id": "<auto-generated, prefixed with `harvested_`>",
|
||||
"subject": "evidence",
|
||||
"predicate": "supports",
|
||||
"object": "claim",
|
||||
"intent_tag": "cause",
|
||||
"source_clause": "<the original prose clause, verbatim>",
|
||||
"source_id": "popper_logic_of_scientific_discovery_1959_ch01",
|
||||
"source_line": 42,
|
||||
"rhetorical_move": "evidence",
|
||||
"extractor_confidence": "high|medium|low",
|
||||
"extracted_at": "<ISO timestamp>",
|
||||
"review_state": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
The `source_clause` field is REQUIRED. Every proposal carries the
|
||||
exact prose it came from so a reviewer can verify the extraction.
|
||||
|
||||
### Stage 4 — Review queue integration
|
||||
|
||||
The harvester writes proposals to the existing teaching-proposal
|
||||
pipeline (`teaching/store.py`). No new review mechanism. No new
|
||||
ratification gate. The proposal is exactly the same shape as a
|
||||
hand-authored one with two extra metadata fields (`source_id`,
|
||||
`source_line`) that the existing pipeline preserves but doesn't
|
||||
require.
|
||||
|
||||
This is the load-bearing design choice: **the harvester adds a
|
||||
proposal producer, not a proposal consumer.** Reviewers see
|
||||
harvested proposals in the same queue as hand-authored ones, judge
|
||||
them with the same criteria, accept/reject with the same commands.
|
||||
|
||||
### Stage 5 — Provenance audit
|
||||
|
||||
`writing_curriculum/harvester/audit.py`:
|
||||
|
||||
A diagnostic tool that, given any chain in any teaching corpus,
|
||||
reports whether it was hand-authored or harvested, and if harvested,
|
||||
which source/line. Operators can run this to spot-check the
|
||||
provenance of any surface the system produces.
|
||||
|
||||
---
|
||||
|
||||
## Determinism + replay
|
||||
|
||||
- Sentence segmentation, classification, and extraction are all
|
||||
deterministic functions of `(source_text, harvester_version)`.
|
||||
- Running the harvester twice on the same input manifest produces
|
||||
byte-identical proposals.
|
||||
- Harvester version is pinned in the proposal `extracted_at` metadata.
|
||||
- Re-running the harvester against an updated input manifest is
|
||||
additive — it generates new proposals for new sources, never
|
||||
retroactively modifies prior proposals.
|
||||
|
||||
This is the same determinism discipline the rest of CORE follows.
|
||||
|
||||
---
|
||||
|
||||
## What the harvester is NOT
|
||||
|
||||
| Not | Why |
|
||||
|---|---|
|
||||
| A summarizer | It extracts triples, not summaries. No prose generation, no paraphrase. |
|
||||
| A learned model | Pattern rules, hand-maintained. No statistical training. |
|
||||
| An auto-ratifier | Every proposal goes through human review. The reviewer is the trust boundary. |
|
||||
| A source admitter | Sources are admitted by `core writing sources add <path>` (human-initiated) only. No web crawler. |
|
||||
| A revision engine | Revision (proposing edits to ratified chains) is a separate spec — possibly the next one. |
|
||||
| A style detector | Source corpus selection encodes the style; the harvester doesn't classify "is this scientific?" — operators decide which corpora to harvest from. |
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria for the spec → first implementation PR
|
||||
|
||||
When the first implementation PR for this spec opens, it must:
|
||||
|
||||
1. Ship Stage 0 (source manifest schema + loader) and Stage 1
|
||||
(segmentation) only. Not Stages 2-5.
|
||||
2. Include one tiny ratified source corpus (e.g., a public-domain
|
||||
short essay) as the fixture.
|
||||
3. Produce a deterministic dry-run report
|
||||
(`core writing harvest --dry-run --source <id>`) that shows the
|
||||
segmented sentences but does NOT write to the teaching-proposal
|
||||
queue.
|
||||
4. Pass a "no surface emission" test: nothing the harvester produces
|
||||
reaches a user surface in this first PR.
|
||||
|
||||
This sequencing — substrate, then dry-run, then propose pipeline,
|
||||
then accept-loop integration — mirrors ADR-0084 → 0085's
|
||||
substrate-first discipline.
|
||||
|
||||
Stages 2-5 land in follow-up PRs, each with the same gate (no
|
||||
surface emission until human review explicitly accepts the
|
||||
proposals).
|
||||
|
||||
---
|
||||
|
||||
## Open questions for the implementation phase
|
||||
|
||||
These are deliberately left for the implementation PR to resolve,
|
||||
not pre-decided here:
|
||||
|
||||
1. **Segmentation library choice.** Pure-Python rule-based (zero
|
||||
dependency, matches CORE's no-statistical-tokenizer discipline)
|
||||
vs `nltk`/`spacy` sentence segmenters (battle-tested but pull in
|
||||
model weights). Recommendation: pure-Python. Open for
|
||||
reconsideration only if the rule-based output is demonstrably
|
||||
worse on the fixture corpus.
|
||||
2. **Source-license boundary.** What counts as an admissible
|
||||
source? Public-domain only? Fair-use short quotation? Operator-
|
||||
licensed corpora? Worth an explicit policy doc before any
|
||||
non-public-domain source is admitted.
|
||||
3. **Reviewer UX.** Harvested proposals carry source-clause context
|
||||
that hand-authored ones don't. The review tool should display
|
||||
this context. Whether that's a CLI flag or a separate review
|
||||
surface is an implementation choice.
|
||||
4. **Cross-corpus chain composition.** Can a harvested chain whose
|
||||
subject is in `en_core_cognition_v1` and whose object is in
|
||||
`en_core_relations_v1` be ratified into a third corpus, or must
|
||||
each pack stay in its own corpus? Likely answer: yes, via the
|
||||
existing cross-pack-chain mechanism (ADR-0064), but worth
|
||||
exercising on a real example before committing.
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- [ADR-0087](../decisions/ADR-0087-rhetorical-style-axis.md) — the
|
||||
axis this harvester ultimately feeds.
|
||||
- [ADR-0084](../decisions/ADR-0084-definitional-layer.md) /
|
||||
[ADR-0085](../decisions/ADR-0085-gloss-aware-cause.md) — substrate-
|
||||
before-consumer sequencing pattern this spec adopts.
|
||||
- [ADR-0064](../decisions/ADR-0064-cross-pack-teaching.md) — cross-
|
||||
pack teaching corpora mechanism the harvester will reuse.
|
||||
- `teaching/store.py` — the proposal-queue integration point.
|
||||
- `core teaching propose / review / supersede` — the existing review
|
||||
pipeline the harvester producer feeds.
|
||||
309
docs/decisions/ADR-0087-rhetorical-style-axis.md
Normal file
309
docs/decisions/ADR-0087-rhetorical-style-axis.md
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# ADR-0087 — Rhetorical Style as Selection Axis (Pre-Work for Writing Curriculum)
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-05-20
|
||||
**Author:** Shay
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
A writing-curriculum extension to CORE is being scoped (informal name:
|
||||
"PhD-level reading/writing"). Before any composer, content, or eval-lane
|
||||
work begins, one architectural decision must be pinned: **what kind of
|
||||
mechanism is rhetorical style?**
|
||||
|
||||
Two answers are plausible:
|
||||
|
||||
1. **A motor.** Style is encoded as a versor applied to the field at
|
||||
compose time. A "scientific" style is a different operator than a
|
||||
"popular" style; the operator transforms the realized state before
|
||||
surface emission.
|
||||
2. **A selection axis.** Style is encoded as a pack the operator
|
||||
mounts at configuration time. A "scientific" deployment loads one
|
||||
pack; a "popular" deployment loads another. The realizer does not
|
||||
change behavior at compose time; it draws from a different
|
||||
substrate.
|
||||
|
||||
These look superficially similar — both can produce different
|
||||
surfaces for the same prompt. They are not similar at all
|
||||
structurally. The choice we make here propagates into every
|
||||
downstream design decision in the writing curriculum.
|
||||
|
||||
### Why this is the right moment to decide
|
||||
|
||||
Two prior results constrain the answer:
|
||||
|
||||
- **The φ-separation result** (memory:
|
||||
[`phi-separation-falsified`](../../.claude/projects/-Users-kaizenpro-Projects-core/memory/MEMORY.md))
|
||||
established that semantic capability lives in *chain composition*,
|
||||
not in φ geometry. By extension, *style* capability — to the
|
||||
extent it is rhetorical-mode selection rather than semantic-content
|
||||
change — should not require a φ change either.
|
||||
|
||||
- **ADR-0085's "fusion operator" rejection.** The proposal to fuse
|
||||
realized template with geometric state before lexical collapse was
|
||||
declined on the grounds that it re-couples the realizer to field
|
||||
dynamics, breaks deterministic replay in a new place, and re-imports
|
||||
the transformer-style coupling CORE deliberately is not. A
|
||||
style-as-motor design has the same anti-pattern shape.
|
||||
|
||||
The current decoupling — geometric field → propositional plan →
|
||||
deterministic realizer → realized surface → register decoration — is
|
||||
load-bearing. Every prior ADR that improved capability did so by
|
||||
*expanding the substrate the realizer draws from*, not by reaching
|
||||
into the geometry. ADR-0083 added depth (transitive chains). ADR-0084
|
||||
added definitions (gloss substrate). ADR-0085 added a frame for the
|
||||
same substrate (gloss-aware CAUSE). None of them changed the field.
|
||||
|
||||
Rhetorical style should follow the same pattern.
|
||||
|
||||
### Existing axes for context
|
||||
|
||||
Two selection axes are already pinned by prior ADRs:
|
||||
|
||||
| Axis | ADR | What varies | `trace_hash` behavior | Engagement point |
|
||||
|---|---|---|---|---|
|
||||
| Register | [ADR-0070](./ADR-0070-register-terse-v1.md) | Surface decoration (terse / convivial / formal) | **CONSTANT** | post-composer (decorate_surface) |
|
||||
| Anchor lens | [ADR-0073](./ADR-0073-anchor-lens-substrate.md) | Semantic-vocabulary tradition (Greek philosophical / Hebrew covenant) | **DISTINCT** | pre-realizer (substrate composer) |
|
||||
|
||||
These are CI-pinned as orthogonal:
|
||||
[`anchor_lens_byte_identity_null_lift`](../../tests/test_anchor_lens_byte_identity.py)
|
||||
holds the unanchored-vs-null identity; the register tour holds the
|
||||
register-vs-trace invariance; the anchor-lens tour holds the
|
||||
lens-vs-trace distinctness. Both invariants hold simultaneously
|
||||
(memory: `adr-0073d-anchor-lens-telemetry-tour`).
|
||||
|
||||
Rhetorical style needs to slot into this taxonomy without breaking
|
||||
either axis.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
**Rhetorical style is a selection axis, not a motor.** It is a
|
||||
*third* substantive axis, sibling to anchor lens, orthogonal to
|
||||
register.
|
||||
|
||||
### Axis definition
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| **Mechanism** | Pack the operator mounts at runtime configuration time. Not a geometric operator, not a φ change, not a fusion at compose time. |
|
||||
| **What it varies** | The rhetorical-mode frames the realizer is permitted to emit AND the rhetorical-move requirements the composer applies (e.g., academic style requires an evidence-bearing chain for every assertion; popular style does not). |
|
||||
| **What it does NOT vary** | The semantic content (that's anchor-lens territory). The surface decoration (that's register territory). The field dynamics, versor closure, vault recall, or trace machinery (those are non-negotiable invariants). |
|
||||
| **`trace_hash` behavior** | **DISTINCT across styles.** Style selection is substantive: it changes which moves the composer requires and which frames the realizer emits, which changes the propositional plan that feeds the trace. (CONTRAST with register, which holds trace constant.) |
|
||||
| **Engagement point** | Pre-realizer, alongside anchor lens. The composer consults the active rhetorical-style pack when assembling the proposition graph; the realizer consults it when choosing frames. |
|
||||
| **Default** | A `default_unstyled_v1` pack identical in effect to the absence of styling — null-lift, byte-identical traces. Mirrors ADR-0073b's `default_unanchored_v1`. |
|
||||
|
||||
### Pack shape (substrate, not consumer)
|
||||
|
||||
Analogous to `packs/anchor_lens/<id>/`:
|
||||
|
||||
```
|
||||
packs/rhetorical_style/<id>/
|
||||
manifest.json — pack identity, version, mastery_report seal
|
||||
rhetorical_style.json — the pack content (see schema below)
|
||||
<id>.mastery_report.json — companion self-seal, verified at load
|
||||
```
|
||||
|
||||
`rhetorical_style.json` schema (proposed v1):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"pack_id": "en_academic_v1",
|
||||
"version": 1,
|
||||
"issued_at": "2026-05-21T00:00:00Z",
|
||||
"default_unstyled": false, // true only for the no-op pack
|
||||
"permitted_frames": [ // realizer frame allow-list
|
||||
"warrant", // "Therefore X, because Y."
|
||||
"concession", // "While X, Y."
|
||||
"hedge", // "This suggests X, though Q."
|
||||
"definitional_move" // "By X we mean {gloss}."
|
||||
],
|
||||
"required_moves_per_claim": [ // composer must include
|
||||
"evidence", // every assertion needs evidence
|
||||
"warrant" // bridging the evidence-claim gap
|
||||
],
|
||||
"forbidden_moves": [ // moves the style refuses
|
||||
"bare_assertion" // no claims without warrant
|
||||
],
|
||||
"provenance": "adr-0087:reviewed:2026-05-21"
|
||||
}
|
||||
```
|
||||
|
||||
Three named frames in the v0 vocabulary (`warrant`, `concession`,
|
||||
`hedge`, `definitional_move`) are the *minimum* set the realizer
|
||||
needs to express the claim-evidence-warrant triad. More frames are
|
||||
added as the writing curriculum proves their necessity, not
|
||||
preemptively.
|
||||
|
||||
### Composer & realizer contract
|
||||
|
||||
This ADR pins the *contract*; it does not implement the
|
||||
consumers. The consumer ADR (call it ADR-0089 in the writing
|
||||
sequence) will add:
|
||||
|
||||
- A `RuntimeConfig.rhetorical_style_id: str | None = None` field
|
||||
(default → `default_unstyled_v1` per mounting discipline).
|
||||
- A `chat/runtime.py` plumb to make the active style pack available
|
||||
to the composer and realizer.
|
||||
- Realizer extensions that read `permitted_frames` and refuse to
|
||||
emit frames outside the allow-list.
|
||||
- Composer extensions that read `required_moves_per_claim` and
|
||||
refuse to ratify a surface that omits a required move.
|
||||
|
||||
The *substrate* lands first (this ADR + pack schema + loader +
|
||||
`default_unstyled_v1` pack). The *consumer* lands second after the
|
||||
substrate ratifies. This is the same sequencing discipline as
|
||||
ADR-0084 → 0085.
|
||||
|
||||
### Forbidden alternatives
|
||||
|
||||
These are explicitly **not** the design, named here so future PRs
|
||||
recognize them as anti-patterns:
|
||||
|
||||
| Anti-pattern | Why rejected |
|
||||
|---|---|
|
||||
| **Style as motor.** A versor applied to the field at compose time. | Re-couples realizer to geometry. Breaks deterministic replay (`trace_hash` now depends on compose-time field state). Same shape as the rejected ADR-0085 fusion proposal. |
|
||||
| **Style as register.** Treating "academic" as a register-pack variant. | Conflates substantive (what moves must appear) with stylistic (how to decorate the surface). Would either bloat register packs with rhetorical-mode logic or silently broaden the trace-constant invariant. |
|
||||
| **Style as identity axis.** A `precision: high` axis under `IdentityPack`. | Identity should be load-bearing and falsifiable (memory: `identity-doctrine`), not a place to encode per-deployment preferences. Adding "is academic style" as an identity axis is the bloat that doctrine refuses. |
|
||||
| **Style detected from user input.** Auto-routing prompts to "scientific" vs "popular" lens based on heuristics. | Substantive variation should be operator-chosen, not auto-detected. The system shouldn't decide that this user "wants science"; the deployment decides what the system is. |
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Required tests for the substrate
|
||||
|
||||
- **Pack loader**:
|
||||
- `default_unstyled_v1` ratifies with the same mastery-report
|
||||
self-seal discipline as `default_unanchored_v1`.
|
||||
- Missing pack → `RhetoricalStylePackError` (fail-closed, sister to
|
||||
`SafetyPackError`).
|
||||
- `permitted_frames` keys validated against a known-frame allow-list.
|
||||
- **Schema validation**:
|
||||
- Unknown keys inside the rhetorical-style block → ratification
|
||||
failure.
|
||||
- `default_unstyled: true` only valid when `permitted_frames`,
|
||||
`required_moves_per_claim`, and `forbidden_moves` are all empty.
|
||||
- **Null-lift invariant (CI-pinned)**:
|
||||
- `rhetorical_style_null_lift`: under `rhetorical_style_id=None`
|
||||
(resolves to `default_unstyled_v1`), every cognition lane case
|
||||
produces a byte-identical surface + `trace_hash` to today's
|
||||
no-style baseline.
|
||||
- **Three-axis orthogonality (CI-pinned)**:
|
||||
- With register + anchor lens + rhetorical style all selectable
|
||||
independently, the orthogonality tour must show:
|
||||
- Register varies → `trace_hash` CONSTANT (existing invariant).
|
||||
- Anchor lens varies → `trace_hash` DISTINCT (existing invariant).
|
||||
- Rhetorical style varies → `trace_hash` DISTINCT (new invariant).
|
||||
- All three axes vary simultaneously without breaking either
|
||||
style or lens axis.
|
||||
|
||||
### Lanes (regression check)
|
||||
|
||||
```
|
||||
core test --suite smoke -q
|
||||
core test --suite cognition -q
|
||||
core test --suite packs -q
|
||||
core test --suite teaching -q
|
||||
core test --suite runtime -q
|
||||
core eval cognition (byte-identical at default config)
|
||||
```
|
||||
|
||||
### What this ADR does NOT verify
|
||||
|
||||
- Realizer frame-emission correctness — that's the consumer ADR.
|
||||
- `required_moves_per_claim` enforcement — that's the consumer ADR.
|
||||
- The harvester (Layer 0 of the writing curriculum) — separate spec.
|
||||
- Genre-specific gloss content for any style other than
|
||||
`default_unstyled_v1` — separate content effort.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### What changes
|
||||
|
||||
- New `packs/rhetorical_style/<id>/` directory layout, sister to
|
||||
`packs/anchor_lens/` and `packs/safety/`.
|
||||
- New `packs/rhetorical_style/loader.py` with `RhetoricalStylePack`
|
||||
dataclass and ratification gates.
|
||||
- New `default_unstyled_v1` pack that produces the null-lift baseline.
|
||||
- `docs/runtime_contracts.md` updated to declare rhetorical style as
|
||||
the third substantive axis with its `trace_hash` invariant.
|
||||
|
||||
### What does not change
|
||||
|
||||
- Composer or realizer behavior. Substrate-only ADR; consumers are a
|
||||
follow-up.
|
||||
- Register or anchor-lens axes. Both remain operational with their
|
||||
current invariants.
|
||||
- Versor / vault / recall / field dynamics. Untouched.
|
||||
- The non-negotiable `versor_condition(F) < 1e-6` invariant.
|
||||
- Identity / safety / ethics packs. Style is a fourth pack family
|
||||
(`identity` | `safety` | `ethics` | `rhetorical_style`), not a
|
||||
modification to any of them.
|
||||
|
||||
---
|
||||
|
||||
## Scope limits
|
||||
|
||||
- **No consumer code.** Realizer + composer integration is the next
|
||||
ADR's job.
|
||||
- **No genre packs.** Only `default_unstyled_v1` ships in this ADR.
|
||||
`en_academic_v1`, `en_technical_v1`, etc., are content efforts that
|
||||
follow once the substrate ratifies and the consumer ADR lands.
|
||||
- **No prompt-routing.** Style selection is operator-set at
|
||||
`RuntimeConfig` time only. Auto-detection is explicitly out of scope.
|
||||
- **No retroactive renaming of anchor lens.** Anchor lens stays
|
||||
substantive-vocabulary; rhetorical style is the substantive-frame
|
||||
axis. They are siblings, not subsets.
|
||||
- **No motor variant accepted.** Future PRs proposing
|
||||
style-as-motor must rebut this ADR; the burden of proof is on the
|
||||
proposer.
|
||||
|
||||
---
|
||||
|
||||
## Why now
|
||||
|
||||
A writing curriculum is being scoped. Before any composer extension,
|
||||
content harvest, or eval lane lands for that curriculum, the
|
||||
mechanism of rhetorical style needs to be fixed in writing. Without
|
||||
this ADR, every downstream PR will face the same question ("should
|
||||
this be a motor?") and the temptation to reach for the geometry will
|
||||
recur every time the realizer produces a stilted surface. Pinning
|
||||
the axis up front prevents that recurrence.
|
||||
|
||||
ADR-0087 is also the smallest possible commitment: pack schema +
|
||||
loader + one null-lift pack + invariants. No consumer code, no
|
||||
content, no realizer change. It's the most reversible-yet-
|
||||
load-bearing step in the writing-curriculum sequence.
|
||||
|
||||
---
|
||||
|
||||
## Cross-References
|
||||
|
||||
- [ADR-0070](./ADR-0070-register-terse-v1.md) — register axis
|
||||
(stylistic, trace CONSTANT). Rhetorical style is the substantive
|
||||
sibling.
|
||||
- [ADR-0073](./ADR-0073-anchor-lens-substrate.md) — anchor lens axis
|
||||
(substantive vocabulary, trace DISTINCT). Rhetorical style adopts
|
||||
the same substrate-pack discipline.
|
||||
- [ADR-0084](./ADR-0084-definitional-layer.md) — substrate-then-
|
||||
consumer sequencing pattern. Reused here verbatim.
|
||||
- [ADR-0085](./ADR-0085-gloss-aware-cause.md) — frame-swap-not-fusion
|
||||
precedent. The "fusion operator" rejection is the load-bearing
|
||||
prior art for this ADR's "motor rejection."
|
||||
- Memory: `phi-separation-falsified` — semantic capability lives in
|
||||
chain composition, not φ geometry. Style capability is the natural
|
||||
next case of the same principle.
|
||||
- Memory: `identity-doctrine` — why style does not belong on the
|
||||
identity axis.
|
||||
- Future ADR-0088: rhetorical-style consumer (composer + realizer
|
||||
integration).
|
||||
- Future ADR-0089: writing-chain harvester (Layer 0 of the writing
|
||||
curriculum, separately specified).
|
||||
- Future spec: `docs/curriculum/writing-chain-harvester-spec.md` —
|
||||
the harvester specification this ADR's substrate ultimately serves.
|
||||
Loading…
Reference in a new issue