project-logbook 0.3.2 → 0.3.4

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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/build.js +33 -76
  3. package/dist/commands/init.js +11 -11
  4. package/dist/commands/lint.js +10 -11
  5. package/dist/commands/list.js +18 -18
  6. package/dist/commands/log.js +5 -5
  7. package/dist/commands/new.js +19 -20
  8. package/dist/commands/preview.js +9 -8
  9. package/dist/commands/release.js +6 -6
  10. package/dist/commands/start.js +26 -21
  11. package/dist/commands/status.d.ts +1 -0
  12. package/dist/commands/status.js +122 -0
  13. package/dist/commands/steer.js +9 -9
  14. package/dist/commands/upgrade.js +5 -5
  15. package/dist/index.js +9 -8
  16. package/dist/lib/build-helpers.d.ts +7 -0
  17. package/dist/lib/build-helpers.js +46 -46
  18. package/dist/lib/build-steps.d.ts +20 -0
  19. package/dist/lib/build-steps.js +57 -0
  20. package/dist/lib/config.d.ts +16 -0
  21. package/dist/lib/config.js +20 -0
  22. package/dist/lib/entry-id.d.ts +22 -0
  23. package/dist/lib/entry-id.js +26 -0
  24. package/dist/lib/entry-paths.d.ts +23 -0
  25. package/dist/lib/entry-paths.js +55 -0
  26. package/dist/lib/git-helpers.d.ts +11 -1
  27. package/dist/lib/git-helpers.js +28 -26
  28. package/dist/lib/hast-helpers.d.ts +10 -0
  29. package/dist/lib/hast-helpers.js +22 -0
  30. package/dist/lib/html-attributes.d.ts +17 -0
  31. package/dist/lib/html-attributes.js +17 -0
  32. package/dist/lib/html-escape.d.ts +16 -0
  33. package/dist/lib/html-escape.js +38 -0
  34. package/dist/lib/image-helpers.d.ts +4 -0
  35. package/dist/lib/image-helpers.js +47 -30
  36. package/dist/lib/lint-runner.js +5 -5
  37. package/dist/lib/markdown-processors.d.ts +18 -0
  38. package/dist/lib/markdown-processors.js +42 -0
  39. package/dist/lib/package-version.d.ts +5 -0
  40. package/dist/lib/package-version.js +16 -0
  41. package/dist/lib/styles.js +5 -2
  42. package/dist/lib/template-helpers.d.ts +1 -0
  43. package/dist/lib/template-helpers.js +35 -24
  44. package/dist/lib/template-types.d.ts +0 -2
  45. package/dist/lib/templates.d.ts +8 -2
  46. package/dist/lib/templates.js +50 -46
  47. package/dist/lib/theme.d.ts +37 -0
  48. package/dist/lib/theme.js +50 -0
  49. package/dist/lib/url-helpers.d.ts +13 -0
  50. package/dist/lib/url-helpers.js +27 -0
  51. package/dist/linters/diff-to-narrative.d.ts +6 -0
  52. package/dist/linters/diff-to-narrative.js +114 -0
  53. package/dist/linters/index.js +2 -0
  54. package/dist/templates/CONTRIBUTING.md +12 -3
  55. package/dist/templates/index.md +6 -6
  56. package/dist/templates/logbook-client.js +42 -16
  57. package/dist/templates/styles.css +5 -0
  58. package/dist/utils/date.d.ts +23 -1
  59. package/dist/utils/date.js +56 -15
  60. package/dist/utils/frontmatter.d.ts +26 -0
  61. package/dist/utils/frontmatter.js +37 -0
  62. package/dist/utils/fs.d.ts +13 -0
  63. package/dist/utils/fs.js +23 -0
  64. package/package.json +3 -2
  65. package/src/templates/CONTRIBUTING.md +12 -3
  66. package/src/templates/index.md +6 -6
  67. package/src/templates/logbook-client.js +42 -16
  68. package/src/templates/styles.css +5 -0
@@ -1,20 +1,25 @@
1
1
  import { linkJiraIds } from './jira-helpers.js';
