project-logbook 0.3.3 → 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.
- package/dist/commands/build.js +32 -75
- package/dist/commands/init.js +11 -11
- package/dist/commands/lint.js +10 -11
- package/dist/commands/list.js +18 -18
- package/dist/commands/log.js +5 -5
- package/dist/commands/new.js +19 -20
- package/dist/commands/preview.js +9 -8
- package/dist/commands/release.js +6 -6
- package/dist/commands/start.js +26 -21
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +122 -0
- package/dist/commands/steer.js +9 -9
- package/dist/commands/upgrade.js +5 -5
- package/dist/index.js +9 -8
- package/dist/lib/build-helpers.js +28 -50
- package/dist/lib/build-steps.d.ts +20 -0
- package/dist/lib/build-steps.js +57 -0
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +20 -0
- package/dist/lib/entry-id.d.ts +22 -0
- package/dist/lib/entry-id.js +26 -0
- package/dist/lib/entry-paths.d.ts +23 -0
- package/dist/lib/entry-paths.js +55 -0
- package/dist/lib/git-helpers.d.ts +11 -1
- package/dist/lib/git-helpers.js +28 -26
- package/dist/lib/hast-helpers.d.ts +10 -0
- package/dist/lib/hast-helpers.js +22 -0
- package/dist/lib/html-attributes.d.ts +17 -0
- package/dist/lib/html-attributes.js +17 -0
- package/dist/lib/html-escape.d.ts +16 -0
- package/dist/lib/html-escape.js +38 -0
- package/dist/lib/image-helpers.js +26 -33
- package/dist/lib/lint-runner.js +5 -5
- package/dist/lib/markdown-processors.d.ts +18 -0
- package/dist/lib/markdown-processors.js +42 -0
- package/dist/lib/package-version.d.ts +5 -0
- package/dist/lib/package-version.js +16 -0
- package/dist/lib/styles.js +5 -2
- package/dist/lib/template-helpers.d.ts +1 -0
- package/dist/lib/template-helpers.js +35 -24
- package/dist/lib/template-types.d.ts +0 -2
- package/dist/lib/templates.d.ts +8 -2
- package/dist/lib/templates.js +50 -46
- package/dist/lib/theme.d.ts +37 -0
- package/dist/lib/theme.js +50 -0
- package/dist/lib/url-helpers.d.ts +13 -0
- package/dist/lib/url-helpers.js +27 -0
- package/dist/linters/diff-to-narrative.d.ts +6 -0
- package/dist/linters/diff-to-narrative.js +114 -0
- package/dist/linters/index.js +2 -0
- package/dist/templates/CONTRIBUTING.md +12 -3
- package/dist/templates/index.md +6 -6
- package/dist/templates/logbook-client.js +42 -16
- package/dist/templates/styles.css +5 -0
- package/dist/utils/date.d.ts +23 -1
- package/dist/utils/date.js +56 -15
- package/dist/utils/frontmatter.d.ts +26 -0
- package/dist/utils/frontmatter.js +37 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +23 -0
- package/package.json +2 -2
- package/src/templates/CONTRIBUTING.md +12 -3
- package/src/templates/index.md +6 -6
- package/src/templates/logbook-client.js +42 -16
- package/src/templates/styles.css +5 -0
|
@@ -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
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -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) =>
|
|
14
|
-
|
|
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
|
+
};
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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, '
|
|
20
|
+
<title>${escapeHtml(title)} | ${escapeHtml(projectName)}</title>
|
|
21
|
+
${description ? `<meta name="description" content="${escapeHtml(description).replace(/"/g, '"').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 ? `
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</
|
|
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;
|
package/dist/linters/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
package/dist/templates/index.md
CHANGED
|
@@ -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
|
|
@@ -2,27 +2,40 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/* ── 1. Real-time relative dates ──────────────────────────────────────── */
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Calculate the relative date string (today, yesterday, 2 days ago, etc)
|
|
7
|
+
* Prepend the relative component to the locally-formatted absolute date.
|
|
8
|
+
*/
|
|
9
|
+
function getRelativeComponent(d) {
|
|
7
10
|
var now = new Date();
|
|
8
11
|
// Normalise both to midnight local time for day-diff calculation
|
|
9
12
|
var dDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
10
13
|
var nDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
11
14
|
var diffDays = Math.round((nDay - dDay) / 86400000);
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
if (diffDays ===
|
|
15
|
-
else if (diffDays
|
|
16
|
-
else if (diffDays <
|
|
17
|
-
else if (diffDays <
|
|
18
|
-
else if (diffDays <
|
|
19
|
-
else if (diffDays <
|
|
20
|
-
else if (diffDays <
|
|
21
|
-
else
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
if (diffDays === 0) return 'today';
|
|
17
|
+
else if (diffDays === 1) return 'yesterday';
|
|
18
|
+
else if (diffDays < 7) return diffDays + ' days ago';
|
|
19
|
+
else if (diffDays < 14) return '1 week ago';
|
|
20
|
+
else if (diffDays < 30) return Math.floor(diffDays / 7) + ' weeks ago';
|
|
21
|
+
else if (diffDays < 60) return '1 month ago';
|
|
22
|
+
else if (diffDays < 365) return Math.floor(diffDays / 30) + ' months ago';
|
|
23
|
+
else if (diffDays < 730) return '1 year ago';
|
|
24
|
+
else return Math.floor(diffDays / 365) + ' years ago';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format a Date object as absolute date and time in browser's local timezone.
|
|
29
|
+
* Format: "26 May 2026, 10:51"
|
|
30
|
+
*/
|
|
31
|
+
function formatAbsoluteComponent(d) {
|
|
32
|
+
var day = d.getDate();
|
|
33
|
+
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
34
|
+
var month = months[d.getMonth()];
|
|
35
|
+
var year = d.getFullYear();
|
|
36
|
+
var hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours();
|
|
37
|
+
var minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes();
|
|
38
|
+
return day + ' ' + month + ' ' + year + ', ' + hours + ':' + minutes;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
function updateRelativeDates() {
|
|
@@ -30,7 +43,20 @@
|
|
|
30
43
|
for (var i = 0; i < els.length; i++) {
|
|
31
44
|
var el = els[i];
|
|
32
45
|
var iso = el.getAttribute('data-date');
|
|
33
|
-
if (iso)
|
|
46
|
+
if (iso) {
|
|
47
|
+
var d = new Date(iso);
|
|
48
|
+
if (!isNaN(d.getTime())) {
|
|
49
|
+
// Format the absolute date in the user's browser/local timezone
|
|
50
|
+
var absolute = formatAbsoluteComponent(d);
|
|
51
|
+
// Prepend the relative component (e.g., "today")
|
|
52
|
+
var relative = getRelativeComponent(d);
|
|
53
|
+
if (relative) {
|
|
54
|
+
el.textContent = relative + ', ' + absolute;
|
|
55
|
+
} else {
|
|
56
|
+
el.textContent = absolute;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
34
60
|
}
|
|
35
61
|
}
|
|
36
62
|
|
package/dist/utils/date.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Format a date as absolute date and time (server-side).
|
|
3
|
+
* This is the primary format used in HTML data-date attributes.
|
|
4
|
+
* The client-side JavaScript will add the relative component (today, 2 days ago, etc).
|
|
5
|
+
* Format: "26 May 2026, 10:51"
|
|
6
|
+
* @param date - The date to format
|
|
7
|
+
* @returns Formatted date string with absolute date and time only
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatAbsoluteDate(date: string | Date): string;
|
|
10
|
+
/**
|
|
11
|
+
* Format a date as a consistent, human-readable format.
|
|
12
|
+
* Combines relative time (today, yesterday, 2 days ago) with absolute date and time.
|
|
13
|
+
* Format: "today, 26 May 2026, 10:51"
|
|
14
|
+
* This is for server-side rendering when client-side JS is not available.
|
|
15
|
+
* Normally, the client-side JS will update formatAbsoluteDate() output with this format.
|
|
16
|
+
* @param date - The date to format
|
|
17
|
+
* @returns Formatted date string with relative and absolute components
|
|
18
|
+
*/
|
|
2
19
|
export declare function formatRelativeDate(date: string | Date): string;
|
|
3
20
|
/** Format a date value as YYYY-MM-DD. */
|
|
4
21
|
export declare function formatIsoDate(value: string | Date): string;
|
|
@@ -6,3 +23,8 @@ export declare function formatIsoDate(value: string | Date): string;
|
|
|
6
23
|
export declare function getMonthYear(date: string | Date): string;
|
|
7
24
|
/** Get a short "HH:mmh" string for sorting/display based on dateEnd or dateStart. */
|
|
8
25
|
export declare function getSortTime(dateStart: string | Date, dateEnd?: string | Date): string;
|
|
26
|
+
/**
|
|
27
|
+
* Format a timestamp for metadata/footer display (build time).
|
|
28
|
+
* Alias for formatAbsoluteDate for clarity in build context.
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatDateTimeForDisplay(date: string | Date): string;
|