core/docs/curriculum/writing-chain-harvester-spec.md
Shay ac75cfc659
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.
2026-05-20 16:09:16 -07:00

294 lines
12 KiB
Markdown

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