АРЧ АП11 - Commit title: Добавить контрактный слой переходов и capability-деклараций ассистента

This commit is contained in:
dctouch 2026-04-15 22:53:57 +03:00
parent 8056bdfaf2
commit 93ad18daa3
30 changed files with 4175 additions and 13 deletions

View File

@ -0,0 +1,256 @@
# 01 - Project Architecture Baseline Map
## Purpose
This note is the compact execution-oriented map of the current project.
It is not a market review and not a historical report. It answers:
- what the main subsystems are;
- where the assistant runtime really starts and ends;
- what already acts as architecture, not as incidental code;
- where the main structural debt sits.
## System Map
### 1. 1C Acquisition And Probe Layer
Main areas:
- `odata_probe/`
- `scripts/*` probe and verification scripts
Role:
- verify read-only access viability;
- inspect published 1C entity sets and links;
- produce source-level readiness for higher layers.
This is a prerequisite layer, not the assistant runtime itself.
### 2. Canonical And Analytical Data Layer
Main areas:
- `canonical_layer/`
- `data/`
- `logs/`
Current runtime entry:
- [canonical_layer/app.py](/x:/1C/NDC_1C/canonical_layer/app.py:1)
Role:
- expose normalized read-only data services;
- refresh and maintain the canonical store;
- run feature engine;
- run risk engine.
This layer is the project's data foundation.
### 3. LLM Backend And Assistant Runtime
Main area:
- `llm_normalizer/backend/`
Current server entry:
- [server.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/server.ts:1)
Role:
- provider gateway;
- normalizer runtime;
- assistant runtime;
- eval runtime;
- autorun runtime;
- session persistence.
This is the architectural center of the interactive product.
### 4. Domain Orchestration And Acceptance Loop
Main areas:
- `.codex/`
- `docs/orchestration/active_domain_contract.json`
- `artifacts/domain_runs/`
Role:
- define the current active domain pack;
- execute scenario-based hardening;
- store machine-readable before/after artifacts;
- turn user runs into acceptance and repair targets.
This is a first-class quality contour, not auxiliary documentation.
### 5. Experimental Routing / Benchmark Contour
Main area:
- `router/`
Role:
- route-selection experiments;
- store-sufficiency heuristics;
- benchmark and validation support via Python scripts.
Important:
- this contour is not the current source of truth for the production assistant runtime;
- current production routing lives in the TypeScript assistant stack.
## Assistant Runtime Map
The assistant runtime currently has these working layers:
1. `provider/model gateway`
2. `living router`
3. `address orchestration runtime`
4. `exact execution lane`
5. `coverage / evidence / grounding`
6. `answer policy / packaging / debug`
7. `session memory / navigation state`
### Provider / Model Gateway
Current main entry:
- [openaiResponsesClient.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/openaiResponsesClient.ts:1)
Current shape:
- pragmatic two-mode provider layer: `openai` and `local`
This layer is functional, but future-fragile if hybrid execution semantics grow.
### Living Router
Current main entry:
- [assistantService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4248)
Current top-level modes:
- `address_data`
- `assistant_data_scope`
- `chat`
This is where the system decides which runtime contour gets control.
### Address Orchestration Runtime
Current main entry:
- [assistantAddressOrchestrationRuntimeAdapter.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantAddressOrchestrationRuntimeAdapter.ts:1)
Current responsibilities:
- predecompose;
- effective message normalization;
- carryover resolution;
- rewrite protection;
- continuation contract assembly;
- address runtime metadata.
### Exact Execution Lane
Current main areas:
- [addressQueryService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressQueryService.ts:1)
- [addressIntentResolver.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressIntentResolver.ts:1)
- [addressFilterExtractor.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressFilterExtractor.ts:1)
- [addressRecipeCatalog.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressRecipeCatalog.ts:1)
- [decomposeStage.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/address_runtime/decomposeStage.ts:1325)
- [composeStage.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/address_runtime/composeStage.ts:1)
This lane already acts as a real exact-data runtime, not as generic chat assistance.
### Coverage / Evidence / Grounding
Current main areas:
- [assistantCoverageGrounding.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantCoverageGrounding.ts:1)
- [assistantClaimBoundEvidence.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts:1)
- [assistantDataLayer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantDataLayer.ts:1)
This layer already exists in code, even if it has not yet been named strongly enough in architecture docs.
### Answer Policy / Packaging / Debug
Current main areas:
- [answerComposer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/answerComposer.ts:1)
- [assistantAnswerPackageBuilder.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantAnswerPackageBuilder.ts:1)
- [assistantDebugPayloadAssembler.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts:1)
This layer should own answer shape, not truth determination.
### Session Memory / Navigation State
Current main areas:
- [assistantSessionStore.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantSessionStore.ts:1)
- [addressNavigationState.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressNavigationState.ts:329)
- [investigationState.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/investigationState.ts:1)
This layer is one of the strongest parts of the architecture today.
## First-Class Runtime Artifacts Already Present
The following should be treated as first-class architectural artifacts, not helper byproducts:
- `dialogContinuationContractV2`
- `result_set`
- `focus_object`
- `date_scope`
- `organization_scope`
- `coverage report`
- `grounding check`
- `evidence bundle`
- `scenario manifest`
- `scenario state`
- `baseline_turn / rerun_turn artifacts`
## Structural Debt
The main structural debt is concentrated in oversized modules with mixed responsibilities:
- `assistantService.ts`
- `composeStage.ts`
- `answerComposer.ts`
- `addressQueryService.ts`
- `assistantDataLayer.ts`
This indicates the following problem:
- the system already has the right layers;
- but too many of those layers are still implemented as tightly coupled code inside a few large files.
## Baseline Constraints
Any architectural turnaround must preserve:
- `AddressQueryService` as exact lane;
- structured navigation state;
- continuation contract semantics;
- selected-object continuity;
- limited-mode truthfulness;
- scenario-based acceptance.
## Baseline Diagnosis
The project is not missing architecture.
The project already has architecture, but it is still expressed too much through:
- god services;
- implicit workflow;
- heuristic branching;
- mixed policy/state/answer concerns.
That is the baseline condition that the rest of this package is designed to address.

View File

@ -0,0 +1,393 @@
# 02 - State And Transition Contracts
## Purpose
This document defines the objects that must become explicit in project design:
1. `state objects`
2. `transition classes`
The goal is to stop treating the assistant as a prompt-driven flow with ad hoc carryover and start treating it as a stateful runtime with explicit transitions.
## State Model
### 1. Assistant Session Aggregate
Top-level first-class runtime state:
`assistant_session_state`
It must aggregate:
- `living_mode_state`
- `address_navigation_state`
- `investigation_state`
- `clarification_state`
- `answer_context_state`
- `coverage_gate_state`
This aggregate is the architectural owner of cross-turn continuity.
### 2. Living Mode State
Purpose:
- remember which major runtime contour is active.
Fields:
- `living_mode`
- `mode_reason`
- `mode_source`
- `mode_entry_turn_id`
Allowed values:
- `address_data`
- `assistant_data_scope`
- `chat`
- `meta_followup`
- `clarification`
### 3. Root Frame State
Purpose:
- represent the currently active root business slice.
Fields:
- `domain_id`
- `root_route_id`
- `organization_scope`
- `date_scope`
- `root_result_set_id`
- `root_answer_object_ref`
- `frame_status`
This state survives more transitions than object state.
### 4. Selected Object Frame State
Purpose:
- represent the currently active drilldown object.
Fields:
- `focus_object_ref`
- `focus_object_kind`
- `source_result_set_id`
- `compatible_route_family`
- `provenance_bundle_ref`
- `temporal_ceiling`
- `frame_status`
This state may only survive compatible object-level transitions.
### 5. Meta Frame State
Purpose:
- support questions about the already returned answer without replaying exact execution blindly.
Fields:
- `source_answer_object_ref`
- `meta_question_kind`
- `source_gate_status`
- `meta_truth_mode`
Allowed meta classes:
- `evaluation`
- `comparison`
- `memory_recap`
- `boundary_explanation`
- `answer_interpretation`
### 6. Clarification State
Purpose:
- represent unfinished business queries that are waiting for missing anchors or disambiguation.
Fields:
- `clarification_kind`
- `missing_anchors`
- `candidate_scopes`
- `resume_target_route`
- `resume_target_frame`
### 7. Coverage Gate State
Purpose:
- capture whether the exact execution result is admissible for full, partial, or blocked downstream behavior.
Fields:
- `coverage_status`
- `evidence_grade`
- `grounding_status`
- `truth_mode`
- `carryover_eligibility`
- `reason_codes`
Allowed values:
- `coverage_status`: `full`, `partial`, `blocked`
- `truth_mode`: `confirmed`, `limited`, `clarification_required`, `unsupported`
- `carryover_eligibility`: `full`, `root_only`, `object_only`, `meta_only`, `none`
## Transition Model
The architecture must distinguish transition classes explicitly.
### T1. Root Query Entry
Trigger:
- new root business question
Inputs:
- raw user query
- living mode state
Outputs:
- new `root_frame_state`
- new `coverage_gate_state`
Must not:
- inherit stale `focus_object`
### T2. Root Follow-Up With Date Or Scope Change
Trigger:
- `а на март 2020`
- `на тот же период`
- `еще раз по этой дате`
Inputs:
- existing `root_frame_state`
- compatible temporal or organization shift
Outputs:
- updated `root_frame_state`
- new exact route execution
Must preserve:
- root domain
- organization scope if not explicitly changed
Must not:
- downgrade into `unknown` if supported root route exists
### T3. Explicit Selected Object Drilldown
Trigger:
- item selected from current result set
- UI selected-object wording
- full explicit object mention
Inputs:
- active `root_frame_state`
- selected object reference
Outputs:
- `selected_object_frame_state`
Must preserve:
- source result set
- compatible temporal ceiling
### T4. Short Action Follow-Up On Selected Object
Trigger:
- `кто поставщик`
- `где купили`
- `кому продали`
- `какие документы`
Inputs:
- active `selected_object_frame_state`
- compatible action request
Outputs:
- exact item-level capability route
Must not:
- fall into generic chat
- fall into data-scope selection
- lose selected object because the wording is short
### T5. Pronoun Or Compressed Object Follow-Up
Trigger:
- `по ней`
- `по этой позиции`
- `а эта кому ушла`
Inputs:
- active `selected_object_frame_state`
- prior compatible object route
Outputs:
- continued object drilldown
Must not:
- degrade full object anchor into vague semantic noise
### T6. Domain Pivot With Root-Only Carryover
Trigger:
- user leaves the current object drilldown but remains in compatible higher business scope
Inputs:
- `root_frame_state`
- incompatible `selected_object_frame_state`
Outputs:
- preserved root context
- dropped object context
Must preserve:
- organization/date root scope
Must not:
- replay object route into another domain
### T7. Clarification Continuation
Trigger:
- user resolves missing anchor or ambiguity
Inputs:
- active `clarification_state`
Outputs:
- resumed target route
- cleared or updated clarification state
Must not:
- forget the suspended target route
### T8. Meta Follow-Up Over Answer Object
Trigger:
- `это много или мало`
- `это мы должны или нам`
- `что из этого важнее`
Inputs:
- `answer_context_state`
- `coverage_gate_state`
Outputs:
- meta answer
Must not:
- blindly replay exact route
### T9. Memory Recap
Trigger:
- `мы это обсуждали?`
- `помнишь, о чем говорили`
Inputs:
- prior grounded answer context
Outputs:
- truthful recap
Must not:
- invent conversation memory
### T10. Unsupported Or Blocked Boundary
Trigger:
- unsupported route
- blocked evidence gate
- execution failure
Inputs:
- exact runtime outcome
- `coverage_gate_state`
Outputs:
- bounded truthful answer or clarification
Must not:
- masquerade blocked execution as confirmed factual answer
## Transition Invariants
Every transition must declare:
- `entry condition`
- `required prior state`
- `allowed carryover depth`
- `state mutations`
- `forbidden carryover`
- `expected answer mode`
## Required Artifacts
Any future implementation of these contracts should produce:
- transition table or registry;
- state schema definitions;
- transition tests;
- scenario acceptance coverage by transition class.
## Done Criteria
This document is only considered implemented in architecture when:
- every major follow-up case maps to a named transition class;
- every transition class has a declared state owner;
- every transition class has a scenario-based regression family;
- no critical follow-up behavior depends only on unnamed heuristic carryover.

View File

@ -0,0 +1,253 @@
# 03 - Capability Contract Specification
## Purpose
This document defines what every runtime capability must declare in order to participate safely in the assistant architecture.
The system should stop treating capabilities as partially implicit products of:
- intent detection;
- filter extraction;
- recipe selection;
- wording heuristics.
Instead, each capability must be a first-class contract object.
## Capability Contract Object
Each capability must declare the following fields.
### 1. Identity
- `capability_id`
- `domain_id`
- `runtime_lane`
- `intent_ids`
Purpose:
- stable identification;
- relation between capability, domain and route family.
### 2. Entry Semantics
- `entry_modes`
- `supported_transition_classes`
- `frame_compatibility`
Minimum allowed values:
- `root_entry`
- `root_followup`
- `selected_object_drilldown`
- `meta_reuse`
- `clarification_resume`
Purpose:
- define where the capability may legally be entered from.
### 3. Anchor Contract
- `required_anchors`
- `optional_anchors`
- `anchor_source_priority`
- `anchor_admissibility_rules`
Purpose:
- make anchor requirements explicit;
- prevent garbage anchor extraction from being treated as business input.
### 4. Scope Contract
- `organization_scope_behavior`
- `date_scope_behavior`
- `temporal_ceiling_policy`
- `root_context_compatibility`
Purpose:
- define whether the capability reuses, narrows, widens, or rejects prior scope.
### 5. Selected-Object Contract
- `requires_focus_object`
- `accepted_focus_object_kinds`
- `focus_object_override_policy`
- `bundle_reuse_policy`
Purpose:
- define whether the capability depends on an existing selected object;
- define how object continuity is preserved.
### 6. Execution Contract
- `resolver_owner`
- `recipe_owner`
- `execution_adapter`
- `result_shape`
- `answer_object_shape`
Purpose:
- clearly identify the exact runtime owner of execution and output form.
### 7. Truth Gate Contract
- `minimum_evidence_policy`
- `coverage_gate_behavior`
- `truth_mode_fallbacks`
- `blocked_reason_codes`
Purpose:
- declare what counts as sufficient evidence for this capability;
- prevent answer policy from inventing truth semantics later.
### 8. Clarification Contract
- `clarification_triggers`
- `clarification_questions`
- `resume_policy`
Purpose:
- define when the capability must ask, not guess.
### 9. Failure Contract
- `empty_match_behavior`
- `route_expectation_failure_behavior`
- `execution_error_behavior`
Purpose:
- keep failures truthful and stable.
### 10. Acceptance Contract
- `required_unit_tests`
- `required_transition_tests`
- `required_scenario_families`
Purpose:
- make capability completion measurable.
## Minimal Contract Template
Each capability should be designable in the following shape:
```yaml
capability_id: inventory_purchase_provenance_for_item
domain_id: inventory_stock
runtime_lane: address_exact
intent_ids:
- inventory_purchase_provenance_for_item
entry_modes:
- selected_object_drilldown
- clarification_resume
supported_transition_classes:
- T3
- T4
- T5
frame_compatibility:
root_frame: required
selected_object_frame: required
required_anchors:
- item
optional_anchors:
- organization
- date_scope
anchor_admissibility_rules:
- no_low_quality_item_rewrite
- no_conversational_noise_as_entity
organization_scope_behavior: reuse_or_clarify
date_scope_behavior: respect_root_temporal_ceiling
temporal_ceiling_policy: must_not_expand_beyond_root_without_reason_code
requires_focus_object: true
accepted_focus_object_kinds:
- inventory_item
bundle_reuse_policy: provenance_bundle_preferred
minimum_evidence_policy: route_specific_threshold
coverage_gate_behavior: partial_or_blocked_if_evidence_insufficient
empty_match_behavior: truthful_empty_match
required_scenario_families:
- canonical
- colloquial
- ui_selected_object
- short_action_followup
- pronoun_followup
```
## Contract Rules
### Rule 1. Capability Must Declare Entry Legality
No capability may be entered only because a heuristic guessed it.
It must declare:
- which transition classes are legal;
- which frames are required;
- which frames are incompatible.
### Rule 2. Capability Must Declare Admissible Anchors
No capability may silently accept low-quality business anchors.
It must declare:
- what entity values are required;
- what counts as an admissible extracted value;
- when clarification is mandatory.
### Rule 3. Capability Must Declare Truth Gate Behavior
No capability may leave truth semantics to answer wording.
It must declare:
- what evidence threshold it needs;
- how it behaves under partial evidence;
- when it becomes blocked.
### Rule 4. Capability Must Declare Selected-Object Compatibility
All selected-object actions must explicitly declare:
- whether they require focus object;
- whether they reuse a bundle from prior steps;
- whether they tolerate short follow-up wording.
### Rule 5. Capability Must Declare Scenario Coverage
A capability is not accepted just because canonical wording works.
At minimum it must declare which of the following wording families are required:
- `canonical`
- `colloquial`
- `ui_selected_object`
- `ui_selected_object_colloquial`
- `short_action_followup`
- `pronoun_followup`
- `followup_date_carryover`
## What This Specification Replaces
This specification is designed to reduce architectural pressure on:
- `assistantService`
- hidden carryover logic
- implicit capability semantics in answer shaping
## Done Criteria
This specification is considered operational only when:
- critical capabilities are represented as explicit contract objects;
- contract fields are sufficient to predict allowed entry, required anchors, truth behavior, and scenario acceptance;
- new capability enablement can be reviewed primarily through contract review and tests, not only by reading large service files.

