Skip to content

Commit fcabf1b

Browse files
Merge pull request #115 from DiogoRibeiro7/chore/restart
test: structured tag TODO
2 parents 459471c + a0db897 commit fcabf1b

23 files changed

+1600
-28
lines changed

.github/workflows/todo.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ jobs:
2929
issue-title-template: src/templates/issueTitle.txt
3030
issue-body-template: src/templates/issueBody.md
3131
report: true
32-
llm: true
32+
structured: true
33+
llm: false
3334
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
3435

3536
- name: Upload TODO report

action.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ inputs:
1212
description: Whether to generate a TODO markdown report
1313
default: 'false'
1414

15+
structured:
16+
required: false
17+
description: Use structured tag extraction with @assignee, #module, and key=value
18+
default: 'false'
19+
1520
issue-title-template:
1621
required: false
1722
description: Optional path to custom issue title template
@@ -29,6 +34,11 @@ inputs:
2934
required: false
3035
description: 'OpenAI API key used when `llm` is true'
3136

37+
openai-model:
38+
required: false
39+
description: OpenAI model to use (e.g., `gpt-3.5-turbo`, `gpt-4`)
40+
default: gpt-3.5-turbo
41+
3242
runs:
3343
using: 'node20'
3444
main: 'dist/index.js'

dist/core/llm/openaiClient.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import OpenAI from 'openai';
2+
export declare const openai: OpenAI;

dist/index.js

