forge-workflow 0.0.4 → 0.0.6
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/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const { normalizeRepoPath } = require('./path-utils.js');
|
|
4
|
-
|
|
5
|
-
function ensureTaskFile(taskFile) {
|
|
6
|
-
if (!taskFile || typeof taskFile !== 'string') {
|
|
7
|
-
throw new Error('Task file path is required');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const absolutePath = path.resolve(taskFile);
|
|
11
|
-
if (!fs.existsSync(absolutePath)) {
|
|
12
|
-
throw new Error(`Task file does not exist: ${absolutePath}`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return absolutePath;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function parseFilesLine(value) {
|
|
19
|
-
if (!value) {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return value
|
|
24
|
-
.split(',')
|
|
25
|
-
.map((file) => file.replaceAll('`', '').trim())
|
|
26
|
-
.filter(Boolean);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function flushTask(tasks, currentTask) {
|
|
30
|
-
if (!currentTask) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
tasks.push({
|
|
35
|
-
number: currentTask.number,
|
|
36
|
-
title: currentTask.title,
|
|
37
|
-
files: currentTask.files,
|
|
38
|
-
whatToImplement: currentTask.whatToImplement.trim(),
|
|
39
|
-
expectedOutput: currentTask.expectedOutput.trim(),
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isIdentifierStart(character) {
|
|
44
|
-
return /[A-Za-z_]/.test(character);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isIdentifierPart(character) {
|
|
48
|
-
return /\w/.test(character);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function extractSymbols(text) {
|
|
52
|
-
const symbols = new Set();
|
|
53
|
-
let searchIndex = 0;
|
|
54
|
-
|
|
55
|
-
while (searchIndex < text.length) {
|
|
56
|
-
const callIndex = text.indexOf('()', searchIndex);
|
|
57
|
-
if (callIndex === -1) {
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let startIndex = callIndex - 1;
|
|
62
|
-
while (startIndex >= 0 && isIdentifierPart(text[startIndex])) {
|
|
63
|
-
startIndex -= 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const symbol = text.slice(startIndex + 1, callIndex);
|
|
67
|
-
if (symbol && isIdentifierStart(symbol[0])) {
|
|
68
|
-
symbols.add(symbol);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
searchIndex = callIndex + 2;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (const match of text.match(/`([^`\r\n]+)`/g) ?? []) {
|
|
75
|
-
const token = match.slice(1, -1).trim();
|
|
76
|
-
if (!token || /[/.\\\s]/.test(token)) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(token)) {
|
|
81
|
-
symbols.add(token);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return Array.from(symbols);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function detectContractType(symbol, annotation = '') {
|
|
89
|
-
if (annotation === 'data-format' || annotation === 'command-contract' || annotation === 'return-shape') {
|
|
90
|
-
return annotation;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (/Schema$|Format$/i.test(symbol)) {
|
|
94
|
-
return 'data-format';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (/Contract$/i.test(symbol)) {
|
|
98
|
-
return 'command-contract';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (symbol.includes('-')) {
|
|
102
|
-
return 'command-contract';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (/Shape$/i.test(symbol)) {
|
|
106
|
-
return 'return-shape';
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return 'modified';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function extractContractMentions(text) {
|
|
113
|
-
const mentions = new Map();
|
|
114
|
-
for (const symbol of extractSymbols(text)) {
|
|
115
|
-
mentions.set(symbol, {
|
|
116
|
-
symbol,
|
|
117
|
-
annotation: text.includes(`${symbol}()`) ? 'modified' : detectContractType(symbol),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
for (const match of text.match(/\b[A-Za-z_]\w*(?:Schema|Format|Contract|Shape)\b/g) ?? []) {
|
|
122
|
-
if (mentions.has(match)) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
mentions.set(match, {
|
|
127
|
-
symbol: match,
|
|
128
|
-
annotation: detectContractType(match),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return Array.from(mentions.values());
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function escapeRegExp(value) {
|
|
136
|
-
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function resolveRepositoryFile(repositoryRoot, filePath) {
|
|
140
|
-
if (!repositoryRoot || !filePath || path.isAbsolute(filePath)) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const absoluteRoot = path.resolve(repositoryRoot);
|
|
145
|
-
const absolutePath = path.resolve(absoluteRoot, filePath);
|
|
146
|
-
const relativePath = path.relative(absoluteRoot, absolutePath);
|
|
147
|
-
if (
|
|
148
|
-
relativePath === ''
|
|
149
|
-
|| relativePath.startsWith(`..${path.sep}`)
|
|
150
|
-
|| relativePath === '..'
|
|
151
|
-
|| path.isAbsolute(relativePath)
|
|
152
|
-
) {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return absolutePath;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function fileContainsSymbol(repositoryRoot, filePath, symbol) {
|
|
160
|
-
const absolutePath = resolveRepositoryFile(repositoryRoot, filePath);
|
|
161
|
-
if (!absolutePath) {
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!fs.existsSync(absolutePath)) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
170
|
-
if (/^[A-Za-z_]\w*$/.test(symbol)) {
|
|
171
|
-
return new RegExp(String.raw`\b${escapeRegExp(symbol)}\b`).test(content);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return content.includes(symbol);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function resolveMentionFiles(task, mention, repositoryRoot) {
|
|
178
|
-
const files = task.files ?? [];
|
|
179
|
-
if (files.length === 0) {
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!repositoryRoot) {
|
|
184
|
-
return files.map((file) => normalizeRepoPath(file));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const matchingFiles = files
|
|
188
|
-
.filter((file) => fileContainsSymbol(repositoryRoot, file, mention.symbol))
|
|
189
|
-
.map((file) => normalizeRepoPath(file));
|
|
190
|
-
return matchingFiles;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function extractTaskContracts(taskContext, options = {}) {
|
|
194
|
-
if (!taskContext || !Array.isArray(taskContext.tasks)) {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const contracts = new Set();
|
|
199
|
-
const repositoryRoot = options.repositoryRoot;
|
|
200
|
-
|
|
201
|
-
for (const task of taskContext.tasks) {
|
|
202
|
-
const mentions = extractContractMentions(task.whatToImplement ?? '');
|
|
203
|
-
for (const mention of mentions) {
|
|
204
|
-
for (const file of resolveMentionFiles(task, mention, repositoryRoot)) {
|
|
205
|
-
contracts.add(`${file}:${mention.symbol}(${mention.annotation})`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return Array.from(contracts).sort((left, right) => left.localeCompare(right));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function isTaskHeader(line) {
|
|
214
|
-
return line.startsWith('## Task ');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function parseTaskHeader(line) {
|
|
218
|
-
if (!isTaskHeader(line)) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const prefixLength = '## Task '.length;
|
|
223
|
-
const separatorIndex = line.indexOf(':', prefixLength);
|
|
224
|
-
if (separatorIndex === -1) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const numberText = line.slice(prefixLength, separatorIndex).trim();
|
|
229
|
-
if (!numberText || Array.from(numberText).some((character) => character < '0' || character > '9')) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const title = line.slice(separatorIndex + 1).trim();
|
|
234
|
-
if (!title) {
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
number: Number(numberText),
|
|
240
|
-
title,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function parseTaskField(currentTask, line, currentField) {
|
|
245
|
-
if (line.startsWith('File(s):')) {
|
|
246
|
-
currentTask.files = parseFilesLine(line.slice('File(s):'.length).trim());
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (line.startsWith('What to implement:')) {
|
|
251
|
-
currentTask.whatToImplement = line.slice('What to implement:'.length).trim();
|
|
252
|
-
return 'whatToImplement';
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (line.startsWith('Expected output:')) {
|
|
256
|
-
currentTask.expectedOutput = line.slice('Expected output:'.length).trim();
|
|
257
|
-
return 'expectedOutput';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (line === 'TDD steps:' || line === '---') {
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (currentField && line.trim()) {
|
|
265
|
-
currentTask[currentField] = `${currentTask[currentField]} ${line.trim()}`.trim();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return currentField;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function parseTaskFile(taskFile) {
|
|
272
|
-
const absolutePath = ensureTaskFile(taskFile);
|
|
273
|
-
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
274
|
-
const lines = content.split(/\r?\n/);
|
|
275
|
-
const tasks = [];
|
|
276
|
-
let currentTask = null;
|
|
277
|
-
let currentField = null;
|
|
278
|
-
|
|
279
|
-
for (const rawLine of lines) {
|
|
280
|
-
const line = rawLine.trimEnd();
|
|
281
|
-
const taskHeader = parseTaskHeader(line);
|
|
282
|
-
|
|
283
|
-
if (taskHeader) {
|
|
284
|
-
flushTask(tasks, currentTask);
|
|
285
|
-
currentTask = {
|
|
286
|
-
number: taskHeader.number,
|
|
287
|
-
title: taskHeader.title,
|
|
288
|
-
files: [],
|
|
289
|
-
whatToImplement: '',
|
|
290
|
-
expectedOutput: '',
|
|
291
|
-
};
|
|
292
|
-
currentField = null;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (!currentTask) {
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
currentField = parseTaskField(currentTask, line, currentField);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
flushTask(tasks, currentTask);
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
path: absolutePath,
|
|
307
|
-
taskCount: tasks.length,
|
|
308
|
-
tasks,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
module.exports = {
|
|
313
|
-
detectContractType,
|
|
314
|
-
extractContractMentions,
|
|
315
|
-
extractSymbols,
|
|
316
|
-
extractTaskContracts,
|
|
317
|
-
parseTaskFile,
|
|
318
|
-
};
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { normalizeRepoPath } = require('./path-utils.js');
|
|
4
|
+
|
|
5
|
+
function ensureTaskFile(taskFile) {
|
|
6
|
+
if (!taskFile || typeof taskFile !== 'string') {
|
|
7
|
+
throw new Error('Task file path is required');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const absolutePath = path.resolve(taskFile);
|
|
11
|
+
if (!fs.existsSync(absolutePath)) {
|
|
12
|
+
throw new Error(`Task file does not exist: ${absolutePath}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return absolutePath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseFilesLine(value) {
|
|
19
|
+
if (!value) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return value
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((file) => file.replaceAll('`', '').trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function flushTask(tasks, currentTask) {
|
|
30
|
+
if (!currentTask) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
tasks.push({
|
|
35
|
+
number: currentTask.number,
|
|
36
|
+
title: currentTask.title,
|
|
37
|
+
files: currentTask.files,
|
|
38
|
+
whatToImplement: currentTask.whatToImplement.trim(),
|
|
39
|
+
expectedOutput: currentTask.expectedOutput.trim(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isIdentifierStart(character) {
|
|
44
|
+
return /[A-Za-z_]/.test(character);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isIdentifierPart(character) {
|
|
48
|
+
return /\w/.test(character);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractSymbols(text) {
|
|
52
|
+
const symbols = new Set();
|
|
53
|
+
let searchIndex = 0;
|
|
54
|
+
|
|
55
|
+
while (searchIndex < text.length) {
|
|
56
|
+
const callIndex = text.indexOf('()', searchIndex);
|
|
57
|
+
if (callIndex === -1) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let startIndex = callIndex - 1;
|
|
62
|
+
while (startIndex >= 0 && isIdentifierPart(text[startIndex])) {
|
|
63
|
+
startIndex -= 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const symbol = text.slice(startIndex + 1, callIndex);
|
|
67
|
+
if (symbol && isIdentifierStart(symbol[0])) {
|
|
68
|
+
symbols.add(symbol);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
searchIndex = callIndex + 2;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const match of text.match(/`([^`\r\n]+)`/g) ?? []) {
|
|
75
|
+
const token = match.slice(1, -1).trim();
|
|
76
|
+
if (!token || /[/.\\\s]/.test(token)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (/^[A-Za-z_][A-Za-z0-9_-]*$/.test(token)) {
|
|
81
|
+
symbols.add(token);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Array.from(symbols);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function detectContractType(symbol, annotation = '') {
|
|
89
|
+
if (annotation === 'data-format' || annotation === 'command-contract' || annotation === 'return-shape') {
|
|
90
|
+
return annotation;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (/Schema$|Format$/i.test(symbol)) {
|
|
94
|
+
return 'data-format';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (/Contract$/i.test(symbol)) {
|
|
98
|
+
return 'command-contract';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (symbol.includes('-')) {
|
|
102
|
+
return 'command-contract';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (/Shape$/i.test(symbol)) {
|
|
106
|
+
return 'return-shape';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return 'modified';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractContractMentions(text) {
|
|
113
|
+
const mentions = new Map();
|
|
114
|
+
for (const symbol of extractSymbols(text)) {
|
|
115
|
+
mentions.set(symbol, {
|
|
116
|
+
symbol,
|
|
117
|
+
annotation: text.includes(`${symbol}()`) ? 'modified' : detectContractType(symbol),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const match of text.match(/\b[A-Za-z_]\w*(?:Schema|Format|Contract|Shape)\b/g) ?? []) {
|
|
122
|
+
if (mentions.has(match)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
mentions.set(match, {
|
|
127
|
+
symbol: match,
|
|
128
|
+
annotation: detectContractType(match),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Array.from(mentions.values());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function escapeRegExp(value) {
|
|
136
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveRepositoryFile(repositoryRoot, filePath) {
|
|
140
|
+
if (!repositoryRoot || !filePath || path.isAbsolute(filePath)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const absoluteRoot = path.resolve(repositoryRoot);
|
|
145
|
+
const absolutePath = path.resolve(absoluteRoot, filePath);
|
|
146
|
+
const relativePath = path.relative(absoluteRoot, absolutePath);
|
|
147
|
+
if (
|
|
148
|
+
relativePath === ''
|
|
149
|
+
|| relativePath.startsWith(`..${path.sep}`)
|
|
150
|
+
|| relativePath === '..'
|
|
151
|
+
|| path.isAbsolute(relativePath)
|
|
152
|
+
) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return absolutePath;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function fileContainsSymbol(repositoryRoot, filePath, symbol) {
|
|
160
|
+
const absolutePath = resolveRepositoryFile(repositoryRoot, filePath);
|
|
161
|
+
if (!absolutePath) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!fs.existsSync(absolutePath)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
170
|
+
if (/^[A-Za-z_]\w*$/.test(symbol)) {
|
|
171
|
+
return new RegExp(String.raw`\b${escapeRegExp(symbol)}\b`).test(content);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return content.includes(symbol);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveMentionFiles(task, mention, repositoryRoot) {
|
|
178
|
+
const files = task.files ?? [];
|
|
179
|
+
if (files.length === 0) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!repositoryRoot) {
|
|
184
|
+
return files.map((file) => normalizeRepoPath(file));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const matchingFiles = files
|
|
188
|
+
.filter((file) => fileContainsSymbol(repositoryRoot, file, mention.symbol))
|
|
189
|
+
.map((file) => normalizeRepoPath(file));
|
|
190
|
+
return matchingFiles;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function extractTaskContracts(taskContext, options = {}) {
|
|
194
|
+
if (!taskContext || !Array.isArray(taskContext.tasks)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const contracts = new Set();
|
|
199
|
+
const repositoryRoot = options.repositoryRoot;
|
|
200
|
+
|
|
201
|
+
for (const task of taskContext.tasks) {
|
|
202
|
+
const mentions = extractContractMentions(task.whatToImplement ?? '');
|
|
203
|
+
for (const mention of mentions) {
|
|
204
|
+
for (const file of resolveMentionFiles(task, mention, repositoryRoot)) {
|
|
205
|
+
contracts.add(`${file}:${mention.symbol}(${mention.annotation})`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return Array.from(contracts).sort((left, right) => left.localeCompare(right));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isTaskHeader(line) {
|
|
214
|
+
return line.startsWith('## Task ');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseTaskHeader(line) {
|
|
218
|
+
if (!isTaskHeader(line)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const prefixLength = '## Task '.length;
|
|
223
|
+
const separatorIndex = line.indexOf(':', prefixLength);
|
|
224
|
+
if (separatorIndex === -1) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const numberText = line.slice(prefixLength, separatorIndex).trim();
|
|
229
|
+
if (!numberText || Array.from(numberText).some((character) => character < '0' || character > '9')) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const title = line.slice(separatorIndex + 1).trim();
|
|
234
|
+
if (!title) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
number: Number(numberText),
|
|
240
|
+
title,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseTaskField(currentTask, line, currentField) {
|
|
245
|
+
if (line.startsWith('File(s):')) {
|
|
246
|
+
currentTask.files = parseFilesLine(line.slice('File(s):'.length).trim());
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (line.startsWith('What to implement:')) {
|
|
251
|
+
currentTask.whatToImplement = line.slice('What to implement:'.length).trim();
|
|
252
|
+
return 'whatToImplement';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (line.startsWith('Expected output:')) {
|
|
256
|
+
currentTask.expectedOutput = line.slice('Expected output:'.length).trim();
|
|
257
|
+
return 'expectedOutput';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (line === 'TDD steps:' || line === '---') {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (currentField && line.trim()) {
|
|
265
|
+
currentTask[currentField] = `${currentTask[currentField]} ${line.trim()}`.trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return currentField;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseTaskFile(taskFile) {
|
|
272
|
+
const absolutePath = ensureTaskFile(taskFile);
|
|
273
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
274
|
+
const lines = content.split(/\r?\n/);
|
|
275
|
+
const tasks = [];
|
|
276
|
+
let currentTask = null;
|
|
277
|
+
let currentField = null;
|
|
278
|
+
|
|
279
|
+
for (const rawLine of lines) {
|
|
280
|
+
const line = rawLine.trimEnd();
|
|
281
|
+
const taskHeader = parseTaskHeader(line);
|
|
282
|
+
|
|
283
|
+
if (taskHeader) {
|
|
284
|
+
flushTask(tasks, currentTask);
|
|
285
|
+
currentTask = {
|
|
286
|
+
number: taskHeader.number,
|
|
287
|
+
title: taskHeader.title,
|
|
288
|
+
files: [],
|
|
289
|
+
whatToImplement: '',
|
|
290
|
+
expectedOutput: '',
|
|
291
|
+
};
|
|
292
|
+
currentField = null;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!currentTask) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
currentField = parseTaskField(currentTask, line, currentField);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
flushTask(tasks, currentTask);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
path: absolutePath,
|
|
307
|
+
taskCount: tasks.length,
|
|
308
|
+
tasks,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
detectContractType,
|
|
314
|
+
extractContractMentions,
|
|
315
|
+
extractSymbols,
|
|
316
|
+
extractTaskContracts,
|
|
317
|
+
parseTaskFile,
|
|
318
|
+
};
|