View File

@ -0,0 +1,196 @@
# 04 - Coverage Evidence Truth Gate
## Purpose
This document defines the missing architectural layer between:
- `exact execution lane`
and
- `answer policy / packaging`
This layer must exist as a first-class object because the system already needs to answer questions such as:
- do we have enough evidence;
- is the result full, partial, or blocked;
- may this result power follow-up carryover;
- is limited mode required;
- can the assistant speak as confirmed truth or only as bounded evidence.
## Architectural Position
The target runtime stack should be read as:
1. route and transition policy
2. exact execution lane
3. `coverage / evidence / truth gate`
4. answer policy
5. packaging and debug
Answer policy must not own the truth gate.
## Current Code Owners
The gate already exists in code in fragmented form:
- [assistantCoverageGrounding.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantCoverageGrounding.ts:1)
- [assistantClaimBoundEvidence.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantClaimBoundEvidence.ts:1)
- [assistantDataLayer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantDataLayer.ts:622)
- [addressQueryService.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/addressQueryService.ts:2806)
- [answerComposer.ts](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/answerComposer.ts:22)
The architectural problem is not absence, but insufficient naming and isolation.
## Gate Inputs
The gate should consume:
- exact runtime result
- normalized retrieval result
- evidence items
- candidate evidence items
- coverage report
- route expectation outcome
- current frame state
- requested truth mode
## Gate Outputs
The gate should produce:
- `coverage_status`
- `grounding_status`
- `truth_mode`
- `carryover_eligibility`
- `reason_codes`
- `evidence_grade`
- `blocked_or_limited_explanation`
These outputs must be machine-readable.
## Status Classes
### 1. `full_confirmed`
Meaning:
- coverage is sufficient;
- evidence is admissible;
- route expectation passed;
- answer may speak in confirmed mode.
### 2. `partial_supported`
Meaning:
- some evidence exists;
- some requirements remain uncovered or weakly covered;
- answer must remain bounded and explicit about limitations.
### 3. `blocked_missing_anchor`
Meaning:
- exact execution cannot safely complete because required anchors are unresolved.
### 4. `blocked_route_expectation_failure`
Meaning:
- route was attempted, but the route contract itself did not pass baseline expectations.
### 5. `blocked_execution_error`
Meaning:
- system failure or execution failure prevents reliable business answer.
### 6. `limited_temporal_or_contextual`
Meaning:
- the system can say something useful, but only within a narrow and explicit evidence window.
## Carryover Eligibility Contract
The gate must also determine follow-up eligibility.
Allowed values:
- `full`
- `root_only`
- `object_only`
- `meta_only`
- `none`
Examples:
- `full_confirmed` result from root stock snapshot may often allow `full` or `root_only`
- selected-object provenance with weak buyer evidence may allow `object_only` or `meta_only`
- blocked route expectation should usually allow `none`
## Truth Rules
### Rule 1. Answer Policy May Downgrade, But Not Upgrade
Answer policy may choose a clearer wording.
Answer policy may not:
- turn blocked into confirmed;
- turn partial into full;
- suppress critical truth reason codes.
### Rule 2. Limited Mode Must Remain Truthful
If evidence is insufficient, limited mode must be explicit about:
- what is confirmed;
- what is not confirmed;
- why the system is limited.
### Rule 3. Carryover Must Respect The Gate
Follow-up policy may not assume that every previous answer is equally reusable.
Carryover must obey:
- gate-produced eligibility;
- evidence window;
- blocked or limited reason codes.
### Rule 4. Route Expectation Failure Is A Truth Event
If route expectation fails, this is not just a technical footnote.
It is a first-class truth gate outcome.
## Required Gate Artifacts
Every gated answer should be traceable through these artifacts:
- `evidence_bundle`
- `coverage_contract`
- `grounding_check`
- `truth_mode`
- `carryover_eligibility`
- `reason_codes`
## Non-Goals
This gate is not responsible for:
- model wording style;
- cosmetic answer formatting;
- provider selection;
- UI packaging details.
## Done Criteria
This layer is considered architecturally established only when:
- it is documented as separate from answer policy;
- its outputs are explicit inputs to answer shaping;
- carryover policy depends on gate output rather than hidden heuristics;
- scenario acceptance can fail specifically on gate behavior, not only on final wording.

View File

@ -0,0 +1,213 @@
# 05 - AssistantService Extraction Map
## Purpose
This document maps the current architectural overload of `assistantService.ts` and identifies what should stop living there over time.
The goal is not to empty the file arbitrarily.
The goal is to turn it from a god-service into a thinner coordinator.
## Current Situation
`assistantService.ts` is currently the largest and most overloaded module in the assistant runtime.
Approximate size:
- `6243` lines
It currently mixes concerns from:
- living route selection;
- follow-up carryover;
- continuation contracts;
- mode boundaries;
- meta-followup logic;
- memory recap detection;
- provider-aware predecompose orchestration;
- chat/data-scope/address boundary policy.
## Extraction Principle
`assistantService` should remain the top-level coordinator.
It should stop being the main owner of:
- policy logic;
- transition semantics;
- boundary semantics;
- meta semantics.
## Extraction Targets
### 1. Living Route Policy
Current owner:
- `resolveAssistantOrchestrationDecision()`
Current location:
- [assistantService.ts:4248](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4248)
Target owner:
- `assistantRoutePolicyRuntimeAdapter`
Expected artifact:
- explicit living-route decision contract
Done when:
- route policy can be reviewed without reading the full coordinator;
- top-level mode decisions are data-driven or contract-driven enough to be testable as a separate unit.
### 2. Carryover And Transition Policy
Current owner:
- `resolveAddressFollowupCarryoverContext()`
- `buildAddressDialogContinuationContractV2()`
Current location:
- [assistantService.ts:2828](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:2828)
- [assistantService.ts:3111](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3111)
Target owner:
- `assistantTransitionRuntimeAdapter`
- `assistantContinuationContractBuilder`
Expected artifact:
- explicit transition classes with state inputs and carryover depth
Done when:
- follow-up continuation is understood in terms of transitions, not just heuristic carryover.
### 3. Meta Follow-Up Policy
Current owner:
- meta and evaluative follow-up detection inside route decision logic
Target owner:
- `assistantMetaFollowupPolicy`
Expected artifact:
- meta question class registry
- legal source answer object types
Done when:
- meta questions no longer depend on implicit branches buried inside living route logic.
### 4. Data-Scope And Boundary Policy
Current owner:
- `assistant_data_scope_query_detected` branches
Current references:
- [assistantService.ts:3974](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:3974)
- [assistantService.ts:4412](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:4412)
- [assistantService.ts:6052](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantService.ts:6052)
Target owner:
- `assistantBoundaryPolicy`
Expected artifact:
- explicit boundary mode contract:
- `data_scope`
- `operational_boundary`
- `capability_contract`
- `non_domain_chat`
Done when:
- emotional or colloquial user messages cannot accidentally trigger data-scope selection through hidden shared logic.
### 5. Memory Recap Policy
Current owner:
- route decision + living chat cooperation
Related consumer:
- [assistantLivingChatRuntimeAdapter.ts:255](/x:/1C/NDC_1C/llm_normalizer/backend/src/services/assistantLivingChatRuntimeAdapter.ts:255)
Target owner:
- `assistantMemoryRecapPolicy`
Expected artifact:
- truthful recap contract over grounded prior answer objects
Done when:
- memory recap logic becomes a named subsystem with its own truth rules.
### 6. Provider-Aware Orchestration Glue
Current owner:
- coordinator-level logic that adapts to `openai` vs `local`
Target owner:
- `assistantProviderExecutionPolicy`
Expected artifact:
- provider/runtime compatibility layer for orchestration expectations
Done when:
- provider quirks no longer leak into business routing logic.
## What Should Stay In AssistantService
After extraction, `assistantService` should still own:
- high-level request coordination;
- dependency wiring;
- stage invocation order;
- final turn assembly and persistence orchestration.
It should not be removed as an architectural object.
It should become thinner.
## Proposed Extraction Order
1. extract route decision contract
2. extract transition/carryover policy
3. extract boundary/data-scope policy
4. extract meta and memory recap policy
5. isolate provider-aware orchestration glue
This order is chosen because route and transition pressure are currently the main source of runtime fragility.
## Non-Goals
- do not split the file mechanically just to reduce line count;
- do not create many tiny helpers with no architectural ownership;
- do not move exact execution logic out of its proper lane.
## Done Criteria
This extraction plan is complete only when:
- the remaining `assistantService` can be described as a coordinator;
- major policy categories have explicit owners outside `assistantService`;
- scenario regressions can point to policy subsystems instead of a single god-service.

View File

@ -0,0 +1,238 @@
# 06 - Phase Acceptance Matrix
## Purpose
This document turns the high-level turnaround direction into a phase-by-phase execution matrix.
Each phase must specify:
- `goal`
- `output artifacts`
- `done criteria`
- `non-goals`
- `acceptance signals`
## Phase 0. Shared Baseline
Goal:
- align the team on one architecture vocabulary and one baseline map.
Output artifacts:
- baseline note in `docs/ARCH/11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md`
- this package under `docs/ARCH/11 - architecture_turnaround/`
Done when:
- project discussions use the same names for layers, state, transitions, truth gate, and capabilities;
- no major planning discussion treats the system as "just a chat with LLM".
Non-goals:
- code changes;
- capability rewrites.
Acceptance signals:
- planning references point to the package rather than ad hoc prose;
- new refactor proposals can be mapped to a package artifact.
## Phase 1. Formal Layer Separation
Goal:
- explicitly separate project subsystems and their sources of truth.
Output artifacts:
- updated architecture notes if needed;
- clear internal naming for:
- data foundation
- assistant runtime
- domain loop
- experimental router contour
Done when:
- production assistant routing is no longer confused with Python router experiments;
- `canonical_layer` and `llm_normalizer/backend` are treated as different subsystems in planning.
Non-goals:
- merging runtimes;
- changing domain behavior.
Acceptance signals:
- no architecture review uses the wrong contour as the current runtime source of truth.
## Phase 2. State And Transition Contracts
Goal:
- make state and transition classes explicit.
Output artifacts:
- state schema note
- transition class registry note
- transition-oriented tests or planning matrix
Done when:
- every critical follow-up path is described as a named transition class;
- root frame, selected object frame, meta frame, clarification state, and coverage gate state are explicit objects.
Non-goals:
- answer wording cleanup;
- adding new business capabilities.
Acceptance signals:
- critical scenario failures can be described as transition failures, not only as "assistant got confused";
- at least one major follow-up-heavy domain can be read through transition contracts alone.
## Phase 3. Capability Contracts
Goal:
- make capabilities explicit contract objects instead of half-implicit route behavior.
Output artifacts:
- capability contract schema
- pilot contract set for critical inventory capabilities
Done when:
- critical capabilities declare entry modes, anchors, scope policy, truth behavior, and scenario families;
- new capability review can happen via contract inspection.
Non-goals:
- low-code workflow migration;
- removing recipe catalog.
Acceptance signals:
- at least one selected-object capability and one root capability are represented as full contract specs.
## Phase 4. Coverage / Evidence / Truth Gate Isolation
Goal:
- separate truth determination from answer policy.
Output artifacts:
- gate contract
- reason-code taxonomy
- carryover-eligibility contract
Done when:
- answer layer no longer decides whether a result is full, partial, or blocked;
- truth mode and carryover eligibility are explicit gate outputs.
Non-goals:
- UI redesign;
- full answer package rewrite.
Acceptance signals:
- scenario regressions can fail specifically on gate semantics;
- limited mode honesty is measurable independently from wording quality.
## Phase 5. AssistantService Extraction
Goal:
- reduce `assistantService` from god-service to coordinator.
Output artifacts:
- extraction map
- named policy owners
- reduced coordinator responsibility map
Done when:
- route policy, transition policy, boundary policy, and meta policy have explicit owners outside the coordinator;
- `assistantService` retains orchestration ownership but not most raw policy logic.
Non-goals:
- file splitting for its own sake;
- replacing the coordinator with a monolithic new router.
Acceptance signals:
- scenario regressions can point to extracted policy owners;
- code review no longer requires reading most of `assistantService.ts` to understand one policy area.
## Phase 6. Provider / Runtime Axis Hardening
Goal:
- make provider/runtime behavior an explicit architectural concern.
Output artifacts:
- provider execution contract
- structured-output compatibility matrix
- local/openai execution semantics note
Done when:
- provider quirks no longer bleed into business routing policy;
- structured output, tool calling expectations, and fallback behavior are documented per provider mode.
Non-goals:
- adding many providers immediately;
- model benchmarking as primary objective.
Acceptance signals:
- changing provider mode does not silently change core business semantics without explicit compatibility review.
## Phase 7. Scenario Acceptance As Primary Gate
Goal:
- enforce scenario-tree acceptance as the refactoring completion standard.
Output artifacts:
- phase-specific acceptance matrix
- updated scenario packs and required wording families
Done when:
- no phase is considered complete based only on unit tests or prettier answers;
- critical paths, critical edges, and selected-object continuity remain mandatory acceptance criteria.
Non-goals:
- shrinking evaluation to smoke tests;
- accepting root-only success as domain completion.
Acceptance signals:
- `pack_state.final_status`
- scenario acceptance matrix
- no unresolved `P0`
- direct answer, temporal honesty, selected-object continuity, and truth gate invariants all pass.
## Cross-Phase Rule
A phase is not done when the code "looks cleaner".
A phase is done only when:
- the declared artifact exists;
- the responsible layer is explicit;
- acceptance signals are green.

View File

@ -0,0 +1,135 @@
# 07 - External Reference Appendix
## Purpose
This appendix keeps external references available without overloading the core planning documents.
It records:
- which pattern is borrowed;
- why it is relevant;
- what should not be copied literally.
## Dify
Sources:
- <https://github.com/langgenius/dify>
- <https://docs.dify.ai/versions/3-0-x/en/user-guide/workflow/key-concepts>
- <https://docs.dify.ai/en/use-dify/nodes/agent>
- <https://docs.dify.ai/en/use-dify/nodes/variable-assigner>
Borrow:
- explicit distinction between workflow and chatflow;
- named nodes for classifier, agent, answer, variable management;
- persistent conversation variables.
Do not borrow literally:
- low-code canvas as a direct replacement for exact 1C runtime logic.
## Open WebUI
Sources:
- <https://github.com/open-webui/open-webui>
- <https://docs.openwebui.com/features/extensibility/>
- <https://docs.openwebui.com/features/chat-conversations/rag/>
Borrow:
- layered extensibility model;
- clear distinction between in-process tools, external APIs, and separate heavy pipelines;
- input/output filter mindset.
Do not borrow literally:
- generic plugin-first shell as the main architecture for exact-data routing.
## Onyx
Sources:
- <https://github.com/onyx-dot-app/onyx>
- <https://docs.onyx.app/admins/actions/overview>
Borrow:
- platform view of chat, agents, actions, connectors;
- separation between indexed knowledge, actions, and chat experience.
Do not borrow literally:
- enterprise search architecture as a substitute for 1C exact route discipline.
## LibreChat
Sources:
- <https://github.com/danny-avila/LibreChat>
- <https://www.librechat.ai/docs/features/agents>
Borrow:
- explicit agent capability surface;
- tool exposure discipline;
- deferred tools concept.
Do not borrow literally:
- treat MCP/tool abundance as a substitute for domain-specific capability contracts.
## Vanna
Source:
- <https://github.com/vanna-ai/vanna>
Borrow:
- exact-data assistant framing;
- tool registry plus structured outputs;
- user-aware execution semantics.
Do not borrow literally:
- SQL assistant assumptions as the whole model for our multi-layer accounting runtime.
## DB-GPT
Source:
- <https://github.com/eosphoros-ai/DB-GPT>
Borrow:
- AI data assistant as platform-plus-domain-skill composition;
- workflows and skills as explicit architectural objects.
Do not borrow literally:
- broad platform scope at the expense of current bounded hardening goals.
## LangGraph
Source:
- <https://docs.langchain.com/oss/javascript/langgraph/durable-execution>
Borrow:
- durable execution mindset;
- checkpointing and deterministic replay;
- explicit state graph thinking.
Do not borrow literally:
- framework migration as an end in itself.
## Summary
The external references collectively support one conclusion:
- the project should move toward more explicit state, transition, and capability contracts;
- it should not dissolve its exact-data rails into a generic agent shell.

