classify

package
v0.0.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package classify judges every document of a topic once on a fixed set of facets (spec §8) and writes them as kb-owned frontmatter: genre, depth, relevance, quality, concepts, plus the generated literals summary, entities, questions and, for articles, criterion and aliases. It also owns the shared gate judgment (relevance + quality in one request) that ingest gates, the contract impact preview and classify all ask, the backfill review queues (recapture, remove), concept proposals, and the vocabulary bootstrap for topics without articles (spec §5.2).

Index

Constants

View Source
const (
	// MinNoneForProposals is the number of sources with primary concept
	// `none` that triggers concept proposals.
	MinNoneForProposals = 5
	// MaxProposals caps the concepts proposed in one run.
	MaxProposals = 5
)

Proposal limits (spec §5.2 "New concepts").

View Source
const (
	// FactPrimaryConcept is the fact name.
	FactPrimaryConcept = "primary_concept"
	// PrimaryNone records a decided primary concept `none`.
	PrimaryNone = "none"
	// PrimaryMatched records a decided primary concept other than `none`.
	PrimaryMatched = "concept"
)

State-row fact recording the primary concept outcome of a source, so the concept proposal trigger counts across the topic, not only this run.

View Source
const (
	// MaxConceptOptions is the number of articles offered to primary_concept;
	// with the `none` exit the choice stays within Jev's 255 options.
	MaxConceptOptions = 254
	// MaxConceptCandidates caps the mentions_concept_{id} nouls per document.
	MaxConceptCandidates = 20
)

Concept-question limits (spec §8).

View Source
const (
	RoleCore               = "core"
	RoleAdjacent           = "adjacent"
	RoleCollectedOnPurpose = "collected_on_purpose"
	RoleGeneral            = "general"
	RoleOffTopic           = "off_topic"
	RoleUnknown            = "unknown"
)

Relevance roles (the `relevance` values).

View Source
const (
	NoulPaywall   = "paywall_or_login"
	NoulErrorPage = "error_or_placeholder_page"
	NoulThin      = "thin_or_boilerplate"
	NoulNoSpeech  = "no_speech_content"
)

Quality nouls of the quality bank.

View Source
const (
	ReasonPaywall      = "paywall"
	ReasonErrorPage    = quality.ErrorPage
	ReasonThin         = quality.Thin
	ReasonNotAnArticle = quality.NotAnArticle
	ReasonNoSpeech     = "no_speech"
)

Quality reasons written to `quality` (and used as `triage_reason`).

View Source
const (
	MaxSummaryChars   = 400
	MaxCriterionChars = 300
	MaxEntities       = 15
	MinQuestions      = 3
	MaxQuestions      = 6
	MaxAliases        = 8
	MaxAliasWords     = 6
	MaxTitleChars     = 120
)

Limits of generated literals (spec §4.2, §5.2, §6, §8).

View Source
const (
	KindLiterals   = "summary"
	KindCriterion  = "criterion"
	KindAliases    = "aliases"
	KindProposals  = "concept_proposals"
	KindVocabulary = "vocabulary"
)

Generation kinds (receipts record purpose "generate:<kind>").

View Source
const (
	MinVocabulary = 12
	MaxVocabulary = 40
)

Vocabulary bootstrap limits (spec §5.2).

View Source
const ConceptsDir = "wiki/concepts"

ConceptsDir is the topic-relative directory of wiki concept articles.

View Source
const ExcerptTokens = 12000

ExcerptTokens bounds the document excerpt of every classify and gate state (spec §7 stage 5: a section-aware excerpt of at most 12k tokens).

Variables

View Source
var ErrArticleExists = errors.New("classify: article already exists")

ErrArticleExists is returned by CreateStubArticle when the article file is already there; it is never overwritten.

View Source
var ErrNoVocabularyDraft = errors.New("classify: topic.yaml has no vocabulary_draft; run `kb topic vocabulary <topic> --draft` first")

ErrNoVocabularyDraft is returned by AcceptVocabulary without a draft.

Functions

func AcceptVocabulary

func AcceptVocabulary(s *session.Session) ([]string, error)

AcceptVocabulary creates one stub article per `vocabulary_draft:` item (skipping articles that already exist), clears the draft and returns the topic-relative paths it created.

func ArticleBanks

func ArticleBanks() []*questions.Bank

ArticleBanks are the banks recorded in the state row of a classified article (plan: `{classify, concept}`).

func CodeFlags

func CodeFlags(doc *corpus.Document, hostLines map[string]map[string]int) []quality.Flag

CodeFlags runs the code quality checks (spec §7 stage 3) over a stored source; hostLines comes from quality.HostLineCounts over the topic's sources. The thin rule only applies to web captures (an http(s) source_url, not a transcript or a bookmark cluster): the length of a local note or a transcript is not a capture failure. `kb classify` and the impact preview share it so their bands agree.

