@rsconcept/rstool 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/mappers/model-adapter.d.ts +1 -2
- package/dist/mappers/schema-adapter.d.ts +1 -2
- package/dist/mappers/types.d.ts +4 -5
- package/dist/models/rstool-agent.js +1 -2
- package/dist/session/session-store.d.ts +3 -3
- package/dist/session/session-store.js +1 -2
- package/dist/wrapper/client.d.ts +3 -4
- package/docs/BRIEF.md +93 -0
- package/docs/CONCEPTUAL-SCHEMA.md +16 -1
- package/docs/CONSTRUCTIONS.md +14 -2
- package/docs/DIAGNOSTICS.md +3 -2
- package/docs/DOMAIN.md +12 -0
- package/docs/METHOD.md +208 -0
- package/docs/PORTAL-API.md +38 -285
- package/docs/PORTAL-MODELS.md +24 -0
- package/docs/PORTAL-OSS.md +56 -0
- package/docs/PORTAL-PUBLISH.md +39 -0
- package/docs/PORTAL-READ.md +301 -0
- package/docs/README.md +7 -4
- package/docs/REQUIREMENTS.md +61 -0
- package/docs/SYNTAX.md +4 -3
- package/docs/TYPIFICATION.md +1 -1
- package/docs/brief.schema.json +173 -0
- package/examples/README.md +16 -2
- package/examples/access-control/NOTES.md +23 -0
- package/examples/access-control/anti/collapsed.json +439 -0
- package/examples/access-control/anti/weakened.json +439 -0
- package/examples/access-control/brief-v1.json +231 -0
- package/examples/access-control/brief.json +310 -0
- package/examples/access-control/build-rsform.ts +151 -0
- package/examples/access-control/requirements-v1.md +9 -0
- package/examples/access-control/requirements-v2.md +4 -0
- package/examples/access-control/rsform-session-v1.json +439 -0
- package/examples/access-control/rsform-session.json +523 -0
- package/examples/audit-diagnostics.ts +9 -1
- package/examples/briefs.test.ts +126 -0
- package/examples/clinic-booking/NOTES.md +24 -0
- package/examples/clinic-booking/anti/collapsed.json +615 -0
- package/examples/clinic-booking/anti/species-as-genus.json +568 -0
- package/examples/clinic-booking/brief-v1.json +332 -0
- package/examples/clinic-booking/brief.json +405 -0
- package/examples/clinic-booking/build-rsform.ts +174 -0
- package/examples/clinic-booking/requirements-v1.md +9 -0
- package/examples/clinic-booking/requirements-v2.md +4 -0
- package/examples/clinic-booking/rsform-session-v1.json +615 -0
- package/examples/clinic-booking/rsform-session.json +700 -0
- package/examples/diagnostics-utils.ts +26 -0
- package/examples/kinship/brief.json +337 -0
- package/examples/kinship/build-rsform.ts +7 -4
- package/examples/kinship/rsform-session.json +20 -63
- package/examples/kinship/rsmodel-session.json +14 -14
- package/examples/kinship-subordination/NOTES.md +49 -0
- package/examples/kinship-subordination/brief.json +161 -0
- package/examples/kinship-subordination/build-rsform.ts +100 -0
- package/examples/kinship-subordination/rsform-session.json +795 -0
- package/examples/library-loans/NOTES.md +46 -0
- package/examples/library-loans/anti/collapsed.json +481 -0
- package/examples/library-loans/anti/species-as-genus.json +334 -0
- package/examples/library-loans/anti/uncovered.json +406 -0
- package/examples/library-loans/anti/weakened.json +464 -0
- package/examples/library-loans/brief-v1.json +340 -0
- package/examples/library-loans/brief.json +502 -0
- package/examples/library-loans/build-rsform.ts +239 -0
- package/examples/library-loans/requirements-v1.md +11 -0
- package/examples/library-loans/requirements-v2.md +5 -0
- package/examples/library-loans/rsform-session-v1.json +481 -0
- package/examples/library-loans/rsform-session.json +652 -0
- package/examples/library-loans/source-markup.md +18 -0
- package/examples/movd/brief.json +220 -0
- package/examples/movd/build-rsform.ts +5 -2
- package/examples/movd/rsform-session.json +14 -46
- package/examples/subordination/NOTES.md +59 -0
- package/examples/subordination/anti/collapsed.json +556 -0
- package/examples/subordination/anti/single-relation.json +125 -0
- package/examples/subordination/anti/transitive.json +573 -0
- package/examples/subordination/anti/weakened.json +539 -0
- package/examples/subordination/brief.json +386 -0
- package/examples/subordination/build-rsform.ts +63 -0
- package/examples/subordination/constants.ts +115 -0
- package/examples/subordination/rsform-session.json +556 -0
- package/examples/subordination/source-markup.md +19 -0
- package/examples/subordination/source.md +40 -0
- package/package.json +18 -10
- package/skills/INSTALL.md +28 -29
- package/skills/README.md +25 -10
- package/skills/rs-core/SKILL.md +56 -0
- package/skills/rs-explain/SKILL.md +28 -0
- package/skills/rs-explicate/SKILL.md +71 -0
- package/skills/rs-interpret/SKILL.md +67 -0
- package/skills/rs-portal/SKILL.md +51 -0
- package/skills/rs-review/SKILL.md +79 -0
- package/skills/rs-scope/SKILL.md +67 -0
- package/skills/rs-synthesis/SKILL.md +52 -0
- package/skills/rstool/SKILL.md +70 -0
- package/skills/rstool-helper/GUIDE.md +18 -299
- package/skills/rstool-helper/SKILL.md +8 -58
- package/skills/{rstool-helper → rstool-runtime}/EXAMPLES.md +16 -12
- package/skills/{rstool-helper → rstool-runtime}/REFERENCE.md +7 -5
- package/skills/rstool-runtime/SKILL.md +96 -0
- package/skills/scripts/brief-check.test.ts +165 -0
- package/skills/scripts/brief-check.ts +826 -0
- package/skills/scripts/check-brief.ts +94 -0
- package/skills/scripts/rstool-skills.mjs +119 -0
- package/skills/scripts/skill-stubs.test.ts +48 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brief check: runs brief scenarios on a session copy and computes readiness.
|
|
3
|
+
*
|
|
4
|
+
* Prototype of the planned `runScenarios` / `getReadiness` contract (rstool 4.0).
|
|
5
|
+
* Brief format — `docs/BRIEF.md`, JSON schema — `docs/brief.schema.json`.
|
|
6
|
+
*/
|
|
7
|
+
import { labelType } from '@rsconcept/domain/rslang/labels';
|
|
8
|
+
import { normalizeValue } from '@rsconcept/domain/rslang/eval/value-api';
|
|
9
|
+
import { TUPLE_ID, type Value } from '@rsconcept/domain/rslang/eval/value';
|
|
10
|
+
import {
|
|
11
|
+
type EchelonFunctional,
|
|
12
|
+
type EchelonPredicate,
|
|
13
|
+
type ExpressionType,
|
|
14
|
+
TypeID,
|
|
15
|
+
type Typification
|
|
16
|
+
} from '@rsconcept/domain/rslang/semantic/typification';
|
|
17
|
+
|
|
18
|
+
import { type ConstituentaState, CstType, EvalStatus, RSToolAgent, type SessionState } from '../../src';
|
|
19
|
+
|
|
20
|
+
// ===== Brief format =====
|
|
21
|
+
|
|
22
|
+
export type BriefStatus = 'draft' | 'assumed' | 'confirmed';
|
|
23
|
+
export type BriefApproach = 'research' | 'education' | 'project';
|
|
24
|
+
|
|
25
|
+
export interface BriefRestriction {
|
|
26
|
+
id: string;
|
|
27
|
+
excludes: string;
|
|
28
|
+
inexpressible?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BriefRequirement {
|
|
32
|
+
id: string;
|
|
33
|
+
concept: string;
|
|
34
|
+
ref?: string;
|
|
35
|
+
via?: string;
|
|
36
|
+
constituent: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BriefDistinction {
|
|
40
|
+
id: string;
|
|
41
|
+
between: [string, string];
|
|
42
|
+
text?: string;
|
|
43
|
+
witness?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BriefInvariant {
|
|
47
|
+
id: string;
|
|
48
|
+
text: string;
|
|
49
|
+
ref?: string;
|
|
50
|
+
constituent: string | null;
|
|
51
|
+
positive?: string | null;
|
|
52
|
+
negative?: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BriefDecision {
|
|
56
|
+
id: string;
|
|
57
|
+
topic: string;
|
|
58
|
+
alternatives?: string[];
|
|
59
|
+
choice: string;
|
|
60
|
+
rationale: string;
|
|
61
|
+
supersedes?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Labelled value: labels for elements of `X#` / `C#`, numbers for `Z`, arrays for tuples and sets. */
|
|
65
|
+
export type LabelValue = string | number | LabelValue[];
|
|
66
|
+
|
|
67
|
+
export interface BriefExpectation {
|
|
68
|
+
of?: string;
|
|
69
|
+
expr?: string;
|
|
70
|
+
/** Arguments of `F#` / `P#` in labels. */
|
|
71
|
+
args?: LabelValue[];
|
|
72
|
+
value?: LabelValue;
|
|
73
|
+
holds?: boolean;
|
|
74
|
+
contains?: LabelValue[];
|
|
75
|
+
excludes?: LabelValue[];
|
|
76
|
+
note?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface BriefScenario {
|
|
80
|
+
id: string;
|
|
81
|
+
title: string;
|
|
82
|
+
base?: string;
|
|
83
|
+
values: Record<string, LabelValue>;
|
|
84
|
+
expect: BriefExpectation[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Brief {
|
|
88
|
+
version: 1;
|
|
89
|
+
status: BriefStatus;
|
|
90
|
+
approach: BriefApproach;
|
|
91
|
+
object: string;
|
|
92
|
+
task: string;
|
|
93
|
+
source?: string;
|
|
94
|
+
restrictions?: BriefRestriction[];
|
|
95
|
+
expressiveness: BriefRequirement[];
|
|
96
|
+
distinctions?: BriefDistinction[];
|
|
97
|
+
invariants?: BriefInvariant[];
|
|
98
|
+
decisions?: BriefDecision[];
|
|
99
|
+
scenarios?: BriefScenario[];
|
|
100
|
+
questions?: unknown[];
|
|
101
|
+
assumptions?: string[];
|
|
102
|
+
openQuestions?: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ===== Results =====
|
|
106
|
+
|
|
107
|
+
export type Stage = 'scope' | 'core' | 'explicate' | 'interpret' | 'review';
|
|
108
|
+
|
|
109
|
+
export interface ExpectationResult {
|
|
110
|
+
target: string;
|
|
111
|
+
passed: boolean;
|
|
112
|
+
expected: string;
|
|
113
|
+
actual: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ScenarioResult {
|
|
117
|
+
id: string;
|
|
118
|
+
title: string;
|
|
119
|
+
passed: boolean;
|
|
120
|
+
checks: ExpectationResult[];
|
|
121
|
+
error?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface Readiness {
|
|
125
|
+
stage: Stage;
|
|
126
|
+
ready: boolean;
|
|
127
|
+
diagnostics: { errors: number; warnings: number };
|
|
128
|
+
coverage: { expressiveness: string; invariants: string; distinctions: string };
|
|
129
|
+
scenarios: { passed: number; failed: number; missing: number };
|
|
130
|
+
blockers: string[];
|
|
131
|
+
warnings: string[];
|
|
132
|
+
next: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface BriefCheckInput {
|
|
136
|
+
/** Session JSON (`exportSession`). */
|
|
137
|
+
session: string;
|
|
138
|
+
brief: Brief;
|
|
139
|
+
/** Session JSON of the previous version: enables weakening checks. */
|
|
140
|
+
base?: string;
|
|
141
|
+
/** `review` — someone else's schema: blockers are report findings, not a plan of edits. */
|
|
142
|
+
mode?: 'build' | 'review';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface BriefCheckResult {
|
|
146
|
+
readiness: Readiness;
|
|
147
|
+
scenarios: ScenarioResult[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ===== Implementation =====
|
|
151
|
+
|
|
152
|
+
const UNDEFINED_TYPES: readonly string[] = [CstType.BASE, CstType.CONSTANT, CstType.STRUCTURED];
|
|
153
|
+
const BASIC_TYPES: readonly string[] = [CstType.BASE, CstType.CONSTANT];
|
|
154
|
+
const LOGIC_TYPES: readonly string[] = [CstType.AXIOM, CstType.STATEMENT];
|
|
155
|
+
const ALIAS_PATTERN = /\b([XCSDFPAT])(\d+)\b/g;
|
|
156
|
+
|
|
157
|
+
interface SchemaIndex {
|
|
158
|
+
byAlias: Map<string, ConstituentaState>;
|
|
159
|
+
items: ConstituentaState[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function indexSchema(state: SessionState): SchemaIndex {
|
|
163
|
+
return { byAlias: new Map(state.items.map(item => [item.alias, item])), items: state.items };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function typeOf(item: ConstituentaState): ExpressionType | null {
|
|
167
|
+
return item.analysis.type as unknown as ExpressionType | null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isUndefinedConcept(item: ConstituentaState): boolean {
|
|
171
|
+
return UNDEFINED_TYPES.includes(item.cstType);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function suppliersOf(item: ConstituentaState): string[] {
|
|
175
|
+
return [...item.definitionFormal.matchAll(ALIAS_PATTERN)]
|
|
176
|
+
.map(match => match[0])
|
|
177
|
+
.filter(alias => alias !== item.alias);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isTrivialFormula(item: ConstituentaState): boolean {
|
|
181
|
+
const formula = item.definitionFormal.replace(/\s+/g, '');
|
|
182
|
+
if (formula === '' || formula === '∅' || /^[XCSDFPAT]\d+$/.test(formula)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const equality = /^([XCSDFPAT]\d+)=([XCSDFPAT]\d+)$/.exec(formula);
|
|
186
|
+
return equality !== null && equality[1] === equality[2];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Maps element labels of basic sets to indices, per scenario. */
|
|
190
|
+
class LabelSpace {
|
|
191
|
+
private readonly labels = new Map<string, string[]>();
|
|
192
|
+
|
|
193
|
+
setBasic(alias: string, labels: string[]): Record<number, string> {
|
|
194
|
+
this.labels.set(alias, labels);
|
|
195
|
+
return Object.fromEntries(labels.map((label, index) => [index, label]));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
encode(value: LabelValue, type: Typification): Value {
|
|
199
|
+
switch (type.typeID) {
|
|
200
|
+
case TypeID.integer:
|
|
201
|
+
if (typeof value !== 'number') {
|
|
202
|
+
throw new Error(`ожидалось целое, получено ${JSON.stringify(value)}`);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
case TypeID.basic: {
|
|
206
|
+
const index = this.labels.get(type.baseID)?.indexOf(String(value)) ?? -1;
|
|
207
|
+
if (index < 0) {
|
|
208
|
+
throw new Error(`нет элемента «${String(value)}» в ${type.baseID}`);
|
|
209
|
+
}
|
|
210
|
+
return index;
|
|
211
|
+
}
|
|
212
|
+
case TypeID.tuple:
|
|
213
|
+
if (!Array.isArray(value) || value.length !== type.factors.length) {
|
|
214
|
+
throw new Error(`ожидался кортеж ${labelType(type)}, получено ${JSON.stringify(value)}`);
|
|
215
|
+
}
|
|
216
|
+
return [TUPLE_ID, ...value.map((component, index) => this.encode(component, type.factors[index]))];
|
|
217
|
+
case TypeID.collection: {
|
|
218
|
+
if (!Array.isArray(value)) {
|
|
219
|
+
throw new Error(`ожидалось множество ${labelType(type)}, получено ${JSON.stringify(value)}`);
|
|
220
|
+
}
|
|
221
|
+
const result = value.map(element => this.encode(element, type.base));
|
|
222
|
+
normalizeValue(result);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
throw new Error(`нельзя задать значение ступени ${labelType(type)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
decode(value: Value, type: Typification): LabelValue {
|
|
231
|
+
switch (type.typeID) {
|
|
232
|
+
case TypeID.basic:
|
|
233
|
+
return this.labels.get(type.baseID)?.[value as number] ?? `#${String(value)}`;
|
|
234
|
+
case TypeID.tuple:
|
|
235
|
+
return (value as Value[]).slice(1).map((component, index) => this.decode(component, type.factors[index]));
|
|
236
|
+
case TypeID.collection:
|
|
237
|
+
return (value as Value[]).map(element => this.decode(element, type.base));
|
|
238
|
+
default:
|
|
239
|
+
return value as number;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function print(value: LabelValue | undefined): string {
|
|
245
|
+
return JSON.stringify(value ?? null);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sameValue(a: Value, b: Value): boolean {
|
|
249
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveValues(brief: Brief, scenario: BriefScenario, depth = 0): Record<string, LabelValue> {
|
|
253
|
+
if (!scenario.base) {
|
|
254
|
+
return scenario.values;
|
|
255
|
+
}
|
|
256
|
+
const base = brief.scenarios?.find(item => item.id === scenario.base);
|
|
257
|
+
if (!base || depth > 16) {
|
|
258
|
+
throw new Error(`базовый сценарий ${scenario.base} не найден`);
|
|
259
|
+
}
|
|
260
|
+
return { ...resolveValues(brief, base, depth + 1), ...scenario.values };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Isolated session with scenario values loaded. */
|
|
264
|
+
class ScenarioRun {
|
|
265
|
+
private readonly tool = new RSToolAgent();
|
|
266
|
+
readonly space = new LabelSpace();
|
|
267
|
+
|
|
268
|
+
private readonly schema: SchemaIndex;
|
|
269
|
+
|
|
270
|
+
private constructor(schema: SchemaIndex) {
|
|
271
|
+
this.schema = schema;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
static async create(session: string, schema: SchemaIndex, values: Record<string, LabelValue>): Promise<ScenarioRun> {
|
|
275
|
+
const run = new ScenarioRun(schema);
|
|
276
|
+
run.tool.importData(session, 'session');
|
|
277
|
+
for (const alias of Object.keys(values)) {
|
|
278
|
+
const item = schema.byAlias.get(alias);
|
|
279
|
+
if (!item) {
|
|
280
|
+
throw new Error(`${alias}: конституента не найдена в схеме`);
|
|
281
|
+
}
|
|
282
|
+
if (!isUndefinedConcept(item)) {
|
|
283
|
+
throw new Error(`${alias}: значения задаются только неопределяемым понятиям (X#, C#, S#)`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const set: Array<{ target: number; value: unknown }> = [];
|
|
287
|
+
for (const item of schema.items.filter(cst => BASIC_TYPES.includes(cst.cstType))) {
|
|
288
|
+
const labels = (values[item.alias] ?? []) as LabelValue[];
|
|
289
|
+
set.push({ target: item.id, value: run.space.setBasic(item.alias, labels.map(String)) });
|
|
290
|
+
}
|
|
291
|
+
for (const item of schema.items.filter(cst => cst.cstType === CstType.STRUCTURED)) {
|
|
292
|
+
const type = typeOf(item) as Typification | null;
|
|
293
|
+
if (!type) {
|
|
294
|
+
throw new Error(`${item.alias}: нет типизации`);
|
|
295
|
+
}
|
|
296
|
+
const given = values[item.alias] ?? (type.typeID === TypeID.collection ? [] : undefined);
|
|
297
|
+
if (given !== undefined) {
|
|
298
|
+
set.push({ target: item.id, value: run.space.encode(given, type) });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await run.tool.setModelValues({ set } as Parameters<RSToolAgent['setModelValues']>[0]);
|
|
302
|
+
return run;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Evaluates a constituent, scratch expression or parametrized call. Returns value and its typification. */
|
|
306
|
+
evaluate(expectation: BriefExpectation): { value: Value; type: ExpressionType | null; status: number } {
|
|
307
|
+
if (expectation.expr) {
|
|
308
|
+
const cstType = expectation.holds !== undefined ? CstType.AXIOM : CstType.TERM;
|
|
309
|
+
const analysis = this.tool.analyzeExpression({ expression: expectation.expr, cstType });
|
|
310
|
+
const result = this.tool.evaluate({ expression: expectation.expr, cstType });
|
|
311
|
+
this.assertEvaluated(expectation.expr, result.status, result.success);
|
|
312
|
+
return {
|
|
313
|
+
value: result.value as Value,
|
|
314
|
+
type: analysis.type as unknown as ExpressionType | null,
|
|
315
|
+
status: result.status
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const item = this.schema.byAlias.get(expectation.of ?? '');
|
|
319
|
+
if (!item) {
|
|
320
|
+
throw new Error(`конституента ${expectation.of ?? '?'} не найдена`);
|
|
321
|
+
}
|
|
322
|
+
if (item.cstType === CstType.FUNCTION || item.cstType === CstType.PREDICATE) {
|
|
323
|
+
return this.evaluateCall(item, expectation.args ?? []);
|
|
324
|
+
}
|
|
325
|
+
const result = this.tool.evaluate({ constituentId: item.id });
|
|
326
|
+
this.assertEvaluated(item.alias, result.status, result.success);
|
|
327
|
+
return { value: result.value as Value, type: typeOf(item), status: result.status };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Graph of `F#` / `P#` over finite argument domains: rows `(args…, result)` or accepted args. */
|
|
331
|
+
graph(item: ConstituentaState): { rows: Value[]; type: EchelonFunctional | EchelonPredicate } {
|
|
332
|
+
const type = typeOf(item) as EchelonFunctional | EchelonPredicate | null;
|
|
333
|
+
if (!type) {
|
|
334
|
+
throw new Error(`${item.alias}: нет типизации`);
|
|
335
|
+
}
|
|
336
|
+
const domains = type.args.map(arg => labelType(arg.type));
|
|
337
|
+
if (domains.some(domain => /R\d|Z/.test(domain))) {
|
|
338
|
+
throw new Error(`${item.alias}: перебор аргументов с радикалами или Z не поддерживается — используй expr`);
|
|
339
|
+
}
|
|
340
|
+
const vars = type.args.map((_, index) => `α${index + 1}`);
|
|
341
|
+
const loops = vars.map((variable, index) => `${variable}:∈${domains[index]}`).join('; ');
|
|
342
|
+
const call = `${item.alias}[${vars.join(', ')}]`;
|
|
343
|
+
const isPredicate = item.cstType === CstType.PREDICATE;
|
|
344
|
+
const head = isPredicate ? (vars.length === 1 ? vars[0] : `(${vars.join(', ')})`) : `(${vars.join(', ')}, ${call})`;
|
|
345
|
+
const expression = isPredicate ? `I{${head} | ${loops}; ${call}}` : `I{${head} | ${loops}}`;
|
|
346
|
+
const result = this.tool.evaluate({ expression, cstType: CstType.TERM });
|
|
347
|
+
// enumerating a boolean domain (`α:∈ℬ(X1)`) is flagged as property usage, yet the value is computed
|
|
348
|
+
const onlyBooleanDomain =
|
|
349
|
+
result.diagnostics.length > 0 && result.diagnostics.every(item => item.name === 'invalidPropertyUsage');
|
|
350
|
+
this.assertEvaluated(`${item.alias}[…]`, result.status, result.success || onlyBooleanDomain);
|
|
351
|
+
return { rows: result.value as Value[], type };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Comparable value of a constituent: its value, or its graph for `F#` / `P#`. */
|
|
355
|
+
valueOf(item: ConstituentaState): Value {
|
|
356
|
+
if (item.cstType === CstType.FUNCTION || item.cstType === CstType.PREDICATE) {
|
|
357
|
+
return this.graph(item).rows;
|
|
358
|
+
}
|
|
359
|
+
return this.evaluate({ of: item.alias }).value;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private evaluateCall(item: ConstituentaState, args: LabelValue[]) {
|
|
363
|
+
const { rows, type } = this.graph(item);
|
|
364
|
+
if (type.args.length !== args.length) {
|
|
365
|
+
throw new Error(`${item.alias}: нужно ${type.args.length} аргумент(а) в args`);
|
|
366
|
+
}
|
|
367
|
+
const encodedArgs = args.map((arg, index) => this.space.encode(arg, type.args[index].type));
|
|
368
|
+
if (item.cstType === CstType.PREDICATE) {
|
|
369
|
+
const key = encodedArgs.length === 1 ? encodedArgs[0] : [TUPLE_ID, ...encodedArgs];
|
|
370
|
+
const holds = rows.some(row => sameValue(row, key));
|
|
371
|
+
return { value: holds ? 1 : 0, type: { typeID: TypeID.logic } as ExpressionType, status: EvalStatus.HAS_DATA };
|
|
372
|
+
}
|
|
373
|
+
const row = rows.find(candidate =>
|
|
374
|
+
encodedArgs.every((arg, index) => sameValue((candidate as Value[])[index + 1], arg))
|
|
375
|
+
) as Value[] | undefined;
|
|
376
|
+
if (!row) {
|
|
377
|
+
throw new Error(`${item.alias}: аргументы ${print(args)} вне области определения`);
|
|
378
|
+
}
|
|
379
|
+
return { value: row[row.length - 1], type: type.result as ExpressionType, status: EvalStatus.HAS_DATA };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private assertEvaluated(target: string, status: number, success: boolean) {
|
|
383
|
+
if (!success || status === EvalStatus.EVAL_FAIL || status === EvalStatus.INVALID_DATA) {
|
|
384
|
+
throw new Error(`${target}: вычисление не удалось (статус ${status})`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function describeExpectation(expectation: BriefExpectation): string {
|
|
390
|
+
if (expectation.holds !== undefined) {
|
|
391
|
+
return expectation.holds ? 'истина' : 'ложь';
|
|
392
|
+
}
|
|
393
|
+
if (expectation.value !== undefined) {
|
|
394
|
+
return print(expectation.value);
|
|
395
|
+
}
|
|
396
|
+
return (
|
|
397
|
+
[
|
|
398
|
+
expectation.contains ? `содержит ${print(expectation.contains)}` : '',
|
|
399
|
+
expectation.excludes ? `не содержит ${print(expectation.excludes)}` : ''
|
|
400
|
+
]
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.join(', ') || '—'
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function checkExpectation(run: ScenarioRun, expectation: BriefExpectation): ExpectationResult {
|
|
407
|
+
const target = expectation.expr ?? `${expectation.of ?? '?'}${expectation.args ? print(expectation.args) : ''}`;
|
|
408
|
+
const expected = describeExpectation(expectation);
|
|
409
|
+
try {
|
|
410
|
+
const { value, type } = run.evaluate(expectation);
|
|
411
|
+
if (expectation.holds !== undefined) {
|
|
412
|
+
const holds = value === 1;
|
|
413
|
+
return {
|
|
414
|
+
target,
|
|
415
|
+
passed: holds === expectation.holds,
|
|
416
|
+
expected,
|
|
417
|
+
actual: holds ? 'истина' : 'ложь'
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (!type || type.typeID === TypeID.logic || type.typeID === TypeID.function || type.typeID === TypeID.predicate) {
|
|
421
|
+
throw new Error('для логического выражения используй holds');
|
|
422
|
+
}
|
|
423
|
+
const actual = run.space.decode(value, type as Typification);
|
|
424
|
+
if (expectation.value !== undefined) {
|
|
425
|
+
return {
|
|
426
|
+
target,
|
|
427
|
+
passed: sameValue(run.space.encode(expectation.value, type as Typification), value),
|
|
428
|
+
expected,
|
|
429
|
+
actual: print(actual)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (type.typeID !== TypeID.collection) {
|
|
433
|
+
throw new Error('contains / excludes применимы только к множествам');
|
|
434
|
+
}
|
|
435
|
+
const elements = value as Value[];
|
|
436
|
+
const missing = (expectation.contains ?? []).filter(
|
|
437
|
+
element => !elements.some(item => sameValue(item, run.space.encode(element, type.base)))
|
|
438
|
+
);
|
|
439
|
+
const extra = (expectation.excludes ?? []).filter(element =>
|
|
440
|
+
elements.some(item => sameValue(item, run.space.encode(element, type.base)))
|
|
441
|
+
);
|
|
442
|
+
return {
|
|
443
|
+
target,
|
|
444
|
+
passed: missing.length === 0 && extra.length === 0,
|
|
445
|
+
expected,
|
|
446
|
+
actual: print(actual)
|
|
447
|
+
};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return { target, passed: false, expected, actual: `ошибка: ${(error as Error).message}` };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** In a scenario that is nobody's counterexample every axiom of the schema must hold. */
|
|
454
|
+
function admissibleChecks(run: ScenarioRun, schema: SchemaIndex, scenario: BriefScenario): ExpectationResult[] {
|
|
455
|
+
const listed = new Set(scenario.expect.map(expectation => expectation.of));
|
|
456
|
+
return schema.items
|
|
457
|
+
.filter(item => item.cstType === CstType.AXIOM && !listed.has(item.alias))
|
|
458
|
+
.map(item => ({
|
|
459
|
+
...checkExpectation(run, { of: item.alias, holds: true }),
|
|
460
|
+
target: `${item.alias} (допустимый сценарий)`
|
|
461
|
+
}))
|
|
462
|
+
.filter(check => !check.passed);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function runScenarios(session: string, schema: SchemaIndex, brief: Brief): Promise<ScenarioResult[]> {
|
|
466
|
+
const results: ScenarioResult[] = [];
|
|
467
|
+
const negatives = new Set((brief.invariants ?? []).map(inv => inv.negative).filter(Boolean));
|
|
468
|
+
for (const scenario of brief.scenarios ?? []) {
|
|
469
|
+
try {
|
|
470
|
+
const run = await ScenarioRun.create(session, schema, resolveValues(brief, scenario));
|
|
471
|
+
const checks = scenario.expect.map(expectation => checkExpectation(run, expectation));
|
|
472
|
+
if (!negatives.has(scenario.id)) {
|
|
473
|
+
checks.push(...admissibleChecks(run, schema, scenario));
|
|
474
|
+
}
|
|
475
|
+
results.push({ id: scenario.id, title: scenario.title, passed: checks.every(check => check.passed), checks });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
results.push({
|
|
478
|
+
id: scenario.id,
|
|
479
|
+
title: scenario.title,
|
|
480
|
+
passed: false,
|
|
481
|
+
checks: [],
|
|
482
|
+
error: (error as Error).message
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return results;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Distinction members resolved to constituent aliases. */
|
|
490
|
+
function resolveMember(brief: Brief, ref: string): string | null {
|
|
491
|
+
if (/^E\d+$/.test(ref)) {
|
|
492
|
+
return brief.expressiveness.find(item => item.id === ref)?.constituent ?? null;
|
|
493
|
+
}
|
|
494
|
+
return ref;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function distinctionCollapsed(
|
|
498
|
+
session: string,
|
|
499
|
+
schema: SchemaIndex,
|
|
500
|
+
brief: Brief,
|
|
501
|
+
members: [string, string],
|
|
502
|
+
witness: string | null | undefined
|
|
503
|
+
): Promise<'kept' | 'collapsed' | 'unchecked'> {
|
|
504
|
+
const [first, second] = members.map(alias => schema.byAlias.get(alias));
|
|
505
|
+
if (!first || !second || isUndefinedConcept(first) || isUndefinedConcept(second)) {
|
|
506
|
+
return 'kept';
|
|
507
|
+
}
|
|
508
|
+
if (labelType(typeOf(first)) !== labelType(typeOf(second))) {
|
|
509
|
+
return 'kept';
|
|
510
|
+
}
|
|
511
|
+
const candidates = (brief.scenarios ?? []).filter(scenario => !witness || scenario.id === witness);
|
|
512
|
+
let evaluated = 0;
|
|
513
|
+
for (const scenario of candidates) {
|
|
514
|
+
try {
|
|
515
|
+
const run = await ScenarioRun.create(session, schema, resolveValues(brief, scenario));
|
|
516
|
+
const a = run.valueOf(first);
|
|
517
|
+
const b = run.valueOf(second);
|
|
518
|
+
evaluated++;
|
|
519
|
+
if (!sameValue(a, b)) {
|
|
520
|
+
return 'kept';
|
|
521
|
+
}
|
|
522
|
+
} catch {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return evaluated > 0 ? 'collapsed' : 'unchecked';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function briefAliases(brief: Brief): Set<string> {
|
|
530
|
+
const aliases = new Set<string>();
|
|
531
|
+
for (const item of [...brief.expressiveness, ...(brief.invariants ?? [])]) {
|
|
532
|
+
if (item.constituent) {
|
|
533
|
+
aliases.add(item.constituent);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const scenario of brief.scenarios ?? []) {
|
|
537
|
+
for (const expectation of scenario.expect) {
|
|
538
|
+
if (expectation.of) {
|
|
539
|
+
aliases.add(expectation.of);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return aliases;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function findOrphans(schema: SchemaIndex, brief: Brief): string[] {
|
|
547
|
+
const needed = new Set<string>(briefAliases(brief));
|
|
548
|
+
for (const item of schema.items) {
|
|
549
|
+
if (item.cstType === CstType.AXIOM) {
|
|
550
|
+
needed.add(item.alias);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const queue = [...needed];
|
|
554
|
+
while (queue.length > 0) {
|
|
555
|
+
const item = schema.byAlias.get(queue.pop()!);
|
|
556
|
+
for (const supplier of item ? suppliersOf(item) : []) {
|
|
557
|
+
if (!needed.has(supplier)) {
|
|
558
|
+
needed.add(supplier);
|
|
559
|
+
queue.push(supplier);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return schema.items
|
|
564
|
+
.filter(item => item.cstType !== CstType.NOMINAL && !needed.has(item.alias))
|
|
565
|
+
.map(item => item.alias);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function weakeningIssues(
|
|
569
|
+
schema: SchemaIndex,
|
|
570
|
+
base: SchemaIndex,
|
|
571
|
+
brief: Brief
|
|
572
|
+
): { blockers: string[]; warnings: string[] } {
|
|
573
|
+
const blockers: string[] = [];
|
|
574
|
+
const warnings: string[] = [];
|
|
575
|
+
// only a new decision that supersedes an earlier one may justify removing or weakening a concept
|
|
576
|
+
const decisionsText = JSON.stringify((brief.decisions ?? []).filter(decision => decision.supersedes));
|
|
577
|
+
const linked = new Set([
|
|
578
|
+
...briefAliases(brief),
|
|
579
|
+
...base.items.filter(item => item.cstType === CstType.AXIOM).map(item => item.alias)
|
|
580
|
+
]);
|
|
581
|
+
for (const alias of linked) {
|
|
582
|
+
const before = base.byAlias.get(alias);
|
|
583
|
+
if (!before) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const justified = new RegExp(`\\b${alias}\\b`).test(decisionsText);
|
|
587
|
+
const report = (message: string) =>
|
|
588
|
+
(justified ? warnings : blockers).push(
|
|
589
|
+
justified
|
|
590
|
+
? `${message} (обосновано новым решением в журнале)`
|
|
591
|
+
: `${message} — без нового решения (supersedes) в журнале`
|
|
592
|
+
);
|
|
593
|
+
const after = schema.byAlias.get(alias);
|
|
594
|
+
if (!after) {
|
|
595
|
+
report(`${alias} «${before.term}» удалена`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (!isUndefinedConcept(before) && isUndefinedConcept(after)) {
|
|
599
|
+
report(`${alias}: производное понятие стало неопределяемым (${after.cstType})`);
|
|
600
|
+
} else if (!isTrivialFormula(before) && isTrivialFormula(after)) {
|
|
601
|
+
report(`${alias}: формула стала тривиальной («${after.definitionFormal}»)`);
|
|
602
|
+
} else if (before.cstType === CstType.AXIOM && before.definitionFormal !== after.definitionFormal) {
|
|
603
|
+
warnings.push(
|
|
604
|
+
`${alias}: формула аксиомы изменена — убедись, что сценарии инварианта проходят и смысл не ослаблен`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return { blockers, warnings };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const NEXT_STEP: Record<Stage, string> = {
|
|
612
|
+
scope: 'Этап 1 (rs-scope): заверши бриф и подтверди его у пользователя (или запиши допущения и статус assumed)',
|
|
613
|
+
core: 'Этап 2 (rs-core): выбери ядро и запиши для каждого требования, как оно выразится через ядро (поле via), и решения в журнал',
|
|
614
|
+
explicate: 'Этап 3 (rs-explicate): выведи недостающие понятия и аксиомы, исправь диагностики без потери смысла',
|
|
615
|
+
interpret: 'Этап 4 (rs-interpret): допиши недостающие сценарии и разбери провалы; при провале смысла вернись к ядру',
|
|
616
|
+
review: 'Этап 5 (rs-review): ревью по брифу и отчёт пользователю'
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const REVIEW_NEXT =
|
|
620
|
+
'Ревью (rs-review): блокеры и предупреждения — находки отчёта; схему без запроса не правь и новую сессию не собирай';
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Session description must carry the brief: Portal does not store the brief yet.
|
|
624
|
+
* Conventions and texts must not cite brief decisions: synthesis does not carry the brief,
|
|
625
|
+
* and conventions are not renumbered.
|
|
626
|
+
*/
|
|
627
|
+
function descriptionIssues(state: SessionState): string[] {
|
|
628
|
+
const issues: string[] = [];
|
|
629
|
+
if (state.comment.trim() === '') {
|
|
630
|
+
issues.push('Описание схемы пусто: запиши назначение, ограничения с невыразимым и краткий журнал решений');
|
|
631
|
+
}
|
|
632
|
+
const citing = state.items
|
|
633
|
+
.filter(item => /\bDC\d+\b/.test(`${item.convention} ${item.definitionText}`))
|
|
634
|
+
.map(item => item.alias);
|
|
635
|
+
if (citing.length > 0) {
|
|
636
|
+
issues.push(
|
|
637
|
+
`Конвенции или текстовые определения ссылаются на решения брифа (DC#): ${citing.join(', ')} — запиши решение словами, бриф не переносится при синтезе`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return issues;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Checks session against brief: diagnostics, coverage, scenarios, distinctions, weakening. */
|
|
644
|
+
export async function checkBrief(input: BriefCheckInput): Promise<BriefCheckResult> {
|
|
645
|
+
const { brief } = input;
|
|
646
|
+
const tool = new RSToolAgent();
|
|
647
|
+
tool.importData(input.session, 'session');
|
|
648
|
+
const state = tool.getSessionState('full') as SessionState;
|
|
649
|
+
const schema = indexSchema(state);
|
|
650
|
+
const isProject = brief.approach === 'project';
|
|
651
|
+
|
|
652
|
+
const blockers: string[] = [];
|
|
653
|
+
const warnings: string[] = [];
|
|
654
|
+
const stages = new Set<Stage>();
|
|
655
|
+
const block = (stage: Stage, message: string) => {
|
|
656
|
+
stages.add(stage);
|
|
657
|
+
blockers.push(message);
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
if (brief.status === 'draft') {
|
|
661
|
+
block('scope', 'Бриф в статусе draft: подтверди у пользователя или переведи в assumed с допущениями');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const diagnostics = tool.listDiagnostics();
|
|
665
|
+
const errors = diagnostics.filter(item => item.severity === 'error');
|
|
666
|
+
if (errors.length > 0) {
|
|
667
|
+
block(
|
|
668
|
+
'explicate',
|
|
669
|
+
`Диагностики: ${errors.length} ошибок (${[...new Set(errors.map(item => `${item.alias ?? '?'} ${item.name}`))].slice(0, 5).join('; ')})`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
for (const item of schema.items.filter(cst => isUndefinedConcept(cst) && cst.convention.trim() === '')) {
|
|
674
|
+
block('explicate', `${item.alias}: пустая конвенция у неопределяемого понятия`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const covered = brief.expressiveness.filter(req => req.constituent && schema.byAlias.has(req.constituent));
|
|
678
|
+
for (const req of brief.expressiveness) {
|
|
679
|
+
if (!req.via) {
|
|
680
|
+
block('core', `${req.id} «${req.concept}»: не записано, как требование выражается через ядро (via)`);
|
|
681
|
+
}
|
|
682
|
+
if (!req.constituent) {
|
|
683
|
+
block(
|
|
684
|
+
'explicate',
|
|
685
|
+
`${req.id} «${req.concept}»: нет конституенты — не хватает исходного понятия, понятие не выведено или нужен синтез`
|
|
686
|
+
);
|
|
687
|
+
} else if (!schema.byAlias.has(req.constituent)) {
|
|
688
|
+
block('explicate', `${req.id}: конституента ${req.constituent} не найдена в схеме`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const invariants = brief.invariants ?? [];
|
|
693
|
+
let missing = 0;
|
|
694
|
+
const invariantsCovered = invariants.filter(inv => {
|
|
695
|
+
const item = inv.constituent ? schema.byAlias.get(inv.constituent) : undefined;
|
|
696
|
+
return item !== undefined && LOGIC_TYPES.includes(item.cstType);
|
|
697
|
+
});
|
|
698
|
+
for (const inv of invariants) {
|
|
699
|
+
const item = inv.constituent ? schema.byAlias.get(inv.constituent) : undefined;
|
|
700
|
+
if (!item || !LOGIC_TYPES.includes(item.cstType)) {
|
|
701
|
+
block('explicate', `${inv.id} «${inv.text}»: нет аксиомы или высказывания`);
|
|
702
|
+
}
|
|
703
|
+
for (const side of ['positive', 'negative'] as const) {
|
|
704
|
+
if (!inv[side]) {
|
|
705
|
+
missing++;
|
|
706
|
+
const message = `${inv.id}: нет ${side === 'positive' ? 'позитивного' : 'негативного'} сценария`;
|
|
707
|
+
if (isProject) {
|
|
708
|
+
block('interpret', message);
|
|
709
|
+
} else {
|
|
710
|
+
warnings.push(message);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (brief.approach === 'education') {
|
|
717
|
+
const tested = briefAliases({ ...brief, expressiveness: [], invariants: [] });
|
|
718
|
+
for (const req of brief.expressiveness.filter(item => item.constituent && !tested.has(item.constituent))) {
|
|
719
|
+
missing++;
|
|
720
|
+
block('interpret', `${req.id}: нет сценария, проверяющего ${req.constituent}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const scenarios = await runScenarios(input.session, schema, brief);
|
|
725
|
+
const scenarioIds = new Set(scenarios.map(item => item.id));
|
|
726
|
+
for (const inv of invariants) {
|
|
727
|
+
for (const side of ['positive', 'negative'] as const) {
|
|
728
|
+
const id = inv[side];
|
|
729
|
+
if (id && !scenarioIds.has(id)) {
|
|
730
|
+
block('interpret', `${inv.id}: сценарий ${id} не найден`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const result of scenarios.filter(item => !item.passed)) {
|
|
735
|
+
const detail =
|
|
736
|
+
result.error ??
|
|
737
|
+
result.checks
|
|
738
|
+
.filter(check => !check.passed)
|
|
739
|
+
.map(check => `${check.target}: ожидалось ${check.expected}, получено ${check.actual}`)
|
|
740
|
+
.join('; ');
|
|
741
|
+
block('interpret', `${result.id} «${result.title}»: ${detail}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const distinctions = brief.distinctions ?? [];
|
|
745
|
+
let distinctionsKept = 0;
|
|
746
|
+
for (const distinction of distinctions) {
|
|
747
|
+
const members = distinction.between.map(ref => resolveMember(brief, ref));
|
|
748
|
+
if (members.some(member => !member || !schema.byAlias.has(member))) {
|
|
749
|
+
block('explicate', `${distinction.id}: понятия различения не выражены в схеме`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const verdict = await distinctionCollapsed(
|
|
753
|
+
input.session,
|
|
754
|
+
schema,
|
|
755
|
+
brief,
|
|
756
|
+
members as [string, string],
|
|
757
|
+
distinction.witness
|
|
758
|
+
);
|
|
759
|
+
if (verdict !== 'kept') {
|
|
760
|
+
const message =
|
|
761
|
+
verdict === 'collapsed'
|
|
762
|
+
? `${distinction.id}: ${members.join(' и ')} совпадают на всех сценариях${distinction.witness ? ` (свидетель ${distinction.witness})` : ''} — различение не подтверждено`
|
|
763
|
+
: `${distinction.id}: не удалось вычислить ${members.join(' и ')} ни в одном сценарии — различение не проверено`;
|
|
764
|
+
if (brief.approach === 'research') {
|
|
765
|
+
block('interpret', message);
|
|
766
|
+
} else {
|
|
767
|
+
warnings.push(message);
|
|
768
|
+
}
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
distinctionsKept++;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const orphans = findOrphans(schema, brief);
|
|
775
|
+
if (orphans.length > 0) {
|
|
776
|
+
const message = `Конституенты вне путей к требованиям и аксиомам: ${orphans.join(', ')}`;
|
|
777
|
+
if (isProject) {
|
|
778
|
+
block('explicate', message);
|
|
779
|
+
} else {
|
|
780
|
+
warnings.push(message);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (input.base) {
|
|
785
|
+
const baseTool = new RSToolAgent();
|
|
786
|
+
baseTool.importData(input.base, 'session');
|
|
787
|
+
const issues = weakeningIssues(schema, indexSchema(baseTool.getSessionState('full') as SessionState), brief);
|
|
788
|
+
issues.blockers.forEach(message => block('explicate', message));
|
|
789
|
+
warnings.push(...issues.warnings);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const req of brief.expressiveness) {
|
|
793
|
+
const item = req.constituent ? schema.byAlias.get(req.constituent) : undefined;
|
|
794
|
+
if (
|
|
795
|
+
item?.cstType === CstType.STRUCTURED &&
|
|
796
|
+
!(brief.decisions ?? []).some(dc => JSON.stringify(dc).includes(item.alias))
|
|
797
|
+
) {
|
|
798
|
+
warnings.push(
|
|
799
|
+
`${req.id}: выражено родовой структурой ${item.alias} — запиши решение, почему понятие не выводится`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
warnings.push(...descriptionIssues(state));
|
|
805
|
+
|
|
806
|
+
const order: Stage[] = ['scope', 'core', 'explicate', 'interpret', 'review'];
|
|
807
|
+
const stage = order.find(item => stages.has(item)) ?? 'review';
|
|
808
|
+
const passed = scenarios.filter(item => item.passed).length;
|
|
809
|
+
return {
|
|
810
|
+
scenarios,
|
|
811
|
+
readiness: {
|
|
812
|
+
stage,
|
|
813
|
+
ready: blockers.length === 0,
|
|
814
|
+
diagnostics: { errors: errors.length, warnings: diagnostics.length - errors.length },
|
|
815
|
+
coverage: {
|
|
816
|
+
expressiveness: `${covered.length}/${brief.expressiveness.length}`,
|
|
817
|
+
invariants: `${invariantsCovered.length}/${invariants.length}`,
|
|
818
|
+
distinctions: `${distinctionsKept}/${distinctions.length}`
|
|
819
|
+
},
|
|
820
|
+
scenarios: { passed, failed: scenarios.length - passed, missing },
|
|
821
|
+
blockers,
|
|
822
|
+
warnings,
|
|
823
|
+
next: input.mode === 'review' ? REVIEW_NEXT : NEXT_STEP[stage]
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|