View File

@ -0,0 +1,70 @@
# 11 - Architecture Turnaround Package
## Purpose
This folder is the execution-oriented continuation of the baseline note:
- [11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md](</x:/1C/NDC_1C/docs/ARCH/11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md:1>)
That baseline note answers:
- what the project is today;
- where the main architectural fragility sits;
- what direction is safe.
This package answers the next question:
- how the team should design the architectural turnaround without breaking the current exact-data baseline.
## Package Contents
1. [01 - project_architecture_baseline_map.md](./01%20-%20project_architecture_baseline_map.md)
2. [02 - state_and_transition_contracts.md](./02%20-%20state_and_transition_contracts.md)
3. [03 - capability_contract_spec.md](./03%20-%20capability_contract_spec.md)
4. [04 - coverage_evidence_truth_gate.md](./04%20-%20coverage_evidence_truth_gate.md)
5. [05 - assistantService_extraction_map.md](./05%20-%20assistantService_extraction_map.md)
6. [06 - phase_acceptance_matrix.md](./06%20-%20phase_acceptance_matrix.md)
7. [07 - external_reference_appendix.md](./07%20-%20external_reference_appendix.md)
## Architectural Objects Of Planning
This package makes five objects explicit:
1. `state model`
2. `transition model`
3. `capability contract model`
4. `coverage / evidence / truth gate`
5. `assistantService extraction plan`
These are the objects that should now drive refactoring discussions.
## How To Use The Package
Read in this order:
1. baseline note in `docs/ARCH/11 - unified_project_architecture_and_reference_update_plan_2026-04-15.md`
2. `01 - project_architecture_baseline_map.md`
3. `02 - state_and_transition_contracts.md`
4. `03 - capability_contract_spec.md`
5. `04 - coverage_evidence_truth_gate.md`
6. `05 - assistantService_extraction_map.md`
7. `06 - phase_acceptance_matrix.md`
8. `07 - external_reference_appendix.md`
## Planning Rules
- Do not treat this package as a rewrite plan.
- Do not dissolve `AddressQueryService` into generic chat logic.
- Do not move state back into transcript-only memory.
- Do not let answer wording substitute for policy/runtime fixes.
- Use scenario-based acceptance as the primary gate for all phases.
## Expected Outcome
When this package is fully operational, the project should stop being described as:
- "a big custom assistant service with many heuristics"
and start being described as:
- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating."

View File

