import { BlackboardEngine } from "./blackboard.js"; import type { Decision, DecisionConfidence, DecisionStatus, RationaleSource } from "../utils/types.js"; import type { Embedder } from "../embeddings/embedder.js"; import { type SearchEngine } from "../embeddings/search.js"; import { GraphAutoPopulator } from "./graph-auto-populator.js"; import type { IDecisionStore, IIndexManager } from "../storage/interfaces.js"; /** Entry in a dependency trace chain. */ export interface TraceEntry { id: string; summary: string; depends_on: string[]; dependents: string[]; status: string; } /** Options for why() (#41). */ export interface WhyOptions { /** Token budget for the full-detail tier (default 4000, matching assemble). */ max_tokens?: number; /** Include superseded decisions (excluded by default). */ include_superseded?: boolean; /** Return full detail for exactly these decision ids; scope and budget are ignored. */ ids?: string[]; /** * Resolve each excluded superseded/overridden record's lineage HEAD by * walking superseded_by (field D13 ask 3) — "what is the current answer", * not "what ranks highest". Off by default (extra store reads). */ lineage?: boolean; } /** Full-detail decision record returned by why(). */ export interface WhyDecision { id: string; summary: string; rationale: string; confidence: string; status: string; timestamp: string; alternatives_count: number; commit_hashes: string[]; superseded_by?: string; context?: string; alternatives?: Decision["alternatives"]; scope?: string; domain?: string; constraints?: string[]; depends_on?: string[]; } /** Compact one-liner for decisions that exceed the why() token budget. */ export interface WhyCompactDecision { id: string; summary: string; status: string; confidence: string; timestamp: string; } export interface WhyResult { decisions: WhyDecision[]; more?: WhyCompactDecision[]; truncated: boolean; total_in_scope: number; superseded_count: number; /** * Compact identity of the superseded/overridden records the default filter * hid, each pointing at its successor (field D10). A bare count read as * "returns nothing" in the field when a multi-part record was wholly * retired; the ids make the exclusion recoverable (include_superseded, or * follow superseded_by). Absent when include_superseded is set or nothing * was hidden. Capped at 20. */ superseded_excluded?: Array<{ id: string; summary: string; superseded_by?: string; /** Terminal record of this decision's supersession chain (lineage: true). */ lineage_head?: { id: string; summary: string; chain_length: number; }; }>; /** * Archived decisions hidden from this result (D3). Present so a blinded * gate reads "0 decisions (N archived in scope)" instead of "no decisions * exist" — a wrongly-archived store is otherwise indistinguishable from an * empty one. Restore with twining_unarchive. */ archived_excluded_count?: number; active_count: number; provisional_count: number; token_estimate: number; omitted_count?: number; missing_ids?: string[]; } export declare class DecisionEngine { private readonly decisionStore; private readonly blackboardEngine; private readonly embedder; private readonly indexManager; private readonly projectRoot; private readonly searchEngine; private readonly graphPopulator; private assemblyChecker?; constructor(decisionStore: IDecisionStore, blackboardEngine: BlackboardEngine, embedder?: Embedder | null, indexManager?: IIndexManager | null, projectRoot?: string | null, searchEngine?: SearchEngine | null, graphPopulator?: GraphAutoPopulator | null); /** Set the function that checks whether an agent assembled context before deciding. */ setAssemblyChecker(checker: (agentId: string) => boolean): void; /** * Sync a decision summary to .planning/STATE.md. * Appends to the "### Decisions" section under "## Accumulated Context". * Never throws — planning sync failure must not prevent decide(). * Uses direct fs calls because STATE.md is a GSD planning file, not Twining data. */ private syncToPlanning; /** Record a decision with full rationale and conflict detection. */ decide(input: { domain: string; scope: string; summary: string; context: string; rationale: string; constraints?: string[]; rationale_source?: RationaleSource; alternatives?: Array<{ option: string; pros?: string[]; cons?: string[]; reason_rejected?: string; }>; depends_on?: string[]; supersedes?: string; confidence?: "high" | "medium" | "low"; reversible?: boolean; status?: "active" | "provisional"; affected_files?: string[]; affected_symbols?: string[]; assumptions?: string[]; agent_id?: string; commit_hash?: string; }): Promise<{ id: string; timestamp: string; conflicts?: { id: string; summary: string; }[]; dropped_depends_on?: string[]; /** Set when the supersedes target does not exist — it was NOT retired (field D10). */ supersedes_dangling?: string; }>; /** * Append-only metadata repair (field D11). Adds affected_files/ * affected_symbols to an existing record — the two fields the retrieval * graph and divergence checks key on — with an in-record provenance trail. * Semantic content is never amendable (that would demand embedding * reindexing and break "a decision record is what was decided, then"). * Works on retired records: the file list is a factual attribute, not a * lifecycle claim. Idempotent: already-present entries append no * provenance and touch no store. */ amend(input: { id: string; add_affected_files?: string[]; add_affected_symbols?: string[]; reason?: string; agent_id?: string; }): Promise<{ id: string; status: DecisionStatus; added_files: string[]; added_symbols: string[]; already_present: string[]; /** False when the amendment persisted but the audit finding could not be posted. */ audit_posted?: boolean; }>; /** * Retrieve decision chain for a scope or file (#41: bounded). * Matches are ranked by scope specificity, then status, then recency, and * full rationale is returned only for the decisions that fit max_tokens; * the remainder comes back as compact one-liners in `more`. Superseded * decisions are excluded unless include_superseded is set. Passing ids * returns full detail (rationale, context, alternatives) for exactly those * decisions with no budget applied — the drill-down path for `more` entries. */ why(scope: string, options?: WhyOptions): Promise; /** * Walk superseded_by to the terminal record of a supersession chain * (field D13 ask 3). Cycle-guarded and depth-capped; on a cycle or a * dangling link the last reachable record is the reported head. */ private resolveLineageHead; /** Full-detail drill-down for explicitly requested decision ids (#41). */ private whyByIds; /** * Link a commit hash to an existing decision. * Posts a status entry to the blackboard for traceability. */ linkCommit(decisionId: string, commitHash: string, agentId?: string): Promise<{ linked: boolean; decision_summary: string; }>; /** * Get decisions linked to a specific commit hash. */ getByCommitHash(commitHash: string): Promise<{ decisions: Array<{ id: string; summary: string; domain: string; scope: string; confidence: string; timestamp: string; commit_hashes: string[]; }>; }>; /** * Trace a decision's dependency chain upstream and/or downstream. * Uses BFS with a visited set to prevent infinite loops from circular dependencies. */ trace(decisionId: string, direction?: "upstream" | "downstream" | "both"): Promise<{ chain: TraceEntry[]; }>; /** * Flag a decision for reconsideration. * Sets active decisions to provisional and posts a warning. */ reconsider(decisionId: string, newContext: string, agentId?: string): Promise<{ flagged: boolean; decision_summary: string; }>; /** * Override a decision with a reason, optionally creating a replacement. */ override(decisionId: string, reason: string, newDecision?: string, overriddenBy?: string): Promise<{ overridden: boolean; old_summary: string; /** Post-write read-back — success claims are self-verifying (field D14). Usually "overridden"; a concurrent writer may have already moved the record on. */ status: DecisionStatus; overridden_by: string; new_decision_id?: string; }>; /** * Promote provisional decisions to active status. * Only provisional decisions can be promoted. */ promote(decisionIds: string[], promotedBy?: string): Promise<{ promoted: string[]; already_active: string[]; /** Attribution for already_active ids — a prior ratification (promoted_by/promoted_at present) is distinguishable from active-since-creation (field D15). */ already_active_detail: Array<{ id: string; promoted_by?: string; promoted_at?: string; }>; not_found: string[]; wrong_status: Array<{ id: string; status: string; }>; }>; /** * Search decisions across all scopes by keyword or semantic similarity. * Supports filtering by domain, status, and confidence. * Never throws — returns empty results on error. */ searchDecisions(query: string, filters?: { domain?: string; status?: DecisionStatus; confidence?: DecisionConfidence; }, limit?: number): Promise<{ results: Array<{ id: string; summary: string; domain: string; scope: string; confidence: string; status: string; timestamp: string; relevance: number; commit_hashes: string[]; }>; /** * Pre-slice match count — never capped by limit (field D9). Semantic * mode counts raw cosine >= the ~0.3 noise floor; keyword mode counts * every literal term hit. Membership is always tested on RAW scores, * before the retired-status de-boost, so a superseded decision counts * exactly like an active one. The results page may include sub-floor * rows for ranking context. */ total_matched: number; /** Page size actually delivered: results.length. */ returned: number; /** Which generation of count semantics this response carries (ask 2). */ count_semantics: string; fallback_mode: boolean; }>; } //# sourceMappingURL=decisions.d.ts.map