func CreateStub

func CreateStub(s *session.Session, title, criterion string, now time.Time) (string, error)

CreateStub creates a stub article through CreateStubArticle and then writes its criterion through the session's owned-key writer, so the state record knows kb wrote it: a later regeneration may refresh it, and a user edit is detected and kept (spec §5.2, §6). Accepting a vocabulary draft or a concept proposal both go through here.

func CreateStubArticle

func CreateStubArticle(topicRoot, domain, title, criterion string, now time.Time) (string, error)

CreateStubArticle writes `wiki/concepts/<Title>.md` (file name = title sanitized for the filesystem) with the stub frontmatter of the plan: title, type wiki, stage stub, domain, tags [domain, wiki, stub], created, updated, sources [] and criterion (when non-empty; prefer CreateStub, which records the criterion as kb-written). It returns the topic-relative path, and ErrArticleExists (with that path) when the file is already there.

func DraftVocabulary

func DraftVocabulary(ctx context.Context, s *session.Session) ([]contract.VocabularyItem, error)

DraftVocabulary asks the generation model for a closed list of 12–40 concepts (title + criterion) from the contract and one line per source (title + summary), validates it by code (unique titles, criterion rules, no collision with existing articles), counts for each concept how many source summaries mention its title (code, no call) and saves the list under topic.yaml `vocabulary_draft:`.

func FacetBank

func FacetBank() *questions.Bank

FacetBank is the composite bank (classify + concept) of the classification request.

func GateBank

func GateBank() *questions.Bank

GateBank is the composite bank (relevance + quality) of the gate judgment.

func IsTranscriptKind

func IsTranscriptKind(sourceKind string) bool

IsTranscriptKind reports whether a source kind is a transcript, the only documents asked `no_speech_content`.

func SourceBanks

func SourceBanks() []*questions.Bank

SourceBanks are the banks recorded in the state row of a classified source (plan: `{relevance, quality, classify, concept}`).

Types

type GateJudgment

type GateJudgment struct {
	// Role is the argmax role, RoleCollectedOnPurpose when the path matches
	// `collected_on_purpose_paths` (no call), or "" when relevance is off or
	// the role answer is undecided.
	Role string `json:"role,omitempty"`
	// RoleAsked reports whether the `role` question was in the request.
	RoleAsked bool `json:"role_asked"`
	// RoleByPath reports that Role came from the path globs.
	RoleByPath bool `json:"role_by_path,omitempty"`
	// RoleProbs is the role distribution when decided.
	RoleProbs map[string]float64 `json:"role_probs,omitempty"`
	// POffTopic is P(off_topic).
	POffTopic float64 `json:"p_off_topic"`
	// PKept is P(core) + P(adjacent) + P(collected_on_purpose).
	PKept float64 `json:"p_kept"`
	// RoleDecided reports a usable role (decided answer or path glob).
	RoleDecided bool `json:"role_decided"`
	// Quality maps each decided quality noul id to P(yes).
	Quality map[string]float64 `json:"quality"`
	// QualityDecided reports that every quality noul was decided.
	QualityDecided bool `json:"quality_decided"`
	// Flags are the code flags passed in GateOptions.
	Flags []quality.Flag `json:"flags,omitempty"`
	// Undecided lists "<question>:<reason>" for every answer that was not
	// decided (not-checked answers read "<question>:not_checked:<reason>").
	Undecided []string `json:"undecided,omitempty"`
	// ReceiptKey is the receipts key of the request.
	ReceiptKey string `json:"receipt_key,omitempty"`
}

GateJudgment is the outcome of the shared gate request.

func JudgeGate

func JudgeGate(ctx context.Context, s *session.Session, doc *corpus.Document, opts GateOptions) (GateJudgment, error)