Lines changed: 241 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34358,6 +34358,7 @@ const core = __importStar(__nccwpck_require__(7484));
3435834358
const github = __importStar(__nccwpck_require__(3228));
3435934359
const fs_1 = __importDefault(__nccwpck_require__(9896));
3436034360
const extractTodosFromDir_1 = __nccwpck_require__(3838);
34361+
const extractTodosWithStructuredTagsFromDir_1 = __nccwpck_require__(6728); // 👈 novo
3436134362
const issueManager_1 = __nccwpck_require__(893);
3436234363
const report_1 = __nccwpck_require__(8557);
3436334364
const todoUtils_1 = __nccwpck_require__(2674);
@@ -34375,7 +34376,10 @@ async function run() {
3437534376
if (useLLM && !process.env.OPENAI_API_KEY) {
3437634377
core.warning('⚠️ LLM is enabled, but OPENAI_API_KEY is not set.');
3437734378
}
34378-
const todos = (0, extractTodosFromDir_1.extractTodosFromDir)(workspace);
34379+
const useStructured = core.getInput('structured') === 'true';
34380+
const todos = useStructured
34381+
? (0, extractTodosWithStructuredTagsFromDir_1.extractTodosWithStructuredTagsFromDir)(workspace)
34382+
: (0, extractTodosFromDir_1.extractTodosFromDir)(workspace);
3437934383
const octokit = github.getOctokit(token);
3438034384
const { owner, repo } = github.context.repo;
3438134385
core.info(`🔍 Found ${todos.length} TODOs`);
@@ -34741,15 +34745,50 @@ async function ensureLabelExists(octokit, owner, repo, label) {
3474134745

3474234746
"use strict";
3474334747

34748+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34749+
if (k2 === undefined) k2 = k;
34750+
var desc = Object.getOwnPropertyDescriptor(m, k);
34751+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
34752+
desc = { enumerable: true, get: function() { return m[k]; } };
34753+
}
34754+
Object.defineProperty(o, k2, desc);
34755+
}) : (function(o, m, k, k2) {
34756+
if (k2 === undefined) k2 = k;
34757+
o[k2] = m[k];
34758+
}));
34759+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34760+
Object.defineProperty(o, "default", { enumerable: true, value: v });
34761+
}) : function(o, v) {
34762+
o["default"] = v;
34763+
});
34764+
var __importStar = (this && this.__importStar) || (function () {
34765+
var ownKeys = function(o) {
34766+
ownKeys = Object.getOwnPropertyNames || function (o) {
34767+
var ar = [];
34768+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34769+
return ar;
34770+
};
34771+
return ownKeys(o);
34772+
};
34773+
return function (mod) {
34774+
if (mod && mod.__esModule) return mod;
34775+
var result = {};
34776+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34777+
__setModuleDefault(result, mod);
34778+
return result;
34779+
};
34780+
})();
3474434781
var __importDefault = (this && this.__importDefault) || function (mod) {
3474534782
return (mod && mod.__esModule) ? mod : { "default": mod };
3474634783
};
3474734784
Object.defineProperty(exports, "__esModule", ({ value: true }));
3474834785
exports.generateIssueTitleAndBodyLLM = generateIssueTitleAndBodyLLM;
3474934786
const openai_1 = __importDefault(__nccwpck_require__(2583));
34787+
const core = __importStar(__nccwpck_require__(7484));
3475034788
const openai = new openai_1.default({
34751-
apiKey: process.env.OPENAI_API_KEY || '', // ou core.getInput('openai-api-key')
34789+
apiKey: core.getInput('openai-api-key'), // correto agora
3475234790
});
34791+
const model = core.getInput('openai-model') || 'gpt-3.5-turbo';
3475334792
async function generateIssueTitleAndBodyLLM(todo) {
3475434793
const prompt = `
3475534794
You are a helpful assistant converting inline TODO comments from source code into GitHub Issues.
@@ -34768,18 +34807,29 @@ TITLE: <title>
3476834807
BODY:
3476934808
<detailed body>
3477034809
`;
34771-
const response = await openai.chat.completions.create({
34772-
model: 'gpt-4',
34773-
messages: [{ role: 'user', content: prompt }],
34774-
temperature: 0.4,
34775-
});
34776-
const result = response.choices[0].message?.content || '';
34777-
const match = result.match(/TITLE:\s*(.+?)\s*BODY:\s*([\s\S]*)/i);
34778-
if (!match) {
34779-
throw new Error('Failed to parse LLM response.');
34810+
// 👇 Adiciona aqui
34811+
console.log('[DEBUG] OpenAI key starts with:', process.env.OPENAI_API_KEY?.slice(0, 5));
34812+
console.log('[DEBUG] Using model:', model);
34813+
console.log('[DEBUG] Sending prompt to OpenAI...');
34814+
try {
34815+
const response = await openai.chat.completions.create({
34816+
model,
34817+
messages: [{ role: 'user', content: prompt }],
34818+
temperature: 0.4,
34819+
});
34820+
// TODO(priority=high): improve retry logic for API errors
34821+
const result = response.choices[0].message?.content || '';
34822+
const match = result.match(/TITLE:\s*(.+?)\s*BODY:\s*([\s\S]*)/i);
34823+
if (!match) {
34824+
throw new Error('Failed to parse LLM response.');
34825+
}
34826+
const [, title, body] = match;
34827+
return { title: title.trim(), body: body.trim() };
34828+
}
34829+
catch (err) {
34830+
console.error('[ERROR] OpenAI call failed:', err);
34831+
throw err;
3478034832
}
34781-
const [, title, body] = match;
34782-
return { title: title.trim(), body: body.trim() };
3478334833
}
3478434834

3478534835

@@ -34883,6 +34933,55 @@ function todoKey(todo) {
3488334933
}
3488434934

3488534935

34936+
/***/ }),
34937+
34938+
/***/ 5748:
34939+
/***/ ((__unused_webpack_module, exports) => {
34940+
34941+
"use strict";
34942+
34943+
// src/parser/extractStructuredTags.ts
34944+
Object.defineProperty(exports, "__esModule", ({ value: true }));
34945+
exports.extractStructuredTags = extractStructuredTags;
34946+
/**
34947+
* Extracts structured tags from TODO comment text.
34948+
*
34949+
* Supports:
34950+
* - @username → assignees
34951+
* - #module → modules
34952+
* - key=value → structured metadata
34953+
*
34954+
* @param text Raw TODO text
34955+
* @returns Partial<TodoItem> with assignees, modules, and structured tags
34956+
*/
34957+
function extractStructuredTags(text) {
34958+
const assignees = [];
34959+
const modules = [];
34960+
const structured = {};
34961+
const words = text.split(/\s+/);
34962+
for (const word of words) {
34963+
if (word.startsWith('@') && word.length > 1) {
34964+
assignees.push(word.slice(1));
34965+
}
34966+
else if (word.startsWith('#') && word.length > 1) {
34967+
modules.push(word.slice(1));
34968+
}
34969+
else if (/^[a-zA-Z0-9_-]+=/.test(word)) {
34970+
const [key, ...valueParts] = word.split('=');
34971+
const value = valueParts.join('=');
34972+
if (key && value) {
34973+
structured[key] = value.replace(/^['"]|['"]$/g, ''); // strip quotes
34974+
}
34975+
}
34976+
}
34977+
return {
34978+
assignees: assignees.length ? assignees : undefined,
34979+
modules: modules.length ? modules : undefined,
34980+
structured: Object.keys(structured).length ? structured : undefined,
34981+
};
34982+
}
34983+
34984+
3488634985
/***/ }),
3488734986

3488834987
/***/ 2001:
@@ -34945,6 +35044,62 @@ function extractTodosFromFile(filePath) {
3494535044
}
3494635045

3494735046

35047+
/***/ }),
35048+
35049+
/***/ 412:
35050+
/***/ ((__unused_webpack_module, exports) => {
35051+
35052+
"use strict";
35053+
35054+
Object.defineProperty(exports, "__esModule", ({ value: true }));
35055+
exports.extractTodosFromString = extractTodosFromString;
35056+
const COMMENT_PATTERNS = [
35057+
{ ext: ['.ts', '.js', '.java', '.go'], pattern: /^\s*\/\/\s*(.*)$/ },
35058+
{ ext: ['.py', '.sh', '.rb'], pattern: /^\s*#\s*(.*)$/ },
35059+
{ ext: ['.html', '.xml'], pattern: /<!--\s*(.*?)\s*-->/ }
35060+
];
35061+
const TAG_REGEX = /(TODO|FIXME|BUG|HACK)(\([^)]*\))?:?\s*(.*)/i;
35062+
function extractMetadata(str) {
35063+
const meta = {};
35064+
const match = str.match(/\((.*?)\)/);
35065+
if (match) {
35066+
const content = match[1];
35067+
content.split(',').forEach(pair => {
35068+
const [key, val] = pair.split('=').map(s => s.trim());
35069+
if (key && val)
35070+
meta[key] = val;
35071+
});
35072+
}
35073+
return meta;
35074+
}
35075+
function extractTodosFromString(content, ext) {
35076+
const pattern = COMMENT_PATTERNS.find(p => p.ext.includes(ext));
35077+
if (!pattern)
35078+
return [];
35079+
const lines = content.split('\n');
35080+
const todos = [];
35081+
lines.forEach((line, idx) => {
35082+
const commentMatch = line.match(pattern.pattern);
35083+
if (commentMatch) {
35084+
const comment = commentMatch[1];
35085+
const tagMatch = comment.match(TAG_REGEX);
35086+
if (tagMatch) {
35087+
const [_, tag, metaRaw, text] = tagMatch;
35088+
const metadata = metaRaw ? extractMetadata(metaRaw) : undefined;
35089+
todos.push({
35090+
file: `inline${ext}`,
35091+
line: idx + 1,
35092+
tag,
35093+
text: text.trim(),
35094+
metadata
35095+
});
35096+
}
35097+
}
35098+
});
35099+
return todos;
35100+
}
35101+
35102+
3494835103
/***/ }),
3494935104

