tenbo-dashboard 0.10.1 → 0.10.3
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/bin/tenbo-dashboard.mjs +2 -6
- package/package.json +1 -1
- package/scripts/bin-output.test.ts +79 -0
- package/scripts/item.ts +25 -9
- package/scripts/itemCommandRegistry.d.mts +12 -0
- package/scripts/itemCommandRegistry.mjs +98 -0
- package/scripts/items.test.ts +25 -0
- package/scripts/next.test.ts +25 -0
- package/scripts/work-queue.test.ts +26 -0
- package/src/api/lib/itemProjection.ts +5 -3
- package/src/api/lib/notes.ts +12 -0
- package/src/api/lib/roadmapStore.ts +1 -13
package/bin/tenbo-dashboard.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { topLevelItemHelpLines } from '../scripts/itemCommandRegistry.mjs';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const root = path.resolve(__dirname, '..');
|
|
@@ -85,12 +86,7 @@ Usage:
|
|
|
85
86
|
tenbo-dashboard Launch the dashboard (http://localhost:5174)
|
|
86
87
|
tenbo-dashboard serve Same as bare invocation (also: 'start', 'dev')
|
|
87
88
|
tenbo-dashboard sync Refresh tenbo state after a change (metrics + init-check + validate, surfaces new findings)
|
|
88
|
-
|
|
89
|
-
tenbo-dashboard item set-status <id> <now|next|later|done|dropped> [--json]
|
|
90
|
-
tenbo-dashboard item add-note <id> "<note>" [--json]
|
|
91
|
-
tenbo-dashboard item verify <id> --status <not_required|pending_live|verified|failed>
|
|
92
|
-
[--evidence "<text>"] [--note "<text>"] [--json]
|
|
93
|
-
tenbo-dashboard item link-commit <id> <sha> [--json]
|
|
89
|
+
${topLevelItemHelpLines().join('\n')}
|
|
94
90
|
tenbo-dashboard items Query roadmap items [--status <status>]
|
|
95
91
|
[--verification <status>] [--goal <goal>]
|
|
96
92
|
[--type <type>] [--priority <p0|p1|p2|p3>]
|
package/package.json
CHANGED
|
@@ -30,6 +30,19 @@ beforeEach(() => {
|
|
|
30
30
|
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
31
31
|
|
|
32
32
|
describe('bin output', () => {
|
|
33
|
+
it('lists item completion in top-level help', () => {
|
|
34
|
+
const bin = path.resolve('bin/tenbo-dashboard.mjs');
|
|
35
|
+
const result = spawnSync(process.execPath, [bin, 'help'], {
|
|
36
|
+
cwd: dir,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
maxBuffer: 1024 * 1024,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result.status).toBe(0);
|
|
42
|
+
expect(result.stderr).toBe('');
|
|
43
|
+
expect(result.stdout).toContain('tenbo-dashboard item complete <id>');
|
|
44
|
+
});
|
|
45
|
+
|
|
33
46
|
it('flushes large JSON output before exiting', () => {
|
|
34
47
|
const bin = path.resolve('bin/tenbo-dashboard.mjs');
|
|
35
48
|
const result = spawnSync(process.execPath, [bin, 'items', '--status', 'done', '--json'], {
|
|
@@ -71,4 +84,70 @@ describe('bin output', () => {
|
|
|
71
84
|
expect(result.stdout).toContain('--fields <a,b>');
|
|
72
85
|
expect(result.stdout).not.toContain('ed-001');
|
|
73
86
|
});
|
|
87
|
+
|
|
88
|
+
it('prints item help without requiring an item id', () => {
|
|
89
|
+
const bin = path.resolve('bin/tenbo-dashboard.mjs');
|
|
90
|
+
const result = spawnSync(process.execPath, [bin, 'item', '--help'], {
|
|
91
|
+
cwd: dir,
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
maxBuffer: 1024 * 1024,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result.status).toBe(0);
|
|
97
|
+
expect(result.stderr).toBe('');
|
|
98
|
+
expect(result.stdout).toContain('Usage: tenbo-dashboard item <command> <id>');
|
|
99
|
+
expect(result.stdout).toContain('complete');
|
|
100
|
+
expect(result.stdout).toContain('link-commit');
|
|
101
|
+
expect(result.stdout).not.toContain('ed-001');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('prints item complete help without requiring an item id', () => {
|
|
105
|
+
const bin = path.resolve('bin/tenbo-dashboard.mjs');
|
|
106
|
+
const result = spawnSync(process.execPath, [bin, 'item', 'complete', '--help'], {
|
|
107
|
+
cwd: dir,
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
maxBuffer: 1024 * 1024,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.status).toBe(0);
|
|
113
|
+
expect(result.stderr).toBe('');
|
|
114
|
+
expect(result.stdout).toContain('Usage: tenbo-dashboard item complete <id>');
|
|
115
|
+
expect(result.stdout).toContain('--doc-update today|YYYY-MM-DD');
|
|
116
|
+
expect(result.stdout).toContain('--commit <sha>');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('completes status, doc_update, verification, and commit through the bin wrapper', () => {
|
|
120
|
+
const bin = path.resolve('bin/tenbo-dashboard.mjs');
|
|
121
|
+
const result = spawnSync(process.execPath, [
|
|
122
|
+
bin,
|
|
123
|
+
'item',
|
|
124
|
+
'complete',
|
|
125
|
+
'ed-001',
|
|
126
|
+
'--evidence',
|
|
127
|
+
'npm test -- --run',
|
|
128
|
+
'--doc-update',
|
|
129
|
+
'today',
|
|
130
|
+
'--commit',
|
|
131
|
+
'abc123',
|
|
132
|
+
'--json',
|
|
133
|
+
], {
|
|
134
|
+
cwd: dir,
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
maxBuffer: 1024 * 1024,
|
|
137
|
+
});
|
|
138
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
139
|
+
|
|
140
|
+
expect(result.status).toBe(0);
|
|
141
|
+
expect(result.stderr).toBe('');
|
|
142
|
+
const payload = JSON.parse(result.stdout);
|
|
143
|
+
expect(payload.item).toMatchObject({
|
|
144
|
+
status: 'done',
|
|
145
|
+
doc_update: today,
|
|
146
|
+
links: ['commit:abc123'],
|
|
147
|
+
verification: {
|
|
148
|
+
status: 'verified',
|
|
149
|
+
evidence: ['npm test -- --run'],
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
});
|
|
74
153
|
});
|
package/scripts/item.ts
CHANGED
|
@@ -10,12 +10,28 @@ import {
|
|
|
10
10
|
import { summarizeItem } from '../src/api/lib/itemProjection';
|
|
11
11
|
import { checkItemEvidence } from '../src/api/lib/itemEvidence';
|
|
12
12
|
import { hasFlag, positionalArgs, readOption, readRepeated } from './cliArgs';
|
|
13
|
-
import { handleCliError, isMain, misuse, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
|
|
13
|
+
import { handleCliError, isMain, misuse, ok, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
|
|
14
|
+
import { findItemCommand, formatItemCommandHelp, formatItemHelp } from './itemCommandRegistry.mjs';
|
|
14
15
|
|
|
15
16
|
export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
16
17
|
const json = hasFlag(args, '--json');
|
|
17
18
|
const [command, itemId, value] = positionalArgs(args);
|
|
18
|
-
|
|
19
|
+
|
|
20
|
+
if (hasFlag(args, '--help') || command === 'help') {
|
|
21
|
+
const helpCommand = command === 'help' ? itemId : command;
|
|
22
|
+
if (helpCommand) {
|
|
23
|
+
const help = formatItemCommandHelp(helpCommand);
|
|
24
|
+
if (help) return ok(help);
|
|
25
|
+
return misuse(`unknown item command: ${helpCommand}`, json);
|
|
26
|
+
}
|
|
27
|
+
return ok(formatItemHelp());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!command) return misuse(formatItemHelp().trimEnd(), json);
|
|
31
|
+
if (!itemId) {
|
|
32
|
+
const definition = findItemCommand(command);
|
|
33
|
+
return misuse(definition ? `Usage: ${definition.usage}` : `unknown item command: ${command}`, json);
|
|
34
|
+
}
|
|
19
35
|
|
|
20
36
|
try {
|
|
21
37
|
if (command === 'show') {
|
|
@@ -76,13 +92,13 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
if (command === 'set-status') {
|
|
79
|
-
if (!value) return misuse(
|
|
95
|
+
if (!value) return misuse(`Usage: ${findItemCommand('set-status')?.usage ?? 'tenbo-dashboard item set-status <id> <status>'}`, json);
|
|
80
96
|
const result = setItemStatus(repoRoot, itemId, value);
|
|
81
97
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} status: ${result.item.status}\n`);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
if (command === 'add-note') {
|
|
85
|
-
if (!value) return misuse(
|
|
101
|
+
if (!value) return misuse(`Usage: ${findItemCommand('add-note')?.usage ?? 'tenbo-dashboard item add-note <id> <note>'}`, json);
|
|
86
102
|
const result = addItemNote(repoRoot, itemId, value);
|
|
87
103
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} note added\n`);
|
|
88
104
|
}
|
|
@@ -93,7 +109,7 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
93
109
|
return serialize(report, json, `${itemId} evidence: ${report.verdict}\n`);
|
|
94
110
|
}
|
|
95
111
|
const status = readOption(args, '--status');
|
|
96
|
-
if (!status) return misuse(
|
|
112
|
+
if (!status) return misuse(`Usage: ${findItemCommand('verify')?.usage ?? 'tenbo-dashboard item verify <id> --status <status>'}`, json);
|
|
97
113
|
const result = setItemVerification(repoRoot, itemId, {
|
|
98
114
|
status,
|
|
99
115
|
evidence: readRepeated(args, '--evidence'),
|
|
@@ -106,8 +122,8 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
106
122
|
const date = readOption(args, '--date');
|
|
107
123
|
const skipped = hasFlag(args, '--skipped');
|
|
108
124
|
const reason = readOption(args, '--reason');
|
|
109
|
-
if (!date && !skipped) return misuse(
|
|
110
|
-
if (skipped && !reason) return misuse('Usage: tenbo item doc-update <
|
|
125
|
+
if (!date && !skipped) return misuse(`Usage: ${findItemCommand('doc-update')?.usage ?? 'tenbo-dashboard item doc-update <id> --date <today|YYYY-MM-DD>'}`, json);
|
|
126
|
+
if (skipped && !reason) return misuse('Usage: tenbo-dashboard item doc-update <id> --skipped --reason "<text>"', json);
|
|
111
127
|
const today = new Date().toISOString().slice(0, 10);
|
|
112
128
|
const docUpdate = skipped ? `skipped — ${reason}` : date === 'today' ? today : date;
|
|
113
129
|
const result = setItemDocUpdate(repoRoot, itemId, docUpdate ?? today);
|
|
@@ -119,7 +135,7 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
119
135
|
const docUpdateArg = readOption(args, '--doc-update');
|
|
120
136
|
const commit = readOption(args, '--commit');
|
|
121
137
|
const verificationStatus = readOption(args, '--verification');
|
|
122
|
-
if (evidence.length === 0) return misuse(
|
|
138
|
+
if (evidence.length === 0) return misuse(`Usage: ${findItemCommand('complete')?.usage ?? 'tenbo-dashboard item complete <id> --evidence "<text>"'}`, json);
|
|
123
139
|
const today = new Date().toISOString().slice(0, 10);
|
|
124
140
|
const docUpdate = docUpdateArg === 'today' ? today : docUpdateArg;
|
|
125
141
|
const result = completeItem(repoRoot, itemId, {
|
|
@@ -132,7 +148,7 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
|
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
if (command === 'link-commit') {
|
|
135
|
-
if (!value) return misuse(
|
|
151
|
+
if (!value) return misuse(`Usage: ${findItemCommand('link-commit')?.usage ?? 'tenbo-dashboard item link-commit <id> <sha>'}`, json);
|
|
136
152
|
const result = linkItemCommit(repoRoot, itemId, value);
|
|
137
153
|
return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} linked commit: ${value}\n`);
|
|
138
154
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ItemCommandDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
usage: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
details?: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ITEM_COMMANDS: ItemCommandDefinition[];
|
|
9
|
+
export function findItemCommand(name: string): ItemCommandDefinition | undefined;
|
|
10
|
+
export function formatItemHelp(): string;
|
|
11
|
+
export function formatItemCommandHelp(name: string): string | undefined;
|
|
12
|
+
export function topLevelItemHelpLines(): string[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const ITEM_COMMANDS = [
|
|
2
|
+
{
|
|
3
|
+
name: 'show',
|
|
4
|
+
usage: 'tenbo-dashboard item show <id> [--json]',
|
|
5
|
+
summary: 'Show the full roadmap item.',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: 'brief',
|
|
9
|
+
usage: 'tenbo-dashboard item brief <id> [--json]',
|
|
10
|
+
summary: 'Print compact execution context for one item.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'handoff',
|
|
14
|
+
usage: 'tenbo-dashboard item handoff <id> [--json]',
|
|
15
|
+
summary: 'Generate a copy/paste handoff prompt.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'set-status',
|
|
19
|
+
usage: 'tenbo-dashboard item set-status <id> <now|next|later|done|dropped> [--json]',
|
|
20
|
+
summary: 'Set the roadmap status.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'add-note',
|
|
24
|
+
usage: 'tenbo-dashboard item add-note <id> "<note>" [--json]',
|
|
25
|
+
summary: 'Append a dated item note.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'verify',
|
|
29
|
+
usage: 'tenbo-dashboard item verify <id> --check [--json]',
|
|
30
|
+
summary: 'Check whether recorded evidence matches completion state.',
|
|
31
|
+
details: [
|
|
32
|
+
'tenbo-dashboard item verify <id> --status <not_required|pending_live|verified|failed>',
|
|
33
|
+
' [--evidence "<text>"] [--note "<text>"] [--json]',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'doc-update',
|
|
38
|
+
usage: 'tenbo-dashboard item doc-update <id> --date <today|YYYY-MM-DD> [--json]',
|
|
39
|
+
summary: 'Record documentation freshness for an item.',
|
|
40
|
+
details: [
|
|
41
|
+
'tenbo-dashboard item doc-update <id> --skipped --reason "<text>" [--json]',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'complete',
|
|
46
|
+
usage: 'tenbo-dashboard item complete <id> --evidence "<text>" [--doc-update today|YYYY-MM-DD] [--commit <sha>] [--verification <status>] [--json]',
|
|
47
|
+
summary: 'Complete status, verification, docs, and optional commit link in one transaction.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'link-commit',
|
|
51
|
+
usage: 'tenbo-dashboard item link-commit <id> <sha> [--json]',
|
|
52
|
+
summary: 'Attach a commit link to an item.',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const commandByName = new Map(ITEM_COMMANDS.map((command) => [command.name, command]));
|
|
57
|
+
|
|
58
|
+
export function findItemCommand(name) {
|
|
59
|
+
return commandByName.get(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatItemHelp() {
|
|
63
|
+
const maxNameLength = Math.max(...ITEM_COMMANDS.map((command) => command.name.length));
|
|
64
|
+
const commandLines = ITEM_COMMANDS.flatMap((command) => [
|
|
65
|
+
` ${command.name.padEnd(maxNameLength)} ${command.summary}`,
|
|
66
|
+
` ${' '.repeat(maxNameLength)} ${command.usage}`,
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
'Usage: tenbo-dashboard item <command> <id> [options]',
|
|
71
|
+
'',
|
|
72
|
+
'Commands:',
|
|
73
|
+
...commandLines,
|
|
74
|
+
'',
|
|
75
|
+
'Run tenbo-dashboard item <command> --help for command-specific usage.',
|
|
76
|
+
'',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatItemCommandHelp(name) {
|
|
81
|
+
const command = findItemCommand(name);
|
|
82
|
+
if (!command) return undefined;
|
|
83
|
+
return [
|
|
84
|
+
`Usage: ${command.usage}`,
|
|
85
|
+
'',
|
|
86
|
+
command.summary,
|
|
87
|
+
...(command.details?.length ? ['', 'Additional forms:', ...command.details.map((line) => ` ${line}`)] : []),
|
|
88
|
+
'',
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function topLevelItemHelpLines() {
|
|
93
|
+
return ITEM_COMMANDS.flatMap((command) => {
|
|
94
|
+
const lines = [` ${command.usage}`];
|
|
95
|
+
if (command.details?.length) lines.push(...command.details.map((line) => ` ${line}`));
|
|
96
|
+
return lines;
|
|
97
|
+
});
|
|
98
|
+
}
|
package/scripts/items.test.ts
CHANGED
|
@@ -120,6 +120,31 @@ describe('items CLI', () => {
|
|
|
120
120
|
expect(JSON.parse(full.stdout).items[0].item.description).toBe('second item');
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
it('summarizes items whose notes are YAML sequences', () => {
|
|
124
|
+
writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
|
|
125
|
+
'items:',
|
|
126
|
+
' - id: ed-001',
|
|
127
|
+
' title: First',
|
|
128
|
+
' layer: app',
|
|
129
|
+
' status: next',
|
|
130
|
+
' description: first item',
|
|
131
|
+
' notes:',
|
|
132
|
+
' - First note',
|
|
133
|
+
' - Latest list note',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n'));
|
|
136
|
+
|
|
137
|
+
const result = runItemsCli(dir, ['--status', 'next', '--summary', '--json']);
|
|
138
|
+
|
|
139
|
+
expect(result.exitCode).toBe(0);
|
|
140
|
+
expect(JSON.parse(result.stdout).items).toEqual([
|
|
141
|
+
expect.objectContaining({
|
|
142
|
+
id: 'ed-001',
|
|
143
|
+
latest_note: 'Latest list note',
|
|
144
|
+
}),
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
123
148
|
it('returns misuse errors for invalid status lists and fields', () => {
|
|
124
149
|
const badStatus = runItemsCli(dir, ['--status', 'next,unknown', '--json']);
|
|
125
150
|
const badField = runItemsCli(dir, ['--fields', 'id,nope', '--json']);
|
package/scripts/next.test.ts
CHANGED
|
@@ -64,4 +64,29 @@ describe('next CLI', () => {
|
|
|
64
64
|
]);
|
|
65
65
|
expect(payload.items[0]).not.toHaveProperty('description');
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it('returns agent summaries when notes are YAML sequences', () => {
|
|
69
|
+
writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
|
|
70
|
+
'items:',
|
|
71
|
+
' - id: ed-001',
|
|
72
|
+
' title: First',
|
|
73
|
+
' layer: app',
|
|
74
|
+
' status: next',
|
|
75
|
+
' description: first item',
|
|
76
|
+
' notes:',
|
|
77
|
+
' - First note',
|
|
78
|
+
' - Latest list note',
|
|
79
|
+
'',
|
|
80
|
+
].join('\n'));
|
|
81
|
+
|
|
82
|
+
const result = runNextCli(dir, ['--agent-summary', '--json']);
|
|
83
|
+
|
|
84
|
+
expect(result.exitCode).toBe(0);
|
|
85
|
+
expect(JSON.parse(result.stdout).items).toEqual([
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
id: 'ed-001',
|
|
88
|
+
latest_note: 'Latest list note',
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
67
92
|
});
|
|
@@ -62,4 +62,30 @@ describe('work-queue CLI', () => {
|
|
|
62
62
|
]);
|
|
63
63
|
expect(payload.items[0]).not.toHaveProperty('description');
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it('returns compact work queue items when notes are YAML sequences', () => {
|
|
67
|
+
writeFileSync(path.join(dir, '.tenbo/scopes/editor/roadmap.yaml'), [
|
|
68
|
+
'items:',
|
|
69
|
+
' - id: ed-001',
|
|
70
|
+
' title: Active Refactor',
|
|
71
|
+
' layer: app',
|
|
72
|
+
' status: now',
|
|
73
|
+
' type: refactor',
|
|
74
|
+
' description: active refactor',
|
|
75
|
+
' notes:',
|
|
76
|
+
' - First note',
|
|
77
|
+
' - Latest list note',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n'));
|
|
80
|
+
|
|
81
|
+
const result = runWorkQueueCli(dir, ['--type', 'refactor', '--json']);
|
|
82
|
+
|
|
83
|
+
expect(result.exitCode).toBe(0);
|
|
84
|
+
expect(JSON.parse(result.stdout).items).toEqual([
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
id: 'ed-001',
|
|
87
|
+
latest_note: 'Latest list note',
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
65
91
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Item, Priority, Status, VerificationStatus } from '../../types';
|
|
2
2
|
import type { LocatedItem } from './roadmapStore';
|
|
3
|
+
import { normalizeNotes } from './notes';
|
|
3
4
|
|
|
4
5
|
export interface ItemSummaryRecord {
|
|
5
6
|
id: string;
|
|
@@ -45,9 +46,10 @@ export function parseItemFields(value: string): ItemFieldName[] {
|
|
|
45
46
|
return fields as ItemFieldName[];
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function latestNote(notes:
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
function latestNote(notes: unknown): string | undefined {
|
|
50
|
+
const normalized = normalizeNotes(notes);
|
|
51
|
+
if (!normalized) return undefined;
|
|
52
|
+
const lines = normalized.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
51
53
|
return lines.at(-1)?.replace(/^- /, '');
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function normalizeNotes(notes: unknown): string {
|
|
2
|
+
if (!notes) return '';
|
|
3
|
+
if (typeof notes === 'string') return notes.trimEnd();
|
|
4
|
+
if (Array.isArray(notes)) {
|
|
5
|
+
return notes
|
|
6
|
+
.map((entry) => String(entry).trim())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((entry) => entry.startsWith('- ') ? entry : `- ${entry}`)
|
|
9
|
+
.join('\n');
|
|
10
|
+
}
|
|
11
|
+
return String(notes).trimEnd();
|
|
12
|
+
}
|
|
@@ -6,6 +6,7 @@ import { readState } from './tenboFs';
|
|
|
6
6
|
import { invalidate as invalidateCache } from './parseCache';
|
|
7
7
|
import { validate } from './validator';
|
|
8
8
|
import { comparePriority } from './priority';
|
|
9
|
+
import { normalizeNotes } from './notes';
|
|
9
10
|
import type { Item, Priority, Status, VerificationStatus, ValidateResult } from '../../types';
|
|
10
11
|
|
|
11
12
|
export type RoadmapErrorCode = 'not_found' | 'conflict' | 'invalid_status' | 'invalid_args';
|
|
@@ -123,19 +124,6 @@ function validateVerificationStatus(status: string): VerificationStatus {
|
|
|
123
124
|
throw new RoadmapStoreError('invalid_status', `invalid verification status: ${status}`);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
function normalizeNotes(notes: unknown): string {
|
|
127
|
-
if (!notes) return '';
|
|
128
|
-
if (typeof notes === 'string') return notes.trimEnd();
|
|
129
|
-
if (Array.isArray(notes)) {
|
|
130
|
-
return notes
|
|
131
|
-
.map((entry) => String(entry).trim())
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
.map((entry) => entry.startsWith('- ') ? entry : `- ${entry}`)
|
|
134
|
-
.join('\n');
|
|
135
|
-
}
|
|
136
|
-
return String(notes).trimEnd();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
127
|
function mutateItem(
|
|
140
128
|
repoRoot: string,
|
|
141
129
|
itemId: string,
|