@rsconcept/domain 2.2.0 → 3.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/dist/{diagnostics-Db1F7ruF.d.ts → diagnostics-DsEhmFh0.d.ts} +6 -4
- package/dist/{diagnostics-B1OfMra1.js → diagnostics-ndGYbvgh.js} +374 -15
- package/dist/diagnostics-ndGYbvgh.js.map +1 -0
- package/dist/index-DpiTybDB.d.ts +376 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/library/diagnostics.d.ts +1 -1
- package/dist/library/diagnostics.js +1 -1
- package/dist/library/index.d.ts +6 -5
- package/dist/library/index.js +1 -1
- package/dist/library/index.js.map +1 -1
- package/dist/library/oss-api.d.ts +1 -1
- package/dist/library/oss-api.js +1 -1
- package/dist/library/oss-api.js.map +1 -1
- package/dist/library/rsengine.d.ts +1 -1
- package/dist/library/rsengine.js +2 -2
- package/dist/library/rsform-api/index.d.ts +2 -146
- package/dist/library/rsform-api/index.js +2 -2
- package/dist/library/rsform.d.ts +2 -2
- package/dist/library/rsform.js.map +1 -1
- package/dist/library/rsmodel-api.d.ts +9 -4
- package/dist/library/rsmodel-api.js +7 -3
- package/dist/library/rsmodel-api.js.map +1 -1
- package/dist/library/rsmodel.d.ts +1 -1
- package/dist/library/structure-planner.d.ts +1 -1
- package/dist/library/structure-planner.js +2 -2
- package/dist/library/structure-planner.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/library/diagnostics.ts +20 -9
- package/src/library/index.ts +1 -0
- package/src/library/oss-api.ts +5 -1
- package/src/library/rsform-api/alias.ts +60 -4
- package/src/library/rsform-api/attribution.ts +87 -0
- package/src/library/rsform-api/cst-type.ts +7 -5
- package/src/library/rsform-api/expression.ts +3 -3
- package/src/library/rsform-api/formal-graph.test.ts +2 -2
- package/src/library/rsform-api/formal-graph.ts +6 -10
- package/src/library/rsform-api/index.ts +38 -2
- package/src/library/rsform-api/semantic-relations.ts +4 -7
- package/src/library/rsform-api/stats.ts +6 -5
- package/src/library/rsform-api/structure.ts +12 -6
- package/src/library/rsform-api/substitution.ts +82 -0
- package/src/library/rsform-api/synthesis.test.ts +157 -0
- package/src/library/rsform-api/synthesis.ts +149 -6
- package/src/library/rsform-api/text-resolution.test.ts +84 -0
- package/src/library/rsform-api/text-resolution.ts +152 -0
- package/src/library/rsform-api/transforms.test.ts +161 -0
- package/src/library/rsform-api/transforms.ts +42 -0
- package/src/library/rsform-api/types.ts +102 -4
- package/src/library/rsform.ts +1 -1
- package/src/library/rsmodel-api.ts +10 -5
- package/src/library/structure-planner.ts +6 -2
- package/src/library/template-instantiation.ts +11 -9
- package/dist/diagnostics-B1OfMra1.js.map +0 -1
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
addAliasReference,
|
|
7
|
+
allocateImportAliases,
|
|
7
8
|
applyMappingToConstituents,
|
|
8
9
|
argumentValuesToMapping,
|
|
10
|
+
buildSequentialAliasMapping,
|
|
9
11
|
generateAlias,
|
|
12
|
+
type MappableFields,
|
|
13
|
+
maxAliasIndex,
|
|
10
14
|
removeAliasReference,
|
|
11
15
|
validateAliasFormat,
|
|
12
16
|
validateNewAlias
|
|
13
17
|
} from './alias';
|
|
14
18
|
export { getAnalysisFor, refineAnalysisForDependencyCycles } from './analysis';
|
|
19
|
+
export { filterAttributions, importAttributions, remapAttributionsUnderSubstitution } from './attribution';
|
|
15
20
|
export {
|
|
16
21
|
canHaveManualTypification,
|
|
17
22
|
canProduceStructure,
|
|
@@ -26,7 +31,38 @@ export {
|
|
|
26
31
|
typeClassForCstType
|
|
27
32
|
} from './cst-type';
|
|
28
33
|
export { applyFilterCategory, inferStatus, inferTemplate, inferTemplatedType, normalizeExpression } from './expression';
|
|
29
|
-
export { restoreConstituentOrder } from './formal-graph';
|
|
34
|
+
export { buildFormalDependencyGraph, restoreConstituentOrder } from './formal-graph';
|
|
30
35
|
export { calculateSchemaStats, isSchemaIssue } from './stats';
|
|
31
36
|
export { findCstByStructure, getStructureName, inferNewSpawnPosition } from './structure';
|
|
32
|
-
export {
|
|
37
|
+
export { applyConstituentSubstitutions, remapSubstitutionsAfterImport, type SubstitutionResult } from './substitution';
|
|
38
|
+
export {
|
|
39
|
+
inlineSynthesis,
|
|
40
|
+
type InlineSynthesisInput,
|
|
41
|
+
type InlineSynthesisResult,
|
|
42
|
+
sortItemsForInlineSynthesis,
|
|
43
|
+
type SynthesizableFields
|
|
44
|
+
} from './synthesis';
|
|
45
|
+
export {
|
|
46
|
+
buildDefinitionReferenceGraph,
|
|
47
|
+
buildTermReferenceGraph,
|
|
48
|
+
resolveAllConstituentTexts,
|
|
49
|
+
resolveConstituentTextChange,
|
|
50
|
+
type TextChangeOptions
|
|
51
|
+
} from './text-resolution';
|
|
52
|
+
export { insertItemAfter, moveIdsInOrder, reorderItemsByIds } from './transforms';
|
|
53
|
+
export type {
|
|
54
|
+
AliasTypedFields,
|
|
55
|
+
BasicTextCheckFields,
|
|
56
|
+
DescribableFields,
|
|
57
|
+
DiagnosticSourceFields,
|
|
58
|
+
FormalOrderFields,
|
|
59
|
+
ModelEvalFields,
|
|
60
|
+
SchemaIssueFields,
|
|
61
|
+
SearchableFields,
|
|
62
|
+
SemanticRelations,
|
|
63
|
+
SpawnPathFields,
|
|
64
|
+
StructureCapableFields,
|
|
65
|
+
TemplateParamSourceFields,
|
|
66
|
+
TemplateSourceFields,
|
|
67
|
+
TextResolvableFields
|
|
68
|
+
} from './types';
|
|
@@ -8,16 +8,13 @@ import { CstType } from '../rsform';
|
|
|
8
8
|
|
|
9
9
|
import { isBaseSet, isFunctional } from './cst-type';
|
|
10
10
|
import { inferTemplate } from './expression';
|
|
11
|
-
import { type
|
|
11
|
+
import { type FormalOrderFields, type SemanticRelations } from './types';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Infer semantic parent/children used for clustering during restore order.
|
|
15
15
|
* Port of backend {@code SemanticInfo}.
|
|
16
16
|
*/
|
|
17
|
-
export function computeSemanticRelations(
|
|
18
|
-
items: readonly OrderableConstituenta[],
|
|
19
|
-
graph: Graph<number>
|
|
20
|
-
): SemanticRelations {
|
|
17
|
+
export function computeSemanticRelations(items: readonly FormalOrderFields[], graph: Graph<number>): SemanticRelations {
|
|
21
18
|
const byId = new Map(items.map(cst => [cst.id, cst]));
|
|
22
19
|
const byAlias = new Map(items.map(cst => [cst.alias, cst]));
|
|
23
20
|
const isTemplate = new Map<number, boolean>();
|
|
@@ -41,7 +38,7 @@ export function computeSemanticRelations(
|
|
|
41
38
|
return !isFunctional(base.cst_type) || splitTemplateDefinition(base.definition_formal).head !== head;
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
function extractSources(target:
|
|
41
|
+
function extractSources(target: FormalOrderFields): Set<number> {
|
|
45
42
|
const sources = new Set<number>();
|
|
46
43
|
if (!isFunctional(target.cst_type)) {
|
|
47
44
|
for (const parentId of graph.at(target.id)?.inputs ?? []) {
|
|
@@ -76,7 +73,7 @@ export function computeSemanticRelations(
|
|
|
76
73
|
return sources;
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
function inferSimple(target:
|
|
76
|
+
function inferSimple(target: FormalOrderFields): boolean {
|
|
80
77
|
if (target.cst_type === CstType.STRUCTURED || isBaseSet(target.cst_type)) {
|
|
81
78
|
return false;
|
|
82
79
|
}
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { TypeID, type Typification, ValueClass } from '../../rslang';
|
|
6
6
|
import { hasCstDiagnostic, RSDiagnosticCode } from '../diagnostics';
|
|
7
|
-
import {
|
|
7
|
+
import { CstStatus, CstType, type RSForm, type RSFormStats } from '../rsform';
|
|
8
8
|
|
|
9
9
|
import { isBasicConcept, isLogical } from './cst-type';
|
|
10
|
+
import { type BasicTextCheckFields, type SchemaIssueFields, type StructureCapableFields } from './types';
|
|
10
11
|
|
|
11
|
-
/** Checks if
|
|
12
|
-
export function isSchemaIssue(cst:
|
|
12
|
+
/** Checks if an item is a schema issue. */
|
|
13
|
+
export function isSchemaIssue(cst: SchemaIssueFields): boolean {
|
|
13
14
|
return (
|
|
14
15
|
(cst.diagnostics?.length ?? 0) > 0 || cst.status === CstStatus.INCORRECT || cst.status === CstStatus.INCALCULABLE
|
|
15
16
|
);
|
|
@@ -125,7 +126,7 @@ export function calculateSchemaStats(target: RSForm): RSFormStats {
|
|
|
125
126
|
return stats;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
function calculateStepComplexity(cst:
|
|
129
|
+
function calculateStepComplexity(cst: StructureCapableFields): number {
|
|
129
130
|
if (cst.cst_type === CstType.AXIOM || cst.cst_type === CstType.NOMINAL || !isBasicConcept(cst.cst_type)) {
|
|
130
131
|
return 0;
|
|
131
132
|
}
|
|
@@ -162,7 +163,7 @@ function calculateTypificationComplexity(type: Typification): number {
|
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/** Non-logical basic concept missing convention and/or resolved term (trimmed). */
|
|
165
|
-
function isMissingBasicText(cst:
|
|
166
|
+
function isMissingBasicText(cst: BasicTextCheckFields): boolean {
|
|
166
167
|
if (!isBasicConcept(cst.cst_type) || isLogical(cst.cst_type)) {
|
|
167
168
|
return false;
|
|
168
169
|
}
|
|
@@ -7,6 +7,8 @@ import { isTypification } from '../../rslang/semantic/typification';
|
|
|
7
7
|
import { applyPath } from '../../rslang/semantic/typification-api';
|
|
8
8
|
import { type Constituenta, type RSForm } from '../rsform';
|
|
9
9
|
|
|
10
|
+
import { type SpawnPathFields, type StructureCapableFields } from './types';
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Id to pass as `insert_after` when creating a new {@link Constituenta} spawned by `targetId`:
|
|
12
14
|
* after the last existing child of that spawner, or after the spawner itself if none.
|
|
@@ -21,8 +23,8 @@ export function inferNewSpawnPosition(schema: RSForm, targetId: number): number
|
|
|
21
23
|
return last ?? targetId;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
/** Finds
|
|
25
|
-
export function findCstByStructure(schema: RSForm, target:
|
|
26
|
+
/** Finds schema item by absolute structure path under `targetId`. */
|
|
27
|
+
export function findCstByStructure(schema: RSForm, target: { id: number }, path: TypePath): Constituenta | null {
|
|
26
28
|
const byId = schema.cstByID ?? new Map(schema.items.map(item => [item.id, item]));
|
|
27
29
|
for (const cst of schema.items) {
|
|
28
30
|
if (!cst.spawner_path) {
|
|
@@ -36,8 +38,12 @@ export function findCstByStructure(schema: RSForm, target: Constituenta, path: T
|
|
|
36
38
|
return null;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/** Retrieves name for piece of target
|
|
40
|
-
export function getStructureName(
|
|
41
|
+
/** Retrieves name for piece of target structure. */
|
|
42
|
+
export function getStructureName(
|
|
43
|
+
schema: RSForm,
|
|
44
|
+
target: StructureCapableFields & { id: number },
|
|
45
|
+
path: TypePath
|
|
46
|
+
): string {
|
|
41
47
|
const representation = findCstByStructure(schema, target, path);
|
|
42
48
|
if (representation) {
|
|
43
49
|
return `${representation.alias}: ${representation.term_resolved}`;
|
|
@@ -56,9 +62,9 @@ export function getStructureName(schema: RSForm, target: Constituenta, path: Typ
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function resolveAbsoluteSpawnerPath(
|
|
59
|
-
cst:
|
|
65
|
+
cst: SpawnPathFields,
|
|
60
66
|
targetId: number,
|
|
61
|
-
byId: Map<number,
|
|
67
|
+
byId: Map<number, SpawnPathFields>
|
|
62
68
|
): TypePath | null {
|
|
63
69
|
if (!cst.spawner_path || cst.spawner == null) {
|
|
64
70
|
return null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constituenta substitution and post-import substitution remapping.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type AliasMapping } from '../../rslang/api';
|
|
6
|
+
import { type Attribution, type Substitution } from '../rsform';
|
|
7
|
+
|
|
8
|
+
import { applyMappingToConstituents, type MappableFields } from './alias';
|
|
9
|
+
import { remapAttributionsUnderSubstitution } from './attribution';
|
|
10
|
+
|
|
11
|
+
/** Result of applying substitutions to a schema fragment. */
|
|
12
|
+
export interface SubstitutionResult<T> {
|
|
13
|
+
items: T[];
|
|
14
|
+
attributions: Attribution[];
|
|
15
|
+
deletedIds: Set<number>;
|
|
16
|
+
/** Replacement constituent ids (survivors referenced by substitutions). */
|
|
17
|
+
replacementIds: number[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Apply constituenta substitutions: remap attributions, drop originals, rewrite aliases in survivors.
|
|
22
|
+
* Does not resolve texts — call {@link resolveAllConstituentTexts} afterwards.
|
|
23
|
+
*/
|
|
24
|
+
export function applyConstituentSubstitutions<T extends MappableFields & { id: number; alias: string }>(
|
|
25
|
+
items: readonly T[],
|
|
26
|
+
attributions: readonly Attribution[],
|
|
27
|
+
substitutions: readonly Substitution[]
|
|
28
|
+
): SubstitutionResult<T> {
|
|
29
|
+
if (substitutions.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
items: [...items],
|
|
32
|
+
attributions: attributions.map(attr => ({ ...attr })),
|
|
33
|
+
deletedIds: new Set(),
|
|
34
|
+
replacementIds: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const byId = new Map(items.map(cst => [cst.id, cst]));
|
|
39
|
+
const mapping: AliasMapping = {};
|
|
40
|
+
const deletedIds = new Set<number>();
|
|
41
|
+
const origToSub = new Map<number, number>();
|
|
42
|
+
const replacementIds: number[] = [];
|
|
43
|
+
|
|
44
|
+
for (const { original, substitution } of substitutions) {
|
|
45
|
+
const originalCst = byId.get(original);
|
|
46
|
+
const substitutionCst = byId.get(substitution);
|
|
47
|
+
if (!originalCst || !substitutionCst) {
|
|
48
|
+
throw new Error(`applyConstituentSubstitutions: unknown constituenta ${original} -> ${substitution}`);
|
|
49
|
+
}
|
|
50
|
+
mapping[originalCst.alias] = substitutionCst.alias;
|
|
51
|
+
deletedIds.add(original);
|
|
52
|
+
origToSub.set(original, substitution);
|
|
53
|
+
replacementIds.push(substitution);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const nextItems = items.filter(cst => !deletedIds.has(cst.id));
|
|
57
|
+
applyMappingToConstituents(nextItems, mapping, false);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
items: nextItems,
|
|
61
|
+
attributions: remapAttributionsUnderSubstitution(attributions, origToSub),
|
|
62
|
+
deletedIds,
|
|
63
|
+
replacementIds
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remap substitution endpoints after source constituents were inserted under new ids.
|
|
69
|
+
* If {@link original} was imported, map it; otherwise map {@link substitution}.
|
|
70
|
+
*/
|
|
71
|
+
export function remapSubstitutionsAfterImport(
|
|
72
|
+
substitutions: readonly Substitution[],
|
|
73
|
+
sourceIdSet: ReadonlySet<number>,
|
|
74
|
+
idMap: Readonly<Record<number, number>>
|
|
75
|
+
): Substitution[] {
|
|
76
|
+
return substitutions.map(sub => {
|
|
77
|
+
if (sourceIdSet.has(sub.original)) {
|
|
78
|
+
return { original: idMap[sub.original], substitution: sub.substitution };
|
|
79
|
+
}
|
|
80
|
+
return { original: sub.original, substitution: idMap[sub.substitution] };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LibraryItemType } from '../library';
|
|
4
|
+
import { CstType, type RSForm } from '../rsform';
|
|
5
|
+
|
|
6
|
+
import { inlineSynthesis, sortItemsForInlineSynthesis } from './synthesis';
|
|
7
|
+
|
|
8
|
+
function item(partial: {
|
|
9
|
+
id: number;
|
|
10
|
+
alias: string;
|
|
11
|
+
cst_type: CstType;
|
|
12
|
+
definition_formal?: string;
|
|
13
|
+
term_raw?: string;
|
|
14
|
+
definition_raw?: string;
|
|
15
|
+
}) {
|
|
16
|
+
return {
|
|
17
|
+
id: partial.id,
|
|
18
|
+
alias: partial.alias,
|
|
19
|
+
cst_type: partial.cst_type,
|
|
20
|
+
definition_formal: partial.definition_formal ?? '',
|
|
21
|
+
typification_manual: '',
|
|
22
|
+
term_raw: partial.term_raw ?? '',
|
|
23
|
+
term_resolved: partial.term_raw ?? '',
|
|
24
|
+
term_forms: [] as { text: string; tags: string }[],
|
|
25
|
+
definition_raw: partial.definition_raw ?? '',
|
|
26
|
+
definition_resolved: partial.definition_raw ?? ''
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('inlineSynthesis', () => {
|
|
31
|
+
it('keeps source aliases when receiver is empty', () => {
|
|
32
|
+
const result = inlineSynthesis({
|
|
33
|
+
receiverItems: [],
|
|
34
|
+
sourceItems: [item({ id: 10, alias: 'X9', cst_type: CstType.BASE, term_raw: 'person' })],
|
|
35
|
+
nextId: 1
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.items).toHaveLength(1);
|
|
39
|
+
expect(result.items[0].alias).toBe('X9');
|
|
40
|
+
expect(result.items[0].id).toBe(1);
|
|
41
|
+
expect(result.idMap).toEqual({ 10: 1 });
|
|
42
|
+
expect(result.nextId).toBe(2);
|
|
43
|
+
expect(result.aliasMapping).toEqual({});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('allocates fresh aliases when receiver is non-empty', () => {
|
|
47
|
+
const result = inlineSynthesis({
|
|
48
|
+
receiverItems: [item({ id: 1, alias: 'X2', cst_type: CstType.BASE })],
|
|
49
|
+
sourceItems: [item({ id: 10, alias: 'X9', cst_type: CstType.BASE, term_raw: 'source' })],
|
|
50
|
+
nextId: 100
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.items).toHaveLength(2);
|
|
54
|
+
expect(result.items[1].alias).toBe('X3');
|
|
55
|
+
expect(result.items[1].id).toBe(100);
|
|
56
|
+
expect(result.aliasMapping).toEqual({ X9: 'X3' });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('applies substitutions after import (source original → receiver survivor)', () => {
|
|
60
|
+
const result = inlineSynthesis({
|
|
61
|
+
receiverItems: [item({ id: 1, alias: 'X1', cst_type: CstType.BASE, term_raw: 'keep' })],
|
|
62
|
+
sourceItems: [
|
|
63
|
+
item({
|
|
64
|
+
id: 20,
|
|
65
|
+
alias: 'X1',
|
|
66
|
+
cst_type: CstType.BASE,
|
|
67
|
+
term_raw: 'drop',
|
|
68
|
+
definition_formal: ''
|
|
69
|
+
})
|
|
70
|
+
],
|
|
71
|
+
substitutions: [{ original: 20, substitution: 1 }],
|
|
72
|
+
nextId: 50
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result.items).toHaveLength(1);
|
|
76
|
+
expect(result.items[0].id).toBe(1);
|
|
77
|
+
expect(result.items[0].term_raw).toBe('keep');
|
|
78
|
+
expect(result.deletedIds).toEqual(new Set([50]));
|
|
79
|
+
expect(result.replacementIds).toEqual([1]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('remaps formal references when importing into non-empty receiver', () => {
|
|
83
|
+
const result = inlineSynthesis({
|
|
84
|
+
receiverItems: [item({ id: 1, alias: 'X1', cst_type: CstType.BASE })],
|
|
85
|
+
sourceItems: [
|
|
86
|
+
item({ id: 10, alias: 'X1', cst_type: CstType.BASE }),
|
|
87
|
+
item({
|
|
88
|
+
id: 11,
|
|
89
|
+
alias: 'S1',
|
|
90
|
+
cst_type: CstType.STRUCTURED,
|
|
91
|
+
definition_formal: 'ℬ(X1)'
|
|
92
|
+
})
|
|
93
|
+
],
|
|
94
|
+
nextId: 2
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const structured = result.items.find(cst => cst.cst_type === CstType.STRUCTURED)!;
|
|
98
|
+
expect(structured.alias).toBe('S1');
|
|
99
|
+
expect(structured.definition_formal).toBe('ℬ(X2)');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('imports attributions with id remapping', () => {
|
|
103
|
+
const result = inlineSynthesis({
|
|
104
|
+
receiverItems: [item({ id: 1, alias: 'X1', cst_type: CstType.BASE })],
|
|
105
|
+
receiverAttributions: [],
|
|
106
|
+
sourceItems: [
|
|
107
|
+
item({ id: 10, alias: 'X1', cst_type: CstType.BASE }),
|
|
108
|
+
item({ id: 11, alias: 'S1', cst_type: CstType.STRUCTURED })
|
|
109
|
+
],
|
|
110
|
+
sourceAttributions: [{ container: 10, attribute: 11 }],
|
|
111
|
+
nextId: 2
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.attributions).toEqual([{ container: 2, attribute: 3 }]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns a clone when source is empty', () => {
|
|
118
|
+
const receiver = [item({ id: 1, alias: 'X1', cst_type: CstType.BASE })];
|
|
119
|
+
const result = inlineSynthesis({
|
|
120
|
+
receiverItems: receiver,
|
|
121
|
+
sourceItems: [],
|
|
122
|
+
nextId: 2
|
|
123
|
+
});
|
|
124
|
+
expect(result.items).toEqual(receiver);
|
|
125
|
+
expect(result.items).not.toBe(receiver);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('sortItemsForInlineSynthesis', () => {
|
|
130
|
+
function libraryItem(partial: { id: number; location: string; owner: number | null; visible: boolean }) {
|
|
131
|
+
return {
|
|
132
|
+
id: partial.id,
|
|
133
|
+
item_type: LibraryItemType.RSFORM,
|
|
134
|
+
alias: `S${partial.id}`,
|
|
135
|
+
title: `Item ${partial.id}`,
|
|
136
|
+
description: '',
|
|
137
|
+
visible: partial.visible,
|
|
138
|
+
read_only: false,
|
|
139
|
+
location: partial.location,
|
|
140
|
+
access_policy: 'public' as const,
|
|
141
|
+
time_create: '',
|
|
142
|
+
time_update: '',
|
|
143
|
+
owner: partial.owner
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it('orders by location, then owner+visible, then visible, then rest', () => {
|
|
148
|
+
const receiver = { location: '/a', owner: 1 } as RSForm;
|
|
149
|
+
const sameLocation = libraryItem({ id: 1, location: '/a', owner: 99, visible: false });
|
|
150
|
+
const sameOwnerVisible = libraryItem({ id: 2, location: '/b', owner: 1, visible: true });
|
|
151
|
+
const otherVisible = libraryItem({ id: 3, location: '/c', owner: 2, visible: true });
|
|
152
|
+
const hidden = libraryItem({ id: 4, location: '/d', owner: 2, visible: false });
|
|
153
|
+
|
|
154
|
+
const sorted = sortItemsForInlineSynthesis(receiver, [hidden, otherVisible, sameOwnerVisible, sameLocation]);
|
|
155
|
+
expect(sorted.map(item => item.id)).toEqual([1, 2, 3, 4]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -1,26 +1,169 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Schema embedding (synthesis): insert constituents from a source schema into a receiver,
|
|
3
|
+
* then apply an identification (substitution) table.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors backend `inline_synthesis` / `insert_from` + `substitute`
|
|
6
|
+
* (Portal UI: «Встраивание схемы»).
|
|
3
7
|
*/
|
|
4
8
|
|
|
9
|
+
import { type AliasMapping } from '../../rslang/api';
|
|
5
10
|
import { type LibraryItem } from '../library';
|
|
6
|
-
import { type RSForm } from '../rsform';
|
|
11
|
+
import { type Attribution, type CstType, type RSForm, type Substitution } from '../rsform';
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
import { allocateImportAliases, applyMappingToConstituents, type MappableFields } from './alias';
|
|
14
|
+
import { importAttributions } from './attribution';
|
|
15
|
+
import { applyConstituentSubstitutions, remapSubstitutionsAfterImport } from './substitution';
|
|
16
|
+
import { resolveAllConstituentTexts } from './text-resolution';
|
|
17
|
+
import { type AliasTypedFields, type TextResolvableFields } from './types';
|
|
18
|
+
|
|
19
|
+
/** Field intersection required for embedding / synthesis transforms. */
|
|
20
|
+
export type SynthesizableFields = MappableFields &
|
|
21
|
+
TextResolvableFields &
|
|
22
|
+
AliasTypedFields & {
|
|
23
|
+
id: number;
|
|
24
|
+
alias: string;
|
|
25
|
+
cst_type: CstType;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Input for {@link inlineSynthesis}. */
|
|
29
|
+
export interface InlineSynthesisInput<T extends SynthesizableFields> {
|
|
30
|
+
/** Receiver schema constituents (order preserved; imports append). */
|
|
31
|
+
receiverItems: readonly T[];
|
|
32
|
+
/** Receiver attributions (default empty). */
|
|
33
|
+
receiverAttributions?: readonly Attribution[];
|
|
34
|
+
/**
|
|
35
|
+
* Source constituents to insert (already filtered to the desired subset).
|
|
36
|
+
* Cloned; originals are not mutated.
|
|
37
|
+
*/
|
|
38
|
+
sourceItems: readonly T[];
|
|
39
|
+
/** Source attributions whose both endpoints are among {@link sourceItems}. */
|
|
40
|
+
sourceAttributions?: readonly Attribution[];
|
|
41
|
+
/**
|
|
42
|
+
* Identification table using pre-import ids:
|
|
43
|
+
* one endpoint is a source id, the other a receiver id
|
|
44
|
+
* (same rules as backend embedding / InlineSynthesis).
|
|
45
|
+
*/
|
|
46
|
+
substitutions?: readonly Substitution[];
|
|
47
|
+
/** Next free numeric id for inserted constituents. */
|
|
48
|
+
nextId: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Result of {@link inlineSynthesis}. */
|
|
52
|
+
export interface InlineSynthesisResult<T extends SynthesizableFields> {
|
|
53
|
+
items: T[];
|
|
54
|
+
attributions: Attribution[];
|
|
55
|
+
/** Updated next-id after inserts. */
|
|
56
|
+
nextId: number;
|
|
57
|
+
/** Source id → inserted id. */
|
|
58
|
+
idMap: Record<number, number>;
|
|
59
|
+
/** Alias remapping applied to imported constituents (empty when receiver was empty). */
|
|
60
|
+
aliasMapping: AliasMapping;
|
|
61
|
+
/** Ids removed by substitutions. */
|
|
62
|
+
deletedIds: Set<number>;
|
|
63
|
+
/** Ids of newly inserted constituents (before any were deleted by substitutions). */
|
|
64
|
+
insertedIds: number[];
|
|
65
|
+
/** Survivor ids referenced by applied substitutions. */
|
|
66
|
+
replacementIds: number[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Embed {@link InlineSynthesisInput.sourceItems} into the receiver and apply substitutions.
|
|
71
|
+
* Resolves term/definition entity references on the result.
|
|
72
|
+
* Does not mutate inputs.
|
|
73
|
+
*/
|
|
74
|
+
export function inlineSynthesis<T extends SynthesizableFields>(
|
|
75
|
+
input: InlineSynthesisInput<T>
|
|
76
|
+
): InlineSynthesisResult<T> {
|
|
77
|
+
const sourceItems = input.sourceItems;
|
|
78
|
+
if (sourceItems.length === 0) {
|
|
79
|
+
return {
|
|
80
|
+
items: input.receiverItems.map(item => structuredClone(item)),
|
|
81
|
+
attributions: (input.receiverAttributions ?? []).map(attr => ({ ...attr })),
|
|
82
|
+
nextId: input.nextId,
|
|
83
|
+
idMap: {},
|
|
84
|
+
aliasMapping: {},
|
|
85
|
+
deletedIds: new Set(),
|
|
86
|
+
insertedIds: [],
|
|
87
|
+
replacementIds: []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const receiverItems = input.receiverItems.map(item => structuredClone(item));
|
|
92
|
+
let attributions = (input.receiverAttributions ?? []).map(attr => ({ ...attr }));
|
|
93
|
+
const aliasMapping = allocateImportAliases(receiverItems, sourceItems);
|
|
94
|
+
const idMap: Record<number, number> = {};
|
|
95
|
+
const inserted: T[] = [];
|
|
96
|
+
const sourceIdSet = new Set(sourceItems.map(cst => cst.id));
|
|
97
|
+
let nextId = input.nextId;
|
|
98
|
+
|
|
99
|
+
for (const cst of sourceItems) {
|
|
100
|
+
const newId = nextId;
|
|
101
|
+
nextId += 1;
|
|
102
|
+
idMap[cst.id] = newId;
|
|
103
|
+
|
|
104
|
+
const cloned = structuredClone(cst);
|
|
105
|
+
cloned.id = newId;
|
|
106
|
+
if (Object.keys(aliasMapping).length > 0) {
|
|
107
|
+
cloned.alias = aliasMapping[cst.alias];
|
|
108
|
+
applyMappingToConstituents([cloned], aliasMapping, false);
|
|
109
|
+
}
|
|
110
|
+
inserted.push(cloned);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
attributions = importAttributions(input.sourceAttributions ?? [], sourceIdSet, idMap, attributions);
|
|
114
|
+
const items = receiverItems.concat(inserted);
|
|
115
|
+
resolveAllConstituentTexts(items);
|
|
116
|
+
|
|
117
|
+
const substitutions = input.substitutions ?? [];
|
|
118
|
+
if (substitutions.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
items,
|
|
121
|
+
attributions,
|
|
122
|
+
nextId,
|
|
123
|
+
idMap,
|
|
124
|
+
aliasMapping,
|
|
125
|
+
deletedIds: new Set(),
|
|
126
|
+
insertedIds: inserted.map(cst => cst.id),
|
|
127
|
+
replacementIds: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const remapped = remapSubstitutionsAfterImport(substitutions, sourceIdSet, idMap);
|
|
132
|
+
const substituted = applyConstituentSubstitutions(items, attributions, remapped);
|
|
133
|
+
resolveAllConstituentTexts(substituted.items);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
items: substituted.items,
|
|
137
|
+
attributions: substituted.attributions,
|
|
138
|
+
nextId,
|
|
139
|
+
idMap,
|
|
140
|
+
aliasMapping,
|
|
141
|
+
deletedIds: substituted.deletedIds,
|
|
142
|
+
insertedIds: inserted.map(cst => cst.id),
|
|
143
|
+
replacementIds: substituted.replacementIds
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Sorts library items relevant for schema embedding with specified {@link RSForm}. */
|
|
9
148
|
export function sortItemsForInlineSynthesis(receiver: RSForm, items: readonly LibraryItem[]): LibraryItem[] {
|
|
10
149
|
const result = items.filter(item => item.location === receiver.location);
|
|
150
|
+
const included = new Set(result);
|
|
11
151
|
for (const item of items) {
|
|
12
|
-
if (item.visible && item.owner === receiver.owner && !
|
|
152
|
+
if (item.visible && item.owner === receiver.owner && !included.has(item)) {
|
|
13
153
|
result.push(item);
|
|
154
|
+
included.add(item);
|
|
14
155
|
}
|
|
15
156
|
}
|
|
16
157
|
for (const item of items) {
|
|
17
|
-
if (item.visible && !
|
|
158
|
+
if (item.visible && !included.has(item)) {
|
|
18
159
|
result.push(item);
|
|
160
|
+
included.add(item);
|
|
19
161
|
}
|
|
20
162
|
}
|
|
21
163
|
for (const item of items) {
|
|
22
|
-
if (!
|
|
164
|
+
if (!included.has(item)) {
|
|
23
165
|
result.push(item);
|
|
166
|
+
included.add(item);
|
|
24
167
|
}
|
|
25
168
|
}
|
|
26
169
|
return result;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { resolveConstituentTextChange } from './text-resolution';
|
|
4
|
+
import { type TextResolvableFields } from './types';
|
|
5
|
+
|
|
6
|
+
function row(data: Partial<TextResolvableFields> & Pick<TextResolvableFields, 'alias' | 'id'>): TextResolvableFields {
|
|
7
|
+
return {
|
|
8
|
+
id: data.id,
|
|
9
|
+
alias: data.alias,
|
|
10
|
+
definition_raw: data.definition_raw ?? '',
|
|
11
|
+
definition_resolved: data.definition_resolved ?? '',
|
|
12
|
+
term_raw: data.term_raw ?? data.alias,
|
|
13
|
+
term_resolved: data.term_resolved ?? data.term_raw ?? data.alias,
|
|
14
|
+
term_forms: data.term_forms ?? []
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('resolveConstituentTextChange', () => {
|
|
19
|
+
it('cascades term changes into dependent terms and definitions', () => {
|
|
20
|
+
const items = [
|
|
21
|
+
row({ id: 1, alias: 'T1', term_raw: 'base concept', term_resolved: 'base concept' }),
|
|
22
|
+
row({
|
|
23
|
+
id: 2,
|
|
24
|
+
alias: 'T2',
|
|
25
|
+
term_raw: '@{T1|nomn,sing}',
|
|
26
|
+
term_resolved: 'base concept',
|
|
27
|
+
definition_raw: '@{T1|nomn,sing}',
|
|
28
|
+
definition_resolved: 'base concept',
|
|
29
|
+
term_forms: [{ text: 'stale manual form', tags: 'gent,sing' }]
|
|
30
|
+
}),
|
|
31
|
+
row({ id: 3, alias: 'T3', definition_raw: '@{T2|nomn,sing}', definition_resolved: 'base concept' })
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
items[0].term_raw = 'updated concept';
|
|
35
|
+
resolveConstituentTextChange(items, 1, {
|
|
36
|
+
termChanged: true,
|
|
37
|
+
termRawChanged: true,
|
|
38
|
+
definitionRawChanged: false,
|
|
39
|
+
clearTargetForms: true
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(items[0].term_resolved).toBe('updated concept');
|
|
43
|
+
expect(items[1].term_resolved).toBe('updated concept');
|
|
44
|
+
expect(items[1].term_forms).toStrictEqual([]);
|
|
45
|
+
expect(items[1].definition_resolved).toBe('updated concept');
|
|
46
|
+
expect(items[2].definition_resolved).toBe('updated concept');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('uses updated manual forms as a term change trigger', () => {
|
|
50
|
+
const items = [
|
|
51
|
+
row({ id: 1, alias: 'T1', term_raw: 'base concept', term_resolved: 'base concept' }),
|
|
52
|
+
row({ id: 2, alias: 'T2', term_raw: '@{T1|gent,sing}', term_resolved: 'base concept' })
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
items[0].term_forms = [{ text: 'manual genitive singular', tags: 'gent,sing' }];
|
|
56
|
+
resolveConstituentTextChange(items, 1, {
|
|
57
|
+
termChanged: true,
|
|
58
|
+
termRawChanged: false,
|
|
59
|
+
definitionRawChanged: false,
|
|
60
|
+
clearTargetForms: false
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(items[1].term_resolved).toBe('manual genitive singular');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not cascade definition-only edits', () => {
|
|
67
|
+
const items = [
|
|
68
|
+
row({ id: 1, alias: 'T1', term_raw: 'base concept', term_resolved: 'base concept' }),
|
|
69
|
+
row({ id: 2, alias: 'T2', definition_raw: '@{T1|nomn,sing}', definition_resolved: 'base concept' }),
|
|
70
|
+
row({ id: 3, alias: 'T3', definition_raw: '@{T2|nomn,sing}', definition_resolved: 'old T2' })
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
items[1].definition_raw = 'Only @{T1|nomn,sing}';
|
|
74
|
+
resolveConstituentTextChange(items, 2, {
|
|
75
|
+
termChanged: false,
|
|
76
|
+
termRawChanged: false,
|
|
77
|
+
definitionRawChanged: true,
|
|
78
|
+
clearTargetForms: false
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(items[1].definition_resolved).toBe('Only base concept');
|
|
82
|
+
expect(items[2].definition_resolved).toBe('old T2');
|
|
83
|
+
});
|
|
84
|
+
});
|