@ -0,0 +1,992 @@
# 11 - Unified Project Architecture And Reference Update Plan 2026-04-15
## 1. Назначение документа
Этот документ фиксирует единый архитектурный срез проекта `NDC_1C` на `2026-04-15` и связывает:
- текущее фактическое устройство репозитория;
- текущее устройство assistant runtime;
- уже существующие архитектурные ограничения и сильные стороны;
- внешний reference landscape из open-source LLM-проектов;
- безопасный план обновления подхода без разрушения рабочего baseline.
Связанный execution-oriented пакет planning artifacts:
- [11 - architecture_turnaround](</x:/1C/NDC_1C/docs/ARCH/11 - architecture_turnaround/README.md:1>)
Документ нужен не для исторического обзора, а как единая опорная карта:
- что в проекте уже является устойчивым фундаментом;
- что является техническим и архитектурным долгом;
- где проект типовой по классу систем;
- где проект уже имеет осмысленную кастомную специализацию;
- как переходить от heuristic-heavy hardening к более устойчивой архитектурной форме.
Этот документ дополняет, а не заменяет:
- `docs/ARCH/10 - current_assistant_architecture_2026-04-15.md`
- `docs/ARCH/10A - current_assistant_hardening_plan_2026-04-15.md`
- `graphify-out/GRAPH_REPORT.md`
## 2. Executive Summary
На текущем этапе `NDC_1C` уже не является "просто чат-приложением с LLM".
Фактически проект состоит из нескольких самостоятельных архитектурных контуров:
1. `1C/OData acquisition + canonical layer`
2. `feature/risk engines over canonical store`
3. `LLM normalizer + assistant runtime`
4. `domain orchestration / scenario loop / eval artifacts`
5. `research and benchmark contour` вокруг Python `router/`
Главный вывод по результатам локального анализа и сравнения с open-source references:
- по классу продукта проект действительно типовой: это `LLM + tools/data + state + UI/API`;
- по реализации текущий runtime у нас уже существенно более специализирован, чем обычный generic chat shell;
- основная проблема проекта не в отсутствии "правильной идеи", а в том, что слишком много системной политики собрано вручную внутри нескольких очень больших модулей;
- мы уже имеем сильные архитектурные элементы, которые нельзя терять;
- правильный следующий шаг это не rewrite в сторону "универсального суперагента", а переход к более явной workflow/state-oriented форме поверх уже существующих exact-capability rails.
Короткая формула:
`проект типовой по классу`
но
`архитектурно перегружен ручной orchestration-логикой`
и потому
`нуждается не в изобретении новых возможностей, а в упорядочивании существующих слоев`.
## 3. Источники для этого среза
### 3.1 Локальные источники
- `graphify-out/GRAPH_REPORT.md`
- `README.md`
- `canonical_layer/app.py`
- `llm_normalizer/backend/src/server.ts`
- `llm_normalizer/backend/src/services/*`
- `docs/orchestration/active_domain_contract.json`
- `.codex/` и `artifacts/domain_runs/*`
### 3.2 Внешние reference sources
- Dify: <https://github.com/langgenius/dify>
- Dify docs, key concepts: <https://docs.dify.ai/versions/3-0-x/en/user-guide/workflow/key-concepts>
- Dify docs, agent node: <https://docs.dify.ai/en/use-dify/nodes/agent>
- Dify docs, variable assigner: <https://docs.dify.ai/en/use-dify/nodes/variable-assigner>
- Open WebUI: <https://github.com/open-webui/open-webui>
- Open WebUI docs, extensibility: <https://docs.openwebui.com/features/extensibility/>
- Open WebUI docs, RAG: <https://docs.openwebui.com/features/chat-conversations/rag/>
- Onyx: <https://github.com/onyx-dot-app/onyx>
- Onyx docs, actions overview: <https://docs.onyx.app/admins/actions/overview>
- LibreChat: <https://github.com/danny-avila/LibreChat>
- LibreChat docs, agents: <https://www.librechat.ai/docs/features/agents>
- Vanna: <https://github.com/vanna-ai/vanna>
- DB-GPT: <https://github.com/eosphoros-ai/DB-GPT>
- LangGraph docs, durable execution: <https://docs.langchain.com/oss/javascript/langgraph/durable-execution>
## 4. Текущая карта проекта целиком
### 4.1 Верхнеуровневые контуры репозитория
Текущий репозиторий содержит не один сервис, а несколько связанных слоев:
- `odata_probe/`
- слой исследования и проверки read-only доступа к 1C OData;
- `canonical_layer/`
- Python/FastAPI слой над канонической моделью и хранилищем;
- `llm_normalizer/backend/`
- TypeScript/Express runtime для LLM normalizer, assistant, eval и autoruns;
- `router/`
- Python policy/benchmark контур для route selection и sufficiency checks;
- `orchestration/`
- служебный orchestration runtime для batch/use-case loops;
- `docs/orchestration/active_domain_contract.json`
- single mutable source для текущего активного domain/scenario pack;
- `.codex/`
- project-scoped loop/orchestration навыки и роли;
- `artifacts/domain_runs/`
- машинно-читаемые артефакты сценарных прогонов и hardening loop;
- `graphify-out/`
- knowledge graph и архитектурный срез кодовой базы.
### 4.2 Ключевой смысл этой композиции
Проект уже организован как многоступенчатая AI/data система:
1. сначала добываются и нормализуются read-only данные 1C;
2. затем они кладутся в канонический слой и feature/risk engines;
3. затем поверх них строится exact-data assistant runtime;
4. затем этот runtime проверяется domain scenarios, evaluation и artifacts;
5. отдельно существует research/policy contour, который не является главным runtime.
Это важно: обсуждая архитектуру, нельзя сводить проект только к `llm_normalizer/backend`.
## 5. Слой 1: 1C acquisition и canonical layer
### 5.1 Что это такое
Базовый контур проекта по-прежнему rooted в read-only интеграции с 1C.
Это видно уже из top-level README:
- `odata_probe/` собирает и проверяет доступность entity sets и link semantics;
- `canonical_layer/` строит каноническую модель поверх 1C данных;
- проект принципиально read-only по policy.
### 5.2 Текущая реализация
`canonical_layer/app.py` поднимает FastAPI-приложение:
- `CanonicalService`
- `RefreshService`
- `FeatureService`
- `RiskService`
Доступные API-поверхности включают:
- `GET /documents`
- `GET /postings`
- `GET /graph/document/{document_id}`
- `POST /refresh/run`
- `POST /features/run`
- `POST /risk/run`
То есть canonical layer уже не ограничивается "просто прокси к 1C", а включает:
- data access API;
- refresh pipeline;
- feature engine;
- risk engine.
### 5.3 Архитектурная роль
Этот слой это не accessory для ассистента, а нижний data foundation проекта.
Именно он отвечает за:
- устойчивый read-only доступ;
- канонизацию сущностей;
- промежуточное локальное store-представление;
- data services, которые могут использоваться не только LLM-слоем.
По классу архитектуры это типичный `data platform substrate` для AI assistant products.
## 6. Слой 2: LLM normalizer backend
### 6.1 Входная точка
`llm_normalizer/backend/src/server.ts` поднимает Express-приложение и регистрирует:
- `/api/health`
- `testConnection`
- `sharedLlmConfig`
- `normalize`
- `eval`
- `assistant`
- `autoRuns`
- `history`
- `presets`
- `accountingAgent`
Текущая сборка сервисов:
- `OpenAIResponsesClient`
- `NormalizerService`
- `EvalService`
- `AssistantSessionStore`
- `AssistantService`
- `InMemoryRuntimeAdapter`
### 6.2 Что это значит
Backend уже совмещает несколько ролей:
- LLM gateway;
- normalizer runtime;
- assistant runtime;
- eval runtime;
- batch/autorun runtime;
- session persistence for assistant mode.
Именно здесь архитектура становится наиболее плотной и наиболее хрупкой.
## 7. Слой 3: Текущая архитектура assistant runtime
### 7.1 Зафиксированная форма
Согласно `ARCH 10` и текущему коду, assistant runtime уже устроен как пятислойная система:
1. `living router`
2. `address orchestration runtime`
3. `address exact execution lane`
4. `session memory + navigation state`
5. `answer/debug contract layer`
### 7.2 Living router
Главная точка выбора режима сейчас это:
- `resolveAssistantOrchestrationDecision()` в `assistantService.ts`
Текущие верхние living modes:
- `address_data`
- `assistant_data_scope`
- `chat`
На этом слое решается:
- идти ли в exact data contour;
- уйти ли в data-scope/meta contract;
- оставить ли запрос в обычном chat mode;
- перехватить ли follow-up/meta/memory case.
### 7.3 Address orchestration runtime
Вынесен в:
- `assistantAddressOrchestrationRuntimeAdapter.ts`
Он уже отвечает за:
- LLM predecompose и fallback predecompose;
- нормализацию effective message;
- resolution carryover context;
- защиту от плохой canonical rewrite;
- сборку `dialogContinuationContract`;
- формирование `addressRuntimeMeta`.
Это уже похоже не на "просто helper", а на отдельный orchestration stage.
### 7.4 Exact execution lane
Текущий exact lane строится вокруг:
- `AddressQueryService`
- `addressRecipeCatalog`
- `addressCapabilityPolicy`
- `addressIntentResolver`
- `addressFilterExtractor`
- `address_runtime/decomposeStage.ts`
- `address_runtime/composeStage.ts`
Именно этот слой исполняет конкретные capabilities, routes и recipes.
### 7.5 Session/navigation state
Критический текущий актив:
- `addressNavigationState.ts`
State уже хранит:
- `active_result_set_id`
- `active_focus_object`
- `last_confirmed_route`
- `date_scope`
- `organization_scope`
- `result_sets`
- `navigation_history`
То есть система уже не полагается только на transcript memory.
### 7.6 Answer/debug contract layer
Ответ формируется не напрямую из LLM output, а через отдельный packaging/debug contour:
- `answerComposer.ts`
- `assistantAnswerPackageBuilder.ts`
- `assistantDebugPayloadAssembler.ts`
- `assistantStage4AnswerContractAudit.ts`
Это означает, что проект уже имеет машинно-читаемые answer/debug rails, а не просто "text in/text out".
## 8. Что graphify говорит о текущей кодовой базе
На `2026-04-15` по `graphify-out/GRAPH_REPORT.md`:
- корпус: `473 files`
- граф: `4865 nodes`
- `10622 edges`
- `132 communities`
Ключевые god nodes:
- `resolveAddressIntent()`
- `composeFactualReply()`
- `resolveAssistantOrchestrationDecision()`
Наиболее важные сообщества для assistant runtime:
- orchestration around `AssistantService`
- exact execution around `AddressQueryService`
- navigation state / focus object / result sets
- address orchestration runtime adapters
Это подтверждает центральный архитектурный факт:
`главная форма системы уже не prompt-centric, а runtime-centric`
но при этом
`runtime слишком сильно сосредоточен вокруг нескольких god modules`.
## 9. Что в проекте уже сделано сильно
### 9.1 Structured state вместо transcript-only memory
Во многих LLM-продуктах контекст держится только историей сообщений.
У нас уже есть явные сущности:
- `result_set`
- `focus_object`
- `date_scope`
- `organization_scope`
- `last_confirmed_route`
Это зрелее, чем у большинства generic agent shells.
### 9.2 Exact-data lane как отдельный контур
`AddressQueryService` у нас уже отделен от chat/mode выбора.
Это означает:
- есть шанс держать business truthfulness;
- есть шанс тестировать exact capabilities отдельно;
- можно hardening делать не только через wording, но и через route policy.
### 9.3 Limited mode и truthfulness rails
В кодовой базе уже присутствуют:
- route expectation contracts;
- limited mode;
- missing anchor handling;
- honesty around empty/incomplete results.
Это правильное направление для exact assistant над бухгалтерскими данными.
### 9.4 Scenario-oriented hardening loop
Через `artifacts/domain_runs/*` и `.codex/skills/domain-case-loop` проект уже имеет не только тесты, но и domain acceptance artifacts:
- `baseline_turn.json`
- `rerun_turn.json`
- `scenario_manifest.json`
- `scenario_state.json`
- acceptance matrices
Это очень сильная инженерная практика, которой нет у многих open-source chat products.
## 10. Где сейчас архитектурная хрупкость
### 10.1 God services
По размеру и плотности ответственности особенно выделяются:
- `assistantService.ts` - 6243 lines
- `address_runtime/composeStage.ts` - 4817 lines
- `answerComposer.ts` - 4681 lines
- `addressQueryService.ts` - 4415 lines
- `assistantDataLayer.ts` - 4272 lines
Это главный симптом того, что:
- routing;
- state evolution;
- policy;
- answer shaping;
- partial chat/meta behavior;
- exact capability glue
слишком часто живут в одних и тех же модулях.
### 10.2 Implicit workflow вместо explicit workflow
Система уже ведет себя как state machine, но выражена в основном через:
- `if/else`
- guard trees
- carryover heuristics
- rewrite protection
- mode decisions
а не через явный graph/workflow model.
Из-за этого:
- локальный фикс легко меняет глобальное поведение;
- сложно визуально понимать допустимые переходы;
- acceptance часто приходится собирать эмпирически по прогонам.
### 10.3 Policy смешана с orchestration и answer layer
В текущей форме слишком много policy decisions живет внутри orchestration ядра:
- domain pivot
- meta-followup
- memory recap
- data-scope boundary
- short follow-up behavior
- raw vs canonical message preference
Это делает систему чувствительной к incremental patching.
### 10.4 Provider/runtime abstraction узкая
`OpenAIResponsesClient` сейчас фактически является главным LLM gateway с режимом:
- `openai`
- `local`
То есть провайдерная архитектура пока прагматичная, но не полноформатная.
Для текущего этапа этого достаточно, но это означает:
- модельный слой пока не является сильной самостоятельной осью архитектуры;
- orchestration и provider logic не так чисто разведены, как в крупных product shells.
### 10.5 Python router не является главным runtime
`router/` полезен как bench/policy contour:
- `query_classifier.py`
- `route_selector.py`
- `store_sufficiency.py`
Но он не является source of truth для текущего assistant runtime.
Это значит, что:
- нельзя проектировать основную архитектуру, думая, что Python router и есть текущий production routing;
- любые будущие сближения этих контуров надо делать явно и документированно.
## 11. Что показывают внешние references
### 11.1 Dify
Полезный architectural pattern:
- явное разделение `Workflow` и `Chatflow`;
- node-based orchestration;
- встроенные classifier, agent, tool, variable, answer nodes;
- отдельные conversation variables, переживающие многоходовый чат.
Что полезно для нас:
- идея сделать часть нашей текущей implicit orchestration более явной;
- отделить workflow state от answer wording;
- использовать более явную модель conversation variables/state transitions.
Что не нужно делать буквально:
- переписывать текущий exact runtime в low-code canvas;
- пытаться заменить существующие domain rails на generic visual flow.
### 11.2 Open WebUI
Полезный architectural pattern:
- product shell + extensibility layers;
- разделение на in-process tools, external OpenAPI/MCP, separate pipelines;
- input/output filters;
- четкое понимание, где живут heavy operations.
Что полезно для нас:
- явнее развести `tool/action layer`, `message filters`, `heavy execution lanes`;
- отделить extensibility concerns от core orchestration;
- думать о boundary/filter layer как о самостоятельном слое.
### 11.3 Onyx
Полезный architectural pattern:
- AI platform как слой поверх connectors/search/chat/agents/actions;
- MCP/OpenAPI actions;
- enterprise-friendly separation между agents, actions и indexed knowledge.
Что полезно для нас:
- видеть assistant не как monolith, а как application layer;
- усиливать distinction между exact domain actions и general conversational shell.
### 11.4 LibreChat
Полезный architectural pattern:
- agent/tool shell;
- MCP integration;
- deferred tools, чтобы не перегружать контекст;
- тонкая настройка agent capabilities.
Что полезно для нас:
- capability exposure должен быть более декларативным;
- tool universe не должен быть размазан по orchestration heuristics;
- selection of available actions может быть более explicit.
### 11.5 Vanna
Полезный architectural pattern:
- exact-data assistant через agent + tool registry;
- user-aware execution;
- structured UI outputs;
- observability и lifecycle hooks.
Что полезно для нас:
- точные data routes надо трактовать как first-class actions;
- identity/scope/date constraints должны протекать через tool execution и answer shape явно;
- structured outputs должны быть устойчивым контрактом.
### 11.6 DB-GPT
Полезный architectural pattern:
- AI data assistant как отдельный класс продукта;
- skills, workflows, SQL/code execution, sandboxing;
- separation between platform and domain skills.
Что полезно для нас:
- наш проект не обязательно "уникален";
- data assistant products уже давно строятся как composable platform + domain logic;
- это аргумент в пользу архитектурной декомпозиции, а не против нее.
### 11.7 LangGraph
Полезный conceptual pattern:
- durable execution;
- persistence/checkpointing;
- deterministic replay;
- explicit state graph;
- idempotent tasks.
Что полезно для нас:
- follow-up heavy assistant с carryover почти неизбежно выигрывает от explicit state graph mindset;
- наш current navigation state уже является хорошей базой для более graph-like architecture;
- особенно важно для multi-turn domain scenarios и bounded recovery после ошибок.
## 12. Где мы типовые, а где уже нет
### 12.1 Типовые части проекта
Мы полностью типовые в следующем:
- есть LLM gateway;
- есть tool/data execution;
- есть retrieval/data grounding;
- есть session state;
- есть answer packaging;
- есть eval/autorun контур;
- есть multi-layer backend over business data.
В этом смысле проект действительно не изобретает новый класс систем.
### 12.2 Нетиповые, но сильные части
Мы уже сильнее generic OSS chat shells в следующем:
- exact capability discipline;
- structured navigation state;
- selected-object continuity;
- scenario-tree acceptance;
- limited-mode truthfulness;
- explicit answer/debug contracts;
- domain hardening through machine-readable artifacts.
Это нельзя терять в погоне за "более красивой" типовой архитектурой.
### 12.3 Нетиповые и хрупкие части
Мы хрупки там, где типовые проекты обычно используют более явный framework layer:
- implicit orchestration inside god services;
- policy encoded in large custom functions;
- state transitions expressed through heuristic branches instead of explicit graph/node semantics;
- boundary/meta handling слишком тесно связано с main assistant coordinator.
## 13. Что нельзя ломать при архитектурном обновлении
При любом дальнейшем update plan нельзя разрушать следующие baseline элементы:
- `AddressQueryService` как отдельный exact lane;
- `addressNavigationState` с `result_set/focus_object/date_scope/organization_scope`;
- `dialogContinuationContractV2`;
- selected-object continuity;
- route expectation audit;
- limited mode truthfulness;
- domain artifacts и scenario acceptance loop;
- `docs/orchestration/active_domain_contract.json` как mutable active domain source.
Принцип:
`generic best practices не должны размывать exact-data baseline`.
## 14. Unified diagnosis
На `2026-04-15` архитектурная проблема проекта формулируется так:
### 14.1 Не проблема
Проблема не в том, что:
- проект слишком специфичен;
- проект делает что-то принципиально уникальное;
- для проекта не существует внешних референсов;
- нужен новый "умный супер-роутер", который заменит все существующие слои.
### 14.2 Реальная проблема
Реальная проблема в том, что:
- правильные слои уже есть;
- правильные state entities уже есть;
- правильный exact runtime уже есть;
но
- orchestration и policy слишком централизованы;
- implicit workflow слишком велик;
- код слишком легко деградирует от incremental heuristics;
- system behavior недостаточно выражен как явные runtime contracts и state transitions.
Коротко:
`архитектура не отсутствует`
а
`архитектура уже есть, но собрана слишком вручную и слишком плотно`.
## 15. Целевой update direction
Правильное направление обновления:
`не rewrite`
и
`не новый универсальный агент`
а
`explicit workflow/state shell поверх существующих exact rails`.
Это значит:
- меньше hidden orchestration;
- больше explicit route/state contracts;
- тоньше coordinator modules;
- более явные boundaries между:
- route policy;
- state transition policy;
- capability registry/execution;
- answer policy;
- meta/chat boundary.
## 16. План обновления по найденным references
### Phase 0. Зафиксировать единый baseline
Цель:
- прекратить обсуждать проект как "просто LLM чат";
- закрепить единый словарь слоев и boundaries.
Что сделать:
- использовать этот документ как общий architecture map;
- сохранить `ARCH 10` и `ARCH 10A` как assistant-runtime specific docs;
- считать текущий baseline immutable without explicit note.
Ожидаемый результат:
- любые дальнейшие изменения обсуждаются относительно явной карты проекта, а не по ощущениям.
### Phase 1. Развести проектные слои формально
Borrowed pattern:
- product shell layering из Open WebUI / Onyx
Цель:
- явно отделить:
- data foundation;
- assistant runtime;
- orchestration/eval loop;
- experimental router contour.
Что обновить концептуально:
- `canonical_layer` и `llm_normalizer/backend` должны описываться как разные subsystems;
- Python `router/` должен быть явно помечен как benchmark/research contour;
- `.codex` и `artifacts/domain_runs` должны считаться отдельным quality/hardening contour.
Ожидаемый результат:
- меньше путаницы, где именно находится source of truth для runtime behavior.
### Phase 2. Вынести orchestration grammar в явные contracts
Borrowed pattern:
- Workflow/Chatflow + conversation variables из Dify
- state graph mindset из LangGraph
Цель:
- превратить главный orchestration policy из "большого дерева if-ов" в более явный contract system.
Что обновить концептуально:
- формализовать переходы между:
- `address_data`
- `assistant_data_scope`
- `chat`
- `meta-followup`
- `memory-recap`
- `organization-clarification`
- для каждого класса перехода иметь:
- входной trigger class;
- допустимый carryover depth;
- allowed state reuse;
- forbidden cross-domain leakage;
- expected answer mode.
Ожидаемый результат:
- routing перестает быть набором локальных heuristic patches и становится описуемой state policy.
### Phase 3. Разделить root policy и object policy
Borrowed pattern:
- explicit conversation variables из Dify
- durable state semantics из LangGraph
Цель:
- окончательно закрепить различие между:
- `root frame`
- `selected object frame`
- `meta frame`
Что обновить концептуально:
- root state живет дольше и используется шире;
- object state переносится только в совместимых сценариях;
- meta state работает поверх answer object/result object, а не replay-ит exact route вслепую.
Ожидаемый результат:
- меньше ложных carryover;
- меньше cross-domain contamination;
- меньше случайных уходов в generic chat.
### Phase 4. Сделать capability exposure более декларативным
Borrowed pattern:
- tool/action registry from LibreChat / Open WebUI / Vanna / Onyx
Цель:
- capabilities должны быть first-class registry, а не только следствием разбросанной policy.
Что обновить концептуально:
- описывать capability не только через intent/resolver;
- дополнительно иметь явные поля:
- supported wording families;
- allowed anchor types;
- selected-object compatibility;
- root-context compatibility;
- meta-followup compatibility;
- answer mode;
- clarification policy.
Ожидаемый результат:
- меньше логики в giant service functions;
- больше declarative routing discipline.
### Phase 5. Выделить answer policy как самостоятельный слой
Borrowed pattern:
- answer nodes / structured outputs из Dify и Vanna
- filters/boundary layers из Open WebUI
Цель:
- отделить:
- exact answer shape;
- limited answer shape;
- clarification shape;
- meta/explanatory shape;
- operational boundary shape.
Что обновить концептуально:
- не позволять routing fix-ам решаться только wording patch-ами;
- answer layer должен знать свой response class и формировать shape по контракту;
- `direct_answer_first` должен быть частью policy, а не удачной случайностью конкретного compose path.
Ожидаемый результат:
- ответы становятся устойчивее и предсказуемее даже при edge-cases.
### Phase 6. Зафиксировать runtime checkpoints и replay-safe state
Borrowed pattern:
- durable execution / replay / idempotent tasks из LangGraph
Цель:
- сблизить session/navigation state с моделью checkpointed execution.
Что обновить концептуально:
- каждый важный state transition должен иметь machine-readable checkpoint semantics;
- follow-up interpretation должна опираться на сохраненные transition objects, а не только на текст прошлых сообщений;
- сценарные прогоны должны проверять не только answer text, но и корректность state transitions.
Ожидаемый результат:
- меньше регрессий, где exact route есть, но follow-up interpretation ломает доступ к нему.
### Phase 7. Упорядочить provider and execution interfaces
Borrowed pattern:
- product shell abstraction из LibreChat / Open WebUI / Dify
Цель:
- постепенно превратить LLM gateway в более явный provider runtime layer.
Что обновить концептуально:
- развести:
- orchestration logic;
- provider execution;
- model-specific JSON / response normalization;
- local/openai compatibility concerns.
Ожидаемый результат:
- меньше provider-driven логики внутри business orchestration.
### Phase 8. Сценарная приемка как главный architectural gate
Borrowed pattern:
- LLMOps/observability from Dify
- enterprise auditability from Onyx/Vanna
Цель:
- закрепить, что acceptance идет по scenario-tree, а не по отдельным красивым ответам.
Что обновить концептуально:
- любой architectural update считается успешным только если:
- сохраняется root path;
- сохраняются critical edges;
- сохраняется selected-object continuity;
- не деградирует limited truthfulness;
- не ломаются neighboring domains.
Ожидаемый результат:
- меньше "качелей", где один кейс улучшается ценой трех соседних.
## 17. Что не нужно делать
В рамках update plan не нужно:
1. Переписывать runtime под Dify/Open WebUI/LibreChat как платформенную миграцию.
2. Вводить "универсальный суперклассификатор", который обходит текущие rails.
3. Возвращать state обратно в transcript-only memory.
4. Маскировать state/policy проблемы только answer wording правками.
5. Смешивать Python `router/` и текущий TS assistant runtime без явного redesign note.
6. Жертвовать exact-data discipline ради более "естественного" чата.
## 18. Целевая формула обновленного проекта
Если выражать желаемое состояние кратко, то проект должен двигаться к следующей форме:
### 18.1 Слой проекта
`1C/OData -> canonical store -> feature/risk data services -> assistant orchestration shell -> exact capabilities -> answer/debug contracts -> scenario acceptance loop`
### 18.2 Архитектурная форма assistant runtime
`thin router + explicit state transitions + declarative capability contracts + isolated answer policy + exact execution lane`
### 18.3 Главный принцип
`не делать систему "умнее любой ценой"`
а
`делать систему более явной, более контрактной и менее хрупкой`.
## 19. Итог
На `2026-04-15` проект `NDC_1C` уже нельзя честно описывать как "обычный чат с LLM и другой базой".
Правильнее описывать его так:
- это read-only AI/data system поверх 1C;
- у нее есть canonical/data foundation;
- есть feature/risk layer;
- есть specialized exact-data assistant runtime;
- есть scenario-based hardening loop;
- и есть существенный orchestration debt, возникший из-за ручной эволюции.
Поэтому правильный next move:
`не ломать то, что уже собрано`
и
`не пытаться заменить систему generic framework-ом`
а
`собрать более явную architecture grammar поверх уже существующих рабочих слоев`.
Это и есть safest update path по итогам локального анализа и внешних references.

View File

