@grafema/util 0.3.0-beta
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/LICENSE +190 -0
- package/dist/api/GraphAPI.d.ts +87 -0
- package/dist/api/GraphAPI.d.ts.map +1 -0
- package/dist/api/GraphAPI.js +212 -0
- package/dist/api/GraphAPI.js.map +1 -0
- package/dist/api/GuaranteeAPI.d.ts +147 -0
- package/dist/api/GuaranteeAPI.d.ts.map +1 -0
- package/dist/api/GuaranteeAPI.js +290 -0
- package/dist/api/GuaranteeAPI.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +214 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +441 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/CoverageAnalyzer.d.ts +65 -0
- package/dist/core/CoverageAnalyzer.d.ts.map +1 -0
- package/dist/core/CoverageAnalyzer.js +199 -0
- package/dist/core/CoverageAnalyzer.js.map +1 -0
- package/dist/core/FileExplainer.d.ts +101 -0
- package/dist/core/FileExplainer.d.ts.map +1 -0
- package/dist/core/FileExplainer.js +140 -0
- package/dist/core/FileExplainer.js.map +1 -0
- package/dist/core/FileOverview.d.ts +124 -0
- package/dist/core/FileOverview.d.ts.map +1 -0
- package/dist/core/FileOverview.js +279 -0
- package/dist/core/FileOverview.js.map +1 -0
- package/dist/core/GrafemaUri.d.ts +66 -0
- package/dist/core/GrafemaUri.d.ts.map +1 -0
- package/dist/core/GrafemaUri.js +191 -0
- package/dist/core/GrafemaUri.js.map +1 -0
- package/dist/core/GraphBackend.d.ts +158 -0
- package/dist/core/GraphBackend.d.ts.map +1 -0
- package/dist/core/GraphBackend.js +85 -0
- package/dist/core/GraphBackend.js.map +1 -0
- package/dist/core/GraphFreshnessChecker.d.ts +33 -0
- package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
- package/dist/core/GraphFreshnessChecker.js +104 -0
- package/dist/core/GraphFreshnessChecker.js.map +1 -0
- package/dist/core/GuaranteeManager.d.ts +254 -0
- package/dist/core/GuaranteeManager.d.ts.map +1 -0
- package/dist/core/GuaranteeManager.js +447 -0
- package/dist/core/GuaranteeManager.js.map +1 -0
- package/dist/core/HashUtils.d.ts +24 -0
- package/dist/core/HashUtils.d.ts.map +1 -0
- package/dist/core/HashUtils.js +46 -0
- package/dist/core/HashUtils.js.map +1 -0
- package/dist/core/IncrementalReanalyzer.d.ts +33 -0
- package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
- package/dist/core/IncrementalReanalyzer.js +67 -0
- package/dist/core/IncrementalReanalyzer.js.map +1 -0
- package/dist/core/ResourceRegistry.d.ts +17 -0
- package/dist/core/ResourceRegistry.d.ts.map +1 -0
- package/dist/core/ResourceRegistry.js +32 -0
- package/dist/core/ResourceRegistry.js.map +1 -0
- package/dist/core/SemanticId.d.ts +159 -0
- package/dist/core/SemanticId.d.ts.map +1 -0
- package/dist/core/SemanticId.js +291 -0
- package/dist/core/SemanticId.js.map +1 -0
- package/dist/core/VersionManager.d.ts +166 -0
- package/dist/core/VersionManager.d.ts.map +1 -0
- package/dist/core/VersionManager.js +239 -0
- package/dist/core/VersionManager.js.map +1 -0
- package/dist/core/brandNodeInternal.d.ts +14 -0
- package/dist/core/brandNodeInternal.d.ts.map +1 -0
- package/dist/core/brandNodeInternal.js +4 -0
- package/dist/core/brandNodeInternal.js.map +1 -0
- package/dist/core/nodes/GuaranteeNode.d.ts +76 -0
- package/dist/core/nodes/GuaranteeNode.d.ts.map +1 -0
- package/dist/core/nodes/GuaranteeNode.js +118 -0
- package/dist/core/nodes/GuaranteeNode.js.map +1 -0
- package/dist/core/nodes/IssueNode.d.ts +73 -0
- package/dist/core/nodes/IssueNode.d.ts.map +1 -0
- package/dist/core/nodes/IssueNode.js +130 -0
- package/dist/core/nodes/IssueNode.js.map +1 -0
- package/dist/core/nodes/NodeKind.d.ts +104 -0
- package/dist/core/nodes/NodeKind.d.ts.map +1 -0
- package/dist/core/nodes/NodeKind.js +166 -0
- package/dist/core/nodes/NodeKind.js.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts +103 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.js +133 -0
- package/dist/diagnostics/DiagnosticCollector.js.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts +122 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.js +300 -0
- package/dist/diagnostics/DiagnosticReporter.js.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.js +44 -0
- package/dist/diagnostics/DiagnosticWriter.js.map +1 -0
- package/dist/diagnostics/categories.d.ts +57 -0
- package/dist/diagnostics/categories.d.ts.map +1 -0
- package/dist/diagnostics/categories.js +71 -0
- package/dist/diagnostics/categories.js.map +1 -0
- package/dist/diagnostics/index.d.ts +17 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +15 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/errors/GrafemaError.d.ts +200 -0
- package/dist/errors/GrafemaError.d.ts.map +1 -0
- package/dist/errors/GrafemaError.js +209 -0
- package/dist/errors/GrafemaError.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions/index.d.ts +8 -0
- package/dist/instructions/index.d.ts.map +1 -0
- package/dist/instructions/index.js +20 -0
- package/dist/instructions/index.js.map +1 -0
- package/dist/instructions/onboarding.md +133 -0
- package/dist/knowledge/KnowledgeBase.d.ts +113 -0
- package/dist/knowledge/KnowledgeBase.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeBase.js +420 -0
- package/dist/knowledge/KnowledgeBase.js.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts +59 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.js +160 -0
- package/dist/knowledge/SemanticAddressResolver.js.map +1 -0
- package/dist/knowledge/git-ingest.d.ts +58 -0
- package/dist/knowledge/git-ingest.d.ts.map +1 -0
- package/dist/knowledge/git-ingest.js +301 -0
- package/dist/knowledge/git-ingest.js.map +1 -0
- package/dist/knowledge/git-queries.d.ts +86 -0
- package/dist/knowledge/git-queries.d.ts.map +1 -0
- package/dist/knowledge/git-queries.js +177 -0
- package/dist/knowledge/git-queries.js.map +1 -0
- package/dist/knowledge/index.d.ts +14 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +10 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/parser.d.ts +39 -0
- package/dist/knowledge/parser.d.ts.map +1 -0
- package/dist/knowledge/parser.js +292 -0
- package/dist/knowledge/parser.js.map +1 -0
- package/dist/knowledge/types.d.ts +133 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +8 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/logging/Logger.d.ts +98 -0
- package/dist/logging/Logger.d.ts.map +1 -0
- package/dist/logging/Logger.js +274 -0
- package/dist/logging/Logger.js.map +1 -0
- package/dist/notation/archetypes.d.ts +36 -0
- package/dist/notation/archetypes.d.ts.map +1 -0
- package/dist/notation/archetypes.js +173 -0
- package/dist/notation/archetypes.js.map +1 -0
- package/dist/notation/fold.d.ts +25 -0
- package/dist/notation/fold.d.ts.map +1 -0
- package/dist/notation/fold.js +598 -0
- package/dist/notation/fold.js.map +1 -0
- package/dist/notation/index.d.ts +18 -0
- package/dist/notation/index.d.ts.map +1 -0
- package/dist/notation/index.js +16 -0
- package/dist/notation/index.js.map +1 -0
- package/dist/notation/lodExtractor.d.ts +32 -0
- package/dist/notation/lodExtractor.d.ts.map +1 -0
- package/dist/notation/lodExtractor.js +149 -0
- package/dist/notation/lodExtractor.js.map +1 -0
- package/dist/notation/nameShortener.d.ts +22 -0
- package/dist/notation/nameShortener.d.ts.map +1 -0
- package/dist/notation/nameShortener.js +24 -0
- package/dist/notation/nameShortener.js.map +1 -0
- package/dist/notation/perspectives.d.ts +11 -0
- package/dist/notation/perspectives.d.ts.map +1 -0
- package/dist/notation/perspectives.js +16 -0
- package/dist/notation/perspectives.js.map +1 -0
- package/dist/notation/renderer.d.ts +31 -0
- package/dist/notation/renderer.d.ts.map +1 -0
- package/dist/notation/renderer.js +315 -0
- package/dist/notation/renderer.js.map +1 -0
- package/dist/notation/traceRenderer.d.ts +39 -0
- package/dist/notation/traceRenderer.d.ts.map +1 -0
- package/dist/notation/traceRenderer.js +358 -0
- package/dist/notation/traceRenderer.js.map +1 -0
- package/dist/notation/types.d.ts +66 -0
- package/dist/notation/types.d.ts.map +1 -0
- package/dist/notation/types.js +10 -0
- package/dist/notation/types.js.map +1 -0
- package/dist/queries/NodeContext.d.ts +81 -0
- package/dist/queries/NodeContext.d.ts.map +1 -0
- package/dist/queries/NodeContext.js +196 -0
- package/dist/queries/NodeContext.js.map +1 -0
- package/dist/queries/findCallsInFunction.d.ts +62 -0
- package/dist/queries/findCallsInFunction.d.ts.map +1 -0
- package/dist/queries/findCallsInFunction.js +169 -0
- package/dist/queries/findCallsInFunction.js.map +1 -0
- package/dist/queries/findContainingFunction.d.ts +57 -0
- package/dist/queries/findContainingFunction.d.ts.map +1 -0
- package/dist/queries/findContainingFunction.js +91 -0
- package/dist/queries/findContainingFunction.js.map +1 -0
- package/dist/queries/index.d.ts +18 -0
- package/dist/queries/index.d.ts.map +1 -0
- package/dist/queries/index.js +14 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/traceDataflow.d.ts +65 -0
- package/dist/queries/traceDataflow.d.ts.map +1 -0
- package/dist/queries/traceDataflow.js +754 -0
- package/dist/queries/traceDataflow.js.map +1 -0
- package/dist/queries/traceValues.d.ts +70 -0
- package/dist/queries/traceValues.d.ts.map +1 -0
- package/dist/queries/traceValues.js +373 -0
- package/dist/queries/traceValues.js.map +1 -0
- package/dist/queries/types.d.ts +166 -0
- package/dist/queries/types.d.ts.map +1 -0
- package/dist/queries/types.js +10 -0
- package/dist/queries/types.js.map +1 -0
- package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
- package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/GraphSchemaExtractor.js +125 -0
- package/dist/schema/GraphSchemaExtractor.js.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.js +113 -0
- package/dist/schema/InterfaceSchemaExtractor.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts +356 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.js +748 -0
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -0
- package/dist/storage/backends/typeValidation.d.ts +47 -0
- package/dist/storage/backends/typeValidation.d.ts.map +1 -0
- package/dist/storage/backends/typeValidation.js +141 -0
- package/dist/storage/backends/typeValidation.js.map +1 -0
- package/dist/utils/findRfdbBinary.d.ts +67 -0
- package/dist/utils/findRfdbBinary.d.ts.map +1 -0
- package/dist/utils/findRfdbBinary.js +261 -0
- package/dist/utils/findRfdbBinary.js.map +1 -0
- package/dist/utils/lazyDownload.d.ts +43 -0
- package/dist/utils/lazyDownload.d.ts.map +1 -0
- package/dist/utils/lazyDownload.js +175 -0
- package/dist/utils/lazyDownload.js.map +1 -0
- package/dist/utils/moduleResolution.d.ts +134 -0
- package/dist/utils/moduleResolution.d.ts.map +1 -0
- package/dist/utils/moduleResolution.js +189 -0
- package/dist/utils/moduleResolution.js.map +1 -0
- package/dist/utils/resolveNodeFile.d.ts +13 -0
- package/dist/utils/resolveNodeFile.d.ts.map +1 -0
- package/dist/utils/resolveNodeFile.js +18 -0
- package/dist/utils/resolveNodeFile.js.map +1 -0
- package/dist/utils/startRfdbServer.d.ts +63 -0
- package/dist/utils/startRfdbServer.d.ts.map +1 -0
- package/dist/utils/startRfdbServer.js +142 -0
- package/dist/utils/startRfdbServer.js.map +1 -0
- package/dist/validation/PathValidator.d.ts +80 -0
- package/dist/validation/PathValidator.d.ts.map +1 -0
- package/dist/validation/PathValidator.js +252 -0
- package/dist/validation/PathValidator.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +26 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
- package/src/api/GraphAPI.ts +307 -0
- package/src/api/GuaranteeAPI.ts +402 -0
- package/src/config/ConfigLoader.ts +653 -0
- package/src/config/index.ts +13 -0
- package/src/core/CoverageAnalyzer.ts +243 -0
- package/src/core/FileExplainer.ts +179 -0
- package/src/core/FileOverview.ts +397 -0
- package/src/core/GrafemaUri.ts +216 -0
- package/src/core/GraphBackend.ts +266 -0
- package/src/core/GraphFreshnessChecker.ts +145 -0
- package/src/core/GuaranteeManager.ts +684 -0
- package/src/core/HashUtils.ts +48 -0
- package/src/core/IncrementalReanalyzer.ts +106 -0
- package/src/core/ResourceRegistry.ts +39 -0
- package/src/core/SemanticId.ts +423 -0
- package/src/core/VersionManager.ts +405 -0
- package/src/core/brandNodeInternal.ts +16 -0
- package/src/core/nodes/GuaranteeNode.ts +162 -0
- package/src/core/nodes/IssueNode.ts +177 -0
- package/src/core/nodes/NodeKind.ts +192 -0
- package/src/diagnostics/DiagnosticCollector.ts +170 -0
- package/src/diagnostics/DiagnosticReporter.ts +395 -0
- package/src/diagnostics/DiagnosticWriter.ts +50 -0
- package/src/diagnostics/categories.ts +104 -0
- package/src/diagnostics/index.ts +30 -0
- package/src/errors/GrafemaError.ts +297 -0
- package/src/index.ts +261 -0
- package/src/instructions/index.ts +21 -0
- package/src/instructions/onboarding.md +133 -0
- package/src/knowledge/KnowledgeBase.ts +486 -0
- package/src/knowledge/SemanticAddressResolver.ts +191 -0
- package/src/knowledge/git-ingest.ts +402 -0
- package/src/knowledge/git-queries.ts +269 -0
- package/src/knowledge/index.ts +29 -0
- package/src/knowledge/parser.ts +294 -0
- package/src/knowledge/types.ts +146 -0
- package/src/logging/Logger.ts +303 -0
- package/src/notation/archetypes.ts +189 -0
- package/src/notation/fold.ts +696 -0
- package/src/notation/index.ts +27 -0
- package/src/notation/lodExtractor.ts +177 -0
- package/src/notation/nameShortener.ts +24 -0
- package/src/notation/perspectives.ts +18 -0
- package/src/notation/renderer.ts +394 -0
- package/src/notation/traceRenderer.ts +458 -0
- package/src/notation/types.ts +79 -0
- package/src/queries/NodeContext.ts +280 -0
- package/src/queries/findCallsInFunction.ts +249 -0
- package/src/queries/findContainingFunction.ts +124 -0
- package/src/queries/index.ts +44 -0
- package/src/queries/traceDataflow.ts +838 -0
- package/src/queries/traceValues.ts +531 -0
- package/src/queries/types.ts +191 -0
- package/src/schema/GraphSchemaExtractor.ts +177 -0
- package/src/schema/InterfaceSchemaExtractor.ts +173 -0
- package/src/schema/index.ts +5 -0
- package/src/storage/backends/RFDBServerBackend.ts +895 -0
- package/src/storage/backends/typeValidation.ts +154 -0
- package/src/utils/findRfdbBinary.ts +288 -0
- package/src/utils/lazyDownload.ts +206 -0
- package/src/utils/moduleResolution.ts +271 -0
- package/src/utils/resolveNodeFile.ts +18 -0
- package/src/utils/startRfdbServer.ts +197 -0
- package/src/validation/PathValidator.ts +334 -0
- package/src/version.ts +28 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notation Fold Engine — structural compression of sibling blocks
|
|
3
|
+
*
|
|
4
|
+
* Pure function: NotationBlock[] → NotationBlock[]
|
|
5
|
+
* Fold is a view-layer transform, not part of DSL grammar.
|
|
6
|
+
*
|
|
7
|
+
* Implements 11 rules from notation-folding.md:
|
|
8
|
+
* Rules 1-5: structural (language-agnostic)
|
|
9
|
+
* Rules 6-7: cleanup (dedup, artifacts)
|
|
10
|
+
* Rules 8-11: semantic (derivation/call patterns)
|
|
11
|
+
*
|
|
12
|
+
* Pipeline order matters — chains/dispatch collapse adjacent siblings first,
|
|
13
|
+
* making the remaining set more amenable to group folding.
|
|
14
|
+
*
|
|
15
|
+
* @module notation/fold
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { NotationBlock } from './types.js';
|
|
19
|
+
|
|
20
|
+
/** Minimum group size to trigger folding */
|
|
21
|
+
const FOLD_THRESHOLD = 3;
|
|
22
|
+
|
|
23
|
+
// === Node Rendering Roles ===
|
|
24
|
+
|
|
25
|
+
type NodeRole = 'actor' | 'container' | 'binding' | 'datum' | 'shape' | 'control';
|
|
26
|
+
|
|
27
|
+
const CONTAINER_TYPES = new Set(['MODULE', 'CLASS', 'OBJECT', 'NAMESPACE', 'PROGRAM']);
|
|
28
|
+
const BINDING_TYPES = new Set(['IMPORT', 'IMPORT_BINDING', 'EXPORT', 'EXPORT_BINDING']);
|
|
29
|
+
const SHAPE_TYPES = new Set(['INTERFACE', 'TYPE_ALIAS', 'ENUM', 'ENUM_MEMBER']);
|
|
30
|
+
const CONTROL_TYPES = new Set(['LOOP', 'BRANCH', 'CASE', 'CONDITIONAL', 'SWITCH', 'TRY']);
|
|
31
|
+
const BEHAVIOR_OPERATORS = new Set(['>', '=>', '>x', '~>>']);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Classify a block's rendering role based on node type + edge content.
|
|
35
|
+
*
|
|
36
|
+
* Roles (determined by graph shape, not AST type alone):
|
|
37
|
+
* Actor — has behavior edges (calls, writes, throws...) → Block
|
|
38
|
+
* Container — MODULE, CLASS, etc. → Block
|
|
39
|
+
* Binding — IMPORT, EXPORT → Line
|
|
40
|
+
* Datum — VARIABLE/CONSTANT with only passive edges → Inline `name < verb source`
|
|
41
|
+
* Shape — INTERFACE, TYPE_ALIAS, ENUM → Block or Name
|
|
42
|
+
* Control — LOOP, BRANCH, CASE → Modifier
|
|
43
|
+
*
|
|
44
|
+
* No suppression: all nodes with edges are preserved (Inv-2 Side Effect Visibility).
|
|
45
|
+
*/
|
|
46
|
+
function classifyRole(block: NotationBlock): NodeRole {
|
|
47
|
+
const type = block.nodeType;
|
|
48
|
+
|
|
49
|
+
if (CONTAINER_TYPES.has(type)) return 'container';
|
|
50
|
+
if (BINDING_TYPES.has(type)) return 'binding';
|
|
51
|
+
if (SHAPE_TYPES.has(type)) return 'shape';
|
|
52
|
+
if (CONTROL_TYPES.has(type)) return 'control';
|
|
53
|
+
|
|
54
|
+
// Check for behavior edges
|
|
55
|
+
const hasBehavior = block.lines.some(l => BEHAVIOR_OPERATORS.has(l.operator));
|
|
56
|
+
if (hasBehavior) return 'actor';
|
|
57
|
+
|
|
58
|
+
// VARIABLE/CONSTANT/PARAMETER with only passive edges → datum
|
|
59
|
+
if (type === 'VARIABLE' || type === 'CONSTANT' || type === 'PARAMETER') {
|
|
60
|
+
return 'datum';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// FUNCTION/METHOD → actor (even without visible behavior)
|
|
64
|
+
if (type === 'FUNCTION' || type === 'METHOD') return 'actor';
|
|
65
|
+
|
|
66
|
+
return 'actor';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply role-based transforms: datum → inline (preserves archetype operator and verb).
|
|
71
|
+
*/
|
|
72
|
+
function applyRoleTransforms(blocks: NotationBlock[]): NotationBlock[] {
|
|
73
|
+
const result: NotationBlock[] = [];
|
|
74
|
+
|
|
75
|
+
for (const block of blocks) {
|
|
76
|
+
if (block.foldMeta) {
|
|
77
|
+
result.push(block);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const role = classifyRole(block);
|
|
82
|
+
|
|
83
|
+
switch (role) {
|
|
84
|
+
case 'datum': {
|
|
85
|
+
// Inline: render as "name < verb source" — no braces, preserves archetype
|
|
86
|
+
const inlineName = formatDatumInline(block);
|
|
87
|
+
result.push({
|
|
88
|
+
...block,
|
|
89
|
+
displayName: inlineName,
|
|
90
|
+
lines: [],
|
|
91
|
+
children: [],
|
|
92
|
+
foldMeta: { kind: 'datum-inline', count: 1 },
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
default:
|
|
98
|
+
result.push(block);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format a datum block as a single inline string: "name < verb source"
|
|
107
|
+
* Preserves the archetype operator and verb — no information loss.
|
|
108
|
+
*/
|
|
109
|
+
function formatDatumInline(block: NotationBlock): string {
|
|
110
|
+
// Find the first flow_in line
|
|
111
|
+
for (const line of block.lines) {
|
|
112
|
+
if (line.operator && line.targets.length > 0) {
|
|
113
|
+
const mod = line.modifier ? `${line.modifier} ` : '';
|
|
114
|
+
return `${block.displayName} ${mod}${line.operator} ${line.verb} ${line.targets.join(', ')}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return block.displayName;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// === Block Signature ===
|
|
121
|
+
|
|
122
|
+
interface BlockSignature {
|
|
123
|
+
nodeType: string;
|
|
124
|
+
lineKeys: string[];
|
|
125
|
+
childCount: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function computeSignature(block: NotationBlock): BlockSignature {
|
|
129
|
+
return {
|
|
130
|
+
nodeType: block.nodeType,
|
|
131
|
+
lineKeys: block.lines
|
|
132
|
+
.map(l => `${l.modifier ?? ''}|${l.operator}|${l.verb}`)
|
|
133
|
+
.sort(),
|
|
134
|
+
childCount: block.children.length,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function signatureKey(sig: BlockSignature): string {
|
|
139
|
+
return `${sig.nodeType}|${sig.lineKeys.join(';')}|${sig.childCount}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// === Main fold function ===
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Apply all folding rules to a list of sibling blocks.
|
|
146
|
+
* Returns a new list where repetitive structures are compressed.
|
|
147
|
+
*
|
|
148
|
+
* Folding is recursive — children of non-folded blocks are also folded.
|
|
149
|
+
*/
|
|
150
|
+
export function foldBlocks(blocks: NotationBlock[]): NotationBlock[] {
|
|
151
|
+
// Rule 4: Dedup by nodeId (per-block cleanup, always applies)
|
|
152
|
+
let current = dedup(blocks);
|
|
153
|
+
|
|
154
|
+
// Rule 6: Target dedup within each block's lines (per-block cleanup, always applies)
|
|
155
|
+
current = current.map(dedupTargets);
|
|
156
|
+
|
|
157
|
+
if (current.length <= 1) {
|
|
158
|
+
// Role transforms still apply to single blocks
|
|
159
|
+
current = applyRoleTransforms(current);
|
|
160
|
+
// Still recurse into children
|
|
161
|
+
return current.map(b => {
|
|
162
|
+
if (b.foldMeta || b.children.length === 0) return b;
|
|
163
|
+
return { ...b, children: foldBlocks(b.children) };
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Rules 8/9: Chain detection and linearization (before role transforms — needs REFERENCE blocks)
|
|
168
|
+
const chainResult = detectChains(current);
|
|
169
|
+
current = [...chainResult.chains, ...chainResult.remaining];
|
|
170
|
+
|
|
171
|
+
// Rule 11: Case dispatch detection
|
|
172
|
+
const dispatchResult = detectDispatch(current);
|
|
173
|
+
current = [...dispatchResult.dispatches, ...dispatchResult.remaining];
|
|
174
|
+
|
|
175
|
+
// Rule 10: Repetitive call fold
|
|
176
|
+
const callResult = detectRepetitiveCalls(current);
|
|
177
|
+
current = [...callResult.folds, ...callResult.remaining];
|
|
178
|
+
|
|
179
|
+
// Role transforms: datum → inline, internal → suppress (after semantic rules)
|
|
180
|
+
current = applyRoleTransforms(current);
|
|
181
|
+
|
|
182
|
+
// Separate already-folded from remaining
|
|
183
|
+
const alreadyFolded = current.filter(b => b.foldMeta);
|
|
184
|
+
const unfolded = current.filter(b => !b.foldMeta);
|
|
185
|
+
|
|
186
|
+
// Rule 7: Repeated leaf fold
|
|
187
|
+
const leafResult = repeatedLeafFold(unfolded);
|
|
188
|
+
|
|
189
|
+
// Rule 5: Structural suppression
|
|
190
|
+
const trivialResult = structuralSuppression(leafResult.remaining);
|
|
191
|
+
|
|
192
|
+
// Rules 1-3: Group fold with anomaly preservation and source aggregation
|
|
193
|
+
const groupResult = groupFold(trivialResult.remaining);
|
|
194
|
+
|
|
195
|
+
// Combine: early folds, group folds, leaf folds, trivial groups, anomalies
|
|
196
|
+
const result = [
|
|
197
|
+
...alreadyFolded,
|
|
198
|
+
...groupResult.folded,
|
|
199
|
+
...leafResult.folds,
|
|
200
|
+
...trivialResult.groups,
|
|
201
|
+
...groupResult.anomalies,
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
// Recurse: fold children of non-folded blocks
|
|
205
|
+
return result.map(b => {
|
|
206
|
+
if (b.foldMeta || b.children.length === 0) return b;
|
|
207
|
+
return { ...b, children: foldBlocks(b.children) };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// === Rule 4: Dedup ===
|
|
212
|
+
|
|
213
|
+
function dedup(blocks: NotationBlock[]): NotationBlock[] {
|
|
214
|
+
const seen = new Map<string, NotationBlock>();
|
|
215
|
+
for (const block of blocks) {
|
|
216
|
+
const existing = seen.get(block.nodeId);
|
|
217
|
+
if (!existing) {
|
|
218
|
+
seen.set(block.nodeId, block);
|
|
219
|
+
} else {
|
|
220
|
+
// Keep the richer block (more lines, children, and targets)
|
|
221
|
+
const richness = (b: NotationBlock) =>
|
|
222
|
+
b.lines.length + b.children.length + b.lines.reduce((s, l) => s + l.targets.length, 0);
|
|
223
|
+
if (richness(block) > richness(existing)) {
|
|
224
|
+
seen.set(block.nodeId, block);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [...seen.values()];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// === Rule 6: Target dedup within lines ===
|
|
232
|
+
|
|
233
|
+
function dedupTargets(block: NotationBlock): NotationBlock {
|
|
234
|
+
const hasDupes = block.lines.some(l => l.targets.length !== new Set(l.targets).size);
|
|
235
|
+
if (!hasDupes) return block;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
...block,
|
|
239
|
+
lines: block.lines.map(line => ({
|
|
240
|
+
...line,
|
|
241
|
+
targets: [...new Set(line.targets)],
|
|
242
|
+
})),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// === Rules 8/9: Chain detection and linearization ===
|
|
247
|
+
|
|
248
|
+
function detectChains(
|
|
249
|
+
blocks: NotationBlock[],
|
|
250
|
+
): { chains: NotationBlock[]; remaining: NotationBlock[] } {
|
|
251
|
+
if (blocks.length < 3) return { chains: [], remaining: blocks };
|
|
252
|
+
|
|
253
|
+
const consumed = new Set<number>();
|
|
254
|
+
const chainBlocks: NotationBlock[] = [];
|
|
255
|
+
|
|
256
|
+
// Find all blocks with "derived from" lines
|
|
257
|
+
const derivesFrom = new Map<number, string>();
|
|
258
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
259
|
+
for (const line of blocks[i].lines) {
|
|
260
|
+
if (line.verb === 'derived from' && line.targets.length > 0) {
|
|
261
|
+
derivesFrom.set(i, line.targets[0]);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (derivesFrom.size < 2) return { chains: [], remaining: blocks };
|
|
268
|
+
|
|
269
|
+
// Group blocks by location
|
|
270
|
+
const locationGroups = new Map<string, number[]>();
|
|
271
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
272
|
+
const loc = blocks[i].location;
|
|
273
|
+
if (!loc) continue;
|
|
274
|
+
if (!locationGroups.has(loc)) locationGroups.set(loc, []);
|
|
275
|
+
locationGroups.get(loc)!.push(i);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const [loc, indices] of locationGroups) {
|
|
279
|
+
if (indices.length < 3) continue;
|
|
280
|
+
|
|
281
|
+
// Build name → indices map within this location group
|
|
282
|
+
const nameToIndices = new Map<string, number[]>();
|
|
283
|
+
for (const idx of indices) {
|
|
284
|
+
const name = blocks[idx].displayName;
|
|
285
|
+
if (!nameToIndices.has(name)) nameToIndices.set(name, []);
|
|
286
|
+
nameToIndices.get(name)!.push(idx);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Build name derivation graph: name → derives from name
|
|
290
|
+
const nameDerivesFrom = new Map<string, string>();
|
|
291
|
+
for (const idx of indices) {
|
|
292
|
+
if (derivesFrom.has(idx) && nameToIndices.has(derivesFrom.get(idx)!)) {
|
|
293
|
+
nameDerivesFrom.set(blocks[idx].displayName, derivesFrom.get(idx)!);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (nameDerivesFrom.size < 2) continue;
|
|
298
|
+
|
|
299
|
+
// Find chain roots: names derived FROM but don't derive from anything
|
|
300
|
+
const derivedFromNames = new Set(nameDerivesFrom.values());
|
|
301
|
+
const derivingNames = new Set(nameDerivesFrom.keys());
|
|
302
|
+
const roots = [...derivedFromNames].filter(n => !derivingNames.has(n));
|
|
303
|
+
|
|
304
|
+
for (const root of roots) {
|
|
305
|
+
// Build chain from root forward
|
|
306
|
+
const steps: string[] = [root];
|
|
307
|
+
let current = root;
|
|
308
|
+
const visited = new Set<string>([root]);
|
|
309
|
+
|
|
310
|
+
while (true) {
|
|
311
|
+
let next: string | undefined;
|
|
312
|
+
for (const [name, from] of nameDerivesFrom) {
|
|
313
|
+
if (from === current && !visited.has(name)) {
|
|
314
|
+
next = name;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!next) break;
|
|
319
|
+
steps.push(next);
|
|
320
|
+
visited.add(next);
|
|
321
|
+
current = next;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (steps.length < 3) continue;
|
|
325
|
+
|
|
326
|
+
// Mark all blocks in this chain as consumed
|
|
327
|
+
for (const name of steps) {
|
|
328
|
+
const idxs = nameToIndices.get(name);
|
|
329
|
+
if (idxs) {
|
|
330
|
+
for (const idx of idxs) consumed.add(idx);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check if last step has "reads" line for source
|
|
335
|
+
let chainSource: string | undefined;
|
|
336
|
+
const lastIdxs = nameToIndices.get(steps[steps.length - 1]) ?? [];
|
|
337
|
+
for (const idx of lastIdxs) {
|
|
338
|
+
for (const line of blocks[idx].lines) {
|
|
339
|
+
if (line.verb === 'reads' && line.targets.length > 0) {
|
|
340
|
+
chainSource = line.targets[0];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
chainBlocks.push({
|
|
346
|
+
nodeId: `chain:${steps.join(':')}`,
|
|
347
|
+
displayName: steps.join(' \u2218 ') + (chainSource ? `(${chainSource})` : ''),
|
|
348
|
+
nodeType: blocks[indices[0]].nodeType,
|
|
349
|
+
lines: [],
|
|
350
|
+
children: [],
|
|
351
|
+
location: loc,
|
|
352
|
+
foldMeta: {
|
|
353
|
+
kind: 'chain',
|
|
354
|
+
count: steps.length,
|
|
355
|
+
chainSteps: steps,
|
|
356
|
+
chainSource,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const remaining = blocks.filter((_, i) => !consumed.has(i));
|
|
363
|
+
return { chains: chainBlocks, remaining };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// === Rule 11: Case dispatch detection ===
|
|
367
|
+
|
|
368
|
+
function detectDispatch(
|
|
369
|
+
blocks: NotationBlock[],
|
|
370
|
+
): { dispatches: NotationBlock[]; remaining: NotationBlock[] } {
|
|
371
|
+
if (blocks.length < 8) return { dispatches: [], remaining: blocks };
|
|
372
|
+
|
|
373
|
+
// Find blocks with "derived from case" lines
|
|
374
|
+
const caseIndices: number[] = [];
|
|
375
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
376
|
+
for (const line of blocks[i].lines) {
|
|
377
|
+
if (line.verb === 'derived from' && line.targets.includes('case')) {
|
|
378
|
+
caseIndices.push(i);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (caseIndices.length < FOLD_THRESHOLD) return { dispatches: [], remaining: blocks };
|
|
385
|
+
|
|
386
|
+
// Each case block has a pattern block before it
|
|
387
|
+
const consumed = new Set<number>();
|
|
388
|
+
const branches: Array<{ pattern: string; handler: string; indices: number[] }> = [];
|
|
389
|
+
|
|
390
|
+
for (const idx of caseIndices) {
|
|
391
|
+
if (consumed.has(idx)) continue;
|
|
392
|
+
|
|
393
|
+
const handler = blocks[idx];
|
|
394
|
+
// Pattern node is at idx-1 with no edges/children
|
|
395
|
+
if (idx > 0 && !consumed.has(idx - 1) &&
|
|
396
|
+
blocks[idx - 1].lines.length === 0 && blocks[idx - 1].children.length === 0) {
|
|
397
|
+
const pattern = blocks[idx - 1];
|
|
398
|
+
// Only consume the pattern + handler pair; dup/arg blocks handled by dedup/leaf rules
|
|
399
|
+
branches.push({
|
|
400
|
+
pattern: pattern.displayName,
|
|
401
|
+
handler: handler.displayName,
|
|
402
|
+
indices: [idx - 1, idx],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
consumed.add(idx - 1);
|
|
406
|
+
consumed.add(idx);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (branches.length < FOLD_THRESHOLD) return { dispatches: [], remaining: blocks };
|
|
411
|
+
|
|
412
|
+
// Exemplar: first branch's blocks as children
|
|
413
|
+
const exemplarBlocks = branches[0].indices.map(i => blocks[i]);
|
|
414
|
+
const summaryBranches = branches.slice(1).map(b => ({
|
|
415
|
+
pattern: b.pattern,
|
|
416
|
+
handler: b.handler,
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
const dispatch: NotationBlock = {
|
|
420
|
+
nodeId: `dispatch:${branches[0].pattern}`,
|
|
421
|
+
displayName: `${branches[0].pattern} \u2192 ${branches[0].handler}`,
|
|
422
|
+
nodeType: 'DISPATCH',
|
|
423
|
+
lines: [],
|
|
424
|
+
children: exemplarBlocks,
|
|
425
|
+
foldMeta: {
|
|
426
|
+
kind: 'dispatch',
|
|
427
|
+
count: branches.length,
|
|
428
|
+
branches: summaryBranches,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Summary block after exemplar
|
|
433
|
+
const branchList = summaryBranches.slice(0, 3)
|
|
434
|
+
.map(b => `${b.pattern} \u2192 ${b.handler}`)
|
|
435
|
+
.join(', ');
|
|
436
|
+
const suffix = summaryBranches.length > 3
|
|
437
|
+
? `, ...+${summaryBranches.length - 3} more`
|
|
438
|
+
: '';
|
|
439
|
+
|
|
440
|
+
const summary: NotationBlock = {
|
|
441
|
+
nodeId: `dispatch-summary:${branches[0].pattern}`,
|
|
442
|
+
displayName: `...+${branches.length - 1} more branches: ${branchList}${suffix}`,
|
|
443
|
+
nodeType: 'DISPATCH',
|
|
444
|
+
lines: [],
|
|
445
|
+
children: [],
|
|
446
|
+
foldMeta: {
|
|
447
|
+
kind: 'dispatch',
|
|
448
|
+
count: branches.length - 1,
|
|
449
|
+
branches: summaryBranches,
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const remaining = blocks.filter((_, i) => !consumed.has(i));
|
|
454
|
+
return { dispatches: [dispatch, summary], remaining };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// === Rule 10: Repetitive call fold ===
|
|
458
|
+
|
|
459
|
+
function detectRepetitiveCalls(
|
|
460
|
+
blocks: NotationBlock[],
|
|
461
|
+
): { folds: NotationBlock[]; remaining: NotationBlock[] } {
|
|
462
|
+
// Group non-leaf blocks by (name + signature) — same function called repeatedly
|
|
463
|
+
const groups = new Map<string, number[]>();
|
|
464
|
+
|
|
465
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
466
|
+
const block = blocks[i];
|
|
467
|
+
if (block.foldMeta) continue;
|
|
468
|
+
if (block.lines.length === 0 && block.children.length === 0) continue; // Skip leaves
|
|
469
|
+
|
|
470
|
+
const sig = computeSignature(block);
|
|
471
|
+
const key = `call:${block.displayName}|${signatureKey(sig)}`;
|
|
472
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
473
|
+
groups.get(key)!.push(i);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const consumed = new Set<number>();
|
|
477
|
+
const folds: NotationBlock[] = [];
|
|
478
|
+
|
|
479
|
+
for (const [, indices] of groups) {
|
|
480
|
+
if (indices.length <= FOLD_THRESHOLD) continue;
|
|
481
|
+
|
|
482
|
+
const exemplar = blocks[indices[0]];
|
|
483
|
+
|
|
484
|
+
// Exemplar shown expanded
|
|
485
|
+
folds.push(exemplar);
|
|
486
|
+
|
|
487
|
+
// Fold summary
|
|
488
|
+
folds.push({
|
|
489
|
+
nodeId: `callfold:${exemplar.displayName}`,
|
|
490
|
+
displayName: `...+${indices.length - 1} more ${exemplar.displayName}`,
|
|
491
|
+
nodeType: exemplar.nodeType,
|
|
492
|
+
lines: [],
|
|
493
|
+
children: [],
|
|
494
|
+
foldMeta: {
|
|
495
|
+
kind: 'fold',
|
|
496
|
+
count: indices.length,
|
|
497
|
+
label: exemplar.displayName,
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
for (const idx of indices) consumed.add(idx);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const remaining = blocks.filter((_, i) => !consumed.has(i));
|
|
505
|
+
return { folds, remaining };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// === Rule 7: Repeated leaf fold ===
|
|
509
|
+
|
|
510
|
+
function repeatedLeafFold(
|
|
511
|
+
blocks: NotationBlock[],
|
|
512
|
+
): { folds: NotationBlock[]; remaining: NotationBlock[] } {
|
|
513
|
+
// Group leaf blocks (no lines, no children) by name
|
|
514
|
+
const leafGroups = new Map<string, number[]>();
|
|
515
|
+
const nonLeafIndices = new Set<number>();
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
518
|
+
const block = blocks[i];
|
|
519
|
+
if (block.foldMeta) {
|
|
520
|
+
nonLeafIndices.add(i);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (block.lines.length === 0 && block.children.length === 0) {
|
|
524
|
+
const name = block.displayName;
|
|
525
|
+
if (!leafGroups.has(name)) leafGroups.set(name, []);
|
|
526
|
+
leafGroups.get(name)!.push(i);
|
|
527
|
+
} else {
|
|
528
|
+
nonLeafIndices.add(i);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const foldedLeafIndices = new Set<number>();
|
|
533
|
+
const folds: NotationBlock[] = [];
|
|
534
|
+
|
|
535
|
+
for (const [name, indices] of leafGroups) {
|
|
536
|
+
if (indices.length <= FOLD_THRESHOLD) continue;
|
|
537
|
+
|
|
538
|
+
for (const idx of indices) foldedLeafIndices.add(idx);
|
|
539
|
+
|
|
540
|
+
folds.push({
|
|
541
|
+
nodeId: `leafrepeat:${name}`,
|
|
542
|
+
displayName: `${name} \u00d7${indices.length}`,
|
|
543
|
+
nodeType: blocks[indices[0]].nodeType,
|
|
544
|
+
lines: [],
|
|
545
|
+
children: [],
|
|
546
|
+
location: blocks[indices[0]].location,
|
|
547
|
+
foldMeta: {
|
|
548
|
+
kind: 'leaf-repeat',
|
|
549
|
+
count: indices.length,
|
|
550
|
+
label: name,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const remaining = blocks.filter((_, i) =>
|
|
556
|
+
nonLeafIndices.has(i) || (leafGroups.get(blocks[i].displayName)?.length ?? 0) <= FOLD_THRESHOLD,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return { folds, remaining };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// === Rule 5: Structural suppression ===
|
|
563
|
+
|
|
564
|
+
function structuralSuppression(
|
|
565
|
+
blocks: NotationBlock[],
|
|
566
|
+
): { groups: NotationBlock[]; remaining: NotationBlock[] } {
|
|
567
|
+
const trivialByType = new Map<string, NotationBlock[]>();
|
|
568
|
+
const nonTrivialBlocks: NotationBlock[] = [];
|
|
569
|
+
|
|
570
|
+
for (const block of blocks) {
|
|
571
|
+
if (block.foldMeta) {
|
|
572
|
+
nonTrivialBlocks.push(block);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const isTrivial =
|
|
577
|
+
(block.lines.length === 0 && block.children.length === 0) ||
|
|
578
|
+
block.lines.every(l => l.operator === '');
|
|
579
|
+
|
|
580
|
+
if (isTrivial) {
|
|
581
|
+
if (!trivialByType.has(block.nodeType)) trivialByType.set(block.nodeType, []);
|
|
582
|
+
trivialByType.get(block.nodeType)!.push(block);
|
|
583
|
+
} else {
|
|
584
|
+
nonTrivialBlocks.push(block);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const groups: NotationBlock[] = [];
|
|
589
|
+
|
|
590
|
+
for (const [type, trivials] of trivialByType) {
|
|
591
|
+
if (trivials.length > FOLD_THRESHOLD) {
|
|
592
|
+
const nameList = trivials.map(b => b.displayName).join(', ');
|
|
593
|
+
groups.push({
|
|
594
|
+
nodeId: `trivial:${type}`,
|
|
595
|
+
displayName: `[${type.toLowerCase()}: ${nameList}]`,
|
|
596
|
+
nodeType: type,
|
|
597
|
+
lines: [],
|
|
598
|
+
children: [],
|
|
599
|
+
foldMeta: {
|
|
600
|
+
kind: 'trivial-group',
|
|
601
|
+
count: trivials.length,
|
|
602
|
+
names: trivials.map(b => b.displayName),
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
} else {
|
|
606
|
+
nonTrivialBlocks.push(...trivials);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return { groups, remaining: nonTrivialBlocks };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// === Rules 1-3: Group fold with anomaly preservation and source aggregation ===
|
|
614
|
+
|
|
615
|
+
function groupFold(
|
|
616
|
+
blocks: NotationBlock[],
|
|
617
|
+
): { folded: NotationBlock[]; anomalies: NotationBlock[] } {
|
|
618
|
+
const unfoldedBlocks = blocks.filter(b => !b.foldMeta);
|
|
619
|
+
const alreadyFolded = blocks.filter(b => b.foldMeta);
|
|
620
|
+
|
|
621
|
+
if (unfoldedBlocks.length <= FOLD_THRESHOLD) {
|
|
622
|
+
return { folded: alreadyFolded, anomalies: unfoldedBlocks };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Group by signature
|
|
626
|
+
const sigGroups = new Map<string, NotationBlock[]>();
|
|
627
|
+
for (const block of unfoldedBlocks) {
|
|
628
|
+
const sig = computeSignature(block);
|
|
629
|
+
const key = signatureKey(sig);
|
|
630
|
+
if (!sigGroups.has(key)) sigGroups.set(key, []);
|
|
631
|
+
sigGroups.get(key)!.push(block);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const folded: NotationBlock[] = [...alreadyFolded];
|
|
635
|
+
const anomalies: NotationBlock[] = [];
|
|
636
|
+
|
|
637
|
+
for (const [, group] of sigGroups) {
|
|
638
|
+
if (group.length > FOLD_THRESHOLD) {
|
|
639
|
+
const exemplar = group[0];
|
|
640
|
+
|
|
641
|
+
// Rule 3: Source aggregation — check if all share same source
|
|
642
|
+
const sourceSummary = findCommonSource(group);
|
|
643
|
+
const label = inferLabel(group);
|
|
644
|
+
const summaryParts = [`...+${group.length - 1} more ${label}`];
|
|
645
|
+
if (sourceSummary) summaryParts.push(`from ${sourceSummary}`);
|
|
646
|
+
|
|
647
|
+
// Exemplar shown expanded
|
|
648
|
+
folded.push(exemplar);
|
|
649
|
+
|
|
650
|
+
// Fold summary
|
|
651
|
+
folded.push({
|
|
652
|
+
nodeId: `fold:${exemplar.nodeId}`,
|
|
653
|
+
displayName: summaryParts.join(' '),
|
|
654
|
+
nodeType: exemplar.nodeType,
|
|
655
|
+
lines: [],
|
|
656
|
+
children: [],
|
|
657
|
+
foldMeta: {
|
|
658
|
+
kind: 'fold',
|
|
659
|
+
count: group.length,
|
|
660
|
+
label,
|
|
661
|
+
sourceSummary,
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
} else {
|
|
665
|
+
// Rule 2: Anomaly — too few to fold
|
|
666
|
+
anomalies.push(...group);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return { folded, anomalies };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// === Helpers ===
|
|
674
|
+
|
|
675
|
+
function findCommonSource(blocks: NotationBlock[]): string | undefined {
|
|
676
|
+
const sources = new Set<string>();
|
|
677
|
+
|
|
678
|
+
for (const block of blocks) {
|
|
679
|
+
for (const line of block.lines) {
|
|
680
|
+
if (line.targets.length > 0) {
|
|
681
|
+
for (const t of line.targets) sources.add(t);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (sources.size === 1) return [...sources][0];
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function inferLabel(group: NotationBlock[]): string {
|
|
691
|
+
if (group.length === 0) return '';
|
|
692
|
+
|
|
693
|
+
const type = group[0].nodeType.toLowerCase();
|
|
694
|
+
if (type.endsWith('s')) return type;
|
|
695
|
+
return type + 's';
|
|
696
|
+
}
|