3495035105
/***/ 3838:
@@ -34984,6 +35139,79 @@ function extractTodosFromDir(dirPath) {
3498435139
}
3498535140

3498635141

35142+
/***/ }),
35143+
35144+
/***/ 903:
35145+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
35146+
35147+
"use strict";
35148+
35149+
var __importDefault = (this && this.__importDefault) || function (mod) {
35150+
return (mod && mod.__esModule) ? mod : { "default": mod };
35151+
};
35152+
Object.defineProperty(exports, "__esModule", ({ value: true }));
35153+
exports.extractTodosWithStructuredTags = extractTodosWithStructuredTags;
35154+
const extractTodosFromContent_1 = __nccwpck_require__(412);
35155+
const extractStructuredTags_1 = __nccwpck_require__(5748);
35156+
const fs_1 = __importDefault(__nccwpck_require__(9896));
35157+
function extractTodosWithStructuredTags(filePath) {
35158+
const ext = filePath.slice(filePath.lastIndexOf('.'));
35159+
const content = fs_1.default.readFileSync(filePath, 'utf8');
35160+
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, ext);
35161+
return todos.map(todo => {
35162+
const structured = (0, extractStructuredTags_1.extractStructuredTags)(todo.text);
35163+
return {
35164+
...todo,
35165+
metadata: {
35166+
...(todo.metadata || {}),
35167+
...Object.fromEntries(Object.entries(structured).map(([key, value]) => [key, String(value)])),
35168+
},
35169+
};
35170+
});
35171+
}
35172+
35173+
35174+
/***/ }),
35175+
35176+
/***/ 6728:
35177+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
35178+
35179+
"use strict";
35180+
35181+
var __importDefault = (this && this.__importDefault) || function (mod) {
35182+
return (mod && mod.__esModule) ? mod : { "default": mod };
35183+
};
35184+
Object.defineProperty(exports, "__esModule", ({ value: true }));
35185+
exports.extractTodosWithStructuredTagsFromDir = extractTodosWithStructuredTagsFromDir;
35186+
// src/parser/extractTodosWithStructuredTagsFromDir.ts
35187+
const path_1 = __importDefault(__nccwpck_require__(6928));
35188+
const fs_1 = __importDefault(__nccwpck_require__(9896));
35189+
const extractTodosWithStructuredTags_1 = __nccwpck_require__(903);
35190+
function extractTodosWithStructuredTagsFromDir(dir) {
35191+
const todos = [];
35192+
function walk(currentPath) {
35193+
const entries = fs_1.default.readdirSync(currentPath, { withFileTypes: true });
35194+
for (const entry of entries) {
35195+
const fullPath = path_1.default.join(currentPath, entry.name);
35196+
if (entry.isDirectory()) {
35197+
walk(fullPath);
35198+
}
35199+
else if (entry.isFile()) {
35200+
try {
35201+
const fileTodos = (0, extractTodosWithStructuredTags_1.extractTodosWithStructuredTags)(fullPath);
35202+
todos.push(...fileTodos);
35203+
}
35204+
catch {
35205+
// opcional: log de ficheiros ignorados
35206+
}
35207+
}
35208+
}
35209+
}
35210+
walk(dir);
35211+
return todos;
35212+
}
35213+
35214+
3498735215
/***/ }),
3498835216

