diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eabab69..c7bb25a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 env: - PY_FIXTURE_REF: 6cedcd955fcf4b4e687ea85771ebbc5302e5fe01 + PY_FIXTURE_REF: caf37689ee6c1c99ddf0067f6486daf6cc9bb0ed PY_FIXTURE_CHECKOUT: /tmp/context-compiler-source steps: diff --git a/README.md b/README.md index 0e8a7bb..9e8fc15 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,14 @@ State snapshots are intentionally opaque. Prefer helpers such as - `getPremiseValue(state)` / `getPolicyItems(state, value?)` -> read helpers for state. - `step(engine, input)` -> controller step envelope (`output_version`, `mode`, `decision`, `state`). - `preview(engine, input)` -> dry-run step envelope with `state_before`, `state_after`, `diff`, and `would_mutate` (live engine state is restored). +- `getStepDecision(stepResult)` / `getStepState(stepResult)` -> read helpers for controller step results. +- `getPreviewDecision(previewResult)` / `getPreviewStateAfter(previewResult)` / `previewWouldMutate(previewResult)` -> read helpers for controller preview results. +- `diffHasChanges(diff)` -> read helper for the structural diff `changed` flag. - `stateDiff(before, after)` -> structural state diff used by preview. - `DECISION_PASSTHROUGH` / `DECISION_UPDATE` / `DECISION_CLARIFY` -> decision kind constants. +Prefer the controller helper accessors over direct controller result property reads in TypeScript examples and app code. + ## Experimental Preprocessor The preprocessor is an optional host-side layer that can recognize some @@ -141,7 +146,12 @@ Safety guidance: Experimental preprocessor APIs are available via package subpath: ```ts -import { preprocessHeuristic, parsePreprocessorOutput, validatePreprocessorOutput } from '@rlippmann/context-compiler/experimental/preprocessor'; +import { + PREPROCESS_OUTCOME_DIRECTIVE, + parsePreprocessorOutput, + preprocessHeuristic, + validatePreprocessorOutput +} from '@rlippmann/context-compiler/experimental/preprocessor'; ``` ### Experimental Preprocessor Quick Start @@ -155,7 +165,7 @@ function stepWithOptionalPreprocessor(engine: ReturnType, u const heuristic = preprocessHeuristic(userInput); let engineInput = userInput; - if (heuristic.classification === 'directive' && heuristic.output !== null) { + if (heuristic.classification === PREPROCESS_OUTCOME_DIRECTIVE && heuristic.output !== null) { const parsed = parsePreprocessorOutput(heuristic.output, { sourceInput: userInput }); if (parsed !== null) { engineInput = parsed; diff --git a/demos/04_llm_tool_denylist_guardrail.ts b/demos/04_llm_tool_denylist_guardrail.ts index 60eda36..7fe8737 100644 --- a/demos/04_llm_tool_denylist_guardrail.ts +++ b/demos/04_llm_tool_denylist_guardrail.ts @@ -1,4 +1,4 @@ -import { createEngine, getPolicyItems } from '../src/index.js'; +import { POLICY_PROHIBIT, createEngine, getPolicyItems } from '../src/index.js'; import { buildBaselineMessages, buildMediatedMessagesFromTranscript, @@ -68,7 +68,7 @@ export async function main(): Promise { const baselineOutput = await completeMessages(baselineMessages); printModelOutput('Baseline', baselineOutput); - const prohibited = getPolicyItems(engine.state, 'prohibit'); + const prohibited = getPolicyItems(engine.state, POLICY_PROHIBIT); const candidateTools = ['docker', 'kubectl']; const filteredTools = candidateTools.filter((tool) => !prohibited.includes(tool)); if (isVerbose()) { diff --git a/demos/common.ts b/demos/common.ts index e65e2fe..292dbb7 100644 --- a/demos/common.ts +++ b/demos/common.ts @@ -40,7 +40,7 @@ export type InfoReport = { let lastReport: DemoReport | null = null; let lastInfoReport: InfoReport | null = null; -function policyValuesText(state: EngineState, value: 'use' | 'prohibit'): string { +function policyValuesText(state: EngineState, value: typeof POLICY_USE | typeof POLICY_PROHIBIT): string { const items = getPolicyItems(state, value); return items.length > 0 ? items.join(', ') : '(none)'; } diff --git a/examples/01_persistent_guardrails.ts b/examples/01_persistent_guardrails.ts index bf67f43..75d3387 100644 --- a/examples/01_persistent_guardrails.ts +++ b/examples/01_persistent_guardrails.ts @@ -1,4 +1,4 @@ -import { createEngine, getPolicyItems } from '../src/index.js'; +import { POLICY_PROHIBIT, createEngine, getPolicyItems } from '../src/index.js'; declare const process: { argv: string[] }; @@ -15,7 +15,7 @@ export function runExample01(): { return { turn1Kind: decision1.kind, turn2Kind: decision2.kind, - prohibitedPolicies: getPolicyItems(engine.state, 'prohibit') + prohibitedPolicies: getPolicyItems(engine.state, POLICY_PROHIBIT) }; } diff --git a/examples/03_ambiguity_with_clarification.ts b/examples/03_ambiguity_with_clarification.ts index 2086208..fbf937d 100644 --- a/examples/03_ambiguity_with_clarification.ts +++ b/examples/03_ambiguity_with_clarification.ts @@ -1,12 +1,12 @@ -import { createEngine, getClarifyPrompt, isClarify } from '../src/index.js'; +import { DECISION_CLARIFY, DECISION_UPDATE, createEngine, getClarifyPrompt, isClarify } from '../src/index.js'; declare const process: { argv: string[] }; export function runExample03(): { - clarifyKind: string; + clarifyKind: typeof DECISION_CLARIFY; clarifyPrompt: string | null; llmCalled: boolean; - resetKind: string; + resetKind: typeof DECISION_UPDATE; } { const engine = createEngine(); @@ -21,10 +21,10 @@ export function runExample03(): { const resetDecision = engine.step('clear state'); return { - clarifyKind: contradictionDecision.kind, + clarifyKind: DECISION_CLARIFY, clarifyPrompt: getClarifyPrompt(contradictionDecision), llmCalled, - resetKind: resetDecision.kind + resetKind: DECISION_UPDATE }; } diff --git a/examples/08_controller_preview_diff.ts b/examples/08_controller_preview_diff.ts index d94ab99..50df1c2 100644 --- a/examples/08_controller_preview_diff.ts +++ b/examples/08_controller_preview_diff.ts @@ -5,13 +5,19 @@ import { POLICY_PROHIBIT, POLICY_USE, createEngine, + diffHasChanges, getPolicyItems, getClarifyPrompt, + getPreviewDecision, + getPreviewStateAfter, getPremiseValue, + getStepDecision, + getStepState, getDecisionState, isClarify, isUpdate, preview, + previewWouldMutate, stateDiff, step, type Decision, @@ -79,21 +85,24 @@ export function runExample08(): { const previewResult = preview(engine, 'prohibit peanuts'); - const stateAfterPreview = engine.state; + const stateAfterPreview = getPreviewStateAfter(previewResult); const diffAfterPreview = stateDiff(stateBeforePreview, stateAfterPreview); const stepResult = step(engine, 'prohibit peanuts'); + const previewDecision = getPreviewDecision(previewResult); + const stepDecision = getStepDecision(stepResult); + const stepState = getStepState(stepResult); return { stateBeforePreview: summarizeState(stateBeforePreview), preview: { - wouldMutate: previewResult.would_mutate, - decision: summarizeDecision(previewResult.decision) + wouldMutate: previewWouldMutate(previewResult), + decision: summarizeDecision(previewDecision) }, - stateChangedAfterPreview: diffAfterPreview.changed, + stateChangedAfterPreview: diffHasChanges(diffAfterPreview), apply: { - decision: summarizeDecision(stepResult.decision), - stateAfterStep: summarizeState(stepResult.state) + decision: summarizeDecision(stepDecision), + stateAfterStep: summarizeState(stepState) } }; } diff --git a/examples/README.md b/examples/README.md index 4b2d4ae..c5b9cf3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -41,6 +41,7 @@ Demonstrates explicit single-policy correction without `reset policies`: Shows controller-layer auditability with `preview(engine, input)` and `stateDiff(before, after)`. Shows that preview does not mutate live engine state, then applies the same input with `step(engine, input)`. +Uses the controller helper accessors such as `getPreviewDecision`, `getPreviewStateAfter`, `previewWouldMutate`, `getStepDecision`, and `getStepState`. ## Integrations diff --git a/examples/integrations/node-basic/server.ts b/examples/integrations/node-basic/server.ts index be0ae33..72614c2 100644 --- a/examples/integrations/node-basic/server.ts +++ b/examples/integrations/node-basic/server.ts @@ -9,6 +9,7 @@ import { type EngineState } from '@rlippmann/context-compiler'; import { + PREPROCESS_OUTCOME_DIRECTIVE, parsePreprocessorOutput, preprocessHeuristic } from '@rlippmann/context-compiler/experimental/preprocessor'; @@ -65,7 +66,7 @@ function minimalRecentContext(history: ChatBody['history']) { function normalizeInputWithPreprocessor(input: string): string { const heuristic = preprocessHeuristic(input); - if (heuristic.classification === 'directive' && heuristic.output !== null) { + if (heuristic.classification === PREPROCESS_OUTCOME_DIRECTIVE && heuristic.output !== null) { const parsed = parsePreprocessorOutput(heuristic.output, { sourceInput: input }); if (parsed !== null) { return parsed; diff --git a/src/controller.ts b/src/controller.ts index a698feb..0bf2eb4 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -90,6 +90,12 @@ export function state_diff(before: EngineState, after: EngineState): StructuralD export const stateDiff = state_diff; +export function diff_has_changes(diff: StructuralDiff): boolean { + return diff.changed; +} + +export const diffHasChanges = diff_has_changes; + export function step(engine: Engine, user_input: string): StepResult { const decision = engine.step(user_input); return { @@ -100,6 +106,18 @@ export function step(engine: Engine, user_input: string): StepResult { }; } +export function get_step_decision(stepResult: StepResult): Decision { + return stepResult.decision; +} + +export const getStepDecision = get_step_decision; + +export function get_step_state(stepResult: StepResult): EngineState { + return stepResult.state; +} + +export const getStepState = get_step_state; + export function preview(engine: Engine, user_input: string): PreviewResult { const checkpoint = engine.exportCheckpoint(); const stateBefore = engine.state; @@ -124,3 +142,21 @@ export function preview(engine: Engine, user_input: string): PreviewResult { would_mutate: diff.changed }; } + +export function get_preview_decision(previewResult: PreviewResult): Decision { + return previewResult.decision; +} + +export const getPreviewDecision = get_preview_decision; + +export function get_preview_state_after(previewResult: PreviewResult): EngineState { + return previewResult.state_after; +} + +export const getPreviewStateAfter = get_preview_state_after; + +export function preview_would_mutate(previewResult: PreviewResult): boolean { + return previewResult.would_mutate; +} + +export const previewWouldMutate = preview_would_mutate; diff --git a/src/experimental/preprocessor/index.ts b/src/experimental/preprocessor/index.ts index e4b3394..3c08c5d 100644 --- a/src/experimental/preprocessor/index.ts +++ b/src/experimental/preprocessor/index.ts @@ -163,7 +163,7 @@ function sourceInputIsStructuredContractDirective(sourceInput: string, directive } return ( - rec.classification === 'directive' && + rec.classification === PREPROCESS_OUTCOME_DIRECTIVE && typeof rec.output === 'string' && rec.output.trim().toLowerCase() === directiveOutput.trim().toLowerCase() ); diff --git a/src/index.ts b/src/index.ts index 34fd219..7732576 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,25 @@ export { getPremiseValue, getPolicyItems } from './engine.js'; -export { OUTPUT_VERSION, preview, state_diff, stateDiff, step } from './controller.js'; +export { + OUTPUT_VERSION, + diff_has_changes, + diffHasChanges, + get_preview_decision, + get_preview_state_after, + get_step_decision, + get_step_state, + getPreviewDecision, + getPreviewStateAfter, + getStepDecision, + getStepState, + preview, + preview_would_mutate, + previewWouldMutate, + state_diff, + stateDiff, + step +} from './controller.js'; export type { Engine, EngineInit } from './engine.js'; export type { PreviewResult, StepResult, StructuralDiff } from './controller.js'; import type { Decision, EngineState } from './types.js'; diff --git a/tests/api_aliases.test.ts b/tests/api_aliases.test.ts index 93969d7..902a13c 100644 --- a/tests/api_aliases.test.ts +++ b/tests/api_aliases.test.ts @@ -10,6 +10,12 @@ describe('root API aliases', () => { expect(cc.isPassthrough).toBe(cc.is_passthrough); expect(cc.getClarifyPrompt).toBe(cc.get_clarify_prompt); expect(cc.getDecisionState).toBe(cc.get_decision_state); + expect(cc.getStepDecision).toBe(cc.get_step_decision); + expect(cc.getStepState).toBe(cc.get_step_state); + expect(cc.getPreviewDecision).toBe(cc.get_preview_decision); + expect(cc.getPreviewStateAfter).toBe(cc.get_preview_state_after); + expect(cc.previewWouldMutate).toBe(cc.preview_would_mutate); + expect(cc.diffHasChanges).toBe(cc.diff_has_changes); expect(cc.stateDiff).toBe(cc.state_diff); }); diff --git a/tests/api_parity.test.ts b/tests/api_parity.test.ts index 90f6b38..c1a782b 100644 --- a/tests/api_parity.test.ts +++ b/tests/api_parity.test.ts @@ -27,6 +27,12 @@ const PYTHON_TO_TS_EXPORT_MAP: Record = { is_passthrough: 'is_passthrough', get_clarify_prompt: 'get_clarify_prompt', get_decision_state: 'get_decision_state', + get_step_decision: 'get_step_decision', + get_step_state: 'get_step_state', + get_preview_decision: 'get_preview_decision', + get_preview_state_after: 'get_preview_state_after', + preview_would_mutate: 'preview_would_mutate', + diff_has_changes: 'diff_has_changes', DECISION_PASSTHROUGH: 'DECISION_PASSTHROUGH', DECISION_UPDATE: 'DECISION_UPDATE', DECISION_CLARIFY: 'DECISION_CLARIFY', diff --git a/tests/controller_helpers.test.ts b/tests/controller_helpers.test.ts new file mode 100644 index 0000000..0c738fd --- /dev/null +++ b/tests/controller_helpers.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import * as cc from '../src/index.js'; + +describe('controller helper accessors', () => { + it('returns step result fields through camelCase helpers', () => { + const engine = cc.createEngine(); + const stepResult = cc.step(engine, 'set premise concise replies'); + + expect(cc.getStepDecision(stepResult)).toBe(stepResult.decision); + expect(cc.getStepState(stepResult)).toBe(stepResult.state); + }); + + it('returns preview result fields through camelCase helpers', () => { + const engine = cc.createEngine(); + const previewResult = cc.preview(engine, 'use sqlite'); + + expect(cc.getPreviewDecision(previewResult)).toBe(previewResult.decision); + expect(cc.getPreviewStateAfter(previewResult)).toBe(previewResult.state_after); + expect(cc.previewWouldMutate(previewResult)).toBe(previewResult.would_mutate); + }); + + it('returns diff changed flag through the camelCase helper', () => { + const diff = cc.stateDiff( + { premise: null, policies: {}, version: 2 }, + { premise: 'concise replies', policies: {}, version: 2 } + ); + + expect(cc.diffHasChanges(diff)).toBe(diff.changed); + }); + + it('keeps snake_case helper aliases behaviorally identical', () => { + const engine = cc.createEngine(); + const stepResult = cc.step(engine, 'set premise concise replies'); + const previewResult = cc.preview(engine, 'use sqlite'); + const diff = cc.stateDiff( + { premise: null, policies: {}, version: 2 }, + { premise: 'concise replies', policies: {}, version: 2 } + ); + + expect(cc.get_step_decision(stepResult)).toBe(cc.getStepDecision(stepResult)); + expect(cc.get_step_state(stepResult)).toBe(cc.getStepState(stepResult)); + expect(cc.get_preview_decision(previewResult)).toBe(cc.getPreviewDecision(previewResult)); + expect(cc.get_preview_state_after(previewResult)).toBe(cc.getPreviewStateAfter(previewResult)); + expect(cc.preview_would_mutate(previewResult)).toBe(cc.previewWouldMutate(previewResult)); + expect(cc.diff_has_changes(diff)).toBe(cc.diffHasChanges(diff)); + }); +}); diff --git a/tests/fixtures/conformance/api/public-api-v1.json b/tests/fixtures/conformance/api/public-api-v1.json index 18a84fc..8df070f 100644 --- a/tests/fixtures/conformance/api/public-api-v1.json +++ b/tests/fixtures/conformance/api/public-api-v1.json @@ -13,6 +13,12 @@ "is_passthrough", "get_clarify_prompt", "get_decision_state", + "get_step_decision", + "get_step_state", + "get_preview_decision", + "get_preview_state_after", + "preview_would_mutate", + "diff_has_changes", "DECISION_PASSTHROUGH", "DECISION_UPDATE", "DECISION_CLARIFY",