feat(quick): replace auto-increment number with YYMMDD-xxx timestamp ID (#1012)

Quick task IDs were sequential integers (001, 002...) computed by reading
the local .planning/quick/ directory max value. Two users running /gsd:quick
simultaneously would get the same number, causing directory collisions on git.

Replace with a collision-resistant format: YYMMDD-xxx where xxx is the
number of 2-second blocks elapsed since midnight, encoded as 3 lowercase
Base36 characters (000–xbz). Practical collision window is ~2 seconds per
user — effectively zero for any realistic team workflow.

- init.cjs: remove nextNum scan logic, generate quickId from wall clock
- quick.md: rename all next_num refs to quick_id, update directory patterns
- init.test.cjs: rewrite cmdInitQuick tests for new ID format

Co-authored-by: yanbing <yanbing@corp.netease.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
闫冰
2026-03-13 01:35:59 +08:00
committed by GitHub
parent e97851ebd2
commit 75d21866c8
3 changed files with 87 additions and 66 deletions

View File

@@ -629,15 +629,28 @@ describe('cmdInitQuick', () => {
cleanup(tmpDir);
});
test('with description generates slug and task_dir', () => {
test('with description generates slug and task_dir with YYMMDD-xxx format', () => {
const result = runGsdTools('init quick "Fix login bug"', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.slug, 'fix-login-bug');
assert.strictEqual(output.next_num, 1);
assert.strictEqual(output.task_dir, '.planning/quick/1-fix-login-bug');
assert.strictEqual(output.description, 'Fix login bug');
// quick_id must match YYMMDD-xxx (6 digits, dash, 3 base36 chars)
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(output.quick_id),
`quick_id should match YYMMDD-xxx, got: "${output.quick_id}"`);
// task_dir must use the new ID format
assert.ok(output.task_dir.startsWith('.planning/quick/'),
`task_dir should start with .planning/quick/, got: "${output.task_dir}"`);
assert.ok(output.task_dir.endsWith('-fix-login-bug'),
`task_dir should end with -fix-login-bug, got: "${output.task_dir}"`);
assert.ok(/^\.planning\/quick\/\d{6}-[0-9a-z]{3}-fix-login-bug$/.test(output.task_dir),
`task_dir format wrong: "${output.task_dir}"`);
// next_num must NOT be present
assert.ok(!('next_num' in output), 'next_num should not be in output');
});
test('without description returns null slug and task_dir', () => {
@@ -648,19 +661,27 @@ describe('cmdInitQuick', () => {
assert.strictEqual(output.slug, null);
assert.strictEqual(output.task_dir, null);
assert.strictEqual(output.description, null);
assert.strictEqual(output.next_num, 1);
// quick_id is still generated even without description
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(output.quick_id),
`quick_id should match YYMMDD-xxx, got: "${output.quick_id}"`);
});
test('next number increments from existing entries', () => {
const quickDir = path.join(tmpDir, '.planning', 'quick');
fs.mkdirSync(path.join(quickDir, '1-old-task'), { recursive: true });
fs.mkdirSync(path.join(quickDir, '3-another-task'), { recursive: true });
test('two rapid calls produce different quick_ids (no collision within 2s window)', () => {
// Both calls happen within the same test, which is sub-second.
// They may or may not land in the same 2-second block. We just verify format.
const r1 = runGsdTools('init quick "Task one"', tmpDir);
const r2 = runGsdTools('init quick "Task two"', tmpDir);
assert.ok(r1.success && r2.success);
const result = runGsdTools('init quick "New task"', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const o1 = JSON.parse(r1.output);
const o2 = JSON.parse(r2.output);
const output = JSON.parse(result.output);
assert.strictEqual(output.next_num, 4);
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(o1.quick_id));
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(o2.quick_id));
// Directories are distinct because slugs differ
assert.notStrictEqual(o1.task_dir, o2.task_dir);
});
test('long description truncates slug to 40 chars', () => {