2
- const html = (strings, ...values) => {
2
+ import { formatAbsoluteDate } from '../utils/date.js';
3
+ import { escapeHtml } from './html-escape.js';
4
+ import { ATTR_ENTRY_SLUG, ATTR_DATA_DATE, CLASS_COMMIT_SHA, CLASS_COMMIT_TIME, CLASS_COMMIT_MESSAGE, } from './html-attributes.js';
5
+ export const html = (strings, ...values) => {
3
6
  return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
4
7
  };
5
8
  export const navLink = (entry, dir) => {
6
9
  const label = dir === 'prev' ? '← Previous' : 'Next →';
7
- const t = `${entry.ticket ? `${entry.ticket}: ` : ''}${entry.title}`;
10
+ const ticket = entry.ticket ? escapeHtml(entry.ticket) : '';
11
+ const title = escapeHtml(entry.title);
12
+ const t = `${ticket ? `${ticket}: ` : ''}${title}`;
8
13
  return `<a href="../${entry.slug}/index.html" class="post-nav-link"><span class="post-nav-label">${label}</span><span class="post-nav-title">${t}</span></a>`;
9
14
  };
10
15
  const renderWorkspaces = (workspaces) => {
11
16
  if (!workspaces || workspaces.length === 0)
12
17
  return '';
13
- return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
18
+ return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${escapeHtml(ws)}</span>`).join('');
14
19
  };
15
20
  export const renderTagsAndWorkspaces = (tags, workspaces) => {
16
21
  const tagsHtml = tags && tags.length > 0
17
- ? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
22
+ ? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${escapeHtml(tag)}</span>`).join('')
18
23
  : '';
19
24
  const wsHtml = renderWorkspaces(workspaces);
20
25
  if (!tagsHtml && !wsHtml)
@@ -26,43 +31,49 @@ export const renderCommits = (commits) => {
26
31
  if (!commits || commits.length === 0)
27
32
  return '';
28
33
  const rows = commits
29
- .map((c) => `<div class="commit-item"><span class="commit-sha">${c.sha}</span><span class="commit-message">${c.message}</span><span class="commit-time" title="${c.timestamp}">${c.relativeTime}</span></div>`)
34
+ .map((c) => {
35
+ const formatted = formatAbsoluteDate(c.timestamp);
36
+ const sha = escapeHtml(c.sha);
37
+ const message = escapeHtml(c.message);
38
+ return `<div class="commit-item"><span class="${CLASS_COMMIT_SHA}">${sha}</span><span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div>`;
39
+ })
30
40
  .join('');
31
41
  return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
32
42
  };
33
- export const renderTimelineEntryItem = (e) => html `<div class="timeline-item" data-entry-slug="${e.slug}">
43
+ export const renderTimelineEntryItem = (e) => {
44
+ const ticket = e.ticket ? escapeHtml(e.ticket) : '';
45
+ const title = e.title ? escapeHtml(e.title) : '';
46
+ const llm = e.llm ? escapeHtml(e.llm) : '';
47
+ const harness = e.harness ? escapeHtml(e.harness) : '';
48
+ const prompter = e.prompter ? escapeHtml(e.prompter) : '';
49
+ const summary = e.summary ? escapeHtml(e.summary) : '';
50
+ return html `<div class="timeline-item" ${ATTR_ENTRY_SLUG}="${e.slug}">
34
51
  <a class="timeline-card" href="./${e.slug}/index.html"
35
52
  ><div class="item-content">
36
53
  <div class="item-meta">
37
- <span data-date="${e.dateStart}">${e.displayDate}</span> • <span class="sort-time">${e.sortTime}</span> •
38
- ${e.llm ? ` ${e.llm} via ` : ''}${e.harness}${e.prompter ? ` / ${e.prompter}` : ''}
54
+ <span ${ATTR_DATA_DATE}="${e.dateStart}">${e.displayDate}</span> •
55
+ ${llm ? ` ${llm} via ` : ''}${harness}${prompter ? ` / ${prompter}` : ''}
39
56
  </div>
40
57
  <h3 class="item-title">
41
- <span class="new-badge" style="display:none;">NEW</span>${e.ticket ? `${e.ticket}: ` : ''}${e.title}
58
+ <span class="new-badge" style="display:none;">NEW</span>${ticket ? `${ticket}: ` : ''}${title}
42
59
  </h3>
43
- <div class="item-summary">${e.summary}</div>
60
+ <div class="item-summary">${summary}</div>
44
61
  ${renderTagsAndWorkspaces(e.tags, e.workspaces)}
45
62
  </div>
46
63
  <span class="item-arrow">›</span></a
47
64
  >
48
65
  </div>`;
66
+ };
49
67
  export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
50
- const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
51
- return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="commit-sha">${c.sha}</span><span class="commit-message">${message}</span><span class="commit-time" title="${c.timestamp}" data-date="${c.timestamp}">${c.relativeTime}</span></div></div>`;
68
+ const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : escapeHtml(c.message);
69
+ const formatted = formatAbsoluteDate(c.timestamp);
70
+ const sha = escapeHtml(c.sha);
71
+ return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="${CLASS_COMMIT_SHA}">${sha}</span><span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div></div>`;
52
72
  };
53
73
  export const renderTimelineTagItem = (t) => {
54
- const formatted = new Date(t.timestamp)
55
- .toLocaleString('de-DE', {
56
- year: 'numeric',
57
- month: '2-digit',
58
- day: '2-digit',
59
- hour: '2-digit',
60
- minute: '2-digit',
61
- second: '2-digit',
62
- hour12: false,
63
- })
64
- .replace(',', '');
65
- return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${t.name}</span><span class="tag-chip-date" title="${t.timestamp}">${formatted}</span></div></div>`;
74
+ const formatted = formatAbsoluteDate(t.timestamp);
75
+ const name = escapeHtml(t.name);
76
+ return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span><span class="tag-chip-name">${name}</span><span class="tag-chip-date" title="${t.timestamp}" ${ATTR_DATA_DATE}="${t.timestamp}">${formatted}</span></div></div>`;
66
77
  };
67
78
  export const paginationLinks = (currentPage, totalPages) => {
68
79
  let links = '';
@@ -60,7 +60,6 @@ export interface TimelineCommitItem {
60
60
  sha: string;
61
61
  message: string;
62
62
  timestamp: string;
63
- relativeTime: string;
64
63
  displayDate: string;
65
64
  monthGroup: string;
66
65
  sortTime: string;
@@ -83,7 +82,6 @@ export interface GitCommit {
83
82
  sha: string;
84
83
  message: string;
85
84
  timestamp: string;
86
- relativeTime: string;
87
85
  }
88
86
  /**
89
87
  * Configuration for the post template
@@ -10,5 +10,11 @@ export declare const layout: ({ title, header, content, projectName, basePath, d
10
10
  bodySlug?: string;
11
11
  buildMeta?: string;
12
12
  }) => string;
13
- export declare const timelineTemplate: (props: TimelineTemplateProps) => string;
14
- export declare const postTemplate: (props: PostTemplateProps) => string;
13
+ export declare const timelineTemplate: (props: TimelineTemplateProps) => {
14
+ header: string;
15
+ content: string;
16
+ };
17
+ export declare const postTemplate: (props: PostTemplateProps) => {
18
+ header: string;
19
+ content: string;
20
+ };
@@ -1,13 +1,12 @@
1
- import { navLink, renderTagsAndWorkspaces, renderCommits, renderTimelineEntryItem, renderTimelineCommitItem, renderTimelineTagItem, paginationLinks, } from './template-helpers.js';
2
- const html = (strings, ...values) => {
3
- return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
4
- };
1
+ import { ATTR_PAGE_SLUG, ATTR_DATA_DATE } from './html-attributes.js';
2
+ import { escapeHtml, validateUrlScheme } from './html-escape.js';
3
+ import { html, navLink, renderTagsAndWorkspaces, renderCommits, renderTimelineEntryItem, renderTimelineCommitItem, renderTimelineTagItem, paginationLinks, } from './template-helpers.js';
5
4
  export const tabsComponent = (tabs, defaultActive = 0) => {
6
5
  const tabsHtml = tabs
7
- .map((tab, index) => `<div class="tab ${index === defaultActive ? 'active' : ''}" role="tab" aria-selected="${index === defaultActive}" aria-controls="${tab.id}" tabindex="${index === defaultActive ? '0' : '-1'}" onclick="showTab(event, '${tab.id}')">${tab.title}</div>`)
6
+ .map((tab, index) => `<div class="tab ${index === defaultActive ? 'active' : ''}" role="tab" aria-selected="${index === defaultActive}" aria-controls="${escapeHtml(tab.id)}" tabindex="${index === defaultActive ? '0' : '-1'}" onclick="showTab(event, '${escapeHtml(tab.id)}')">${escapeHtml(tab.title)}</div>`)
8
7
  .join('');
9
8
  const sectionsHtml = tabs
10
- .map((tab, index) => `<div id="${tab.id}" class="content-section" role="tabpanel" style="${index === defaultActive ? '' : 'display:none;'}">${tab.content}</div>`)
9
+ .map((tab, index) => `<div id="${escapeHtml(tab.id)}" class="content-section" role="tabpanel" style="${index === defaultActive ? '' : 'display:none;'}">${tab.content}</div>`)
11
10
  .join('');
12
11
  return html `<div class="tabs-wrapper">
13
12
  <div class="tabs" id="tabs" role="tablist" aria-label="Navigation tabs">${tabsHtml}</div>
@@ -18,12 +17,12 @@ export const layout = ({ title, header, content, projectName, basePath = './', d
18
17
  <html lang="en">
19
18
  <head>
20
19
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
21
- <title>${title} | ${projectName}</title>
22
- ${description ? `<meta name="description" content="${description.replace(/"/g, '"').substring(0, 160)}">` : ''}
20
+ <title>${escapeHtml(title)} | ${escapeHtml(projectName)}</title>
21
+ ${description ? `<meta name="description" content="${escapeHtml(description).replace(/"/g, '&quot;').substring(0, 160)}">` : ''}
23
22
  <link rel="icon" type="image/svg+xml" href="${basePath}favicon.svg">
24
23
  <link rel="stylesheet" href="${basePath}style.css">
25
24
  </head>
26
- <body${bodySlug ? ` data-page-slug="${bodySlug}"` : ''}>
25
+ <body${bodySlug ? ` ${ATTR_PAGE_SLUG}="${bodySlug}"` : ''}>
27
26
  ${header}<main>${content}</main>
28
27
  <footer>${buildMeta ? `<p class="build-time">${buildMeta}</p>` : ''}</footer>
29
28
  <script src="${basePath}logbook.js"></script>
@@ -47,51 +46,56 @@ export const timelineTemplate = (props) => {
47
46
  .join('')}
48
47
  ${paginationLinks(currentPage, totalPages)}
49
48
  </div>`;
50
- return html `<header>
51
- <h1><a href="./index.html">${projectName}</a></h1>
52
- <p class="tagline">A brief summary of the recent changes to the project.</p>
53
- </header>
54
- ${tabsComponent([
49
+ const header = html `<header>
50
+ <h1><a href="./index.html">${escapeHtml(projectName)}</a></h1>
51
+ <p class="tagline">A brief summary of the recent changes to the project.</p>
52
+ </header>`;
53
+ const content = tabsComponent([
55
54
  { id: 'timeline', title: 'Timeline', content: timelineTab },
56
55
  { id: 'readme', title: 'Project Readme', content: readmeHtml || '<p>No README available.</p>' },
57
56
  { id: 'about', title: 'About this Logbook', content: aboutHtml || '<p>No information available.</p>' },
58
- ], 0)}`;
57
+ ], 0);
58
+ return { header, content };
59
59
  };
60
60
  export const postTemplate = (props) => {
61
61
  const { title, displayDate, dateStart, harness, llm, prompter, content, ticketHtml, logHtml, commits, version, jiraUrl, prevEntry, nextEntry, tags, workspaces } = props; // prettier-ignore
62
- const jiraBtn = jiraUrl
63
- ? `<a href="${jiraUrl}" class="jira-link-btn" target="_blank" rel="noopener noreferrer">View in Jira ↗</a>`
62
+ // Validate jiraUrl scheme to prevent javascript: URLs
63
+ const validatedJiraUrl = jiraUrl ? validateUrlScheme(jiraUrl) : undefined;
64
+ const jiraBtn = validatedJiraUrl
65
+ ? `<a href="${escapeHtml(validatedJiraUrl)}" class="jira-link-btn" target="_blank" rel="noopener noreferrer">View in Jira ↗</a>`
64
66
  : '';
65
- const tabBtn = (id, label, active = false) => `<div class="tab${active ? ' active' : ''}" role="tab" aria-selected="${active}" aria-controls="${id}" tabindex="${active ? '0' : '-1'}" onclick="showTab(event, '${id}')">${label}</div>`;
66
- return html `<header>
67
- <h1>${title}</h1>
68
- <div class="tagline">
69
- <span data-date="${dateStart}">${displayDate}</span> •
70
- ${llm ? `${llm} via ` : ''}${harness}${prompter ? ` / ${prompter}` : ''} • v${version}
71
- </div>
72
- </header>
73
- <article class="tabs-wrapper">
74
- <div class="action-bar">
75
- <a href="../index.html" class="back-link">← Back to Timeline</a>
76
- <div class="action-tabs-wrapper">
77
- <div class="tabs" role="tablist" aria-label="Entry sections">
78
- ${tabBtn('story', 'Story', true)}${tabBtn('spec', 'Original Spec')}${tabBtn('log', 'Technical Log')}
79
- </div>
67
+ const tabBtn = (id, label, active = false) => `<div class="tab${active ? ' active' : ''}" role="tab" aria-selected="${active}" aria-controls="${escapeHtml(id)}" tabindex="${active ? '0' : '-1'}" onclick="showTab(event, '${escapeHtml(id)}')">${escapeHtml(label)}</div>`;
68
+ const header = html `<header>
69
+ <h1>${escapeHtml(title)}</h1>
70
+ <div class="tagline">
71
+ <span ${ATTR_DATA_DATE}="${dateStart}">${displayDate}</span> •
72
+ ${llm ? `${escapeHtml(llm)} via ` : ''}${escapeHtml(harness)}${prompter ? ` / ${escapeHtml(prompter)}` : ''} •
73
+ v${version}
74
+ </div>
75
+ </header>`;
76
+ const mainContent = html `<article class="tabs-wrapper">
77
+ <div class="action-bar">
78
+ <a href="../index.html" class="back-link">← Back to Timeline</a>
79
+ <div class="action-tabs-wrapper">
80
+ <div class="tabs" role="tablist" aria-label="Entry sections">
81
+ ${tabBtn('story', 'Story', true)}${tabBtn('spec', 'Original Spec')}${tabBtn('log', 'Technical Log')}
80
82
  </div>
81
- ${jiraBtn ? `${jiraBtn}` : ''}
82
83
  </div>
83
- <div class="content-sections">
84
- <div id="story" class="content-section" role="tabpanel">
85
- ${renderTagsAndWorkspaces(tags, workspaces)}${content}
86
- </div>
87
- <div id="spec" class="content-section" role="tabpanel" style="display:none;">${ticketHtml}</div>
88
- <div id="log" class="content-section" role="tabpanel" style="display:none;">
89
- ${logHtml}${renderCommits(commits)}
90
- </div>
84
+ ${jiraBtn ? `${jiraBtn}` : ''}
85
+ </div>
86
+ <div class="content-sections">
87
+ <div id="story" class="content-section" role="tabpanel">
88
+ ${renderTagsAndWorkspaces(tags, workspaces)}${content}
89
+ </div>
90
+ <div id="spec" class="content-section" role="tabpanel" style="display:none;">${ticketHtml}</div>
91
+ <div id="log" class="content-section" role="tabpanel" style="display:none;">
92
+ ${logHtml}${renderCommits(commits)}
91
93
  </div>
92
- <nav class="post-nav">
93
- <div class="post-nav-prev">${prevEntry ? navLink(prevEntry, 'prev') : '<span></span>'}</div>
94
- <div class="post-nav-next">${nextEntry ? navLink(nextEntry, 'next') : '<span></span>'}</div>
95
- </nav>
96
- </article>`;
94
+ </div>
95
+ <nav class="post-nav">
96
+ <div class="post-nav-prev">${prevEntry ? navLink(prevEntry, 'prev') : '<span></span>'}</div>
97
+ <div class="post-nav-next">${nextEntry ? navLink(nextEntry, 'next') : '<span></span>'}</div>
98
+ </nav>
99
+ </article>`;
100
+ return { header, content: mainContent };
97
101
  };
@@ -0,0 +1,37 @@
1
+ /** Success message styling (green) */
2
+ export declare const success: import("chalk").ChalkInstance;
3
+ /** Error message styling (red) */
4
+ export declare const error: import("chalk").ChalkInstance;
5
+ /** Warning message styling (yellow) */
6
+ export declare const warning: import("chalk").ChalkInstance;
7
+ /** Info/secondary message styling (blue) */
8
+ export declare const info: import("chalk").ChalkInstance;
9
+ /** Neutral/disabled styling (gray) */
10
+ export declare const neutral: import("chalk").ChalkInstance;
11
+ /** Highlight/emphasis styling (cyan) */
12
+ export declare const highlight: import("chalk").ChalkInstance;
13
+ /** Bold text */
14
+ export declare const bold: import("chalk").ChalkInstance;
15
+ /**
16
+ * Format a section header (e.g., "Checking project integrity...").
17
+ * Uses blue color for visual separation.
18
+ */
19
+ export declare function header(text: string): string;
20
+ /**
21
+ * Format an error message with context.
22
+ * @param prefix - The category/command name
23
+ * @param message - The error message
24
+ */
25
+ export declare function errorMessage(prefix: string, message: string): string;
26
+ /**
27
+ * Format a warning message with context.
28
+ * @param prefix - The category/command name
29
+ * @param message - The warning message
30
+ */
31
+ export declare function warningMessage(prefix: string, message: string): string;
32
+ /**
33
+ * Format a success message with context.
34
+ * @param prefix - The category/command name
35
+ * @param message - The success message
36
+ */
37
+ export declare function successMessage(prefix: string, message: string): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Centralized styling/theme for CLI output.
3
+ * Ensures consistent colors and visual hierarchy across all commands.
4
+ */
5
+ import chalk from 'chalk';
6
+ /** Success message styling (green) */
7
+ export const success = chalk.green;
8
+ /** Error message styling (red) */
9
+ export const error = chalk.red;
10
+ /** Warning message styling (yellow) */
11
+ export const warning = chalk.yellow;
12
+ /** Info/secondary message styling (blue) */
13
+ export const info = chalk.blue;
14
+ /** Neutral/disabled styling (gray) */
15
+ export const neutral = chalk.gray;
16
+ /** Highlight/emphasis styling (cyan) */
17
+ export const highlight = chalk.cyan;
18
+ /** Bold text */
19
+ export const bold = chalk.bold;
20
+ /**
21
+ * Format a section header (e.g., "Checking project integrity...").
22
+ * Uses blue color for visual separation.
23
+ */
24
+ export function header(text) {
25
+ return info(text);
26
+ }
27
+ /**
28
+ * Format an error message with context.
29
+ * @param prefix - The category/command name
30
+ * @param message - The error message
31
+ */
32
+ export function errorMessage(prefix, message) {
33
+ return `${error(prefix)}: ${message}`;
34
+ }
35
+ /**
36
+ * Format a warning message with context.
37
+ * @param prefix - The category/command name
38
+ * @param message - The warning message
39
+ */
40
+ export function warningMessage(prefix, message) {
41
+ return `${warning(prefix)}: ${message}`;
42
+ }
43
+ /**
44
+ * Format a success message with context.
45
+ * @param prefix - The category/command name
46
+ * @param message - The success message
47
+ */
48
+ export function successMessage(prefix, message) {
49
+ return `${success(prefix)}: ${message}`;
50
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check if a URL/path is external (has an absolute URI scheme).
3
+ * @param url - The URL or path to check
4
+ * @returns true if the URL has an absolute URI scheme (http://, https://, ftp://, etc.)
5
+ */
6
+ export declare function isExternalUrl(url: string): boolean;
7
+ /**
8
+ * Check if a value is an external URL or an anchor link.
9
+ * Used to identify URLs that should not be processed (rewritten, collected, etc.)
10
+ * @param href - The href/src value to check
11
+ * @returns true if the value is external or an anchor
12
+ */
13
+ export declare function isExternalUrlOrAnchor(href: string): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Regex pattern to detect absolute URLs with URI schemes.
3
+ * Matches URIs like: http://, https://, ftp://, data:, etc.
4
+ * Pattern breakdown:
5
+ * ^[a-z] - Starts with a letter (lowercase)
6
+ * [a-z\d+\-.]*: - Followed by alphanumeric, +, -, or . characters, ending with :
7
+ * Example matches: http://, https://, ftp://, mailto:, data:
8
+ * Used to distinguish external URLs from relative paths.
9
+ */
10
+ const EXTERNAL_URL_SCHEME_REGEX = /^[a-z][a-z\d+\-.]*:/i;
11
+ /**
12
+ * Check if a URL/path is external (has an absolute URI scheme).
13
+ * @param url - The URL or path to check
14
+ * @returns true if the URL has an absolute URI scheme (http://, https://, ftp://, etc.)
15
+ */
16
+ export function isExternalUrl(url) {
17
+ return EXTERNAL_URL_SCHEME_REGEX.test(url);
18
+ }
19
+ /**
20
+ * Check if a value is an external URL or an anchor link.
21
+ * Used to identify URLs that should not be processed (rewritten, collected, etc.)
22
+ * @param href - The href/src value to check
23
+ * @returns true if the value is external or an anchor
24
+ */
25
+ export function isExternalUrlOrAnchor(href) {
26
+ return EXTERNAL_URL_SCHEME_REGEX.test(href) || href.startsWith('#');
27
+ }
@@ -0,0 +1,6 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ export declare function stripFrontmatter(content: string): string;
3
+ export declare function stripMarkdown(text: string): string;
4
+ export declare function getWordCount(content: string): number;
5
+ declare const linter: Linter;
6
+ export default linter;
@@ -0,0 +1,114 @@
1
+ import { getActiveEntry } from '../lib/session.js';
2
+ import { getChangedLOC } from '../lib/git-helpers.js';
3
+ import fs from 'fs-extra';
4
+ import { join } from 'node:path';
5
+ // Thresholds & Limits Configuration
6
+ const TRIVIAL_LOC_MAX = 20;
7
+ const STANDARD_LOC_MAX = 150;
8
+ const TRIVIAL_WORDS = { min: 30, max: 150 };
9
+ const STANDARD_WORDS = { min: 50, max: 300 };
10
+ const LARGE_WORDS = { min: 100, max: 400 };
11
+ export function stripFrontmatter(content) {
12
+ const lines = content.split(/\r?\n/);
13
+ if (lines[0]?.trim() === '---') {
14
+ const endIdx = lines.indexOf('---', 1);
15
+ if (endIdx !== -1) {
16
+ return lines.slice(endIdx + 1).join('\n');
17
+ }
18
+ }
19
+ return content;
20
+ }
21
+ export function stripMarkdown(text) {
22
+ return text
23
+ .replace(/<[^>]*>/g, '') // HTML tags
24
+ .replace(/!\[([^\]]*)]\([^)]*\)/g, '$1') // images
25
+ .replace(/\[([^\]]*)]\([^)]*\)/g, '$1') // links
26
+ .replace(/```[\s\S]*?```/g, '') // code blocks
27
+ .replace(/`([^`]+)`/g, '$1') // inline code
28
+ .replace(/(\*\*|__)(.*?)\1/g, '$2') // bold
29
+ .replace(/(\*|_)(.*?)\1/g, '$2') // italic
30
+ .replace(/^\s*>\s+/gm, '') // blockquotes
31
+ .replace(/^\s*#+\s+/gm, '') // headings
32
+ .replace(/^\s*[-*+]\s+/gm, '') // list bullets
33
+ .replace(/^\s*\d+\.\s+/gm, ''); // ordered lists
34
+ }
35
+ export function getWordCount(content) {
36
+ const clean = stripMarkdown(stripFrontmatter(content));
37
+ return clean.split(/\s+/).filter((w) => w.trim().length > 0).length;
38
+ }
39
+ const linter = {
40
+ name: 'diff-to-narrative',
41
+ description: 'Checks word count of active index.md against total LOC changed',
42
+ async check(context) {
43
+ // Only target project-level checks
44
+ if (context.entryName)
45
+ return [];
46
+ const active = await getActiveEntry();
47
+ if (!active || active.source !== 'lockfile') {
48
+ return [];
49
+ }
50
+ const logbookDir = context.config.logbookDir;
51
+ const { total: totalLOC } = await getChangedLOC(logbookDir);
52
+ // If there are no active changes and the tree is completely clean, skip gracefully
53
+ if (totalLOC === 0) {
54
+ return [];
55
+ }
56
+ const indexPath = join(process.cwd(), logbookDir, active.slug, 'index.md');
57
+ if (!(await fs.pathExists(indexPath))) {
58
+ return [];
59
+ }
60
+ const content = await fs.readFile(indexPath, 'utf-8');
61
+ const wordCount = getWordCount(content);
62
+ const issues = [];
63
+ if (totalLOC < TRIVIAL_LOC_MAX) {
64
+ if (wordCount < TRIVIAL_WORDS.min) {
65
+ issues.push({
66
+ level: 'warning',
67
+ category: 'story-ratio',
68
+ message: `Code change is trivial (${totalLOC} LOC), but index.md has only ${wordCount} words (minimum is ${TRIVIAL_WORDS.min}). Please expand the narrative.`,
69
+ });
70
+ }
71
+ else if (wordCount > TRIVIAL_WORDS.max) {
72
+ issues.push({
73
+ level: 'warning',
74
+ category: 'story-ratio',
75
+ message: `Warning: Code change is trivial, but index.md exceeds ${TRIVIAL_WORDS.max} words. Please summarize.`,
76
+ });
77
+ }
78
+ }
79
+ else if (totalLOC <= STANDARD_LOC_MAX) {
80
+ if (wordCount < STANDARD_WORDS.min) {
81
+ issues.push({
82
+ level: 'warning',
83
+ category: 'story-ratio',
84
+ message: `Code change is standard (${totalLOC} LOC), but index.md has only ${wordCount} words (minimum is ${STANDARD_WORDS.min}). Please expand the narrative.`,
85
+ });
86
+ }
87
+ else if (wordCount > STANDARD_WORDS.max) {
88
+ issues.push({
89
+ level: 'warning',
90
+ category: 'story-ratio',
91
+ message: `Warning: Code change is standard (${totalLOC} LOC), but index.md exceeds ${STANDARD_WORDS.max} words. Please summarize.`,
92
+ });
93
+ }
94
+ }
95
+ else {
96
+ if (wordCount < LARGE_WORDS.min) {
97
+ issues.push({
98
+ level: 'warning',
99
+ category: 'story-ratio',
100
+ message: `Code change is large (${totalLOC} LOC), but index.md has only ${wordCount} words (minimum is ${LARGE_WORDS.min}). Please expand the narrative.`,
101
+ });
102
+ }
103
+ else if (wordCount > LARGE_WORDS.max) {
104
+ issues.push({
105
+ level: 'warning',
106
+ category: 'story-ratio',
107
+ message: `Warning: Story exceeds ${LARGE_WORDS.max} words. Consider summarizing and linking to a Wiki/ADR.`,
108
+ });
109
+ }
110
+ }
111
+ return issues;
112
+ },
113
+ };
114
+ export default linter;
@@ -1,3 +1,4 @@
1
+ import diffToNarrative from './diff-to-narrative.js';
1
2
  import frontmatter from './frontmatter.js';
2
3
  import jiraPrefix from './jira-prefix.js';
3
4
  import links from './links.js';
@@ -7,6 +8,7 @@ import projectIntegrity from './project-integrity.js';
7
8
  import readability from './readability.js';
8
9
  import workspaces from './workspaces.js';
9
10
  export const linters = [
11
+ diffToNarrative,
10
12
  frontmatter,
11
13
  jiraPrefix,
12
14
  links,
@@ -47,12 +47,21 @@ Only update the currently active logbook entry. Do not edit other existing entri
47
47
  When you finish writing `index.md`, **remove the boilerplate link line** that the template inserts:
48
48
 
49
49
  ### 3. Quality Assurance
50
- Before submitting your work, you MUST release the active entry and run the linter:
50
+ Before finalizing your work, you should run the linter to verify there are no errors:
51
51
  ```bash
52
- logbook release
53
52
  logbook lint
54
53
  ```
55
- `logbook release` removes the `.logbook-active` lockfile. The linter will fail if the lockfile is still present, enforcing that no entry is left dangling in a committed state.
54
+
55
+ You can also check the overall status of the logbook, configuration settings, and statistics (total tasks, done, and drafts) using:
56
+ ```bash
57
+ logbook status
58
+ ```
59
+
60
+ The linter will warn you if the lockfile `.logbook-active` is still present, which is expected during active development.
61
+
62
+ Once the work is ready:
63
+ - **If you are an AI agent**: Do not release the entry. Hand back to the prompter, who will review and run `logbook release`.
64
+ - **If you are a human developer**: Run `logbook release` to remove the lockfile and finalize the entry.
56
65
 
57
66
  The linter enforces rules to be followed.
58
67
 
@@ -3,19 +3,19 @@
3
3
  ticket: {{id}}
4
4
  # The title should be a human-readable description of the work
5
5
  title: {{title}}
6
- prompter: [PROMPTER]
7
- harness: [HARNESS]
8
- llm: [LLM]
9
- summary: [WRITE_SUMMARY_HERE]
6
+ prompter: "[PROMPTER]"
7
+ harness: "[HARNESS]"
8
+ llm: "[LLM]"
9
+ summary: "[WRITE_SUMMARY_HERE]"
10
10
  # Tags for categorizing the change (must be from allowed list in .project-logbook)
11
11
  tags: []
12
12
  # Workspace(s) this change affects (must match package.json workspaces; leave empty for single-project)
13
13
  # Example: workspaces: ["frontend", "shared"]
14
14
  workspaces: []
15
15
  # Set automatically by `logbook start`
16
- dateStart: [DATE_START]
16
+ dateStart: "[DATE_START]"
17
17
  # Set automatically by `logbook release`
18
- dateEnd: [DATE_END]
18
+ dateEnd: "[DATE_END]"
19
19
  ---
20
20
 
21
21
  ## Summary