3498935217
/***/ 450:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Extracts structured metadata from a TODO text string.
3+
* Supports patterns like:
4+
* - @key:value
5+
* - #key=value
6+
* - key=value
7+
* - key="multi word string"
8+
*
9+
* @param text The text from which to extract metadata.
10+
* @returns A dictionary of metadata keys and values.
11+
*/
12+
export declare function extractStructuredMetadata(text: string): Record<string, string>;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { TodoItem } from './types';
2+
/**
3+
* Extracts structured tags from TODO comment text.
4+
*
5+
* Supports:
6+
* - @username → assignees
7+
* - #module → modules
8+
* - key=value → structured metadata
9+
*
10+
* @param text Raw TODO text
11+
* @returns Partial<TodoItem> with assignees, modules, and structured tags
12+
*/
13+
export declare function extractStructuredTags(text: string): Partial<TodoItem>;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { TodoItem } from './types';
2+
export declare function extractTodosWithStructuredTags(filePath: string): TodoItem[];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { TodoItem } from './types';
2+
export declare function extractTodosWithStructuredTagsFromDir(dir: string): TodoItem[];

dist/parser/types.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ export interface TodoItem {
44
file: string;
55
line: number;
66
metadata?: Record<string, string>;
7-
[key: string]: string | number | Record<string, string> | undefined;
7+
assignees?: string[];
8+
modules?: string[];
9+
structured?: Record<string, string>;
10+
[key: string]: string | number | string[] | Record<string, string> | undefined;
811
}

dist/utils/isTextFile.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Checks whether a filename likely corresponds to a text-based source file.
3+
* Useful for filtering files before parsing for TODOs.
4+
*/
5+
export declare function isTextFile(filename: string): boolean;

0 commit comments

Comments
 (0)