@ -1040,12 +1040,78 @@ function trimInventoryItemArrowSuffix(rawValue) {
return cleanupAnchorValue(cleanupAnchorValue(rawValue).replace(/\s*(?:->|=>|→).+$/u, ""));
}
function isTemporalWarehousePhrase(candidate) {
const temporalZaPattern = /^(?:за)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu;
if (temporalZaPattern.test(cleanupAnchorValue(candidate).toLowerCase().replace(/ё/g, "е").trim())) {
return true;
}
const normalized = cleanupAnchorValue(candidate)
.toLowerCase()
.replace(/ё/g, "е")
.trim();
return /^(?:в|на)\s+(?:январ(?:е|ь)|феврал(?:е|ь)|март(?:е)?|апрел(?:е|ь)|ма(?:й|е)|июн(?:е|ь)|июл(?:е|ь)|август(?:е)?|сентябр(?:е|ь)|октябр(?:е|ь)|ноябр(?:е|ь)|декабр(?:е|ь))(?:\s+\d{4}(?:\s+г(?:\.|ода)?)?)?$/iu.test(normalized);
}
function isLowQualityWarehouseAnchorValue(rawValue) {
const value = cleanupAnchorValue(rawValue)
.toLowerCase()
.replace(/ё/g, "е")
.trim();
if (!value) {
return true;
}
if (isTemporalWarehousePhrase(value) || isImplicitSelfScopeWarehouseAnchor(value)) {
return true;
}
const hasQuestionOrRepairCue = /(?:^|[\s,.;:!?()\-])(?:что|какой|какая|какие|как|где|когда|почему|зачем|имел(?:ось|ся)\s+в\s+виду|имеется\s+в\s+виду|в\s+смысле|то\s+есть|which|what|where|when|why)(?=$|[\s,.;:!?()\-])/iu.test(value) || /[?]/u.test(rawValue);
const hasProfanityCue = /(?:^|[\s,.;:!?()\-])(?:аху|оху|хуе|хуё|хуй|ебан|ебуч|бля|блять|пизд|нахуй|shit|fuck|damn)(?=$|[\s,.;:!?()\-])/iu.test(value);
const lowQualityTokens = new Set([
"что",
"какой",
"какая",
"какие",
"как",
"где",
"когда",
"почему",
"зачем",
"имелось",
"имелся",
"имеется",
"в",
"виду",
"то",
"есть",
"лежит",
"лежат",
"лежало",
"лежали",
"на",
"по",
"складе",
"складу",
"складом",
"ебаном",
"ахуеть",
"охуеть",
"пиздец",
"блять",
"бля"
]);
const tokens = value
.split(/[^a-zа-я0-9]+/iu)
.map((token) => token.trim())
.filter(Boolean);
if (tokens.length === 0) {
return true;
}
const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1);
if (meaningfulTokens.length === 0) {
return true;
}
if ((hasQuestionOrRepairCue || hasProfanityCue) && meaningfulTokens.length <= 1) {
return true;
}
return false;
}
function normalizeSemanticAnchorCandidate(value) {
return cleanupAnchorValue(value)
.toLowerCase()
@ -1079,6 +1145,7 @@ function extractInventoryWarehouseAnchor(text) {
candidate.includes("->") ||
candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
isLowQualityWarehouseAnchorValue(candidate) ||
normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) ||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {

View File

@ -1598,6 +1598,14 @@ function resolveAddressIntent(userMessage) {
reasons: ["inventory_selected_object_sale_trace_signal_detected"]
};
}
if (/(?:кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|кому\s+в\s+итоге\s+мы\s+впарили)/iu.test(text) &&
/(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text)) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) {
return {
intent: "inventory_sale_trace_for_item",

View File

@ -1588,7 +1588,13 @@ function hasExplicitPeriodWindow(filters) {
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0));
}
function canAutoBroadenPeriodWindow(intent, filters) {
if (!hasExplicitPeriodWindow(filters)) {
const hasRecoverableAsOfOnlyWindow = !hasExplicitPeriodWindow(filters) &&
typeof filters.as_of_date === "string" &&
filters.as_of_date.trim().length > 0 &&
typeof filters.item === "string" &&
filters.item.trim().length > 0 &&
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item");
if (!hasExplicitPeriodWindow(filters) && !hasRecoverableAsOfOnlyWindow) {
return false;
}
return (intent === "list_documents_by_counterparty" ||

View File

@ -32,6 +32,9 @@ function hasAllTimeHint(text) {
function hasSameDateHint(text) {
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date)/iu.test(String(text ?? ""));
}
function hasSamePeriodHint(text) {
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
}
function hasExplicitPeriodLiteral(text) {
return /(?:^|[^\d*×xх])((?:19|20)\d{2}(?:[./-](?:0?[1-9]|1[0-2]))?)(?=$|[^\d*×xх])/iu.test(String(text ?? ""));
}
@ -383,10 +386,15 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
const currentFrameKind = followupContext.current_frame_kind ?? null;
const previousIntent = followupContext.previous_intent;
const comingFromInventoryDrilldown = currentFrameKind === "inventory_drilldown" || isInventoryDrilldownFrameIntent(previousIntent);
if (!comingFromInventoryDrilldown) {
const normalized = String(userMessage ?? "");
const hasInventoryRootRestatementCue = /(?:склад|остат(?:ок|ки)|позици(?:я|и|ю)|товар(?:ы|ов)?|номенклатур)/iu.test(normalized) &&
/(?:покажи|показать|выведи|раскрой|еще\s+раз|ещ[её]\s+раз|снова|опять|верни|вернись|повтори|тот\s+же|этот\s+же|same|again)/iu.test(normalized);
const canReenterInventoryRoot = comingFromInventoryDrilldown ||
(currentFrameKind === "inventory_root" && hasSamePeriodHint(normalized)) ||
(currentFrameKind === "generic" && hasInventoryRootRestatementCue && hasSamePeriodHint(normalized));
if (!canReenterInventoryRoot) {
return false;
}
const normalized = String(userMessage ?? "");
if (hasSelectedObjectInventorySignal(normalized) ||
hasInventorySupplierFollowupCue(normalized) ||
hasInventoryPurchaseDocumentsFollowupCue(normalized) ||
@ -401,6 +409,7 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
}
const hasTemporalPatch = hasExplicitPeriodWindow(extractedFilters) ||
Boolean(toNonEmptyString(extractedFilters.as_of_date)) ||
hasSamePeriodHint(normalized) ||
hasExplicitPeriodLiteral(normalized) ||
Boolean(resolveRelativeMonthPeriodFromInventoryRoot(normalized, followupContext));
return hasTemporalPatch;
@ -408,6 +417,26 @@ function shouldRestoreInventoryRootFrame(userMessage, intent, extractedFilters,
function hasSelectedObjectInventorySignal(text) {
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
}
function hasSelectedObjectInlineSnapshotMetadata(text) {
return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(String(text ?? ""));
}
function extractSelectedObjectItemFromFollowupText(text) {
const rawSelectedObject = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(text));
if (!rawSelectedObject) {
return null;
}
const firstLine = rawSelectedObject
.replace(/\r\n?/g, "\n")
.split("\n")
.map((line) => line.trim())
.find(Boolean);
const primarySegment = String(firstLine ?? rawSelectedObject)
.replace(/^\d+\.\s*/, "")
.split("|")[0]
?.trim();
const normalized = toNonEmptyString(primarySegment);
return normalized;
}
function hasInventorySupplierFollowupCue(text) {
return (0, inventoryLifecycleCueHelpers_1.hasInventorySupplierCue)(String(text ?? ""));
}
@ -506,6 +535,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage);
const samePeriodRequested = hasSamePeriodHint(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
merged.organization = previousOrganization;
reasons.push("organization_from_followup_context");
@ -618,7 +648,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date")) {
const inheritedItem = previousItem ?? previousAnchorItem;
const explicitQuotedItem = toNonEmptyString((0, addressFilterExtractor_1.extractSelectedObjectQuotedValue)(userMessage));
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
const currentItem = toNonEmptyString(merged.item);
const shouldAdoptExplicitQuotedItem = Boolean(explicitQuotedItem) &&
(!currentItem ||
@ -653,6 +683,22 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
reasons.push("as_of_date_from_followup_context");
}
}
if (samePeriodRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date")) {
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
reasons.push("period_from_from_followup_context");
}
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
merged.period_to = previousPeriodTo;
reasons.push("period_to_from_followup_context");
}
const inheritedAsOfDate = previousAsOfDate ?? previousPeriodTo ?? previousPeriodFrom;
if (inheritedAsOfDate && merged.as_of_date !== inheritedAsOfDate) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
}
if (!sameDateRequested &&
(intent === "inventory_aging_by_purchase_date" || isInventoryLifecycleHistoryIntent(intent)) &&
!hasExplicitPeriodLiteral(userMessage) &&
@ -668,6 +714,21 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
}
}
}
if ((Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) &&
hasSelectedObjectInventorySignal(userMessage) &&
hasSelectedObjectInlineSnapshotMetadata(userMessage) &&
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item") &&
!hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage)) {
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
reasons.push("period_from_from_followup_context");
}
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
merged.period_to = previousPeriodTo;
reasons.push("period_to_from_followup_context");
}
}
if (!sameDateRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
!hasExplicitPeriodLiteral(userMessage) &&

View File

@ -38,6 +38,47 @@ function findLastGroundedInventoryAddressDebug(items) {
}
return null;
}
function findLastAddressDebugWithItem(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
const debug = item.debug;
if (String(debug.execution_lane ?? "") !== "address_query") {
continue;
}
const extractedFilters = debug.extracted_filters && typeof debug.extracted_filters === "object"
? debug.extracted_filters
: null;
const itemLabel = String(extractedFilters?.item ?? "").trim() ||
(String(debug.anchor_type ?? "") === "item"
? String(debug.anchor_value_resolved ?? debug.anchor_value_raw ?? "").trim()
: "");
if (itemLabel) {
return debug;
}
}
return null;
}
function findLastAddressDebug(items) {
if (!Array.isArray(items)) {
return null;
}
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index];
if (!item || item.role !== "assistant" || !item.debug || typeof item.debug !== "object") {
continue;
}
if (String(item.debug.execution_lane ?? "") === "address_query") {
return item.debug;
}
}
return null;
}
function buildInventoryHistoryCapabilityFollowupReply(input) {
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
@ -65,6 +106,42 @@ function buildInventoryHistoryCapabilityFollowupReply(input) {
"Если хочешь, сразу покажу нужный исторический период."
].join("\n");
}
function buildAddressMemoryRecapReply(input) {
const extractedFilters = input.addressDebug?.extracted_filters && typeof input.addressDebug.extracted_filters === "object"
? input.addressDebug.extracted_filters
: null;
const rootFrameContext = input.addressDebug?.address_root_frame_context && typeof input.addressDebug.address_root_frame_context === "object"
? input.addressDebug.address_root_frame_context
: null;
const item = input.toNonEmptyString(extractedFilters?.item) ??
(String(input.addressDebug?.anchor_type ?? "") === "item"
? input.toNonEmptyString(input.addressDebug?.anchor_value_resolved) ??
input.toNonEmptyString(input.addressDebug?.anchor_value_raw)
: null);
const organization = input.organization ??
input.toNonEmptyString(extractedFilters?.organization) ??
input.toNonEmptyString(rootFrameContext?.organization);
const scopedDate = formatIsoDateForReply(extractedFilters?.as_of_date) ??
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.period_to);
if (item) {
const datePart = scopedDate ? ` в срезе на ${scopedDate}` : "";
const organizationPart = organization ? ` по компании «${organization}»` : "";
return [
`Да, помню. Мы обсуждали позицию «${item}»${organizationPart}${datePart}.`,
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
].join(" ");
}
if (organization || scopedDate) {
const organizationPart = organization ? ` по компании «${organization}»` : "";
const datePart = scopedDate ? ` на ${scopedDate}` : "";
return [
`Да, помню. Мы уже смотрели адресный контур${organizationPart}${datePart}.`,
"Могу кратко напомнить контекст или сразу продолжить следующий шаг по этому же сценарию."
].join(" ");
}
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
async function runAssistantLivingChatRuntime(input) {
const userMessage = String(input.userMessage ?? "");
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
@ -83,9 +160,13 @@ async function runAssistantLivingChatRuntime(input) {
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
const contextualInventoryHistoryCapabilityFollowup = input.modeDecision?.reason === "inventory_history_capability_followup_detected";
const contextualMemoryRecapFollowup = input.modeDecision?.reason === "memory_recap_followup_detected";
const lastGroundedInventoryAddressDebug = contextualInventoryHistoryCapabilityFollowup
? findLastGroundedInventoryAddressDebug(input.sessionItems)
: null;
const lastMemoryAddressDebug = contextualMemoryRecapFollowup
? findLastAddressDebugWithItem(input.sessionItems) ?? findLastAddressDebug(input.sessionItems)
: null;
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
chatText = input.buildAssistantSafetyRefusalReply();
livingChatSource = "deterministic_safety_refusal";
@ -139,6 +220,16 @@ async function runAssistantLivingChatRuntime(input) {
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_inventory_history_capability_contract";
}
else if (contextualMemoryRecapFollowup) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = buildAddressMemoryRecapReply({
organization: scopedOrganization,
addressDebug: lastMemoryAddressDebug,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_memory_recap_contract";
}
else if (capabilityMetaQuery) {
chatText = input.buildAssistantCapabilityContractReply();
livingChatSource = "deterministic_capability_contract";

View File

@ -0,0 +1,286 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.INVENTORY_CAPABILITY_CONTRACTS = exports.ASSISTANT_TRANSITION_CONTRACTS = void 0;
exports.listAssistantTransitionContracts = listAssistantTransitionContracts;
exports.getAssistantTransitionContract = getAssistantTransitionContract;
exports.listInventoryCapabilityContracts = listInventoryCapabilityContracts;
exports.getAssistantCapabilityContract = getAssistantCapabilityContract;
exports.getAssistantCapabilityContractByIntent = getAssistantCapabilityContractByIntent;
const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts");
exports.ASSISTANT_TRANSITION_CONTRACTS = [
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T1",
title: "Root Query Entry",
trigger_class: "new_root_business_question",
required_prior_state: ["living_mode_state"],
allowed_carryover_depth: "none",
state_mutations: ["create_root_frame_state", "clear_selected_object_frame_state", "create_coverage_gate_state"],
forbidden_carryover: ["stale_focus_object", "stale_object_intent"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T2",
title: "Root Follow-Up With Date Or Scope Change",
trigger_class: "root_followup_temporal_or_organization_shift",
required_prior_state: ["root_frame_state"],
allowed_carryover_depth: "root_only",
state_mutations: ["update_root_frame_state", "run_exact_route", "refresh_coverage_gate_state"],
forbidden_carryover: ["incompatible_selected_object_route"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T3",
title: "Explicit Selected Object Drilldown",
trigger_class: "explicit_selected_object_or_ui_object_selection",
required_prior_state: ["root_frame_state"],
allowed_carryover_depth: "object_only",
state_mutations: ["create_selected_object_frame_state", "bind_source_result_set", "preserve_temporal_ceiling"],
forbidden_carryover: ["unrelated_prior_focus_object"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T4",
title: "Short Action Follow-Up On Selected Object",
trigger_class: "short_action_followup_on_active_focus_object",
required_prior_state: ["selected_object_frame_state"],
allowed_carryover_depth: "object_only",
state_mutations: ["reuse_selected_object_frame_state", "route_to_compatible_item_action"],
forbidden_carryover: ["generic_chat_fallback", "data_scope_selection_fallback", "object_focus_reset"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T5",
title: "Pronoun Or Compressed Object Follow-Up",
trigger_class: "pronoun_or_compressed_reference_to_active_focus_object",
required_prior_state: ["selected_object_frame_state"],
allowed_carryover_depth: "object_only",
state_mutations: ["reuse_selected_object_frame_state", "resolve_pronoun_to_focus_object"],
forbidden_carryover: ["low_quality_object_rewrite", "semantic_noise_as_anchor"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T6",
title: "Domain Pivot With Root-Only Carryover",
trigger_class: "supported_domain_pivot_from_active_drilldown",
required_prior_state: ["root_frame_state", "selected_object_frame_state"],
allowed_carryover_depth: "root_only",
state_mutations: ["preserve_root_frame_state", "drop_selected_object_frame_state"],
forbidden_carryover: ["object_route_replay_into_new_domain"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T7",
title: "Clarification Continuation",
trigger_class: "user_resolves_missing_anchor_or_scope",
required_prior_state: ["clarification_state"],
allowed_carryover_depth: "full",
state_mutations: ["resume_target_route", "update_or_clear_clarification_state"],
forbidden_carryover: ["forget_suspended_route"],
expected_answer_mode: "confirmed"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T8",
title: "Meta Follow-Up Over Answer Object",
trigger_class: "evaluation_comparison_or_interpretation_of_previous_answer",
required_prior_state: ["answer_context_state", "coverage_gate_state"],
allowed_carryover_depth: "meta_only",
state_mutations: ["create_meta_frame_state", "reuse_answer_object_without_blind_replay"],
forbidden_carryover: ["blind_exact_route_replay"],
expected_answer_mode: "meta"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T9",
title: "Memory Recap",
trigger_class: "conversation_memory_recap_request",
required_prior_state: ["answer_context_state"],
allowed_carryover_depth: "meta_only",
state_mutations: ["reuse_grounded_prior_answer_context"],
forbidden_carryover: ["invented_conversation_memory"],
expected_answer_mode: "recap"
},
{
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T10",
title: "Unsupported Or Blocked Boundary",
trigger_class: "unsupported_route_or_blocked_evidence_gate",
required_prior_state: ["coverage_gate_state"],
allowed_carryover_depth: "none",
state_mutations: ["emit_bounded_boundary_or_clarification"],
forbidden_carryover: ["blocked_as_confirmed_factual_answer"],
expected_answer_mode: "boundary"
}
];
const SHARED_INVENTORY_ACCEPTANCE_FAMILIES = [
"canonical",
"colloquial",
"ui_selected_object",
"ui_selected_object_colloquial",
"short_action_followup",
"pronoun_followup",
"followup_date_carryover"
];
const INVENTORY_ITEM_ANCHOR_RULES = [
"no_low_quality_item_rewrite",
"no_numeric_tail_account_poisoning",
"no_conversational_noise_as_entity",
"confirmed_focus_object_beats_semantic_hint"
];
const INVENTORY_SELECTED_OBJECT_TESTS = [
"selected_object_memory_survives_short_followup",
"new_explicit_selected_object_overrides_old_focus",
"full_anchor_not_degraded_by_canonical_rewrite"
];
function inventoryExactCapability(input) {
return {
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
capability_id: input.capability_id,
domain_id: "inventory_stock",
runtime_lane: "address_exact",
intent_ids: input.intent_ids,
entry_modes: input.entry_modes,
supported_transition_classes: input.transitions,
frame_compatibility: {
root_frame: input.entry_modes.includes("root_entry") ? "optional" : "required",
selected_object_frame: input.requiresFocusObject ? "required" : "optional",
meta_frame: "forbidden"
},
required_anchors: input.requiredAnchors,
optional_anchors: ["organization", "warehouse", "date_scope"],
anchor_source_priority: ["explicit_user_anchor", "ui_selected_object", "selected_object_frame", "root_frame", "semantic_hint"],
anchor_admissibility_rules: [...INVENTORY_ITEM_ANCHOR_RULES],
organization_scope_behavior: "reuse_or_clarify",
date_scope_behavior: "reuse",
temporal_ceiling_policy: input.requiresFocusObject ? "respect_root_temporal_ceiling" : "must_not_expand_without_reason_code",
root_context_compatibility: "required",
requires_focus_object: input.requiresFocusObject,
accepted_focus_object_kinds: input.requiresFocusObject ? ["inventory_item", "item"] : [],
focus_object_override_policy: input.requiresFocusObject ? "explicit_new_object_wins" : "not_applicable",
bundle_reuse_policy: input.bundleReusePolicy,
resolver_owner: "addressIntentResolver",
recipe_owner: "addressRecipeCatalog",
execution_adapter: "AddressQueryService",
result_shape: input.resultShape,
answer_object_shape: input.answerObjectShape,
minimum_evidence_policy: "route_specific_threshold",
coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"],
blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"],
clarification_triggers: ["missing_required_item_anchor", "ambiguous_organization_scope", "ambiguous_date_scope"],
clarification_questions: ["Уточните товар, организацию или дату, чтобы не подставлять неподтвержденный anchor."],
resume_policy: "resume_original_route_with_resolved_anchors",
empty_match_behavior: "truthful_empty_match",
route_expectation_failure_behavior: "blocked_route_expectation_failure",
execution_error_behavior: "blocked_execution_error",
required_unit_tests: input.requiresFocusObject
? [...INVENTORY_SELECTED_OBJECT_TESTS, "limited_mode_remains_truthful"]
: ["root_context_survives_domain_pivot_without_object_leak", "limited_mode_remains_truthful"],
required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`),
required_scenario_families: input.scenarioFamilies ?? [...SHARED_INVENTORY_ACCEPTANCE_FAMILIES]
};
}
exports.INVENTORY_CAPABILITY_CONTRACTS = [
inventoryExactCapability({
capability_id: "confirmed_inventory_on_hand_as_of_date",
intent_ids: ["inventory_on_hand_as_of_date"],
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
transitions: ["T1", "T2", "T7"],
requiresFocusObject: false,
requiredAnchors: [],
resultShape: "item_list_with_quantity_cost_warehouse_organization",
answerObjectShape: "inventory_stock_snapshot",
bundleReusePolicy: "none",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
}),
inventoryExactCapability({
capability_id: "inventory_inventory_purchase_provenance_for_item",
intent_ids: ["inventory_purchase_provenance_for_item"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "supplier_purchase_provenance_trace",
answerObjectShape: "inventory_provenance_bundle",
bundleReusePolicy: "provenance_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_purchase_documents_for_item",
intent_ids: ["inventory_purchase_documents_for_item"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "purchase_document_list_for_selected_item",
answerObjectShape: "inventory_purchase_documents_bundle",
bundleReusePolicy: "provenance_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date",
intent_ids: ["inventory_supplier_stock_overlap_as_of_date"],
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
transitions: ["T1", "T2", "T7"],
requiresFocusObject: false,
requiredAnchors: ["supplier"],
resultShape: "supplier_to_stock_item_overlap",
answerObjectShape: "inventory_supplier_overlap",
bundleReusePolicy: "none",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
}),
inventoryExactCapability({
capability_id: "inventory_inventory_sale_trace_for_item",
intent_ids: ["inventory_sale_trace_for_item"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "buyer_sale_trace_for_selected_item",
answerObjectShape: "inventory_sale_trace_bundle",
bundleReusePolicy: "sale_trace_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_purchase_to_sale_chain",
intent_ids: ["inventory_purchase_to_sale_chain"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "purchase_stock_sale_document_chain",
answerObjectShape: "inventory_purchase_to_sale_chain",
bundleReusePolicy: "sale_trace_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_aging_by_purchase_date",
intent_ids: ["inventory_aging_by_purchase_date"],
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
transitions: ["T1", "T2", "T6", "T7"],
requiresFocusObject: false,
requiredAnchors: [],
resultShape: "oldest_first_inventory_aging_list",
answerObjectShape: "inventory_aging_snapshot",
bundleReusePolicy: "none",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
})
];
function listAssistantTransitionContracts() {
return exports.ASSISTANT_TRANSITION_CONTRACTS;
}
function getAssistantTransitionContract(transitionId) {
return exports.ASSISTANT_TRANSITION_CONTRACTS.find((contract) => contract.transition_id === transitionId) ?? null;
}
function listInventoryCapabilityContracts() {
return exports.INVENTORY_CAPABILITY_CONTRACTS;
}
function getAssistantCapabilityContract(capabilityId) {
return exports.INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
}
function getAssistantCapabilityContractByIntent(intent) {
return exports.INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
}

View File

@ -2508,6 +2508,32 @@ function isInventoryDrilldownFrameIntent(intent) {
intent === "inventory_purchase_to_sale_chain" ||
intent === "inventory_aging_by_purchase_date";
}
function resolveAddressIntentFamily(intent) {
const normalizedIntent = toNonEmptyString(intent);
if (!normalizedIntent) {
return null;
}
if (normalizedIntent.startsWith("inventory_")) {
return "inventory";
}
if (normalizedIntent.startsWith("vat_")) {
return "vat";
}
if (normalizedIntent === "account_balance_snapshot" || normalizedIntent === "documents_forming_balance") {
return "balance";
}
if (normalizedIntent === "open_items_by_counterparty_or_contract" ||
normalizedIntent === "list_documents_by_counterparty" ||
normalizedIntent === "bank_operations_by_counterparty" ||
normalizedIntent === "list_contracts_by_counterparty" ||
normalizedIntent === "list_documents_by_contract" ||
normalizedIntent === "bank_operations_by_contract" ||
normalizedIntent === "receivables_confirmed_for_period" ||
normalizedIntent === "payables_confirmed_for_period") {
return "settlements";
}
return null;
}
function extractAddressCarryoverAnchor(addressDebug) {
if (!isAddressLaneDebugPayload(addressDebug)) {
return {
@ -2877,6 +2903,18 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
? extractDisplayedEntityIndexMention(String(alternateMessage ?? "")) !== null
: false;
const hasIndexReferenceSignal = hasPrimaryIndexReferenceSignal || hasAlternateIndexReferenceSignal;
const hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation ||
hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate ||
Boolean(debtRoleSwapIntent) ||
hasFollowupMarker(userMessage) ||
hasReferentialPointer(userMessage) ||
(toNonEmptyString(alternateMessage)
? hasFollowupMarker(String(alternateMessage ?? "")) || hasReferentialPointer(String(alternateMessage ?? ""))
: false);
const hasStandaloneAddressTopic = hasStandaloneAddressTopicSignal(userMessage) ||
(toNonEmptyString(alternateMessage) ? hasStandaloneAddressTopicSignal(alternateMessage) : false);
if (hasStandaloneAddressTopic &&
@ -2898,6 +2936,23 @@ function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMes
return null;
}
const sourceIntent = toNonEmptyString(previousAddressDebug.detected_intent);
const llmExplicitIntent = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const resolvedPrimaryIntent = (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(userMessage ?? ""))).intent;
const resolvedAlternateIntent = toNonEmptyString(alternateMessage)
? (0, addressIntentResolver_1.resolveAddressIntent)(repairAddressMojibake(String(alternateMessage ?? ""))).intent
: null;
const explicitIntent = llmExplicitIntent && llmExplicitIntent !== "unknown"
? llmExplicitIntent
: resolvedPrimaryIntent && resolvedPrimaryIntent !== "unknown"
? resolvedPrimaryIntent
: resolvedAlternateIntent && resolvedAlternateIntent !== "unknown"
? resolvedAlternateIntent
: null;
const sourceIntentFamily = resolveAddressIntentFamily(sourceIntent);
const explicitIntentFamily = resolveAddressIntentFamily(explicitIntent);
if (sourceIntentFamily && explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily && !hasStrongFollowupReference) {
return null;
}
let previousIntent = sourceIntent;
let followupSelectionMode = "carry_previous_intent";
if (debtRoleSwapIntent) {
@ -4375,6 +4430,17 @@ function resolveAssistantOrchestrationDecision(input) {
hasHistoricalCapabilityFollowupSignal(effectiveAddressUserMessage) ||
hasHistoricalCapabilityFollowupSignal(repairedEffectiveAddressUserMessage)) &&
isGroundedInventoryContextDebug(lastGroundedAddressDebug));
const contextualMemoryRecapFollowupDetected = Boolean(!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal &&
!strongDataSignal &&
!aggregateBusinessAnalyticsSignal &&
(hasConversationMemoryRecallFollowupSignal(rawUserMessage) ||
hasConversationMemoryRecallFollowupSignal(repairedRawUserMessage) ||
hasConversationMemoryRecallFollowupSignal(effectiveAddressUserMessage) ||
hasConversationMemoryRecallFollowupSignal(repairedEffectiveAddressUserMessage)) &&
(lastGroundedAddressDebug ||
findLastAddressAssistantItem(sessionItems)?.debug));
const hardMetaMode = dataScopeMetaQuery
? "data_scope"
: capabilityMetaQuery && !dataRetrievalSignal
@ -4465,6 +4531,34 @@ function resolveAssistantOrchestrationDecision(input) {
};
}
if (nonDomainQueryIndexed) {
if (contextualMemoryRecapFollowupDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "memory_recap_followup_detected",
livingMode: "chat",
livingReason: "memory_recap_followup_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: "non_domain",
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext || lastGroundedAddressDebug),
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "memory_recap_followup_detected",
living_mode: "chat",
living_reason: "memory_recap_followup_detected"
}
}
};
}
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
@ -4569,6 +4663,9 @@ function resolveAssistantOrchestrationDecision(input) {
const vatExplainFollowupSignal = Boolean(followupContext &&
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
/(?:\u043f\u043e\u0447\u0435\u043c\u0443|why).*(?:\u043f\u0440\u043e\u0433\u043d\u043e\u0437|forecast).*(?:\u0443\u043f\u043b\u0430\u0442|payable|\b0\b)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
const vatEvaluativeFollowupSignal = Boolean(followupContext &&
toNonEmptyString(followupContext.previous_intent) === "vat_payable_forecast" &&
/(?:^|\s)(?:это\s+)?много\s+или\s+мало(?:\?|$)|(?:^|\s)(?:это\s+)?нормально(?:\?|$)|(?:^|\s)(?:это\s+)?плохо(?:\?|$)|(?:^|\s)(?:это\s+)?хорошо(?:\?|$)/iu.test(compactWhitespace(`${repairedRawUserMessage} ${repairedEffectiveAddressUserMessage}`)));
const deepAnalysisSignalFallbackToDeep = Boolean(baseToolGate?.runAddressLane &&
!llmRuntimeUnavailableDetected &&
(deepAnalysisPreferenceDetected || semanticDeepInvestigationHintDetected) &&
@ -4599,7 +4696,7 @@ function resolveAssistantOrchestrationDecision(input) {
const hasPriorAddressAnswerContext = Boolean(lastGroundedAddressDebug || toNonEmptyString(followupContext?.previous_intent));
const metaFollowupOverGroundedAnswer = Boolean(followupContext &&
hasPriorAddressAnswerContext &&
metaAnswerFollowupSignal &&
(metaAnswerFollowupSignal || vatEvaluativeFollowupSignal) &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!aggregateBusinessAnalyticsSignal &&
@ -4844,7 +4941,28 @@ function hasMetaAnswerFollowupSignal(userMessage) {
sample.includes("по этому поводу") ||
sample.includes("об этом") ||
(sample.includes("это") && hasReferentialPointer(sample)));
if (!(hasReflectionCue && hasTopicPointerCue)) {
const hasEvaluationCue = samples.some((sample) => /\b(?:много|мало|нормально|хорошо|плохо|критично|перебор|слабо)\b/iu.test(sample));
if (!((hasReflectionCue || hasEvaluationCue) &&
(hasTopicPointerCue || (hasEvaluationCue && samples.some((sample) => /^(?:это|ну это)\b/iu.test(sample)))))) {
return false;
}
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
shouldHandleAsAssistantCapabilityMetaQuery(sample) ||
hasDataRetrievalRequestSignal(sample) ||
hasStrongDataIntentSignal(sample));
}
function hasConversationMemoryRecallFollowupSignal(userMessage) {
const rawText = compactWhitespace(String(userMessage ?? "").toLowerCase());
const repairedText = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
const samples = [rawText, repairedText]
.filter((item) => item.length > 0)
.map((item) => item.replace(/ё/g, "е"));
if (samples.length === 0) {
return false;
}
const hasMemoryCue = samples.some((sample) => /(?:помни(?:шь|те|м)?|remember|recall)/iu.test(sample));
const hasDiscussionCue = samples.some((sample) => /(?:обсуждал[аи]?|говорил[аи]?|смотрел[аи]?|разбирал[аи]?|спрашивал[аи]?)/iu.test(sample));
if (!hasMemoryCue || !hasDiscussionCue) {
return false;
}
return !samples.some((sample) => hasAssistantDataScopeMetaQuestionSignal(sample) ||
@ -4920,6 +5038,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization)
if (hasSelectionCue) {
return true;
}
const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:ну|мда|ох|ах|офигеть|офигенно|ахуеть|охуеть|пиздец|пизда|нихуя|хуево|хуёво|ебать|ебан|бля|блять|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) ||
normalized.includes("\u0430\u0445\u0443") ||
normalized.includes("\u043e\u0445\u0443") ||
normalized.includes("\u043f\u0438\u0437\u0434") ||
normalized.includes("\u0431\u043b\u044f");
if (hasAffectiveReactionCue) {
return false;
}
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
}
function hasOperationalAdminActionRequestSignal(text) {

View File

@ -11,7 +11,10 @@ function hasInventoryPurchaseStem(text) {
}
function hasInventorySupplierCue(text) {
const value = toText(text);
if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) {
if (/(?:купил(?:и|о)?\s+у\s+кого|куплен(?:о)?\s+у\s+кого|купил(?:и|о)?\s+от\s+кого|куплен(?:о)?\s+от\s+кого)/iu.test(value)) {
return true;
}
if (/(?:кто\s+(?:(?:это|этот\s+товар|эту\s+позицию)\s+)?(?:нам\s+)?поставил|кто\s+(?:нам\s+)?поставил\s+(?:это|этот\s+товар|эту\s+позицию)|от\s+какого\s+поставщика|у\s+какого\s+поставщика|от\s+кого\s+куплен|у\s+кого\s+купили|у\s+кого\s+куплено|где\s+(?:мы\s+)?купили(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|откуда\s+(?:мы\s+)?взяли(?:\s+(?:это|его|этот\s+товар|эту\s+позицию))?|где\s+куплено|supplier|vendor|поставщик)/iu.test(value)) {
return true;
}
return hasInventoryPurchaseStem(value) && /(?:у\s+кого|от\s+кого|где)/iu.test(value);
@ -21,11 +24,11 @@ function hasInventorySaleCue(text) {
if (/(?:buyer|покупател)/iu.test(value)) {
return true;
}
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) {
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?)/iu.test(value)) {
return true;
}
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(value);
const hasSaleVerb = /(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(value);
if (hasDirectionCue && hasSaleVerb) {
return true;
}

View File

@ -0,0 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = void 0;
exports.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1";

View File

@ -1191,6 +1191,75 @@ function isTemporalWarehousePhrase(candidate: string): boolean {
);
}
function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
const value = cleanupAnchorValue(rawValue)
.toLowerCase()
.replace(/ё/g, "е")
.trim();
if (!value) {
return true;
}
if (isTemporalWarehousePhrase(value) || isImplicitSelfScopeWarehouseAnchor(value)) {
return true;
}
const hasQuestionOrRepairCue =
/(?:^|[\s,.;:!?()\-])(?:что|какой|какая|какие|как|где|когда|почему|зачем|имел(?:ось|ся)\s+в\s+виду|имеется\s+в\s+виду|в\s+смысле|то\s+есть|which|what|where|when|why)(?=$|[\s,.;:!?()\-])/iu.test(
value
) || /[?]/u.test(rawValue);
const hasProfanityCue =
/(?:^|[\s,.;:!?()\-])(?:аху|оху|хуе|хуё|хуй|ебан|ебуч|бля|блять|пизд|нахуй|shit|fuck|damn)(?=$|[\s,.;:!?()\-])/iu.test(
value
);
const lowQualityTokens = new Set([
"что",
"какой",
"какая",
"какие",
"как",
"где",
"когда",
"почему",
"зачем",
"имелось",
"имелся",
"имеется",
"в",
"виду",
"то",
"есть",
"лежит",
"лежат",
"лежало",
"лежали",
"на",
"по",
"складе",
"складу",
"складом",
"ебаном",
"ахуеть",
"охуеть",
"пиздец",
"блять",
"бля"
]);
const tokens = value
.split(/[^a-zа-я0-9]+/iu)
.map((token) => token.trim())
.filter(Boolean);
if (tokens.length === 0) {
return true;
}
const meaningfulTokens = tokens.filter((token) => !lowQualityTokens.has(token) && token.length > 1);
if (meaningfulTokens.length === 0) {
return true;
}
if ((hasQuestionOrRepairCue || hasProfanityCue) && meaningfulTokens.length <= 1) {
return true;
}
return false;
}
function normalizeSemanticAnchorCandidate(value: string): string {
return cleanupAnchorValue(value)
.toLowerCase()
@ -1236,6 +1305,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
candidate.includes("->") ||
candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
isLowQualityWarehouseAnchorValue(candidate) ||
normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) ||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)

View File

@ -1949,6 +1949,17 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
};
}
if (
/(?:кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?|кому\s+в\s+итоге\s+мы\s+впарили)/iu.test(text) &&
/(?:товар|номенклатур|sku|item|product|позици(?:я|ю|и)|продукци(?:я|ю|и))/iu.test(text)
) {
return {
intent: "inventory_sale_trace_for_item",
confidence: "medium",
reasons: ["inventory_sale_trace_signal_detected"]
};
}
if (hasInventorySaleTraceSignalV2(text)) {
return {
intent: "inventory_sale_trace_for_item",

View File

@ -1978,7 +1978,14 @@ function hasExplicitPeriodWindow(filters: AddressFilterSet): boolean {
}
function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilterSet): boolean {
if (!hasExplicitPeriodWindow(filters)) {
const hasRecoverableAsOfOnlyWindow =
!hasExplicitPeriodWindow(filters) &&
typeof filters.as_of_date === "string" &&
filters.as_of_date.trim().length > 0 &&
typeof filters.item === "string" &&
filters.item.trim().length > 0 &&
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item");
if (!hasExplicitPeriodWindow(filters) && !hasRecoverableAsOfOnlyWindow) {
return false;
}
return (

View File

@ -533,6 +533,30 @@ function hasSelectedObjectInventorySignal(text: string): boolean {
return /(?:по\s+выбранному\s+объекту|for\s+selected\s+object)/iu.test(String(text ?? ""));
}
function hasSelectedObjectInlineSnapshotMetadata(text: string): boolean {
return /(?:дата\s+строки|строка\s+от|количество\s*:|стоимость\s*:|склад\s*:|организация\s*:|\|\s*(?:склад|количество|стоимость|организация|дата\s+строки)\s*:)/iu.test(
String(text ?? "")
);
}
function extractSelectedObjectItemFromFollowupText(text: string): string | null {
const rawSelectedObject = toNonEmptyString(extractSelectedObjectQuotedValue(text));
if (!rawSelectedObject) {
return null;
}
const firstLine = rawSelectedObject
.replace(/\r\n?/g, "\n")
.split("\n")
.map((line) => line.trim())
.find(Boolean);
const primarySegment = String(firstLine ?? rawSelectedObject)
.replace(/^\d+\.\s*/, "")
.split("|")[0]
?.trim();
const normalized = toNonEmptyString(primarySegment);
return normalized;
}
export function hasInventorySupplierFollowupCue(text: string): boolean {
return hasInventorySupplierCue(String(text ?? ""));
}
@ -800,7 +824,7 @@ function mergeFollowupFilters(
intent === "inventory_aging_by_purchase_date")
) {
const inheritedItem = previousItem ?? previousAnchorItem;
const explicitQuotedItem = toNonEmptyString(extractSelectedObjectQuotedValue(userMessage));
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
const currentItem = toNonEmptyString(merged.item);
const shouldAdoptExplicitQuotedItem =
Boolean(explicitQuotedItem) &&
@ -873,6 +897,23 @@ function mergeFollowupFilters(
}
}
}
if (
(Boolean(previousPeriodFrom) || Boolean(previousPeriodTo)) &&
hasSelectedObjectInventorySignal(userMessage) &&
hasSelectedObjectInlineSnapshotMetadata(userMessage) &&
(intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_documents_for_item") &&
!hasExplicitPeriodLiteral(userMessage) &&
!hasExplicitCurrentDateHint(userMessage)
) {
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
reasons.push("period_from_from_followup_context");
}
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
merged.period_to = previousPeriodTo;
reasons.push("period_to_from_followup_context");
}
}
if (
!sameDateRequested &&
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&

View File

@ -0,0 +1,306 @@
import {
ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
type AssistantCapabilityContract,
type AssistantTransitionClassId,
type AssistantTransitionContract
} from "../types/assistantRuntimeContracts";
import type { AddressIntent } from "../types/addressQuery";
export const ASSISTANT_TRANSITION_CONTRACTS: readonly AssistantTransitionContract[] = [
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T1",
title: "Root Query Entry",
trigger_class: "new_root_business_question",
required_prior_state: ["living_mode_state"],
allowed_carryover_depth: "none",
state_mutations: ["create_root_frame_state", "clear_selected_object_frame_state", "create_coverage_gate_state"],
forbidden_carryover: ["stale_focus_object", "stale_object_intent"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T2",
title: "Root Follow-Up With Date Or Scope Change",
trigger_class: "root_followup_temporal_or_organization_shift",
required_prior_state: ["root_frame_state"],
allowed_carryover_depth: "root_only",
state_mutations: ["update_root_frame_state", "run_exact_route", "refresh_coverage_gate_state"],
forbidden_carryover: ["incompatible_selected_object_route"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T3",
title: "Explicit Selected Object Drilldown",
trigger_class: "explicit_selected_object_or_ui_object_selection",
required_prior_state: ["root_frame_state"],
allowed_carryover_depth: "object_only",
state_mutations: ["create_selected_object_frame_state", "bind_source_result_set", "preserve_temporal_ceiling"],
forbidden_carryover: ["unrelated_prior_focus_object"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T4",
title: "Short Action Follow-Up On Selected Object",
trigger_class: "short_action_followup_on_active_focus_object",
required_prior_state: ["selected_object_frame_state"],
allowed_carryover_depth: "object_only",
state_mutations: ["reuse_selected_object_frame_state", "route_to_compatible_item_action"],
forbidden_carryover: ["generic_chat_fallback", "data_scope_selection_fallback", "object_focus_reset"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T5",
title: "Pronoun Or Compressed Object Follow-Up",
trigger_class: "pronoun_or_compressed_reference_to_active_focus_object",
required_prior_state: ["selected_object_frame_state"],
allowed_carryover_depth: "object_only",
state_mutations: ["reuse_selected_object_frame_state", "resolve_pronoun_to_focus_object"],
forbidden_carryover: ["low_quality_object_rewrite", "semantic_noise_as_anchor"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T6",
title: "Domain Pivot With Root-Only Carryover",
trigger_class: "supported_domain_pivot_from_active_drilldown",
required_prior_state: ["root_frame_state", "selected_object_frame_state"],
allowed_carryover_depth: "root_only",
state_mutations: ["preserve_root_frame_state", "drop_selected_object_frame_state"],
forbidden_carryover: ["object_route_replay_into_new_domain"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T7",
title: "Clarification Continuation",
trigger_class: "user_resolves_missing_anchor_or_scope",
required_prior_state: ["clarification_state"],
allowed_carryover_depth: "full",
state_mutations: ["resume_target_route", "update_or_clear_clarification_state"],
forbidden_carryover: ["forget_suspended_route"],
expected_answer_mode: "confirmed"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T8",
title: "Meta Follow-Up Over Answer Object",
trigger_class: "evaluation_comparison_or_interpretation_of_previous_answer",
required_prior_state: ["answer_context_state", "coverage_gate_state"],
allowed_carryover_depth: "meta_only",
state_mutations: ["create_meta_frame_state", "reuse_answer_object_without_blind_replay"],
forbidden_carryover: ["blind_exact_route_replay"],
expected_answer_mode: "meta"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T9",
title: "Memory Recap",
trigger_class: "conversation_memory_recap_request",
required_prior_state: ["answer_context_state"],
allowed_carryover_depth: "meta_only",
state_mutations: ["reuse_grounded_prior_answer_context"],
forbidden_carryover: ["invented_conversation_memory"],
expected_answer_mode: "recap"
},
{
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
transition_id: "T10",
title: "Unsupported Or Blocked Boundary",
trigger_class: "unsupported_route_or_blocked_evidence_gate",
required_prior_state: ["coverage_gate_state"],
allowed_carryover_depth: "none",
state_mutations: ["emit_bounded_boundary_or_clarification"],
forbidden_carryover: ["blocked_as_confirmed_factual_answer"],
expected_answer_mode: "boundary"
}
] as const;
const SHARED_INVENTORY_ACCEPTANCE_FAMILIES = [
"canonical",
"colloquial",
"ui_selected_object",
"ui_selected_object_colloquial",
"short_action_followup",
"pronoun_followup",
"followup_date_carryover"
] as const;
const INVENTORY_ITEM_ANCHOR_RULES = [
"no_low_quality_item_rewrite",
"no_numeric_tail_account_poisoning",
"no_conversational_noise_as_entity",
"confirmed_focus_object_beats_semantic_hint"
] as const;
const INVENTORY_SELECTED_OBJECT_TESTS = [
"selected_object_memory_survives_short_followup",
"new_explicit_selected_object_overrides_old_focus",
"full_anchor_not_degraded_by_canonical_rewrite"
] as const;
function inventoryExactCapability(input: {
capability_id: string;
intent_ids: AddressIntent[];
entry_modes: AssistantCapabilityContract["entry_modes"];
transitions: AssistantTransitionClassId[];
requiresFocusObject: boolean;
requiredAnchors: string[];
resultShape: string;
answerObjectShape: string;
bundleReusePolicy: AssistantCapabilityContract["bundle_reuse_policy"];
scenarioFamilies?: string[];
}): AssistantCapabilityContract {
return {
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
capability_id: input.capability_id,
domain_id: "inventory_stock",
runtime_lane: "address_exact",
intent_ids: input.intent_ids,
entry_modes: input.entry_modes,
supported_transition_classes: input.transitions,
frame_compatibility: {
root_frame: input.entry_modes.includes("root_entry") ? "optional" : "required",
selected_object_frame: input.requiresFocusObject ? "required" : "optional",
meta_frame: "forbidden"
},
required_anchors: input.requiredAnchors,
optional_anchors: ["organization", "warehouse", "date_scope"],
anchor_source_priority: ["explicit_user_anchor", "ui_selected_object", "selected_object_frame", "root_frame", "semantic_hint"],
anchor_admissibility_rules: [...INVENTORY_ITEM_ANCHOR_RULES],
organization_scope_behavior: "reuse_or_clarify",
date_scope_behavior: "reuse",
temporal_ceiling_policy: input.requiresFocusObject ? "respect_root_temporal_ceiling" : "must_not_expand_without_reason_code",
root_context_compatibility: "required",
requires_focus_object: input.requiresFocusObject,
accepted_focus_object_kinds: input.requiresFocusObject ? ["inventory_item", "item"] : [],
focus_object_override_policy: input.requiresFocusObject ? "explicit_new_object_wins" : "not_applicable",
bundle_reuse_policy: input.bundleReusePolicy,
resolver_owner: "addressIntentResolver",
recipe_owner: "addressRecipeCatalog",
execution_adapter: "AddressQueryService",
result_shape: input.resultShape,
answer_object_shape: input.answerObjectShape,
minimum_evidence_policy: "route_specific_threshold",
coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"],
blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"],
clarification_triggers: ["missing_required_item_anchor", "ambiguous_organization_scope", "ambiguous_date_scope"],
clarification_questions: ["Уточните товар, организацию или дату, чтобы не подставлять неподтвержденный anchor."],
resume_policy: "resume_original_route_with_resolved_anchors",
empty_match_behavior: "truthful_empty_match",
route_expectation_failure_behavior: "blocked_route_expectation_failure",
execution_error_behavior: "blocked_execution_error",
required_unit_tests: input.requiresFocusObject
? [...INVENTORY_SELECTED_OBJECT_TESTS, "limited_mode_remains_truthful"]
: ["root_context_survives_domain_pivot_without_object_leak", "limited_mode_remains_truthful"],
required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`),
required_scenario_families: input.scenarioFamilies ?? [...SHARED_INVENTORY_ACCEPTANCE_FAMILIES]
};
}
export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
inventoryExactCapability({
capability_id: "confirmed_inventory_on_hand_as_of_date",
intent_ids: ["inventory_on_hand_as_of_date"],
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
transitions: ["T1", "T2", "T7"],
requiresFocusObject: false,
requiredAnchors: [],
resultShape: "item_list_with_quantity_cost_warehouse_organization",
answerObjectShape: "inventory_stock_snapshot",
bundleReusePolicy: "none",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
}),
inventoryExactCapability({
capability_id: "inventory_inventory_purchase_provenance_for_item",
intent_ids: ["inventory_purchase_provenance_for_item"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "supplier_purchase_provenance_trace",
answerObjectShape: "inventory_provenance_bundle",
bundleReusePolicy: "provenance_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_purchase_documents_for_item",
intent_ids: ["inventory_purchase_documents_for_item"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "purchase_document_list_for_selected_item",
answerObjectShape: "inventory_purchase_documents_bundle",
bundleReusePolicy: "provenance_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_supplier_stock_overlap_as_of_date",
intent_ids: ["inventory_supplier_stock_overlap_as_of_date"],
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
transitions: ["T1", "T2", "T7"],
requiresFocusObject: false,
requiredAnchors: ["supplier"],
resultShape: "supplier_to_stock_item_overlap",
answerObjectShape: "inventory_supplier_overlap",
bundleReusePolicy: "none",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
}),
inventoryExactCapability({
capability_id: "inventory_inventory_sale_trace_for_item",
intent_ids: ["inventory_sale_trace_for_item"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "buyer_sale_trace_for_selected_item",
answerObjectShape: "inventory_sale_trace_bundle",
bundleReusePolicy: "sale_trace_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_purchase_to_sale_chain",
intent_ids: ["inventory_purchase_to_sale_chain"],
entry_modes: ["selected_object_drilldown", "clarification_resume"],
transitions: ["T3", "T4", "T5", "T7"],
requiresFocusObject: true,
requiredAnchors: ["item"],
resultShape: "purchase_stock_sale_document_chain",
answerObjectShape: "inventory_purchase_to_sale_chain",
bundleReusePolicy: "sale_trace_bundle_preferred"
}),
inventoryExactCapability({
capability_id: "inventory_inventory_aging_by_purchase_date",
intent_ids: ["inventory_aging_by_purchase_date"],
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
transitions: ["T1", "T2", "T6", "T7"],
requiresFocusObject: false,
requiredAnchors: [],
resultShape: "oldest_first_inventory_aging_list",
answerObjectShape: "inventory_aging_snapshot",
bundleReusePolicy: "none",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
})
] as const;
export function listAssistantTransitionContracts(): readonly AssistantTransitionContract[] {
return ASSISTANT_TRANSITION_CONTRACTS;
}
export function getAssistantTransitionContract(transitionId: AssistantTransitionClassId): AssistantTransitionContract | null {
return ASSISTANT_TRANSITION_CONTRACTS.find((contract) => contract.transition_id === transitionId) ?? null;
}
export function listInventoryCapabilityContracts(): readonly AssistantCapabilityContract[] {
return INVENTORY_CAPABILITY_CONTRACTS;
}
export function getAssistantCapabilityContract(capabilityId: string): AssistantCapabilityContract | null {
return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
}
export function getAssistantCapabilityContractByIntent(intent: AddressIntent): AssistantCapabilityContract | null {
return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
}

View File

@ -4997,6 +4997,14 @@ function shouldEmitOrganizationSelectionReply(userMessage, selectedOrganization)
if (hasSelectionCue) {
return true;
}
const hasAffectiveReactionCue = /(?:^|[\s,.;:!?()\-])(?:РЅСѓ|РјРґР°|РѕС|Р°С|РѕСРёРіРµССЊ|РѕСигенно|Р°ССѓРµССЊ|РѕССѓРµССЊ|РїРёР·РґРµС|РїРёР·РґР°|РЅРёССѓСЏ|Суево|ССѓСРІРѕ|ебаССЊ|ебан|бля|бляССЊ|fuck|shit|damn)(?=$|[\s,.;:!?()\-])/iu.test(normalized) ||
normalized.includes("\u0430\u0445\u0443") ||
normalized.includes("\u043e\u0445\u0443") ||
normalized.includes("\u043f\u0438\u0437\u0434") ||
normalized.includes("\u0431\u043b\u044f");
if (hasAffectiveReactionCue) {
return false;
}
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
}
function hasOperationalAdminActionRequestSignal(text) {

View File

@ -26,12 +26,12 @@ export function hasInventorySaleCue(text: string): boolean {
if (/(?:buyer|покупател)/iu.test(value)) {
return true;
}
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил)/iu.test(value)) {
if (/(?:куда\s+ушла\s+позиция|куда\s+ушел\s+товар|кто\s+купил|кому\s+(?:мы\s+)?впарили(?:\s+(?:это|его|товар|позицию))?)/iu.test(value)) {
return true;
}
const hasDirectionCue = /(?:кому|каму|куда)/iu.test(value);
const hasSaleVerb =
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано)/iu.test(
/(?:продал(?:и|а|о|ы)?|продан(?:а|о|ы)?|продано|реализовал(?:и|а|о|ы)?|реализован(?:а|о|ы)?|реализовано|впарил(?:и|а|о|ы)?|отгрузил(?:и|а|о|ы)?|ушло|ушел|ушла)/iu.test(
value
);
if (hasDirectionCue && hasSaleVerb) {

View File

@ -0,0 +1,154 @@
import type { AddressIntent } from "./addressQuery";
export const ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION = "assistant_runtime_contracts_v1" as const;
export type AssistantLivingMode = "address_data" | "assistant_data_scope" | "chat" | "meta_followup" | "clarification";
export type AssistantFrameStatus = "active" | "suspended" | "closed" | "blocked";
export type AssistantTransitionClassId = "T1" | "T2" | "T3" | "T4" | "T5" | "T6" | "T7" | "T8" | "T9" | "T10";
export type AssistantStateSlice =
| "living_mode_state"
| "root_frame_state"
| "selected_object_frame_state"
| "meta_frame_state"
| "clarification_state"
| "coverage_gate_state"
| "answer_context_state";
export type AssistantCarryoverDepth = "full" | "root_only" | "object_only" | "meta_only" | "none";
export type AssistantAnswerMode = "confirmed" | "limited" | "clarification" | "boundary" | "meta" | "recap";
export interface AssistantDateScopeState {
as_of_date: string | null;
period_from: string | null;
period_to: string | null;
}
export interface AssistantRootFrameState {
domain_id: string | null;
root_route_id: string | null;
organization_scope: string | null;
date_scope: AssistantDateScopeState;
root_result_set_id: string | null;
root_answer_object_ref: string | null;
frame_status: AssistantFrameStatus;
}
export interface AssistantSelectedObjectFrameState {
focus_object_ref: string | null;
focus_object_kind: string | null;
source_result_set_id: string | null;
compatible_route_family: string[];
provenance_bundle_ref: string | null;
temporal_ceiling: AssistantDateScopeState;
frame_status: AssistantFrameStatus;
}
export interface AssistantMetaFrameState {
source_answer_object_ref: string | null;
meta_question_kind: "evaluation" | "comparison" | "memory_recap" | "boundary_explanation" | "answer_interpretation" | null;
source_gate_status: AssistantCoverageGateState["coverage_status"] | null;
meta_truth_mode: AssistantCoverageGateState["truth_mode"] | null;
}
export interface AssistantClarificationState {
clarification_kind: string | null;
missing_anchors: string[];
candidate_scopes: string[];
resume_target_route: string | null;
resume_target_frame: AssistantStateSlice | null;
}
export interface AssistantCoverageGateState {
coverage_status: "full" | "partial" | "blocked";
evidence_grade: "none" | "weak" | "medium" | "strong";
grounding_status: "grounded" | "partial" | "route_mismatch_blocked" | "no_grounded_answer" | "unsupported";
truth_mode: "confirmed" | "limited" | "clarification_required" | "unsupported";
carryover_eligibility: AssistantCarryoverDepth;
reason_codes: string[];
}
export interface AssistantSessionAggregateState {
schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION;
living_mode_state: {
living_mode: AssistantLivingMode;
mode_reason: string | null;
mode_source: "router" | "transition" | "clarification" | "manual" | null;
mode_entry_turn_id: string | null;
};
root_frame_state: AssistantRootFrameState | null;
selected_object_frame_state: AssistantSelectedObjectFrameState | null;
meta_frame_state: AssistantMetaFrameState | null;
clarification_state: AssistantClarificationState | null;
coverage_gate_state: AssistantCoverageGateState | null;
answer_context_state: Record<string, unknown> | null;
}
export interface AssistantTransitionContract {
schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION;
transition_id: AssistantTransitionClassId;
title: string;
trigger_class: string;
required_prior_state: AssistantStateSlice[];
allowed_carryover_depth: AssistantCarryoverDepth;
state_mutations: string[];
forbidden_carryover: string[];
expected_answer_mode: AssistantAnswerMode;
}
export type AssistantCapabilityEntryMode =
| "root_entry"
| "root_followup"
| "selected_object_drilldown"
| "meta_reuse"
| "clarification_resume";
export type AssistantRuntimeLane = "address_exact" | "assistant_data_scope" | "chat" | "meta";
export type AssistantFrameRequirement = "required" | "optional" | "forbidden";
export type AssistantScopeBehavior = "create" | "reuse" | "reuse_or_clarify" | "narrow" | "reject" | "none";
export type AssistantTemporalCeilingPolicy = "none" | "respect_root_temporal_ceiling" | "must_not_expand_without_reason_code";
export type AssistantBundleReusePolicy = "none" | "provenance_bundle_preferred" | "sale_trace_bundle_preferred";
export type AssistantCoverageGateBehavior = "full_required" | "partial_or_blocked_if_evidence_insufficient";
export type AssistantTruthFallback = "limited" | "clarification_required" | "unsupported";
export interface AssistantCapabilityContract {
schema_version: typeof ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION;
capability_id: string;
domain_id: string;
runtime_lane: AssistantRuntimeLane;
intent_ids: AddressIntent[];
entry_modes: AssistantCapabilityEntryMode[];
supported_transition_classes: AssistantTransitionClassId[];
frame_compatibility: {
root_frame: AssistantFrameRequirement;
selected_object_frame: AssistantFrameRequirement;
meta_frame: AssistantFrameRequirement;
};
required_anchors: string[];
optional_anchors: string[];
anchor_source_priority: string[];
anchor_admissibility_rules: string[];
organization_scope_behavior: AssistantScopeBehavior;
date_scope_behavior: AssistantScopeBehavior;
temporal_ceiling_policy: AssistantTemporalCeilingPolicy;
root_context_compatibility: "required" | "optional" | "not_applicable";
requires_focus_object: boolean;
accepted_focus_object_kinds: string[];
focus_object_override_policy: "explicit_new_object_wins" | "preserve_existing" | "not_applicable";
bundle_reuse_policy: AssistantBundleReusePolicy;
resolver_owner: string;
recipe_owner: string;
execution_adapter: string;
result_shape: string;
answer_object_shape: string;
minimum_evidence_policy: string;
coverage_gate_behavior: AssistantCoverageGateBehavior;
truth_mode_fallbacks: AssistantTruthFallback[];
blocked_reason_codes: string[];
clarification_triggers: string[];
clarification_questions: string[];
resume_policy: string;
empty_match_behavior: string;
route_expectation_failure_behavior: string;
execution_error_behavior: string;
required_unit_tests: string[];
required_transition_tests: string[];
required_scenario_families: string[];
}

View File

@ -47,4 +47,21 @@ describe("inventory warehouse anchor extraction", () => {
expect(result.semantic_frame?.date_scope_kind).toBe("implicit_current");
expect(result.semantic_frame?.date_basis_hint).toBe("implicit_current_snapshot");
});
it("does not materialize profanity tail as warehouse anchor in slang stock query", () => {
const filters = extractAddressFilters(
"ассистент, рассказывай что нам на складе ебаном лежит",
"inventory_on_hand_as_of_date"
).extracted_filters;
expect(filters.warehouse).toBeUndefined();
});
it("does not materialize repair phrasing as warehouse anchor in stock follow-up", () => {
const filters = extractAddressFilters(
"остатки на складе какие имелось в виду",
"inventory_on_hand_as_of_date"
).extracted_filters;
expect(filters.warehouse).toBeUndefined();
});
});

View File

@ -5038,6 +5038,11 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
expect(result.intent).toBe("inventory_sale_trace_for_item");
});
it("routes colloquial buyer wording with 'впарили' to inventory sale trace intent", () => {
const result = resolveAddressIntent("Кому мы впарили этот товар Шкаф картотечный?");
expect(result.intent).toBe("inventory_sale_trace_for_item");
});
it("keeps inventory provenance wording out of inventory-on-hand routing", () => {
const result = resolveAddressIntent("От кого куплен товар Шкаф картоотечный и когда был куплен?");
expect(result.intent).toBe("inventory_purchase_provenance_for_item");

View File

@ -1064,4 +1064,65 @@ describe("assistant living chat mode", () => {
expect(addressQueryService.tryHandle).toHaveBeenCalledTimes(1);
expect(chatClient.chat).toHaveBeenCalledTimes(0);
});
it("does not treat short emotional reaction as organization-selection confirmation", async () => {
const normalizer = {
normalize: vi.fn().mockResolvedValue({
ok: false,
trace_id: "norm-chat-affective-reaction",
prompt_version: "normalizer_v2_0_2",
schema_version: "v2_0_2",
normalized: null,
validation: { passed: false, errors: ["mock"] },
route_hint_summary: null,
raw_model_output: {},
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
latency_ms: 1,
request_count_for_case: 1
})
} as any;
const sessions = new AssistantSessionStore();
const sessionId = "asst-living-chat-affective-reaction";
sessions.ensureSession(sessionId);
sessions.appendItem(sessionId, {
message_id: "msg-seed-selected-org",
session_id: sessionId,
role: "assistant",
text: "Отлично, фиксирую рабочую организацию: ООО Альтернатива Плюс.",
reply_type: "factual_with_explanation",
created_at: new Date().toISOString(),
trace_id: "chat-seed-selected-org",
debug: {
assistant_known_organizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд"],
assistant_selected_organization: "ООО Альтернатива Плюс",
assistant_active_organization: "ООО Альтернатива Плюс"
}
} as any);
const addressQueryService = {
tryHandle: vi.fn().mockResolvedValue({ handled: false })
} as any;
const chatClient = {
chat: vi.fn().mockResolvedValue({
raw: { id: "chat-affective-reaction" },
outputText: "Понимаю, это выглядит неприятно. Давай разберем следующий шаг.",
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 }
})
} as any;
const service = new AssistantService(normalizer as any, sessions, undefined as any, undefined as any, addressQueryService, chatClient);
const response = await service.handleMessage({
session_id: sessionId,
user_message: "ну ахуеть",
llmProvider: "local",
model: "qwen2.5",
useMock: false
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual_with_explanation");
expect(response.debug?.living_chat_response_source).not.toBe("deterministic_data_scope_selection_contract");
expect(String(response.assistant_reply)).not.toContain("фиксирую рабочую организацию");
expect(chatClient.chat).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import { resolveAddressCapabilityRouteDecision } from "../src/services/addressCapabilityPolicy";
import {
getAssistantCapabilityContract,
getAssistantCapabilityContractByIntent,
getAssistantTransitionContract,
listAssistantTransitionContracts,
listInventoryCapabilityContracts
} from "../src/services/assistantRuntimeContractRegistry";
describe("assistant runtime contract registry", () => {
it("declares the architecture turnaround transition set T1-T10", () => {
const transitions = listAssistantTransitionContracts();
expect(transitions.map((item) => item.transition_id)).toEqual(["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9", "T10"]);
const ids = new Set(transitions.map((item) => item.transition_id));
expect(ids.size).toBe(10);
expect(transitions.every((item) => item.schema_version === "assistant_runtime_contracts_v1")).toBe(true);
});
it("keeps selected-object action follow-ups object-scoped instead of generic-chat scoped", () => {
const transition = getAssistantTransitionContract("T4");
expect(transition).not.toBeNull();
expect(transition?.required_prior_state).toContain("selected_object_frame_state");
expect(transition?.allowed_carryover_depth).toBe("object_only");
expect(transition?.forbidden_carryover).toContain("generic_chat_fallback");
expect(transition?.forbidden_carryover).toContain("object_focus_reset");
});
it("declares meta follow-up as answer-object reuse, not blind exact-route replay", () => {
const transition = getAssistantTransitionContract("T8");
expect(transition).not.toBeNull();
expect(transition?.required_prior_state).toEqual(["answer_context_state", "coverage_gate_state"]);
expect(transition?.allowed_carryover_depth).toBe("meta_only");
expect(transition?.state_mutations).toContain("reuse_answer_object_without_blind_replay");
expect(transition?.forbidden_carryover).toContain("blind_exact_route_replay");
expect(transition?.expected_answer_mode).toBe("meta");
});
it("keeps pilot inventory capability ids aligned with the current address capability policy", () => {
for (const contract of listInventoryCapabilityContracts()) {
for (const intent of contract.intent_ids) {
const decision = resolveAddressCapabilityRouteDecision(intent);
expect(decision.capability_id).toBe(contract.capability_id);
expect(decision.capability_route_mode).toBe("exact");
}
}
});
it("declares root inventory snapshot as root-capable and focus-object-free", () => {
const contract = getAssistantCapabilityContract("confirmed_inventory_on_hand_as_of_date");
expect(contract).not.toBeNull();
expect(contract?.entry_modes).toEqual(["root_entry", "root_followup", "clarification_resume"]);
expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T7"]);
expect(contract?.requires_focus_object).toBe(false);
expect(contract?.result_shape).toBe("item_list_with_quantity_cost_warehouse_organization");
expect(contract?.required_scenario_families).toContain("colloquial");
});
it("declares selected-item provenance as focus-object and bundle-aware", () => {
const contract = getAssistantCapabilityContractByIntent("inventory_purchase_provenance_for_item");
expect(contract?.capability_id).toBe("inventory_inventory_purchase_provenance_for_item");
expect(contract?.requires_focus_object).toBe(true);
expect(contract?.accepted_focus_object_kinds).toEqual(["inventory_item", "item"]);
expect(contract?.supported_transition_classes).toEqual(["T3", "T4", "T5", "T7"]);
expect(contract?.required_anchors).toEqual(["item"]);
expect(contract?.bundle_reuse_policy).toBe("provenance_bundle_preferred");
expect(contract?.anchor_admissibility_rules).toContain("confirmed_focus_object_beats_semantic_hint");
expect(contract?.required_scenario_families).toContain("ui_selected_object_colloquial");
expect(contract?.required_scenario_families).toContain("pronoun_followup");
});
it("keeps truth semantics outside answer wording for every pilot inventory capability", () => {
for (const contract of listInventoryCapabilityContracts()) {
expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient");
expect(contract.truth_mode_fallbacks).toEqual(["limited", "clarification_required", "unsupported"]);
expect(contract.blocked_reason_codes).toEqual(
expect.arrayContaining(["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"])
);
expect(contract.route_expectation_failure_behavior).toBe("blocked_route_expectation_failure");
expect(contract.execution_error_behavior).toBe("blocked_execution_error");
}
});
});