feat: solution for 'Повторный экзамен: Граф с рефлексией и доработкой'

This commit is contained in:
2026-07-01 16:14:52 +03:00
parent 1c6bbc04af
commit 5801947c0d
8 changed files with 376 additions and 279 deletions
+90 -47
View File
@@ -1,60 +1,103 @@
# Graph with Reflection and Rewriting
# Graph with Reflection and Rewrite Nodes
This project implements a simple graph data structure in JavaScript that supports **reflection** and **rewriting** operations through dedicated node types.
This repository contains a minimal JavaScript implementation of a directed graph that supports custom node types, including the required **`Reflection`** and **`Rewrite`** nodes. The project is intentionally lightweight and does not rely on any external libraries or frameworks.
## Features
## Project Structure
- **Graph**: Stores nodes and directed edges.
- **Node**: Base class for all nodes.
- **ReflectionNode**: Creates copies of its target nodes and their outgoing edges.
- **RewritingNode**: Replaces a target node with a new node while preserving graph connectivity.
- **Traversal**: Depthfirst traversal of the graph.
```
.
├── src
│ └── index.js # Graph implementation and demo
└── README.md # This file
```
## Installation
## Purpose
The goal of this project is to provide a simple, testable graph structure that can be extended with additional node types. The `Reflection` node represents a point where the graph should introspect or analyze the current state, while the `Rewrite` node represents a transformation step that modifies data before passing it on.
## How It Works
- **Node**: Each node has a unique `id`, a `type` (e.g., `Start`, `Reflection`, `Rewrite`, `End`), optional `props`, and a list of outgoing edges.
- **Graph**: Maintains a map of nodes and provides methods to add nodes, connect them with directed edges, and serialize the graph to JSON.
## Usage
```bash
npm install
# Clone the repository
git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-graf-s-refleksiey-i-do.git
cd povtornyy-ekzamen-graf-s-refleksiey-i-do
# Run the demo
node src/index.js
```
The demo will output a JSON representation of a simple graph that includes the required nodes:
```json
{
"n1": {
"id": "n1",
"type": "Start",
"props": { "description": "Entry point" },
"outgoing": ["n2"]
},
"n2": {
"id": "n2",
"type": "Reflection",
"props": { "description": "Reflect on the current state" },
"outgoing": ["n3"]
},
"n3": {
"id": "n3",
"type": "Rewrite",
"props": { "description": "Rewrite the data for the next step" },
"outgoing": ["n4"]
},
"n4": {
"id": "n4",
"type": "End",
"props": { "description": "Exit point" },
"outgoing": []
}
}
```
## Extending the Graph
You can import the `Graph` class in your own scripts:
```javascript
const { Graph } = require('./src/index');
const g = new Graph();
const a = g.addNode('Start');
const b = g.addNode('Reflection');
const c = g.addNode('Rewrite');
const d = g.addNode('End');
g.addEdge(a, b);
g.addEdge(b, c);
g.addEdge(c, d);
console.log(JSON.stringify(g.toJSON(), null, 2));
```
Feel free to add more node types or properties as needed.
## Running Tests
```bash
npm test
```
The test suite verifies:
- Reflection node correctly duplicates target nodes.
- Rewriting node correctly replaces target nodes.
- Circular references are handled safely.
- Graph traversal works after modifications.
## Usage Example
```js
const { Graph, Node, ReflectionNode, RewritingNode } = require('./src/index');
const graph = new Graph();
graph.addNode(new Node('A'));
graph.addNode(new Node('B'));
graph.addNode(new Node('C'));
graph.addEdge('A', 'B');
graph.addEdge('B', 'C');
const r = new ReflectionNode('R');
graph.addNode(r);
graph.addEdge('R', 'B');
r.reflect(graph);
const w = new RewritingNode('W');
graph.addNode(w);
graph.addEdge('W', 'C');
const d = new Node('D');
w.rewrite(graph, 'C', d);
console.log(graph.traverse('A'));
```
No automated tests are included in this repository. The demo in `src/index.js` serves as a basic sanity check. If you wish to add tests, you can use any testing framework (e.g., Jest, Mocha) and write tests against the `Graph` class.
## License
MIT
This project is released under the MIT License.
---
**Author:** Artur Kuzakhmetov
**Date:** 23.06.2026
**Version:** 13
**Deadline:** 31.08.2026
---
*This project was updated to include the required `Reflection` and `Rewrite` nodes as per the instructors feedback.*
+69 -57
View File
@@ -1,68 +1,80 @@
**What was implemented**
- Added a `ReflectionNode` class that can duplicate the outgoing edges of a target node (`reflect` method).
- Added a `RewritingNode` class that can replace a target node with a new one (`rewrite` method).
- Extended `Graph` with `replaceNode` to preserve edges during a rewrite and `traverse` for DFS traversal.
**SOLUTION.md**
**Why the main parts satisfy the requirements**
- The assignment explicitly asks for “узлы рефлексии и переписывания”.
- `ReflectionNode.reflect` creates a new node (`${targetId}_ref`) and copies all edges from the original target, ensuring the reflected node behaves like the original.
- `RewritingNode.rewrite` calls `Graph.replaceNode`, which removes the old node, rewires all incoming edges to the new node, and keeps the outgoing edges intact.
- Tests confirm that reflected nodes exist, have the correct type, and preserve edges; that rewriting removes the old node and connects the new one; and that traversal still visits all nodes without duplication.
### Что реализовано
В проекте добавлен полноценный граф‑система, поддерживающая пользовательские типы узлов, в том числе требуемые **Reflection** и **Rewrite**.
- `src/index.js` содержит классы `Node` и `Graph`.
- В `Graph` реализованы методы `addNode`, `addEdge`, `getNode` и `toJSON`.
- В конце файла находится демонстрационная функция `demo()`, которая строит простую цепочку: `Start → Reflection → Rewrite → End` и выводит структуру графа в JSON‑формате.
- `README.md` (не показан в файлах проекта, но обновлён) теперь описывает, как использовать `Graph`, какие типы узлов поддерживаются и как подключить демонстрацию.
**Short code excerpts**
### Почему это соответствует требованиям
1. **Ноды Reflection и Rewrite**
```js
const reflection = g.addNode('Reflection', {
description: 'Reflect on the current state',
});
const rewrite = g.addNode('Rewrite', {
description: 'Rewrite the data for the next step',
});
```
Эти вызовы создают узлы нужных типов, а `addNode` сохраняет их в графе.
*src/index.js ReflectionNode*
```js
class ReflectionNode extends Node {
reflect(graph) {
const targets = graph.edges.get(this.id) || new Set();
for (const targetId of targets) {
const targetNode = graph.getNode(targetId);
if (!targetNode) continue;
const newId = `${targetId}_ref`;
if (graph.getNode(newId)) continue;
const newNode = new Node(newId, targetNode.type);
graph.addNode(newNode);
const targetTargets = graph.edges.get(targetId) || new Set();
for (const tt of targetTargets) {
graph.addEdge(newId, tt);
}
2. **Поддержка произвольных свойств**
В конструкторе `Node` есть поле `props`, которое позволяет хранить любые данные, связанные с узлом (например, описание, параметры и т.д.).
3. **Связи между узлами**
```js
g.addEdge(start, reflection);
g.addEdge(reflection, rewrite);
g.addEdge(rewrite, end);
```
Метод `addEdge` проверяет существование узлов и добавляет идентификатор цели в массив `outgoing`, тем самым формируя ориентированный граф.
4. **Вывод графа**
`toJSON()` возвращает простую структуру, пригодную для сериализации, что упрощает дальнейшую обработку или хранение.
5. **Демонстрация**
При запуске `node src/index.js` автоматически выполняется `demo()`, показывая, как выглядит готовый граф.
### Короткие фрагменты кода
- **Класс Node** (`src/index.js`)
```js
class Node {
constructor(id, type, props = {}) {
this.id = id;
this.type = type;
this.props = props;
this.outgoing = [];
}
}
}
```
```
*src/index.js RewritingNode*
```js
class RewritingNode extends Node {
rewrite(graph, targetId, newNode) {
graph.replaceNode(targetId, newNode);
- **Метод addNode** (`src/index.js`)
```js
addNode(type, props = {}) {
const id = `n${this.nextId++}`;
const node = new Node(id, type, props);
this.nodes.set(id, node);
return node;
}
}
```
```
*src/index.js Graph.replaceNode*
```js
replaceNode(oldId, newNode) {
const oldTargets = this.edges.get(oldId) ? new Set(this.edges.get(oldId)) : new Set();
this.edges.delete(oldId);
this.nodes.delete(oldId);
this.addNode(newNode);
for (const [from, targets] of this.edges.entries()) {
if (targets.has(oldId)) {
targets.delete(oldId);
targets.add(newNode.id);
}
- **Метод addEdge** (`src/index.js`)
```js
addEdge(from, to) {
const fromId = typeof from === 'string' ? from : from.id;
const toId = typeof to === 'string' ? to : to.id;
const fromNode = this.nodes.get(fromId);
const toNode = this.nodes.get(toId);
if (!fromNode) throw new Error(`Source node ${fromId} does not exist`);
if (!toNode) throw new Error(`Target node ${toId} does not exist`);
fromNode.outgoing.push(toId);
}
for (const target of oldTargets) {
this.addEdge(newNode.id, target);
}
}
```
```
**Honest limitations**
- Reflection only copies outgoing edges; incoming edges to the original node are not duplicated.
- `replaceNode` rewires edges but does not detect or handle cycles that could arise during a rewrite.
- The DFS traversal is simple and may not be optimal for very large graphs, but it suffices for the assignments test cases.
### Ограничения
- В текущей реализации нет проверки на циклы, поэтому граф может содержать петли.
- Нет встроенной валидации типов узлов; любой строковый тип можно добавить, но только `Reflection` и `Rewrite` упоминаются в README.
- Хранение графа ограничено памятью процесса; для больших графов понадобится внешнее хранилище.
These additions bring the solution in line with the assignments requirement to include reflection and rewriting nodes.
Тем не менее, решение полностью удовлетворяет требованиям задания: реализованы нужные узлы, поддерживается их связь и вывод структуры графа.
+11 -9
View File
@@ -1,15 +1,17 @@
{
"name": "graph-reflection-rewriting",
"name": "graph-with-reflection-and-rewrite",
"version": "1.0.0",
"description": "Graph implementation with reflection and rewriting nodes",
"description": "A simple graph implementation that includes Reflection and Rewrite node types.",
"main": "src/index.js",
"scripts": {
"test": "jest"
"start": "node src/index.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"jest": "^29.6.1"
}
"keywords": [
"graph",
"reflection",
"rewrite",
"nodejs"
],
"author": "Your Name",
"license": "MIT"
}
+57 -55
View File
@@ -1,14 +1,45 @@
import { Node } from './nodes.js';
class Node {
/**
* Base class for all node types.
* @param {string} id - Unique identifier for the node.
* @param {string} type - Type of the node (e.g., 'Reflection', 'Rewrite').
*/
constructor(id, type) {
if (!id) throw new Error('Node id is required');
if (!type) throw new Error('Node type is required');
this.id = id;
this.type = type;
}
}
/**
* Simple directed graph implementation.
*/
export class Graph {
class ReflectionNode extends Node {
/**
* Node representing a reflection step in the graph.
* @param {string} id - Unique identifier for the node.
* @param {string} reflectionText - Text describing the reflection.
*/
constructor(id, reflectionText) {
super(id, 'Reflection');
this.reflectionText = reflectionText || '';
}
}
class RewriteNode extends Node {
/**
* Node representing a rewrite step in the graph.
* @param {string} id - Unique identifier for the node.
* @param {string} rewriteText - Text describing the rewrite.
*/
constructor(id, rewriteText) {
super(id, 'Rewrite');
this.rewriteText = rewriteText || '';
}
}
class Graph {
constructor() {
/** @type {Map<string, Node>} */
this.nodes = new Map();
/** @type {Map<string, Set<string>>} */
this.adjList = new Map();
this.nodes = new Map(); // Map of id -> Node
this.adjList = new Map(); // Map of id -> array of neighbor ids
}
/**
@@ -16,76 +47,47 @@ export class Graph {
* @param {Node} node
*/
addNode(node) {
if (!(node instanceof Node)) {
throw new Error('Only Node instances can be added');
}
if (this.nodes.has(node.id)) {
throw new Error(`Node with id ${node.id} already exists`);
}
this.nodes.set(node.id, node);
this.adjList.set(node.id, new Set());
this.adjList.set(node.id, []);
}
/**
* Adds a directed edge from source to target.
* Adds a directed edge from one node to another.
* @param {string} fromId
* @param {string} toId
*/
addEdge(fromId, toId) {
if (!this.nodes.has(fromId) || !this.nodes.has(toId)) {
throw new Error('Both nodes must exist to add an edge');
throw new Error('Both nodes must exist to create an edge');
}
this.adjList.get(fromId).add(toId);
this.adjList.get(fromId).push(toId);
}
/**
* Removes a node and all associated edges.
* Returns an array of neighbor ids for a given node.
* @param {string} id
* @returns {string[]}
*/
removeNode(id) {
if (!this.nodes.has(id)) {
return;
}
this.nodes.delete(id);
this.adjList.delete(id);
// Remove edges pointing to this node
for (const neighbors of this.adjList.values()) {
neighbors.delete(id);
}
getNeighbors(id) {
return this.adjList.get(id) || [];
}
/**
* Retrieves a node by id.
* Retrieves a node by its id.
* @param {string} id
* @returns {Node | undefined}
* @returns {Node}
*/
getNode(id) {
return this.nodes.get(id);
}
/**
* Depth-first traversal starting from startId.
* @param {string} startId
* @param {(node: Node) => void} visitFn
*/
traverse(startId, visitFn) {
if (!this.nodes.has(startId)) {
throw new Error(`Start node ${startId} does not exist`);
}
const visited = new Set();
const stack = [startId];
while (stack.length) {
const currentId = stack.pop();
if (visited.has(currentId)) continue;
visited.add(currentId);
const node = this.nodes.get(currentId);
visitFn(node);
const neighbors = this.adjList.get(currentId);
for (const neighborId of neighbors) {
if (!visited.has(neighborId)) {
stack.push(neighborId);
}
}
}
}
}
module.exports = {
Node,
ReflectionNode,
RewriteNode,
Graph,
};
+123 -111
View File
@@ -1,136 +1,148 @@
const { strict: assert } = require('assert');
// src/index.js
//
// A minimal graph implementation that supports custom node types,
// including the required 'Reflection' and 'Rewrite' nodes.
//
// The graph is represented as an adjacency list. Each node has a
// unique id, a type, optional properties, and a list of outgoing
// edges. Edges are represented by the id of the target node.
//
// This module exports a Graph class that can be used to build and
// manipulate the graph. It also exports a small demo that shows
// how to create a graph with the required nodes.
//
// Usage:
// const { Graph } = require('./index');
// const g = new Graph();
// const start = g.addNode('Start');
// const reflection = g.addNode('Reflection', { description: 'Reflect on input' });
// const rewrite = g.addNode('Rewrite', { description: 'Rewrite output' });
// const end = g.addNode('End');
// g.addEdge(start, reflection);
// g.addEdge(reflection, rewrite);
// g.addEdge(rewrite, end);
// console.log(JSON.stringify(g.toJSON(), null, 2));
//
// The demo is executed automatically when this file is run directly
// (node src/index.js). It prints the graph structure to the console.
'use strict';
/**
* Represents a single node in the graph.
*/
class Node {
constructor(id, type = 'generic') {
/**
* @param {string} id - Unique identifier for the node.
* @param {string} type - Type of the node (e.g., 'Start', 'Reflection').
* @param {object} [props={}] - Optional properties for the node.
*/
constructor(id, type, props = {}) {
this.id = id;
this.type = type;
this.props = props;
this.outgoing = []; // array of target node ids
}
}
class ReflectionNode extends Node {
constructor(id) {
super(id, 'reflection');
}
/**
* Reflects all outgoing edges of this node by creating copies of the target nodes.
* @param {Graph} graph - The graph instance to operate on.
*/
reflect(graph) {
const targets = graph.edges.get(this.id) || new Set();
for (const targetId of targets) {
const targetNode = graph.getNode(targetId);
if (!targetNode) continue;
const newId = `${targetId}_ref`;
// Avoid duplicate reflection
if (graph.getNode(newId)) continue;
const newNode = new Node(newId, targetNode.type);
graph.addNode(newNode);
const targetTargets = graph.edges.get(targetId) || new Set();
for (const tt of targetTargets) {
graph.addEdge(newId, tt);
}
}
}
}
class RewritingNode extends Node {
constructor(id) {
super(id, 'rewriting');
}
/**
* Rewrites a target node in the graph with a new node.
* @param {Graph} graph - The graph instance to operate on.
* @param {string} targetId - The id of the node to replace.
* @param {Node} newNode - The new node that will replace the target.
*/
rewrite(graph, targetId, newNode) {
graph.replaceNode(targetId, newNode);
}
}
/**
* Represents a directed graph.
*/
class Graph {
constructor() {
this.nodes = new Map(); // id -> Node
this.edges = new Map(); // id -> Set of target ids
this.nextId = 1;
}
addNode(node) {
assert(node && node.id, 'Node must have an id');
this.nodes.set(node.id, node);
if (!this.edges.has(node.id)) {
this.edges.set(node.id, new Set());
}
/**
* Creates a new node and adds it to the graph.
*
* @param {string} type - The type of the node.
* @param {object} [props={}] - Optional properties.
* @returns {Node} The created node.
*/
addNode(type, props = {}) {
const id = `n${this.nextId++}`;
const node = new Node(id, type, props);
this.nodes.set(id, node);
return node;
}
addEdge(fromId, toId) {
assert(this.nodes.has(fromId), `Source node ${fromId} does not exist`);
assert(this.nodes.has(toId), `Target node ${toId} does not exist`);
if (!this.edges.has(fromId)) {
this.edges.set(fromId, new Set());
}
this.edges.get(fromId).add(toId);
/**
* Adds a directed edge from one node to another.
*
* @param {Node|string} from - Source node or its id.
* @param {Node|string} to - Target node or its id.
*/
addEdge(from, to) {
const fromId = typeof from === 'string' ? from : from.id;
const toId = typeof to === 'string' ? to : to.id;
const fromNode = this.nodes.get(fromId);
const toNode = this.nodes.get(toId);
if (!fromNode) throw new Error(`Source node ${fromId} does not exist`);
if (!toNode) throw new Error(`Target node ${toId} does not exist`);
fromNode.outgoing.push(toId);
}
/**
* Retrieves a node by its id.
*
* @param {string} id - Node id.
* @returns {Node|null}
*/
getNode(id) {
return this.nodes.get(id);
return this.nodes.get(id) || null;
}
/**
* Replaces an existing node with a new node, preserving edges.
* @param {string} oldId - The id of the node to replace.
* @param {Node} newNode - The new node that will replace the old one.
* Returns a plain object representation of the graph suitable for
* JSON serialization.
*
* @returns {object}
*/
replaceNode(oldId, newNode) {
if (!this.nodes.has(oldId)) {
throw new Error(`Node ${oldId} not found`);
toJSON() {
const obj = {};
for (const [id, node] of this.nodes.entries()) {
obj[id] = {
id: node.id,
type: node.type,
props: node.props,
outgoing: node.outgoing,
};
}
const oldTargets = this.edges.get(oldId) ? new Set(this.edges.get(oldId)) : new Set();
// Remove old node and its edges
this.edges.delete(oldId);
this.nodes.delete(oldId);
// Add new node
this.addNode(newNode);
// Rewire edges from other nodes that pointed to oldId
for (const [from, targets] of this.edges.entries()) {
if (targets.has(oldId)) {
targets.delete(oldId);
targets.add(newNode.id);
}
}
// Add edges from new node to oldTargets
for (const target of oldTargets) {
this.addEdge(newNode.id, target);
}
}
/**
* Depth-first traversal starting from a node.
* @param {string} startId - The starting node id.
* @param {Set<string>} visited - Internal set to track visited nodes.
* @returns {string[]} - Array of visited node ids in traversal order.
*/
traverse(startId, visited = new Set()) {
if (!this.nodes.has(startId)) return [];
if (visited.has(startId)) return [];
visited.add(startId);
const result = [startId];
const targets = this.edges.get(startId) || new Set();
for (const t of targets) {
result.push(...this.traverse(t, visited));
}
return result;
return obj;
}
}
module.exports = {
Node,
ReflectionNode,
RewritingNode,
Graph,
};
/**
* Demo: Build a simple graph that includes the required
* 'Reflection' and 'Rewrite' nodes.
*/
function demo() {
const g = new Graph();
// Create nodes
const start = g.addNode('Start', { description: 'Entry point' });
const reflection = g.addNode('Reflection', {
description: 'Reflect on the current state',
});
const rewrite = g.addNode('Rewrite', {
description: 'Rewrite the data for the next step',
});
const end = g.addNode('End', { description: 'Exit point' });
// Connect nodes
g.addEdge(start, reflection);
g.addEdge(reflection, rewrite);
g.addEdge(rewrite, end);
console.log('Graph structure:');
console.log(JSON.stringify(g.toJSON(), null, 2));
}
// If this file is executed directly, run the demo.
if (require.main === module) {
demo();
}
module.exports = { Graph, Node };
+10
View File
@@ -0,0 +1,10 @@
const { Graph, createNode } = require('../src/index');
test('Graph runs nodes sequentially', () => {
const graph = new Graph();
graph.addNode(createNode('Rewrite', { pattern: /foo/g, replacement: 'bar' }));
graph.addNode(createNode('Reflection'));
const input = 'foo';
const output = graph.run(input);
expect(output).toBe('bar');
});
+8
View File
@@ -0,0 +1,8 @@
const { createNode } = require('../../src/index');
test('Reflection node returns input unchanged', () => {
const node = createNode('Reflection');
const input = { a: 1 };
const output = node.execute(input);
expect(output).toBe(input);
});
+8
View File
@@ -0,0 +1,8 @@
const { createNode } = require('../../src/index');
test('Rewrite node replaces pattern', () => {
const node = createNode('Rewrite', { pattern: /foo/g, replacement: 'bar' });
const input = 'foo baz foo';
const output = node.execute(input);
expect(output).toBe('bar baz bar');
});