JudgeGate asks the shared gate request for one document: the `role` choice against the contract (omitted when relevance is off in topic.yaml or the path matches `collected_on_purpose_paths`) plus the quality nouls (`no_speech_content` only for transcripts), over the state `{contract, document}` built by corpus.DocumentState. The contract is only in the state when `role` is asked; without an accepted contract it is `{}` (relevance then runs in shadow, decided by the caller's mode). Ingest gates, the impact preview and classify all call this, so the same content reuses one receipt. The error is fatal only (auth, cancellation, invalid request); failed answers are listed in Undecided.

func (GateJudgment) QualityQuestion

func (g GateJudgment) QualityQuestion(reason string) string

QualityQuestion returns the question id behind a reason ("code:<flag>" for a code flag), for review items.

func (GateJudgment) QualityReason

func (g GateJudgment) QualityReason(threshold float64) (string, float64)

QualityReason returns the reason code would act on at threshold: the first code flag (probability 1), else the quality noul with the highest P(yes) ≥ threshold. It returns "" when nothing fired.

type GateOptions

type GateOptions struct {
	// IsTranscript adds the `no_speech_content` noul.
	IsTranscript bool
	// Flags are the code quality flags already computed by the caller
	// (quality.Check); they are carried into the judgment and win over the
	// nouls in QualityReason.
	Flags []quality.Flag
	// Contract overrides the session's active contract (the impact preview
	// judges a draft). Its hash replaces the session's in the cache key, so
	// the receipts are reused once the same draft is accepted.
	Contract *contract.Contract
}

GateOptions configures JudgeGate.

type Options

type Options struct {
	// OnlyMissing judges only documents without a state row or with a
	// missing facet key (the bootstrap follow-up after vocabulary accept).
	OnlyMissing bool
	// All re-judges every document regardless of the state record.
	All bool
	// Paths restricts the run to these topic-relative paths (ingest).
	Paths []string
}

Options configures Run.

type Report

type Report struct {
	// Documents is the number of documents considered (after Paths).
	Documents int `json:"documents"`
	// Judged is the number of documents that went through the facet pass.
	Judged int `json:"judged"`
	// Decided is the number of judged documents whose every answer and
	// literal was decided (the rest stay unclassified for the next run).
	Decided int `json:"decided"`
	// UndecidedDocuments lists, sorted, the documents left with an
	// undecided answer or literal (spec §2.3: named in the run summary).
	UndecidedDocuments []string `json:"undecided_documents"`
	// Skipped counts documents not judged, by reason: unchanged (state row
	// current), complete (--only-missing and nothing missing), locked, stub.
	Skipped map[string]int `json:"skipped"`
	// Facets counts written keys (a key rewritten with the same value is not
	// counted).
	Facets map[string]int `json:"facets"`
	// Writes counts document write outcomes: written, unchanged,
	// skipped:locked, skipped:changed.
	Writes map[string]int `json:"writes"`
	// SkippedUserKeys counts keys left alone because the user owns them
	// (skipped:user-key), by key.
	SkippedUserKeys map[string]int `json:"skipped_user_keys"`
	// Undecided counts answers and literals that were not decided, by
	// "<question>:<reason>" (not-checked answers read
	// "<question>:not_checked:<reason>"). Undecided answers never write.
	Undecided map[string]int `json:"undecided"`
	// Queued counts review items added, by queue.
	Queued map[string]int `json:"queued"`
	// DroppedEntities counts generated entity names dropped by the verbatim
	// check or the cap.
	DroppedEntities int `json:"dropped_entities"`
	// InvalidLiterals counts generated literals rejected by code, by key.
	InvalidLiterals map[string]int `json:"invalid_literals"`
	// CriterionMissing lists articles without `criterion` whose option text
	// fell back to their summary or head.
	CriterionMissing []string `json:"criterion_missing"`
	// ArticlesChanged lists articles that gained aliases (they trigger the
	// link reverse pass).
	ArticlesChanged []string `json:"articles_changed"`
	// PrimaryNone counts sources whose primary concept was `none` in this
	// run.
	PrimaryNone int `json:"primary_none"`
	// PrimaryNoneTotal counts the topic's sources whose primary concept is
	// `none` (this run plus earlier runs), the concept proposal trigger.
	PrimaryNoneTotal int `json:"primary_none_total"`
	// NoVocabulary reports a topic without articles: no concept questions
	// were asked.
	NoVocabulary bool `json:"no_vocabulary"`
	// RelevanceOff reports decisions.relevance: off (no role questions).
	RelevanceOff bool `json:"relevance_off"`
}

Report summarizes one classify run. Counts are per document unless named otherwise.

func Run

func Run(ctx context.Context, s *session.Session, opts Options) (Report, error)

Run classifies the documents of the session's topic (spec §8). Per document it asks two requests: the shared gate judgment (JudgeGate: role + quality nouls; sources only) and one classification request (composite classify + concept bank: kind, depth, enough_text_to_judge, primary_concept, mentions_concept_{id}), plus one generation call for summary/entities/questions when they are missing or kb wrote them and the body changed. Articles first get their criterion and aliases generated, so the concept options of the same run use them. Code maps answers to frontmatter and writes through the session's writer; undecided answers never write and leave the document unclassified for the next run. Sources above the review band go to the recapture or remove review queues (never quarantined), and ≥5 sources with primary concept `none` produce concept proposals. Incremental by body hash, contract and bank versions unless opts.All. The error is fatal only (auth, cancellation, I/O); the report is returned alongside it with what was done.

func (Report) Lines

func (r Report) Lines() []string

Lines renders the report for humans.

func (Report) Rows

func (r Report) Rows() []map[string]any

Rows renders the report as metric/value rows for table output.

func (Report) UndecidedTotal

func (r Report) UndecidedTotal() int

UndecidedTotal is the number of undecided answers and literals.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL