{"id":50388250,"url":"https://github.com/minte9/recall-forge","last_synced_at":"2026-05-30T16:30:32.794Z","repository":{"id":359627643,"uuid":"1246874942","full_name":"minte9/recall-forge","owner":"minte9","description":"Ask review questions and evaluate answers (OpenAI)","archived":false,"fork":false,"pushed_at":"2026-05-30T14:33:22.000Z","size":175,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-30T16:13:23.743Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/minte9.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T16:46:47.000Z","updated_at":"2026-05-30T14:33:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/minte9/recall-forge","commit_stats":null,"previous_names":["minte9/recall-forge"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/minte9/recall-forge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minte9%2Frecall-forge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minte9%2Frecall-forge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minte9%2Frecall-forge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minte9%2Frecall-forge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/minte9","download_url":"https://codeload.github.com/minte9/recall-forge/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minte9%2Frecall-forge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33700863,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-30T16:30:31.919Z","updated_at":"2026-05-30T16:30:32.789Z","avatar_url":"https://github.com/minte9.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Recall Forge - v1.0.1\n\nThe application import topics from a local README.md, ask review questions,  \nevaluates answers with OpenAI, and stores memory in database.  \n\n## 1. Backend-only\nv1.0.1\n\n- 1.1 Project structure\n- 1.2 Gradle Settings \n- 1.3 Gradle Build\n- 1.4 Docker Compose\n- 1.5 OpenAPI \n- 1.6 Application Properties\n- 1.7 Main App\n- 1.8 Domain model (JPA)\n- 1.9 Repositories\n- 1.10 DTOs\n- 1.11 OpenAI Config\n- 1.12 Services\n- 1.13 Controllers\n- 1.14 Run the project\n- 1.15 Api Requests\n\n## 2. Repetition System\nv1.0.2\n\n### 2.1 Review History Endpoint\n\n#### Add DTO\n\nsrc/main/../dto/ReviewHistoryResponse.java\n\n\u003cdetails\u003e\n\u003csummary\u003eReviewHistoryResponse.java\u003c/summary\u003e\n\n~~~java\npackage dev.recallforge.dto;\n\nimport dev.recallforge.domain.Review;\n\nimport java.time.LocalDateTime;\n\npublic record ReviewHistoryResponse(\n        Long id,\n        Long topicId,\n        String topicTitle,\n        String question,\n        String userAnswer,\n        double score,\n        String feedback,\n        LocalDateTime reviewedAt\n) {\n    public static ReviewHistoryResponse from(Review review) {\n        return new ReviewHistoryResponse(\n                review.getId(),\n                review.getTopic().getId(),\n                review.getTopic().getTitle(),\n                review.getQuestion(),\n                review.getUserAnswer(),\n                review.getScore(),\n                review.getFeedback(),\n                review.getReviewedAt()\n        );\n    }\n}\n~~~\n\u003c/details\u003e\n\n#### Update ReviewService\n\n~~~java\npublic List\u003cReviewHistoryResponse\u003e getReviewHistoryForTopic(Long topicId) {\n    topicService.getTopic(topicId);\n\n    return reviewRepository.findByTopicIdOrderByReviewedAtDesc(topicId)\n            .stream()\n            .map(ReviewHistoryResponse::from)\n            .toList();\n}\n~~~\n\n### Update Controller\n\n~~~java\n@GetMapping(\"/{topicId}/reviews\")\npublic List\u003cReviewHistoryResponse\u003e getReviewHistory(\n        @PathVariable Long topicId\n) {\n    return reviewService.getReviewHistoryForTopic(topicId);\n}\n~~~\n\n#### Test it\n\n~~~sh\n./gradlew bootRun\n~~~\n\n#### API request\n\n~~~sh\ncurl http://localhost:9090/api/topics/2/reviews | jq\n~~~\n\n\u003cdetails\u003e\n\u003csummary\u003eReview History Response\u003c/summary\u003e\n\n~~~json\n[\n  {\n    \"id\": 4,\n    \"topicId\": 2,\n    \"topicTitle\": \"Pipeline Model\",\n    \"question\": \"What is the main purpose of using a pipeline model in data processing?\",\n    \"userAnswer\": \"A pipeline model process data in steps\",\n    \"score\": 0.5,\n    \"feedback\": \"The answer correctly states that a pipeline processes data in steps but does not explain the main purpose, such as simplifying complex workflows or improving understanding and debugging.\",\n    \"reviewedAt\": \"2026-05-23T16:58:06.344937\"\n  },\n  {\n    \"id\": 3,\n    \"topicId\": 2,\n    \"topicTitle\": \"Pipeline Model\",\n    \"question\": \"What is the main purpose of using a pipeline model in data processing?\",\n    \"userAnswer\": \"A pipeline model process data in steps\",\n    \"score\": 0.5,\n    \"feedback\": \"The answer correctly states that a pipeline processes data in steps but does not explain the main purpose, such as simplifying complex workflows or improving understanding and debugging.\",\n    \"reviewedAt\": \"2026-05-23T16:51:18.051198\"\n  },\n  {\n    \"id\": 2,\n    \"topicId\": 2,\n    \"topicTitle\": \"Pipeline Model\",\n    \"question\": \"What is the main purpose of using a pipeline model in data processing?\",\n    \"userAnswer\": \"A pipeline model process data in steps\",\n    \"score\": 0.5,\n    \"feedback\": \"The answer correctly identifies that a pipeline processes data in steps but does not explain the main purpose, such as simplifying complex workflows or improving understanding and debugging.\",\n    \"reviewedAt\": \"2026-05-23T16:24:28.434414\"\n  }\n]\n~~~\n\u003c/details\u003e\n\n\n### 2.2 Due Topics Endpoint\n\nRight now GET /topics returns all topics.  \nBut in real spaced repetition GET /topics/due returns only topics for review.  \n\nTopicController.java\n\n~~~java\n@GetMapping(\"/due\")\npublic List\u003cTopicResponse\u003e getDueTopics() {\n\n    return topicService\n            .getDueTopics()\n            .stream()\n            .map(TopicResponse::from)\n            .toList();\n}\n~~~\n\n### 2.3 Test it\n\n~~~sh\ncurl http://localhost:9090/api/topics/due | jq\n~~~\n\n~~~json\n[\n  {\n    \"id\": 3,\n    \"title\": \"Agent Loop\",\n    \"memoryScore\": 0.5,\n    \"nextReviewAt\": \"2026-05-22T18:29:01.600257\"\n  },\n  {\n    \"id\": 4,\n    \"title\": \"Tool Calling\",\n    \"memoryScore\": 0.5,\n    \"nextReviewAt\": \"2026-05-22T18:29:01.604235\"\n  },\n  {\n    \"id\": 5,\n    \"title\": \"Spaced Repetition\",\n    \"memoryScore\": 0.5,\n    \"nextReviewAt\": \"2026-05-22T18:29:01.609641\"\n  }\n]\n~~~\n~~~sh\ncurl -X POST http://localhost:9090/api/reviews/start | jq\n~~~\n~~~json\n{\n  \"topicId\": 3,\n  \"topicTitle\": \"Agent Loop\",\n  \"question\": \"What are the four main steps in an agent loop?\"\n}\n~~~\n\nYou should see reviewd topic disappears (until nextReviewAt).  \n\n\n## 3. Dashboard\n\n### 3.1 Index file\n\nCreate this file:\n\n~~~sh\nsrc/main/resources/static/index.html\n~~~\n\nRun\n\n~~~sh\n./gradlew bootRun\n~~~\n\nOpen:\n\n~~~sh\nhttp://localhost:9090\n~~~\n\n~~~sh\nlsof -i :9090\n\nCOMMAND    PID    USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME\njava    882866 catalin   89u  IPv6 2652026      0t0  TCP *:9090 (LISTEN)\n\nkill -9 882866\n~~~\n\n## 4. Upload markdown file\n\nMarkdown become part of the Topic stored in DB.  \nWhen /api/reviews/start returns the next review, it also returns the markdown content.  \n\n### 4.1 MarkdownFile Entity\n\nCreate Markdown file entity:\n\n/domain/MarkdownFile.java\n\nConnect Topic to MarkdownFile\n\n/domain/Topic.java\n\n~~~java\n@ManyToOne(fetch = FetchType.LAZY)\n@JoinColumn(name = \"markdown_file_id\")\nprivate MarkdownFile markdownFile;\n~~~\n\n### 4.2 Add Repository\n\n/repository/MarkdownFileRepository\n\n~~~java\npublic interface MarkdownFileRepository extends JpaRepository\u003cMarkdownFile, Long\u003e {\n}\n~~~\n\n### 4.3 Add upload endpoint\n\n/controller/MarkdownController.java\n\n~~~java\n@RestController\n@RequestMapping(\"/api/markdown\")\npublic class MarkdownUploadController {\n\n    private final MarkdownUploadService markdownUploadService;\n\n    public MarkdownUploadController(MarkdownUploadService markdownUploadService) {\n        this.markdownUploadService = markdownUploadService;\n    }\n\n    @PostMapping(\"/upload\")\n    public void uploadMarkdown(@RequestParam(\"file\") MultipartFile file) throws Exception {\n        markdownUploadService.upload(file);\n    }\n}\n~~~\n\n### 4.4 Upload Service\n\nThis stores the full file, than creates topics from markdown headings.  \n\n/service/MarkdownService.java\n\n~~~java\n@Service\npublic class MarkdownService {\n\n    private final MarkdownFileRepository markdownFileRepository;\n    private final TopicRepository topicRepository;\n    private final MarkdownTopicImporter markdownTopicImporter;\n\n    public MarkdownService(\n            MarkdownFileRepository markdownFileRepository,\n            TopicRepository topicRepository,\n            MarkdownTopicImporter markdownTopicImporter\n    ) {\n        this.markdownFileRepository = markdownFileRepository;\n        this.topicRepository = topicRepository;\n        this.markdownTopicImporter = markdownTopicImporter;\n    }\n\n    public void upload(MultipartFile file) throws Exception {\n        String content = new String(file.getBytes(), StandardCharsets.UTF_8);\n\n        MarkdownFile markdownFile = new MarkdownFile(file.getOriginalFilename(), content);\n        markdownFileRepository.save(markdownFile);\n\n        createTopicsFromMarkdown(markdownFile, content);\n    }\n\n    private void createTopicsFromMarkdown(MarkdownFile markdownFile, String markdown) {\n        List\u003cMarkdownTopicImporter.ParsedTopic\u003e parseTopics = \n            markdownTopicImporter.parseMarkdown(markdown);\n\n        for (MarkdownTopicImporter.ParsedTopic parsedTopic : parseTopics) {\n            Topic topic = new Topic(\n                parsedTopic.title(),\n                parsedTopic.content(),\n                markdownFile\n            );\n\n            topicRepository.save(topic);\n        }\n    }\n}\n~~~\n\nUpdate review response DTO\n\n~~~java\npackage dev.recallforge.dto;\n\npublic record ReviewQuestionResponse(\n    Long topicId,\n    String topicTitle,\n    String question,\n    String markdownContent\n) {\n}\n~~~\n\nUpdate review service\n\n~~~java\npublic ReviewQuestionResponse startReview() {\n        Topic topic = topicService.selectNextTopic();\n\n        String question = openAiService.generateQuestion(\n            topic.getTitle(), \n            topic.getContent()\n        );\n\n        return new ReviewQuestionResponse(\n            topic.getId(), \n            topic.getTitle(), \n            question,\n            topic.getMarkdownFile().getContent()\n        );\n    }\n~~~\n\n\n### 4.5 Update dashboar upload\n\nReplace the browser-only upload with real backend upload.  \n\n~~~javascript\nasync uploadMarkdown(event) {\n  const file = event.target.files[0];\n\n  if (!file) {\n    return;\n  }\n\n  const formData = new FormData();\n  formData.append(\"file\", file);\n\n  await fetch(\"/api/markdown/upload\", {\n    method: \"POST\",\n    body: formData\n  });\n\n  await this.startReview();\n}\n~~~\n\n\n### 4.6 Clear old data\n\n~~~sh\nsudo apt install postgresql-client\n\npsql -h localhost -p 5432 -U recallforge -d recallforge\n\n\\dt\n\ndelete from reviews;\ndelete from topics;\ndelete from markdown_files;\n\nOR\n\ntruncate reviews, topics, markdown_files restart identity cascade;\n~~~\n\nSince you already created the table:\n\n~~~sh\nalter table markdown_files\nadd column content_hash varchar(64);\n\nupdate markdown_files\nset content_hash = md5(content);\n\nalter table markdown_files\nalter column content_hash set not null;\n\nalter table markdown_files\nadd constraint uk_markdown_hash\nunique(content_hash);\n~~~\n\nNow:\n\n\n~~~sh\nREADME.md (content A) → accepted\nREADME.md (content B) → accepted\nREADME.md (same content A) → rejected\nnotes.md (same content A) → rejected\n~~~\n\n\n### 4.7 Check due topics\n\n~~~sh\nsudo apt install postgresql-client\n\npsql -h localhost -p 5432 -U recallforge -d recallforge\n\n\\dt\n\nselect id, title, next_review_at, memory_score\nfrom topics\norder by next_review_at;\n~~~\n\n### 4.8 Add categories\n\n### 4.9 Dump db\n\nDump from the container:\n\n~~~sh\ndocker exec -t recallforge-postgres pg_dump -U recallforge -d recallforge \u003e recallforge.sql\n\nls -lh recallforge.sql\n~~~\n\nRestore on another computer/container:\n\n~~~sh\npsql -h localhost -p 5432 -U recallforge -d postgres\nDROP DATABASE recallforge;\nCREATE DATABASE recallforge OWNER recallforge;\n\\q\n\ncat recallforge.sql | docker exec -i recallforge-postgres psql -U recallforge -d recallforge\n~~~\n\nSee how many reviews are due now:\n\n~~~sh\nselect count(*) from topics where next_review_at \u003c= now();\n~~~\n\nForce \"all done\" state:\n\n~~~sh\nupdate topics set next_review_at = now() + interval '1 day' where next_review_at \u003c= now();\n~~~\n\n\n### 4.10 Queue Summary\n\nImplement this as a read-only “queue summary” first: backend count + next review time, then a small UI badge.\n\nIn ReviewController:\n\n~~~java\n@GetMapping(\"/queue/today\")\npublic ReviewQueueResponse getDailyQueue() {\n    return reviewService.getDailyQueue();\n}\n~~~\n\nTest it:\n\n~~~sh\ncurl http://localhost:9090/api/reviews/queue/today | jq\n~~~\n~~~json\n{\n  \"dueCount\": 0,\n  \"nextReviewAt\": \"2026-05-30T23:26:37.379304\",\n  \"doneForToday\": true\n}\n~~~","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminte9%2Frecall-forge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fminte9%2Frecall-forge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminte9%2Frecall-forge/lists"}