From f0229e912ea820be972fb34b0a71736d3ddbecab Mon Sep 17 00:00:00 2001 From: Layne Penney Date: Wed, 29 Jul 2026 11:55:31 -0500 Subject: [PATCH] chore: privacy/hygiene pass (extract) --- CHANGELOG.md | 10 +- SECURITY.md | 2 +- docs/callback-signature.md | 261 ------------------ packages/python/src/synapt/extract/batch.py | 48 ++-- packages/python/src/synapt/extract/builder.py | 4 +- .../python/src/synapt/extract/finalize.py | 4 +- .../python/src/synapt/extract/validate.py | 6 +- packages/ts/src/builder.ts | 4 +- packages/ts/src/finalize.ts | 2 +- packages/ts/src/validate.ts | 6 +- packages/ts/tests/test_extract.ts | 26 +- packages/ts/tests/test_finalize.ts | 2 +- packages/ts/tests/test_prompt.ts | 8 +- packages/ts/tests/test_validate.ts | 2 +- .../extract-batch-real-failures-v1.json | 9 +- tests/python/test_extract.py | 26 +- tests/python/test_extract_batch.py | 28 +- tests/python/test_finalize.py | 4 +- tests/python/test_prompt.py | 4 +- tests/python/test_validate.py | 6 +- 20 files changed, 96 insertions(+), 366 deletions(-) delete mode 100644 docs/callback-signature.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 514971a..fa14a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## v0.6.0 -Temporal validity role + resolution anchor — additive Stage-1 IL enrichment (config/design/extract-temporal-role-2026-07-14.md). `synapt-extract` (PyPI) bumps to 0.6.0. `@synapt-dev/extract` (npm) also bumps to 0.6.0 — main already carries the full additive-role parity slice, so the version number is honest — but publishes on a short lag behind PyPI (npm's publish workflow had no `workflow_dispatch` retrigger, added alongside this bump). The FULL ts/py parity effort (a TS `extract_batch`/`batch.ts` port) remains the deferred post-validation follow-up, separate from this version-sync. +Temporal validity role + resolution anchor — additive Stage-1 IL enrichment. `synapt-extract` (PyPI) bumps to 0.6.0. `@synapt-dev/extract` (npm) also bumps to 0.6.0 — main already carries the full additive-role parity slice, so the version number is honest — but publishes on a short lag behind PyPI (npm's publish workflow had no `workflow_dispatch` retrigger, added alongside this bump). The FULL ts/py parity effort (a TS `extract_batch`/`batch.ts` port) remains the deferred post-validation follow-up, separate from this version-sync. - Added `role` (`effective` | `expiry` | `range` | `superseded` | `point`) to the temporal-ref schema, capturing the validity DIRECTION a date constrains (e.g. "expires April 30" → `expiry`, vs "effective March 2026" → `effective`) — a semantic distinction the source sentence carries but prior extraction dropped - `role` and `resolved_end` are now BASE-tier on the `temporal_refs` capability (no longer gated behind the separate `temporal_classes` capability) — always available to any caller requesting `temporal_refs`; `type`/`context` remain `temporal_classes`-gated @@ -11,7 +11,7 @@ Temporal validity role + resolution anchor — additive Stage-1 IL enrichment (c - Fixed a prompt-rendering gap where an absent `date` param rendered the literal string "Resolve relative dates using: None." instead of omitting the instruction (wrapped in `{{#if date}}`; supported identically by both prompt renderers) - `_detect_capabilities`'s `temporal_classes` heuristic now keys on `type` presence only (`resolved_end` no longer implies the gated capability was exercised, since it moved to base tier) - Published JSON schema (`schemas/temporal-ref/v1.json`, both the repo-root canonical copy and the Python package copy) updated to match — schema-drift-check green -- **ts/py parity — the additive `role` coherence slice IS included** (`@synapt-dev/extract` TypeScript): `schema.ts` types `role`, `builder.ts` emits `role`/`resolved_end` base-tier, `validate.ts` accepts/enum-validates `role` (+ `role === "range"` → `resolved_end`), `finalize.ts` drops the `resolved_end` inference, and the embedded prompt fragment matches the shared file byte-for-byte. TS and Python produce identical schema/validation/finalize output on the same inputs (verified cross-language). The FULL parity effort (a TS `extract_batch`/`batch.ts` port) remains the post-validation follow-up per config/design/extract-ts-py-parity-plan-2026-07-15.md — the version number now matches PyPI (see above), but the additive-role coherence slice is still what's shipped, not the full `batch.ts` port. +- **ts/py parity — the additive `role` coherence slice IS included** (`@synapt-dev/extract` TypeScript): `schema.ts` types `role`, `builder.ts` emits `role`/`resolved_end` base-tier, `validate.ts` accepts/enum-validates `role` (+ `role === "range"` → `resolved_end`), `finalize.ts` drops the `resolved_end` inference, and the embedded prompt fragment matches the shared file byte-for-byte. TS and Python produce identical schema/validation/finalize output on the same inputs (verified cross-language). The FULL parity effort (a TS `extract_batch`/`batch.ts` port) remains the post-validation follow-up — the version number now matches PyPI (see above), but the additive-role coherence slice is still what's shipped, not the full `batch.ts` port. ## v0.5.0 @@ -63,7 +63,7 @@ Builder release for coupled prompts, Stage 1 response schemas, and finalized pac ## v0.3.1 -Three rounds of Atlas adversarial review. Schema/runtime parity, artifact bundling, no-network CI guard hardening, Python schema self-containment, behavioral-shift documentation, doc corrections. +Three rounds of adversarial review. Schema/runtime parity, artifact bundling, no-network CI guard hardening, Python schema self-containment, behavioral-shift documentation, doc corrections. ### Behavioral shifts @@ -118,14 +118,14 @@ v0.3.1 tightens both JSON Schema constraints and runtime validators. **7 of 9 ch - Scans source, compiled dist, and packed artifact on every CI run - Detects: direct forbidden globals, computed property access on global objects, string concatenation that assembles forbidden names, array `.join("")` assembling forbidden names, `Reflect.get` on global objects, `Function()` constructor (with or without `new`), base64 decode, dynamic imports, forbidden module imports, `importlib.import_module` - Runtime dependency allowlist (`scripts/allowed-deps.json`) with CI enforcement -- Negative test fixtures (`tests/security-probes/`) for all 4 Atlas bypass probes: `Reflect.get(globalThis, "fetch")`, `Function("return 1")`, `["fe","tch"].join("")`, `importlib.import_module("http.client")` +- Negative test fixtures (`tests/security-probes/`) for all 4 adversarial bypass probes: `Reflect.get(globalThis, "fetch")`, `Function("return 1")`, `["fe","tch"].join("")`, `importlib.import_module("http.client")` ### Doc corrections (moderate) - SECURITY.md: reproducible builds section clarifies wheel byte-identity vs sdist content-equivalence - SECURITY.md: callback architecture marked as proposed (target v0.4.0), not shipped - SECURITY.md: forbidden API enforcement described as best-effort regex (not AST-aware) -- docs/callback-signature.md: status changed to PROPOSED, target v0.4.0 +- Callback API doc: status changed to PROPOSED, target v0.4.0 - README.md: install strings updated to 0.3.1 - Schema URL smoke gate (`scripts/check-schema-urls.sh`) verifies against GitHub Pages source (`raw.githubusercontent.com`) rather than live CDN; Cloudflare blocks GitHub Actions datacenter IPs. Live CDN verification deferred to v0.3.2 (requires Cloudflare allowlist). diff --git a/SECURITY.md b/SECURITY.md index 2ce36d6..415940a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,7 +26,7 @@ The `extract()` callback architecture delegates all network operations to the ca - Retry logic, rate limiting, and fallback providers - Credential management and rotation -The callback API is exported in v0.4.0 and documented in `docs/callback-signature.md`. Its plain-object request/response shapes are intended to remain symmetric with the future WASM host-import contract. +The callback API is exported in v0.4.0. Its plain-object request/response shapes are intended to remain symmetric with the future WASM host-import contract (see [universal-host-boundary.md](docs/universal-host-boundary.md)). ### Forbidden APIs diff --git a/docs/callback-signature.md b/docs/callback-signature.md deleted file mode 100644 index fcbd578..0000000 --- a/docs/callback-signature.md +++ /dev/null @@ -1,261 +0,0 @@ -# Extract Callback Signature Design - -**Status:** SHIPPED in v0.4.0 as provider callbacks in TypeScript and Python. The 0.5.0 universal host boundary is tracked in [universal-host-boundary.md](universal-host-boundary.md). -**Author:** Apollo -**Anchored to:** -- `config/research/conversa/2026-05-04-wasm-conditions-acceptance.md` (Condition 6: API symmetry) -- `config/design/conversa-integration-alignment.md` (CDP3: host import contract) - -## Summary - -The `extract()` function accepts raw text and a callbacks object. The implemented callback names are `callLlm`, optional `getEmbedding`, and optional `log` in TypeScript; Python also accepts `call_llm` and `get_embedding`. `randomUuid` remains a deferred v2/WASM host-import candidate. - -## Design principle: binding substitution - -The v1.2 TypeScript callback interface and the v2 WASM host-import interface MUST be structurally identical. v2 migration is a binding substitution (function calls become WASM imports), not a redesign. This means: - -- Same names, same argument shapes, same return shapes -- No TypeScript-specific types that can't cross the WASM ABI (no classes, no generics, no union types beyond tagged discriminants) -- All complex types are plain objects/records that serialize to JSON -- Async operations use `Promise` in v1.2 and WASI Preview 2 async in v2 - -## Top-level API - -```typescript -interface ExtractCallbacks { - callLlm: (request: LlmRequest) => Promise; - getEmbedding?: (request: EmbeddingRequest) => Promise; - log?: (entry: LogEntry) => void; -} - -function extract( - text: string, - callbacks: ExtractCallbacks, - options?: ExtractOptions, -): Promise; -``` - -```typescript -interface ExtractOptions { - capabilities?: Array; - profile?: "minimal" | "standard" | "full"; - source_type?: string; - source_id?: string; - user_id?: string; - kind?: string; - date?: string; - categories?: string[]; - extensions?: Record; - embeddingInputs?: "all" | Array<"source" | "summary" | "entities" | "goals" | "themes" | "keywords" | "facts" | "questions" | "actions" | "decisions" | "temporal_refs" | "sentiment" | { input: string; text: string }>; - responseTranslator?: (context: LlmResponseTranslatorContext) => Partial | undefined; - responseTranslators?: Array<(context: LlmResponseTranslatorContext) => Partial | undefined>; - extend?: (context: ExtensionResolverContext) => Record | Promise>; - extensionErrors?: "throw" | "warn"; -} - -interface ExtractResult { - extraction: SynaptExtraction; - validation: ValidationResult; - warnings: string[]; - usage: UsageSummary; - stage1: Record; - embeddings: Array>; -} - -interface UsageSummary { - llm_calls: number; - embedding_calls: number; - input_tokens?: number; - output_tokens?: number; - total_tokens?: number; -} -``` - -## Callback signatures - -### `callLlm` - -Sends a prompt to an LLM and returns the text response. The caller (Conversa) owns the model, API key, and routing. Synapt never sees credentials. - -```typescript -interface LlmRequest { - prompt: string; - messages: LlmMessage[]; - capabilities: ExtractionCapability[]; - schema: Record; - responseFormat: Record; - temperature?: number; - max_tokens?: number; -} - -interface LlmMessage { - role: "system" | "user"; - content: string; -} - -interface LlmResponse { - content?: string; - json?: Record; - output?: Record; - produced_by?: string | Omit; - provider?: "openai" | "anthropic" | string; - response_id?: string; - id?: string; - status?: string; - model?: string; - model_version?: string; - usage?: LlmUsage; - raw?: unknown; -} - -interface NormalizedLlmResponse { - provider?: "openai" | "anthropic" | string; - id?: string; - status?: string; - model?: string; - model_version?: string; - stop_reason?: string; - produced_by?: string | Omit; - content?: string; - usage?: LlmUsage; - raw?: unknown; -} - -interface ExtensionResolverContext { - sourceText: string; - capabilities: ExtractionCapability[]; - prompt: string; - schema: Record; - responseFormat: Record; - llmRequest: LlmRequest; - response: NormalizedLlmResponse; - llmResponse: NormalizedLlmResponse; - stage1: Record; - embeddings: Array>; - usage: UsageSummary; - warnings: string[]; -} - -interface LlmUsage { - input_tokens: number; - output_tokens: number; -} -``` - -**Design notes:** - -- `messages` uses a system/user role pair. The user message contains the complete builder prompt. The caller forwards these to their LLM provider. -- `responseFormat` carries the builder-generated strict `json_schema` response format. The caller should pass this to their provider's structured-output mechanism if available. -- The response may provide parsed `output`/`json` or JSON string `content`. `produced_by` is preferred. If omitted, `model` must already be a provider URI for the runner to derive a producer. -- The runner normalizes provider-specific LLM responses before passing them to extension resolvers. Built-in translators handle raw OpenAI Responses and Anthropic Messages objects. Extensions should use `context.response.provider/id/status/model/stop_reason/usage/raw` instead of depending on a callback-specific response shape. -- Custom providers can pass `responseTranslator` / `response_translator` to map raw provider output into the same `NormalizedLlmResponse` envelope. -- `usage` is optional. If the caller's LLM provider returns token counts, pass them through. Synapt aggregates these into `UsageSummary` for metering. If omitted, metering is best-effort. -- `temperature` defaults to `0` if not specified by the caller's provider. Synapt may set this based on the extraction profile. -- `max_tokens` is advisory. Synapt sets it based on expected output size for the requested capabilities. - -**Retry semantics:** Synapt does NOT retry. If `callLlm` throws, extraction fails with the error propagated. The caller owns retry logic, rate limiting, and fallback providers. This keeps synapt's behavior deterministic and avoids surprising the caller with retries against their API quota. - -**Error contract:** Throw a standard `Error` on failure. The experimental runner does not retry and propagates callback, parse, and missing-context errors to the caller. - -### `getEmbedding` - -Computes a vector embedding for a text input. Used for pre-computed embeddings on the extraction (e.g., embedding the summary or source text for downstream similarity search). - -```typescript -interface EmbeddingRequest { - text: string; - input: "source" | "summary" | "entities" | "goals" | "themes" | "keywords" | "facts" | "questions" | "actions" | "decisions" | "temporal_refs" | "sentiment" | string; -} - -interface EmbeddingResponse { - vector: number[]; - model: string; - dimensions: number; -} -``` - -**Design notes:** - -- `input` tells the caller what is being embedded. This is informational; some providers optimize for different input types (e.g., passage vs query). -- `model` in the response is the model identifier the caller used. Synapt records this in the extraction's `embeddings[]` array. The caller decides which embedding model to use; synapt just records what was used. -- `dimensions` must equal `vector.length`. Synapt validates this. - -**Retry semantics:** Same as `callLlm`. No retries from synapt. Caller owns retry and fallback. - -**When called:** Only when the caller requests embeddings via `ExtractOptions.embeddingInputs` (`embedding_inputs` in Python). Not called by default. This keeps the common extraction path free of embedding API calls. - -Capability entries may also request embeddings inline: `{ name: "entities", embed: true }`. Inline capability embeddings are merged with explicit embedding inputs, so callers can request `{ name: "summary", embed: true }` plus `embeddingInputs: ["source"]`. - -### `extend` - -Builds dynamic extensions from the normalized response, parsed Stage 1 output, embeddings, and usage. It runs after embeddings and before finalization, so returned extension objects receive `version: "1"` like static extensions. - -```typescript -extend: ({ response, stage1, embeddings }) => ({ - "synapt/response_binding": { - response_id: response.id, - response_model: response.model, - stage1_fields: Object.keys(stage1).length, - embedding_count: embeddings.length, - }, -}) -``` - -### `log` - -Structured logging for observability. Fire-and-forget; return value is ignored. Synapt emits log entries at key extraction stages for the caller to route to their logging infrastructure. - -```typescript -interface LogEntry { - level: "debug" | "info" | "warn" | "error"; - stage: "prompt_build" | "llm_call" | "parse" | "validate" | "finalize" | "embed"; - message: string; - data?: Record; -} -``` - -**Design notes:** - -- `stage` identifies which extraction pipeline step emitted the log. This maps 1:1 to the extract pipeline stages and is stable across versions. -- `data` carries structured context (e.g., `{ capabilities: [...], profile: "standard" }` for prompt_build, `{ input_tokens: 500 }` for llm_call, `{ error_count: 3 }` for validate). -- Synapt logs at `info` for normal stages, `warn` for recoverable issues (validation errors, missing optional capabilities), `error` for failures that stop extraction, `debug` for verbose trace (prompt content, raw LLM output). - -**Error handling:** If `log` throws, synapt silently ignores the error. Logging must never break extraction. - -## v2 WASM symmetry - -The table below shows how each v1.2 TypeScript callback maps to v2 WASM: - -| v1.2 TypeScript | v2 WASM host import | Async model | -|-----------------|---------------------|-------------| -| `callLlm(req) => Promise` | `(extern "synapt") call_llm(req: LlmRequest) -> LlmResponse` | WASI Preview 2 async | -| `getEmbedding(req) => Promise` | `(extern "synapt") get_embedding(req: EmbeddingRequest) -> EmbeddingResponse` | WASI Preview 2 async | -| `log(entry) => void` | `(extern "synapt") log(entry: LogEntry)` | Sync (fire and forget) | -| deferred `randomUuid() => string` | `(extern "synapt") random_uuid() -> String` | Sync | - -**Name mapping:** TypeScript uses camelCase (`callLlm`); Python and future WASM imports use snake_case (`call_llm`). The mapping is mechanical and documented here. - -**Serialization:** In v1.2, arguments are TypeScript objects passed by reference. In v2, arguments cross the WASM ABI as JSON-serialized buffers (Component Model canonical ABI for records). The shapes are identical; only the transport changes. - -**Capability negotiation:** In v1.2, the caller provides callbacks at call time. In v2, the WASM module declares required imports at load time, and the host fulfills them. Missing imports fail at module instantiation, not at extraction time. This is the `synapt_capabilities()` export pattern from CDP3. - -## For Anchor's review - -**Settled (non-negotiable):** -- Implemented callback names: `callLlm`, optional `getEmbedding`, optional `log` -- No-retry policy: synapt never retries; caller owns retry logic -- `responseFormat` carries the builder-generated JSON schema on LLM calls -- `log` is fire-and-forget; errors silently ignored -- v1.2/v2 structural symmetry remains the design constraint for callback records - -**Negotiable (want Anchor's eyes on):** -- `LlmRequest.messages` shape: is system+user sufficient, or does Conversa need assistant/tool roles for multi-turn extraction? Current design is single-turn (one system + one user message). -- `LlmUsage` granularity: is input/output tokens sufficient, or does Conversa need cache-read/cache-write breakdowns for cost tracking? -- `LogEntry.stage` enum values: are these the right pipeline stages for Conversa's observability needs? Missing any? -- Whether `"all"` is the right default convenience selector for embedding coverage, or whether callers should always name embedding inputs explicitly. -- `ExtractResult.usage`: is a flat summary sufficient, or does Conversa need per-call usage breakdown? - -**Questions for Anchor:** -1. Does Conversa's edge function environment need host-supplied UUID generation, or can entity IDs remain model-emitted local IDs? -2. What is Conversa's preferred error shape? Plain `Error` with message, or structured error with code/details? -3. Does Conversa want streaming LLM responses in v1.2, or is single-response sufficient for launch? (We recommend deferring streaming to v2.) diff --git a/packages/python/src/synapt/extract/batch.py b/packages/python/src/synapt/extract/batch.py index c42d982..c3d3944 100644 --- a/packages/python/src/synapt/extract/batch.py +++ b/packages/python/src/synapt/extract/batch.py @@ -1,14 +1,13 @@ """Batch Stage-1 extraction primitive for SynaptExtraction. -Implements the pinned contract (config/design/extract-batch-limits-characterization- -2026-07-13.md §"Contract decisions") and Sentinel's spec (extract#28, -tests/python/test_extract_batch.py). Reliability logic is per-unit: shaping + +Implements the pinned batch contract tracked in extract#28 +(tests/python/test_extract_batch.py). Reliability logic is per-unit: shaping + per-item validation + fail-closed fallback, with every failure contained to its own unit slot (count-invariant). Why this primitive exists ------------------------- -Atlas's characterization found the generic single-text builder cannot reliably +Characterization testing found the generic single-text builder cannot reliably produce a schema-valid packet even for ONE clean pre-identified unit (NO_VIABLE_N at N=1). The failure is MALFORMATION on GROUNDED content (40/40 source-supported), not confabulation — the model returns the right facts in the wrong shape. Fixed @@ -20,8 +19,8 @@ • Input: list[BatchUnit(id, text, capabilities?, date?)] — explicit attribution; the id rides into the output as source_unit_id (boundaries stay out-of-band, never in model-visible text). `date` (optional) is the unit's SOURCE date, threaded into - Stage-1 as the temporal resolution anchor (config/design/extract-temporal-role- - 2026-07-14.md) so partial/relative dates resolve against the source, not a guess. + Stage-1 as the temporal resolution anchor so partial/relative dates resolve + against the source, not a guess. • Inference: an injected `infer` seam receiving a request {prompt, messages, capabilities} and returning a completion string. ZERO recall dependency. • v1 strategy: PER-UNIT (one infer call per unit) — trivially out-of-band, clean @@ -64,17 +63,17 @@ # did not request them (mirrors recall's backfill so validation does not fail on # containers we deliberately did not request). _ALWAYS_BACKFILL = ("entities", "goals", "themes") -# One deterministic retry per failed unit → 2 attempts total (Q-B, Sentinel). +# One deterministic retry per failed unit → 2 attempts total. _MAX_ATTEMPTS = 2 -# Terminal per-unit failure reasons (Q5). A Literal (not an Enum) so the spec's +# Terminal per-unit failure reasons. A Literal (not an Enum) so the spec's # get_args(BatchFailureReason) reads the members. "merged" is reserved for a future # batch-all path; the per-unit v1 path never emits it. BatchFailureReason = Literal["unparseable", "schema_invalid", "dropped", "merged"] class BatchInferRequest(TypedDict): - """The exact request the injected `infer` seam receives (Q-D). No unit id / + """The exact request the injected `infer` seam receives. No unit id / boundary tag ever appears here — boundaries stay in extract_batch bookkeeping, out of model-visible text.""" @@ -83,18 +82,18 @@ class BatchInferRequest(TypedDict): capabilities: list[str] -# The injected inference seam (Q4): request → completion. The caller (recall) passes +# The injected inference seam: request → completion. The caller (recall) passes # a model-backed callable; tests pass a deterministic/recorded one. Zero recall dep. Inferer = Callable[[BatchInferRequest], str] @dataclass class BatchUnit: - """One pre-identified unit to extract (Q1). ``id`` is stable and rides into the + """One pre-identified unit to extract. ``id`` is stable and rides into the output as ``source_unit_id`` so merge/split/drop is detectable. ``capabilities`` optionally overrides the per-call default for this unit. ``date`` is the unit's SOURCE - date (config/design/extract-temporal-role-2026-07-14.md) — the resolution anchor Stage-1 - uses to resolve partial/relative dates in ``unit.text`` (e.g. "expires April 30") against + date — the resolution anchor Stage-1 uses to resolve partial/relative dates in + ``unit.text`` (e.g. "expires April 30") against the ACTUAL date the source material was written, not "today" or an unanchored guess. Optional: a caller with no source date (or extracting non-temporal-sensitive units) simply omits it, degrading gracefully to unanchored resolution.""" @@ -107,7 +106,7 @@ class BatchUnit: @dataclass class BatchUnitResult: - """Per-unit outcome (Q5). ``status`` "ok" sets ``extraction``; "failed" sets + """Per-unit outcome. ``status`` "ok" sets ``extraction``; "failed" sets ``reason``. ``source_unit_id`` ties the slot back to its BatchUnit.""" source_unit_id: str @@ -126,10 +125,10 @@ async def extract_batch( """Shape + validate a batch of pre-identified units into per-unit envelopes. COUNT-INVARIANT: returns exactly one BatchUnitResult per input unit, in a 1:1 - slot mapping (Q5). extract_batch owns the reliability orchestration (v1 = + slot mapping. extract_batch owns the reliability orchestration (v1 = per-unit calls with one deterministic retry per failed unit) driven through the - injected ``infer`` seam, with zero dependency on any specific model client (Q4). - ``capabilities`` defaults to the standard profile when omitted (Q3). + injected ``infer`` seam, with zero dependency on any specific model client. + ``capabilities`` defaults to the standard profile when omitted. """ if not units: return [] @@ -158,14 +157,13 @@ def _extract_unit( """Run one unit through the reliability ladder: build an out-of-band request → infer → Class-A hygiene + parse → Class-B coerce → finalize/validate. One deterministic retry on failure (2 attempts total); a persisting failure yields a - terminal marker carrying the last failure's reason (Q-B).""" + terminal marker carrying the last failure's reason.""" reason: BatchFailureReason = "dropped" for _attempt in range(_MAX_ATTEMPTS): # Out-of-band: the model sees the unit TEXT only — never its id or a boundary - # tag (Q-D). The id lives in bookkeeping and rides into the packet post-hoc. - # unit.date threads as the temporal RESOLUTION anchor (config/design/extract- - # temporal-role-2026-07-14.md) — None degrades gracefully (build_extraction_prompt - # already handles an absent date). + # tag. The id lives in bookkeeping and rides into the packet post-hoc. + # unit.date threads as the temporal RESOLUTION anchor — None degrades gracefully + # (build_extraction_prompt already handles an absent date). prompt = build_extraction_prompt( unit.text, capabilities=list(capabilities), stage="stage1", date=unit.date, ) @@ -177,7 +175,7 @@ def _extract_unit( # Contain the injected seam per-unit: an infer failure (e.g. RuntimeError) # must NOT escape and void the whole batch — it is this unit's failure, # retried once then terminal, while neighbours still produce their slots. - # No output was produced, so the closest Q5 class is "dropped". + # No output was produced, so the closest failure class is "dropped". try: completion = infer(request) except Exception: @@ -268,7 +266,7 @@ def _strip_output_hygiene(raw: str) -> str: def _coerce_shape(parsed: dict, capabilities: list[str]) -> dict: """Class-B POST-parse (harvest ``_sanitize_stage1_output`` whitelist backbone): - the capability set is the arbiter (Q2). Per the Stage-1 schema for the requested + the capability set is the arbiter. Per the Stage-1 schema for the requested capabilities, whitelist each item type to its fields, coerce (scalar→array, null/non-string optional → omit), and drop out-of-scope item types. ``entity_refs`` is retained only when the ``entities`` capability is in scope; ``temporal_refs`` @@ -325,7 +323,7 @@ def _coerce_item( new_item: dict[str, Any] = {} for field, field_schema in item_props.items(): if field == "entity_refs" and not entities_in_scope: - continue # out-of-scope reference field → drop (Q2) + continue # out-of-scope reference field → drop if field not in item: continue value = item[field] diff --git a/packages/python/src/synapt/extract/builder.py b/packages/python/src/synapt/extract/builder.py index 14db8bf..c265fbb 100644 --- a/packages/python/src/synapt/extract/builder.py +++ b/packages/python/src/synapt/extract/builder.py @@ -334,8 +334,8 @@ def _source_metadata_schema(finalized: bool = False) -> JsonSchema: def _temporal_ref_schema(capabilities: set[str], finalized: bool = False) -> JsonSchema: - # role + resolved_end are BASE-tier (config/design/extract-temporal-role-2026-07-14.md): - # always available with just the "temporal_refs" capability, NOT gated behind + # role + resolved_end are BASE-tier: always available with just the + # "temporal_refs" capability, NOT gated behind # "temporal_classes" — role is the load-bearing direction signal recall's deterministic # mapper needs, and role=="range" needs resolved_end to be usable at all. type/context stay # temporal_classes-gated: non-load-bearing extras once role carries the direction recall diff --git a/packages/python/src/synapt/extract/finalize.py b/packages/python/src/synapt/extract/finalize.py index c59c790..c408124 100644 --- a/packages/python/src/synapt/extract/finalize.py +++ b/packages/python/src/synapt/extract/finalize.py @@ -129,8 +129,8 @@ def _detect_capabilities(doc: dict[str, Any]) -> list[str]: caps.append("temporal_refs") # `type` alone still implies temporal_classes was exercised (it stays gated behind # that capability). `resolved_end` no longer does — it moved to the BASE temporal_refs - # tier alongside `role` (config/design/extract-temporal-role-2026-07-14.md), so its - # presence can no longer be used to infer temporal_classes was requested. + # tier alongside `role`, so its presence can no longer be used to infer + # temporal_classes was requested. if any(r.get("type") is not None for r in temporal): caps.append("temporal_classes") diff --git a/packages/python/src/synapt/extract/validate.py b/packages/python/src/synapt/extract/validate.py index 009e63c..59b8af1 100644 --- a/packages/python/src/synapt/extract/validate.py +++ b/packages/python/src/synapt/extract/validate.py @@ -10,7 +10,7 @@ VALID_GOAL_STATUSES = frozenset(["open", "resolved", "abandoned", "in_progress"]) VALID_TEMPORAL_TYPES = frozenset(["point", "range", "duration", "unresolved"]) -# The validity ROLE (direction) enrichment — config/design/extract-temporal-role-2026-07-14.md. +# The validity ROLE (direction) enrichment. VALID_TEMPORAL_ROLES = frozenset(["effective", "expiry", "range", "superseded", "point"]) VALID_SENTIMENT_VALENCES = frozenset(["positive", "negative", "neutral", "mixed"]) VALID_ACTION_ORIGINS = frozenset(["extracted", "proposed_from_goals"]) @@ -394,8 +394,8 @@ def _check_temporal_ref(obj: Any, path: str, errors: list[ValidationError]) -> N errors.append(ValidationError(f"{path}.resolved", "must not be present when type is 'unresolved'")) if "resolved_end" in obj: errors.append(ValidationError(f"{path}.resolved_end", "must not be present when type is 'unresolved'")) - # Validity ROLE (direction) — BASE-tier, optional, independent of `type` (config/design/ - # extract-temporal-role-2026-07-14.md). A separate role=="range"->resolved_end check + # Validity ROLE (direction) — BASE-tier, optional, independent of `type`. + # A separate role=="range"->resolved_end check # mirrors the type=="range" one above, since role can appear without type now that role # doesn't require the temporal_classes capability. if "role" in obj: diff --git a/packages/ts/src/builder.ts b/packages/ts/src/builder.ts index fc71bb7..90c0f69 100644 --- a/packages/ts/src/builder.ts +++ b/packages/ts/src/builder.ts @@ -406,8 +406,8 @@ function sourceMetadataSchema(finalized = false): JsonSchema { } function temporalRefSchema(capabilities: Set, finalized = false): JsonSchema { - // role + resolved_end are BASE-tier (config/design/extract-temporal-role-2026-07-14.md): - // always available with just the "temporal_refs" capability, NOT gated behind + // role + resolved_end are BASE-tier: always available with just the + // "temporal_refs" capability, NOT gated behind // "temporal_classes" — role is the load-bearing direction signal recall's deterministic // mapper needs, and role === "range" needs resolved_end to be usable at all. type/context // stay temporal_classes-gated (non-load-bearing extras once role carries the direction). diff --git a/packages/ts/src/finalize.ts b/packages/ts/src/finalize.ts index bc0c0e7..2b080b5 100644 --- a/packages/ts/src/finalize.ts +++ b/packages/ts/src/finalize.ts @@ -145,7 +145,7 @@ function detectCapabilities(doc: Record): ExtractionCapability[ const refs = doc.temporal_refs as Record[]; // `type` alone still implies temporal_classes was exercised (it stays gated behind that // capability). `resolved_end` no longer does — it moved to the BASE temporal_refs tier - // alongside `role` (config/design/extract-temporal-role-2026-07-14.md). Mirrors Python. + // alongside `role`. Mirrors Python. if (refs.some((r) => r.type !== undefined)) caps.push("temporal_classes"); } diff --git a/packages/ts/src/validate.ts b/packages/ts/src/validate.ts index 5f38461..07df997 100644 --- a/packages/ts/src/validate.ts +++ b/packages/ts/src/validate.ts @@ -20,7 +20,7 @@ const VALID_TEMPORAL_TYPES: Set = new Set([ "point", "range", "duration", "unresolved", ]); -// The validity ROLE (direction) enrichment — config/design/extract-temporal-role-2026-07-14.md. +// The validity ROLE (direction) enrichment. const VALID_TEMPORAL_ROLES: Set = new Set([ "effective", "expiry", "range", "superseded", "point", ]); @@ -538,8 +538,8 @@ function validateTemporalRef(obj: unknown, path: string, errors: ValidationError } } } - // Validity ROLE (direction) — BASE-tier, optional, independent of `type` (config/design/ - // extract-temporal-role-2026-07-14.md). A separate role === "range" -> resolved_end check + // Validity ROLE (direction) — BASE-tier, optional, independent of `type`. + // A separate role === "range" -> resolved_end check // mirrors the type === "range" one above, since role can appear without type now that role // doesn't require the temporal_classes capability. Mirrors the Python _check_temporal_ref. if (ref.role !== undefined) { diff --git a/packages/ts/tests/test_extract.ts b/packages/ts/tests/test_extract.ts index daa0eb1..0dfd092 100644 --- a/packages/ts/tests/test_extract.ts +++ b/packages/ts/tests/test_extract.ts @@ -16,9 +16,9 @@ import { import { extractOpenAI, type OpenAICompatibleClient } from "../src/openai.js"; const SAMPLE_TEXT = [ - "On May 10, 2026, Layne told Mark that Synapt should ship the extraction builder by Friday.", - "Mark asked whether embeddings should cover the source and summary.", - "Layne said the first version might use local entity IDs, and if validation passes, they will publish the gist.", + "On May 10, 2026, Dana told Priya that Synapt should ship the extraction builder by Friday.", + "Priya asked whether embeddings should cover the source and summary.", + "Dana said the first version might use local entity IDs, and if validation passes, they will publish the gist.", ].join(" "); const STAGE1_FULL = { @@ -26,11 +26,11 @@ const STAGE1_FULL = { entities: [ { id: "e1", - name: "Layne", + name: "Dana", type: "person", - aliases: ["Layne Penney"], + aliases: ["Dana Whitfield"], state: "coordinating Synapt extraction work", - context: "Asked Mark to review builder and embedding behavior.", + context: "Asked Priya to review builder and embedding behavior.", date_hint: "2026-05-10", relations: [ { @@ -40,19 +40,19 @@ const STAGE1_FULL = { signals: { confidence: 0.91 }, }, ], - source: { snippet: "Layne told Mark", sentence_index: 0 }, + source: { snippet: "Dana told Priya", sentence_index: 0 }, signals: { confidence: 0.93 }, }, { id: "e2", - name: "Mark", + name: "Priya", type: "person", - aliases: ["Mark Hendrickson"], + aliases: ["Priya Shah"], state: "reviewing Synapt extraction ideas", context: "Asked about embedding coverage.", date_hint: "2026-05-10", relations: [], - source: { snippet: "Mark asked", sentence_index: 1 }, + source: { snippet: "Priya asked", sentence_index: 1 }, signals: { confidence: 0.9 }, }, ], @@ -68,11 +68,11 @@ const STAGE1_FULL = { ], themes: ["extraction pipeline", "embeddings", "schema validation"], keywords: ["Synapt", "extraction builder", "embeddings", "gist"], - summary: "Layne and Mark discussed shipping a Synapt extraction builder with embedding coverage and validation.", + summary: "Dana and Priya discussed shipping a Synapt extraction builder with embedding coverage and validation.", sentiment: { valence: "positive", intensity: 0.55, confidence: 0.72 }, facts: [ { - text: "Mark asked whether embeddings should cover the source and summary.", + text: "Priya asked whether embeddings should cover the source and summary.", category: "technical_question", source: { snippet: "embeddings should cover the source and summary", sentence_index: 1 }, signals: { confidence: 0.95 }, @@ -81,7 +81,7 @@ const STAGE1_FULL = { questions: [ { text: "Should embeddings cover the source and summary?", - directed_to: "Layne", + directed_to: "Dana", source: { snippet: "whether embeddings should cover the source and summary", sentence_index: 1 }, signals: { confidence: 0.95 }, }, diff --git a/packages/ts/tests/test_finalize.ts b/packages/ts/tests/test_finalize.ts index b652b93..07cd54d 100644 --- a/packages/ts/tests/test_finalize.ts +++ b/packages/ts/tests/test_finalize.ts @@ -194,7 +194,7 @@ describe("finalizeExtraction", () => { }); test("resolved_end alone does not imply temporal_classes (base-tier role)", () => { - // role + resolved_end are BASE-tier (config/design/extract-temporal-role-2026-07-14.md) — + // role + resolved_end are BASE-tier — // a range-role ref can carry resolved_end WITHOUT temporal_classes ever being exercised. // The old heuristic (type OR resolved_end) would mislabel this; only `type` should trigger // detection now. Mirrors the Python test_resolved_end_alone_does_not_imply_temporal_classes. diff --git a/packages/ts/tests/test_prompt.ts b/packages/ts/tests/test_prompt.ts index 203ca5b..ad4a8c0 100644 --- a/packages/ts/tests/test_prompt.ts +++ b/packages/ts/tests/test_prompt.ts @@ -162,8 +162,8 @@ describe("buildExtractionPrompt", () => { }); test("temporal_refs fragment carries role classification instructions", () => { - // config/design/extract-temporal-role-2026-07-14.md — the Stage-1 prompt classifies each - // temporal ref's validity role, with the 5 enum values named. Mirrors the Python test. + // The Stage-1 prompt classifies each temporal ref's validity role, with the 5 enum + // values named. Mirrors the Python test. const result = buildExtractionPrompt(SAMPLE_TEXT, { capabilities: ["temporal_refs"] }); expect(result).toContain('"role"'); for (const role of ["effective", "expiry", "range", "superseded", "point"]) { @@ -501,8 +501,8 @@ describe("buildExtractionSchema", () => { }); test("temporal role + resolved_end are base-tier; type/context stay temporal_classes-gated", () => { - // config/design/extract-temporal-role-2026-07-14.md — role is the load-bearing direction - // signal, always available with just "temporal_refs"; type/context remain gated. Mirrors + // role is the load-bearing direction signal, always available with just + // "temporal_refs"; type/context remain gated. Mirrors // the Python base-tier coverage (test_role_and_resolved_end_survive_coercion + the // type/context negative control). Requesting ONLY temporal_refs, NOT temporal_classes. const base = buildExtractionSchema({ capabilities: ["temporal_refs"] }); diff --git a/packages/ts/tests/test_validate.ts b/packages/ts/tests/test_validate.ts index 282d890..007f9e6 100644 --- a/packages/ts/tests/test_validate.ts +++ b/packages/ts/tests/test_validate.ts @@ -310,7 +310,7 @@ describe("validateExtraction", () => { false, ["temporal_refs[0].resolved_end"], ], - // Validity ROLE (direction) enrichment — config/design/extract-temporal-role-2026-07-14.md. + // Validity ROLE (direction) enrichment. // Mirrors the Python TestTemporalRefRole cases. role + resolved_end are BASE-tier (no // temporal_classes capability needed); role is enum-constrained; role === "range" needs // resolved_end. diff --git a/tests/python/fixtures/extract-batch-real-failures-v1.json b/tests/python/fixtures/extract-batch-real-failures-v1.json index aa4e8ae..9df5398 100644 --- a/tests/python/fixtures/extract-batch-real-failures-v1.json +++ b/tests/python/fixtures/extract-batch-real-failures-v1.json @@ -2,14 +2,7 @@ "fixture_set": "extract-batch-real-failures-2026-07-13", "fixture_schema_version": 1, "source": { - "repository": "synapt-dev/config", - "merge_commit": "3b3d528", - "leaf_labels_path": "config/design/results/extract-batch-limits-2026-07-13/manual-leaf-labels.jsonl", - "leaf_labels_sha256": "272599669916bb2eec16aaebeca6ccee50dde3ef05bc6ae0fd1d51436e70ca6f", - "unit_labels_path": "config/design/results/extract-batch-limits-2026-07-13/manual-unit-labels.jsonl", - "unit_labels_sha256": "362c8412fb862ffc6cb300ae2b1b68218e58b7bc4d2a4313b78bb2a84e298764", - "raw_results_path": "config/design/results/extract-batch-limits-2026-07-13/raw-results.jsonl", - "raw_results_sha256": "30412251126a4f1fe83ae3c2329c3c65416db21a9779a246feeb5b6ab14e68da" + "provenance": "Curated fixtures for the extract batch failure-path regression tests." }, "boundary": { "target": "OSS synapt-extract tests", diff --git a/tests/python/test_extract.py b/tests/python/test_extract.py index 90fba3b..f5c0442 100644 --- a/tests/python/test_extract.py +++ b/tests/python/test_extract.py @@ -14,9 +14,9 @@ SAMPLE_TEXT = ( - "On May 10, 2026, Layne told Mark that Synapt should ship the extraction builder by Friday. " - "Mark asked whether embeddings should cover the source and summary. " - "Layne said the first version might use local entity IDs, and if validation passes, they will publish the gist." + "On May 10, 2026, Dana told Priya that Synapt should ship the extraction builder by Friday. " + "Priya asked whether embeddings should cover the source and summary. " + "Dana said the first version might use local entity IDs, and if validation passes, they will publish the gist." ) STAGE1_FULL = { @@ -24,11 +24,11 @@ "entities": [ { "id": "e1", - "name": "Layne", + "name": "Dana", "type": "person", - "aliases": ["Layne Penney"], + "aliases": ["Dana Whitfield"], "state": "coordinating Synapt extraction work", - "context": "Asked Mark to review builder and embedding behavior.", + "context": "Asked Priya to review builder and embedding behavior.", "date_hint": "2026-05-10", "relations": [ { @@ -38,19 +38,19 @@ "signals": {"confidence": 0.91}, } ], - "source": {"snippet": "Layne told Mark", "sentence_index": 0}, + "source": {"snippet": "Dana told Priya", "sentence_index": 0}, "signals": {"confidence": 0.93}, }, { "id": "e2", - "name": "Mark", + "name": "Priya", "type": "person", - "aliases": ["Mark Hendrickson"], + "aliases": ["Priya Shah"], "state": "reviewing Synapt extraction ideas", "context": "Asked about embedding coverage.", "date_hint": "2026-05-10", "relations": [], - "source": {"snippet": "Mark asked", "sentence_index": 1}, + "source": {"snippet": "Priya asked", "sentence_index": 1}, "signals": {"confidence": 0.9}, }, ], @@ -66,11 +66,11 @@ ], "themes": ["extraction pipeline", "embeddings", "schema validation"], "keywords": ["Synapt", "extraction builder", "embeddings", "gist"], - "summary": "Layne and Mark discussed shipping a Synapt extraction builder with embedding coverage and validation.", + "summary": "Dana and Priya discussed shipping a Synapt extraction builder with embedding coverage and validation.", "sentiment": {"valence": "positive", "intensity": 0.55, "confidence": 0.72}, "facts": [ { - "text": "Mark asked whether embeddings should cover the source and summary.", + "text": "Priya asked whether embeddings should cover the source and summary.", "category": "technical_question", "source": {"snippet": "embeddings should cover the source and summary", "sentence_index": 1}, "signals": {"confidence": 0.95}, @@ -79,7 +79,7 @@ "questions": [ { "text": "Should embeddings cover the source and summary?", - "directed_to": "Layne", + "directed_to": "Dana", "source": {"snippet": "whether embeddings should cover the source and summary", "sentence_index": 1}, "signals": {"confidence": 0.95}, } diff --git a/tests/python/test_extract_batch.py b/tests/python/test_extract_batch.py index 8be040c..ff51352 100644 --- a/tests/python/test_extract_batch.py +++ b/tests/python/test_extract_batch.py @@ -27,7 +27,7 @@ RECALL_CAPABILITIES = ["facts", "decisions", "temporal_refs"] PRODUCED_BY = "mlx://mlx-community/Ministral-3-3B-Instruct-2512-4bit" EXTRACTED_AT = "2026-07-13T10:00:00Z" -FIXTURE_SHA256 = "9b183f18ab5116cfb1f5ee67d0e99cd5af3fb7f7b99d649b1d58821f9e7489f1" +FIXTURE_SHA256 = "d01bda9b4369c56a681cd9861bc9ed78293e32cb7fd1ed0310535758fe3adf2c" FIXTURE_PATH = Path(__file__).parent / "fixtures" / "extract-batch-real-failures-v1.json" FIXTURE_BYTES = FIXTURE_PATH.read_bytes() FIXTURES = json.loads(FIXTURE_BYTES) @@ -156,8 +156,8 @@ def test_temporal_prompt_schema_conflict_is_explicitly_normalized(case): def test_role_and_resolved_end_survive_coercion_at_base_capability_tier(): - """THE root cause this whole fix chain traces back to (config/design/extract-temporal- - role-2026-07-14.md): role + resolved_end are BASE-tier now, not gated behind + """THE root cause this whole fix chain traces back to: role + resolved_end are + BASE-tier now, not gated behind temporal_classes — _coerce_shape's whitelist is schema-driven (build_extraction_schema), so this is a pure consequence of the builder.py fix, not separate coercion code (VERIFIED empirically before this test existed, ad hoc; formalized here as a permanent regression @@ -275,9 +275,9 @@ def infer(request): def test_extract_batch_threads_unit_date_as_temporal_resolution_anchor(): - """config/design/extract-temporal-role-2026-07-14.md 'Temporal RESOLUTION needs the - source date': each unit's SOURCE date threads into Stage-1 as the resolution anchor for - partial/relative dates. Uses a NON-2026 source date (Sentinel's explicit ask — every prior + """'Temporal RESOLUTION needs the source date': each unit's SOURCE date threads + into Stage-1 as the resolution anchor for + partial/relative dates. Uses a NON-2026 source date (every prior temporal test used 2026, which masked exactly this class of bug).""" unit = BatchUnit(id="anchored", text="the API key expires April 30", date="2025-03-01") seen_prompts = [] @@ -295,12 +295,12 @@ def infer(request): _assert_success(outputs[0], "anchored") -def test_extract_batch_replicates_sentinels_wrong_year_scenario_end_to_end(): +def test_extract_batch_replicates_wrong_year_scenario_end_to_end(): """THE capstone: role (direction) + resolution (source-date anchor) working TOGETHER - through a REAL extract_batch call, replicating Sentinel's exact real-path finding - (config/design/extract-temporal-role-2026-07-14.md 'Temporal RESOLUTION needs the source - date') — a 2025-03-01-sourced unit with "API key expires April 30" must NOT silently - resolve to 2026 (the c791018 duct-tape bug this whole fix chain traces back to). This test + through a REAL extract_batch call, replicating the exact real-path finding + ('Temporal RESOLUTION needs the source date') — a 2025-03-01-sourced unit with + "API key expires April 30" must NOT silently + resolve to 2026 (the original wrong-year regression this fix chain traces back to). This test proves the CONTRACT end-to-end: given a correctly-anchored+classified model response, the persisted envelope carries role="expiry" and the ANCHORED year — not whether a real model reliably produces that response (a model-quality question for Phase-C), but that nothing @@ -320,7 +320,7 @@ def infer(request): _assert_success(outputs[0], "clu:0:done:0") ref = outputs[0].extraction["temporal_refs"][0] assert ref["role"] == "expiry" # direction preserved through coercion + validation - assert ref["resolved"] == "2025-04-30" # ANCHORED year, not 2026 (Sentinel's bug) + assert ref["resolved"] == "2025-04-30" # ANCHORED year, not 2026 def test_extract_batch_unit_without_date_omits_resolution_anchor_gracefully(): @@ -510,7 +510,7 @@ def test_empty_input_is_a_noop(): assert outputs == [] -# --- Fidelity-gate regression locks (Sentinel, extract#30 re-gate) ------------- +# --- Fidelity-gate regression locks ------------- def test_infer_exception_in_one_unit_never_voids_the_batch(): """HIGH-1: an infer() exception must be contained to its own unit — neighbours @@ -561,7 +561,7 @@ def test_non_dict_leaves_reach_strict_validation(label, malformed): def test_valid_metadata_only_extraction_is_not_dropped(): - """HIGH-3: extract_batch is a GENERAL primitive (Q3) — an entities-only (array) + """HIGH-3: extract_batch is a GENERAL primitive — an entities-only (array) or summary-only (scalar) extraction is real content for its requested capability set, not the empty "dropped" mode.""" entities_unit = BatchUnit(id="entities-only", text="Synapt is an organization.") diff --git a/tests/python/test_finalize.py b/tests/python/test_finalize.py index 1c9d916..361c7d9 100644 --- a/tests/python/test_finalize.py +++ b/tests/python/test_finalize.py @@ -372,8 +372,8 @@ def test_detects_temporal_classes(self): assert "temporal_classes" in caps def test_resolved_end_alone_does_not_imply_temporal_classes(self): - """role + resolved_end are BASE-tier (config/design/extract-temporal-role-2026-07-14.md) - — a range-role ref can legitimately carry resolved_end WITHOUT the temporal_classes + """role + resolved_end are BASE-tier — a range-role ref can legitimately carry + resolved_end WITHOUT the temporal_classes capability ever being requested/exercised. The old heuristic (`type is not None OR resolved_end is not None`) would have mislabeled this as having used temporal_classes; only `type`'s presence (still temporal_classes-gated) should trigger detection now.""" diff --git a/tests/python/test_prompt.py b/tests/python/test_prompt.py index 43dd25e..59f19b0 100644 --- a/tests/python/test_prompt.py +++ b/tests/python/test_prompt.py @@ -262,8 +262,8 @@ def test_temporal_refs_fragment_present(self): assert '"temporal_refs"' in result def test_temporal_refs_role_instructions_present(self): - """config/design/extract-temporal-role-2026-07-14.md: the Stage-1 prompt classifies - each temporal ref's validity role, with the 5 enum values named.""" + """The Stage-1 prompt classifies each temporal ref's validity role, with + the 5 enum values named.""" result = build_extraction_prompt(SAMPLE_TEXT, capabilities=["temporal_refs"]) assert '"role"' in result for role in ("effective", "expiry", "range", "superseded", "point"): diff --git a/tests/python/test_validate.py b/tests/python/test_validate.py index 042ba53..bc46acf 100644 --- a/tests/python/test_validate.py +++ b/tests/python/test_validate.py @@ -334,12 +334,12 @@ def test_invalid_temporal_type(self): class TestTemporalRefRole: - """The validity ROLE (direction) enrichment — config/design/extract-temporal-role- - 2026-07-14.md. role is BASE-tier (no temporal_classes capability needed), optional + """The validity ROLE (direction) enrichment. role is BASE-tier (no + temporal_classes capability needed), optional (existing {raw, resolved} consumers still validate), and enum-constrained.""" def test_public_typeddict_declares_role(self): - """GUARD (Sentinel's finding): the exported public SynaptTemporalRef TypedDict must + """GUARD: the exported public SynaptTemporalRef TypedDict must declare `role`. The v0.6 runtime EMITS role (builder schema + validation accept it), so a public type that omitted it would reject its own runtime output — the exact type/runtime mismatch this guards against. Also confirms the declared enum members