project-logbook 0.3.0

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 (102) hide show
  1. package/README.md +34 -0
  2. package/dist/commands/build.d.ts +1 -0
  3. package/dist/commands/build.js +174 -0
  4. package/dist/commands/init.d.ts +1 -0
  5. package/dist/commands/init.js +83 -0
  6. package/dist/commands/lint.d.ts +1 -0
  7. package/dist/commands/lint.js +78 -0
  8. package/dist/commands/list.d.ts +3 -0
  9. package/dist/commands/list.js +127 -0
  10. package/dist/commands/log.d.ts +3 -0
  11. package/dist/commands/log.js +31 -0
  12. package/dist/commands/new.d.ts +1 -0
  13. package/dist/commands/new.js +135 -0
  14. package/dist/commands/preview.d.ts +1 -0
  15. package/dist/commands/preview.js +19 -0
  16. package/dist/commands/release.d.ts +3 -0
  17. package/dist/commands/release.js +66 -0
  18. package/dist/commands/start.d.ts +1 -0
  19. package/dist/commands/start.js +87 -0
  20. package/dist/commands/steer.d.ts +1 -0
  21. package/dist/commands/steer.js +22 -0
  22. package/dist/commands/upgrade.d.ts +1 -0
  23. package/dist/commands/upgrade.js +23 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +92 -0
  26. package/dist/lib/about-content.d.ts +1 -0
  27. package/dist/lib/about-content.js +7 -0
  28. package/dist/lib/build-helpers.d.ts +15 -0
  29. package/dist/lib/build-helpers.js +171 -0
  30. package/dist/lib/config.d.ts +14 -0
  31. package/dist/lib/config.js +41 -0
  32. package/dist/lib/git-helpers.d.ts +16 -0
  33. package/dist/lib/git-helpers.js +93 -0
  34. package/dist/lib/image-helpers.d.ts +19 -0
  35. package/dist/lib/image-helpers.js +121 -0
  36. package/dist/lib/jira-helpers.d.ts +5 -0
  37. package/dist/lib/jira-helpers.js +5 -0
  38. package/dist/lib/lint-runner.d.ts +2 -0
  39. package/dist/lib/lint-runner.js +31 -0
  40. package/dist/lib/lint-types.d.ts +24 -0
  41. package/dist/lib/lint-types.js +1 -0
  42. package/dist/lib/lockfile.d.ts +6 -0
  43. package/dist/lib/lockfile.js +1 -0
  44. package/dist/lib/logbook-client.d.ts +5 -0
  45. package/dist/lib/logbook-client.js +11 -0
  46. package/dist/lib/migrations.d.ts +11 -0
  47. package/dist/lib/migrations.js +34 -0
  48. package/dist/lib/session.d.ts +16 -0
  49. package/dist/lib/session.js +74 -0
  50. package/dist/lib/styles.d.ts +1 -0
  51. package/dist/lib/styles.js +21 -0
  52. package/dist/lib/template-types.d.ts +118 -0
  53. package/dist/lib/template-types.js +1 -0
  54. package/dist/lib/templates.d.ts +14 -0
  55. package/dist/lib/templates.js +158 -0
  56. package/dist/linters/frontmatter.d.ts +3 -0
  57. package/dist/linters/frontmatter.js +68 -0
  58. package/dist/linters/index.d.ts +1 -0
  59. package/dist/linters/index.js +18 -0
  60. package/dist/linters/jira-prefix.d.ts +3 -0
  61. package/dist/linters/jira-prefix.js +34 -0
  62. package/dist/linters/links.d.ts +3 -0
  63. package/dist/linters/links.js +28 -0
  64. package/dist/linters/lockfile.d.ts +3 -0
  65. package/dist/linters/lockfile.js +20 -0
  66. package/dist/linters/placeholders.d.ts +3 -0
  67. package/dist/linters/placeholders.js +62 -0
  68. package/dist/linters/project-integrity.d.ts +3 -0
  69. package/dist/linters/project-integrity.js +29 -0
  70. package/dist/linters/readability.d.ts +3 -0
  71. package/dist/linters/readability.js +41 -0
  72. package/dist/linters/workspaces.d.ts +3 -0
  73. package/dist/linters/workspaces.js +26 -0
  74. package/dist/templates/ABOUT.md +23 -0
  75. package/dist/templates/CONTRIBUTING.md +78 -0
  76. package/dist/templates/favicon.svg +21 -0
  77. package/dist/templates/index.md +30 -0
  78. package/dist/templates/log.md +4 -0
  79. package/dist/templates/logbook-client.js +162 -0
  80. package/dist/templates/steer.txt +25 -0
  81. package/dist/templates/styles.css +641 -0
  82. package/dist/templates/ticket.md +4 -0
  83. package/dist/utils/date.d.ts +8 -0
  84. package/dist/utils/date.js +47 -0
  85. package/dist/utils/fs.d.ts +13 -0
  86. package/dist/utils/fs.js +38 -0
  87. package/dist/utils/id.d.ts +7 -0
  88. package/dist/utils/id.js +36 -0
  89. package/dist/utils/slug.d.ts +2 -0
  90. package/dist/utils/slug.js +9 -0
  91. package/dist/utils/string.d.ts +2 -0
  92. package/dist/utils/string.js +4 -0
  93. package/package.json +69 -0
  94. package/src/templates/ABOUT.md +23 -0
  95. package/src/templates/CONTRIBUTING.md +78 -0
  96. package/src/templates/favicon.svg +21 -0
  97. package/src/templates/index.md +30 -0
  98. package/src/templates/log.md +4 -0
  99. package/src/templates/logbook-client.js +162 -0
  100. package/src/templates/steer.txt +25 -0
  101. package/src/templates/styles.css +641 -0
  102. package/src/templates/ticket.md +4 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Configuration for a single tab
3
+ */
4
+ export interface TabConfig {
5
+ id: string;
6
+ title: string;
7
+ content: string;
8
+ }
9
+ /**
10
+ * Configuration for the timeline template
11
+ */
12
+ export interface TimelineTemplateProps {
13
+ projectName: string;
14
+ version: string;
15
+ buildTime: string;
16
+ groups: TimelineGroup[];
17
+ readmeHtml: string;
18
+ aboutHtml: string;
19
+ jiraBaseUrl?: string;
20
+ jiraPrefix?: string;
21
+ }
22
+ /**
23
+ * Configuration for a month group in the timeline
24
+ */
25
+ export interface TimelineGroup {
26
+ month: string;
27
+ items: TimelineItem[];
28
+ }
29
+ /**
30
+ * A timeline item is either a logbook entry card, a git commit, or a git tag.
31
+ */
32
+ export type TimelineItem = TimelineEntry | TimelineCommitItem | TimelineTagItem;
33
+ /**
34
+ * Configuration for a timeline entry
35
+ */
36
+ export interface TimelineEntry {
37
+ kind: 'entry';
38
+ slug: string;
39
+ dateStart: string;
40
+ dateEnd?: string;
41
+ displayDate: string;
42
+ sortTime: string;
43
+ harness?: string;
44
+ llm?: string;
45
+ prompter?: string;
46
+ ticket?: string;
47
+ title?: string;
48
+ summary: string;
49
+ tags?: string[];
50
+ workspaces?: string[];
51
+ }
52
+ /**
53
+ * A standalone git commit shown as a non-clickable info item in the timeline.
54
+ */
55
+ export interface TimelineCommitItem {
56
+ kind: 'commit';
57
+ sha: string;
58
+ message: string;
59
+ timestamp: string;
60
+ relativeTime: string;
61
+ displayDate: string;
62
+ monthGroup: string;
63
+ sortTime: string;
64
+ }
65
+ /**
66
+ * A standalone git tag shown as a non-clickable info item in the timeline.
67
+ */
68
+ export interface TimelineTagItem {
69
+ kind: 'tag';
70
+ name: string;
71
+ timestamp: string;
72
+ displayDate: string;
73
+ monthGroup: string;
74
+ sortTime: string;
75
+ }
76
+ /**
77
+ * A single git commit associated with a logbook entry.
78
+ */
79
+ export interface GitCommit {
80
+ sha: string;
81
+ message: string;
82
+ timestamp: string;
83
+ relativeTime: string;
84
+ }
85
+ /**
86
+ * Configuration for the post template
87
+ */
88
+ export interface PostTemplateProps {
89
+ title: string;
90
+ displayDate: string;
91
+ dateStart: string;
92
+ harness: string;
93
+ llm?: string;
94
+ prompter?: string;
95
+ ticket?: string;
96
+ summary?: string;
97
+ content: string;
98
+ ticketHtml: string;
99
+ logHtml: string;
100
+ commits?: GitCommit[];
101
+ version: string;
102
+ jiraUrl?: string;
103
+ prevEntry: EntryLink | null;
104
+ nextEntry: EntryLink | null;
105
+ slug?: string;
106
+ tags?: string[];
107
+ workspaces?: string[];
108
+ }
109
+ /**
110
+ * Configuration for navigation between entries
111
+ */
112
+ export interface EntryLink {
113
+ slug: string;
114
+ title: string;
115
+ ticket?: string;
116
+ tags?: string[];
117
+ [key: string]: unknown;
118
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { TabConfig, TimelineTemplateProps, PostTemplateProps } from './template-types.js';
2
+ export declare const tabsComponent: (tabs: TabConfig[], defaultActive?: number) => string;
3
+ export declare const layout: ({ title, header, content, projectName, basePath, description, bodySlug, buildMeta, }: {
4
+ title: string;
5
+ header: string;
6
+ content: string;
7
+ projectName: string;
8
+ basePath?: string;
9
+ description?: string;
10
+ bodySlug?: string;
11
+ buildMeta?: string;
12
+ }) => string;
13
+ export declare const timelineTemplate: (props: TimelineTemplateProps) => string;
14
+ export declare const postTemplate: (props: PostTemplateProps) => string;
@@ -0,0 +1,158 @@
1
+ import { linkJiraIds } from './jira-helpers.js';
2
+ const html = (strings, ...values) => {
3
+ return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
4
+ };
5
+ export const tabsComponent = (tabs, defaultActive = 0) => {
6
+ 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>`)
8
+ .join('');
9
+ const sectionsHtml = tabs
10
+ .map((tab, index) => `<div id="${tab.id}" class="content-section" role="tabpanel" style="${index === defaultActive ? '' : 'display:none;'}">${tab.content}</div>`)
11
+ .join('');
12
+ return html `<div class="tabs-wrapper">
13
+ <div class="tabs" id="tabs" role="tablist" aria-label="Navigation tabs">${tabsHtml}</div>
14
+ ${sectionsHtml}
15
+ </div>`;
16
+ };
17
+ export const layout = ({ title, header, content, projectName, basePath = './', description, bodySlug, buildMeta, }) => html `<!DOCTYPE html>
18
+ <html lang="en">
19
+ <head>
20
+ <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)}">` : ''}
23
+ <link rel="icon" type="image/svg+xml" href="${basePath}favicon.svg">
24
+ <link rel="stylesheet" href="${basePath}style.css">
25
+ </head>
26
+ <body${bodySlug ? ` data-page-slug="${bodySlug}"` : ''}>
27
+ ${header}<main>${content}</main>
28
+ <footer>${buildMeta ? `<p class="build-time">${buildMeta}</p>` : ''}</footer>
29
+ <script src="${basePath}logbook.js"></script>
30
+ </body></html>`;
31
+ const navLink = (entry, dir) => {
32
+ const label = dir === 'prev' ? '← Previous' : 'Next →';
33
+ const t = `${entry.ticket ? `${entry.ticket}: ` : ''}${entry.title}`;
34
+ 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>`;
35
+ };
36
+ const renderWorkspaces = (workspaces) => {
37
+ if (!workspaces || workspaces.length === 0)
38
+ return '';
39
+ return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
40
+ };
41
+ const renderTagsAndWorkspaces = (tags, workspaces) => {
42
+ const tagsHtml = tags && tags.length > 0
43
+ ? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
44
+ : '';
45
+ const wsHtml = renderWorkspaces(workspaces);
46
+ if (!tagsHtml && !wsHtml)
47
+ return '';
48
+ return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
49
+ };
50
+ // Renders a compact list of git commits to embed in the Technical Log tab.
51
+ const renderCommits = (commits) => {
52
+ if (!commits || commits.length === 0)
53
+ return '';
54
+ const rows = commits
55
+ .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>`)
56
+ .join('');
57
+ return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
58
+ };
59
+ const renderTimelineEntryItem = (e) => html `<div class="timeline-item" data-entry-slug="${e.slug}">
60
+ <a class="timeline-card" href="./${e.slug}/index.html"
61
+ ><div class="item-content">
62
+ <div class="item-meta">
63
+ <span data-date="${e.dateStart}">${e.displayDate}</span> • <span class="sort-time">${e.sortTime}</span> •
64
+ ${e.llm ? ` ${e.llm} via ` : ''}${e.harness}${e.prompter ? ` / ${e.prompter}` : ''}
65
+ </div>
66
+ <h3 class="item-title">
67
+ <span class="new-badge" style="display:none;">NEW</span>${e.ticket ? `${e.ticket}: ` : ''}${e.title}
68
+ </h3>
69
+ <div class="item-summary">${e.summary}</div>
70
+ ${renderTagsAndWorkspaces(e.tags, e.workspaces)}
71
+ </div>
72
+ <span class="item-arrow">›</span></a
73
+ >
74
+ </div>`;
75
+ const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
76
+ const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
77
+ 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}">${c.relativeTime}</span></div></div>`;
78
+ };
79
+ const renderTimelineTagItem = (t) => {
80
+ const formatted = new Date(t.timestamp)
81
+ .toLocaleString('de-DE', {
82
+ year: 'numeric',
83
+ month: '2-digit',
84
+ day: '2-digit',
85
+ hour: '2-digit',
86
+ minute: '2-digit',
87
+ second: '2-digit',
88
+ hour12: false,
89
+ })
90
+ .replace(',', '');
91
+ 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>`;
92
+ };
93
+ export const timelineTemplate = (props) => {
94
+ const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix } = props;
95
+ const timelineTab = html `<div class="timeline">
96
+ ${groups
97
+ .map((g) => html `<section class="month-group">
98
+ <h2>${g.month}</h2>
99
+ ${g.items
100
+ .map((item) => {
101
+ if (item.kind === 'entry')
102
+ return renderTimelineEntryItem(item);
103
+ if (item.kind === 'tag')
104
+ return renderTimelineTagItem(item);
105
+ return renderTimelineCommitItem(item, jiraBaseUrl, jiraPrefix);
106
+ })
107
+ .join('')}
108
+ </section>`)
109
+ .join('')}
110
+ </div>`;
111
+ return html `<header>
112
+ <h1>${projectName}</h1>
113
+ <p class="tagline">A brief summary of the recent changes to the project.</p>
114
+ </header>
115
+ ${tabsComponent([
116
+ { id: 'timeline', title: 'Timeline', content: timelineTab },
117
+ { id: 'readme', title: 'Project Readme', content: readmeHtml || '<p>No README available.</p>' },
118
+ { id: 'about', title: 'About this Logbook', content: aboutHtml || '<p>No information available.</p>' },
119
+ ], 0)}`;
120
+ };
121
+ export const postTemplate = (props) => {
122
+ const { title, displayDate, dateStart, harness, llm, prompter, content, ticketHtml, logHtml, commits, version, jiraUrl, prevEntry, nextEntry, tags, workspaces } = props; // prettier-ignore
123
+ const jiraBtn = jiraUrl
124
+ ? `<a href="${jiraUrl}" class="jira-link-btn" target="_blank" rel="noopener noreferrer">View in Jira ↗</a>`
125
+ : '';
126
+ 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>`;
127
+ return html `<header>
128
+ <h1>${title}</h1>
129
+ <div class="tagline">
130
+ <span data-date="${dateStart}">${displayDate}</span> •
131
+ ${llm ? `${llm} via ` : ''}${harness}${prompter ? ` / ${prompter}` : ''} • v${version}
132
+ </div>
133
+ </header>
134
+ <article class="tabs-wrapper">
135
+ <div class="action-bar">
136
+ <a href="../index.html" class="back-link">← Back to Timeline</a>
137
+ <div class="action-tabs-wrapper">
138
+ <div class="tabs" role="tablist" aria-label="Entry sections">
139
+ ${tabBtn('story', 'Story', true)}${tabBtn('spec', 'Original Spec')}${tabBtn('log', 'Technical Log')}
140
+ </div>
141
+ </div>
142
+ ${jiraBtn ? `${jiraBtn}` : ''}
143
+ </div>
144
+ <div class="content-sections">
145
+ <div id="story" class="content-section" role="tabpanel">
146
+ ${renderTagsAndWorkspaces(tags, workspaces)}${content}
147
+ </div>
148
+ <div id="spec" class="content-section" role="tabpanel" style="display:none;">${ticketHtml}</div>
149
+ <div id="log" class="content-section" role="tabpanel" style="display:none;">
150
+ ${logHtml}${renderCommits(commits)}
151
+ </div>
152
+ </div>
153
+ <nav class="post-nav">
154
+ <div class="post-nav-prev">${prevEntry ? navLink(prevEntry, 'prev') : '<span></span>'}</div>
155
+ <div class="post-nav-next">${nextEntry ? navLink(nextEntry, 'next') : '<span></span>'}</div>
156
+ </nav>
157
+ </article>`;
158
+ };
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,68 @@
1
+ // These fields are defined by the tool schema — not user-configurable.
2
+ const ALLOWED_FRONTMATTER_FIELDS = [
3
+ 'ticket',
4
+ 'title',
5
+ 'prompter',
6
+ 'harness',
7
+ 'llm',
8
+ 'summary',
9
+ 'tags',
10
+ 'dateStart',
11
+ 'dateEnd',
12
+ 'workspaces',
13
+ 'author',
14
+ ];
15
+ const linter = {
16
+ name: 'frontmatter',
17
+ description: 'Validates frontmatter fields and tags',
18
+ async check(context) {
19
+ if (!context.entryName || !context.frontmatter)
20
+ return [];
21
+ const { frontmatter, config } = context;
22
+ const issues = [];
23
+ // 1. Mandatory Fields
24
+ const requiredFields = ['ticket', 'title', 'prompter', 'harness', 'llm', 'summary', 'tags', 'dateStart', 'dateEnd'];
25
+ for (const field of requiredFields) {
26
+ if (!frontmatter[field]) {
27
+ issues.push({
28
+ level: 'error',
29
+ category: 'frontmatter',
30
+ message: `Missing mandatory field: ${field}`,
31
+ });
32
+ }
33
+ }
34
+ // 2. Allowed Fields — enforced by tool schema, not user config
35
+ const actualFields = Object.keys(frontmatter);
36
+ for (const field of actualFields) {
37
+ if (!ALLOWED_FRONTMATTER_FIELDS.includes(field)) {
38
+ issues.push({
39
+ level: 'warning',
40
+ category: 'frontmatter',
41
+ message: `Unknown frontmatter field: '${field}'. Allowed: ${ALLOWED_FRONTMATTER_FIELDS.join(', ')}`,
42
+ });
43
+ }
44
+ }
45
+ // 3. Tags validation
46
+ const allowedTags = config.tags?.allowed || [];
47
+ if (!frontmatter.tags || !Array.isArray(frontmatter.tags) || frontmatter.tags.length === 0) {
48
+ issues.push({
49
+ level: 'error',
50
+ category: 'frontmatter',
51
+ message: 'Missing mandatory field: tags (at least one tag required)',
52
+ });
53
+ }
54
+ else if (allowedTags.length > 0) {
55
+ for (const tag of frontmatter.tags) {
56
+ if (!allowedTags.includes(tag)) {
57
+ issues.push({
58
+ level: 'error',
59
+ category: 'tag',
60
+ message: `Invalid tag "${tag}". Allowed tags: "${allowedTags.join('", "')}". Use double quotes to format tags in markdown.`,
61
+ });
62
+ }
63
+ }
64
+ }
65
+ return issues;
66
+ },
67
+ };
68
+ export default linter;
@@ -0,0 +1 @@
1
+ export declare const linters: import("../lib/lint-types.js").Linter[];
@@ -0,0 +1,18 @@
1
+ import frontmatter from './frontmatter.js';
2
+ import jiraPrefix from './jira-prefix.js';
3
+ import links from './links.js';
4
+ import lockfile from './lockfile.js';
5
+ import placeholders from './placeholders.js';
6
+ import projectIntegrity from './project-integrity.js';
7
+ import readability from './readability.js';
8
+ import workspaces from './workspaces.js';
9
+ export const linters = [
10
+ frontmatter,
11
+ jiraPrefix,
12
+ links,
13
+ lockfile,
14
+ placeholders,
15
+ projectIntegrity,
16
+ readability,
17
+ workspaces,
18
+ ];
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,34 @@
1
+ const linter = {
2
+ name: 'jira-prefix',
3
+ description: 'Validates entry names and ticket IDs against jiraPrefix config',
4
+ async check(context) {
5
+ if (!context.entryName || !context.frontmatter || !context.config.jiraPrefix)
6
+ return [];
7
+ const { entryName, frontmatter, config } = context;
8
+ const issues = [];
9
+ const jiraPrefix = config.jiraPrefix;
10
+ if (!jiraPrefix)
11
+ return [];
12
+ const jiraPrefixPattern = new RegExp(`^${jiraPrefix}-\\d+-`, 'i');
13
+ if (!jiraPrefixPattern.test(entryName)) {
14
+ issues.push({
15
+ level: 'error',
16
+ category: 'jira_prefix',
17
+ message: `Entry folder '${entryName}' does not match configured prefix '${jiraPrefix}'. Expected format: ${jiraPrefix}-<ID>-<slug>`,
18
+ });
19
+ }
20
+ const ticketMatch = frontmatter.ticket;
21
+ if (typeof ticketMatch === 'string') {
22
+ const ticketPrefix = ticketMatch.split('-')[0];
23
+ if (ticketPrefix.toUpperCase() !== jiraPrefix.toUpperCase()) {
24
+ issues.push({
25
+ level: 'error',
26
+ category: 'jira_prefix',
27
+ message: `Entry '${entryName}' has ticket '${frontmatter.ticket}' which doesn't match configured prefix '${jiraPrefix}'.`,
28
+ });
29
+ }
30
+ }
31
+ return issues;
32
+ },
33
+ };
34
+ export default linter;
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,28 @@
1
+ import fs from 'fs-extra';
2
+ import { join } from 'node:path';
3
+ const linter = {
4
+ name: 'links',
5
+ description: 'Validates relative links within entries',
6
+ async check(context) {
7
+ if (!context.entryName || !context.content || !context.entryPath)
8
+ return [];
9
+ const issues = [];
10
+ const linkRegex = /\[.*?\]\((.*?)\)/g;
11
+ let match;
12
+ while ((match = linkRegex.exec(context.content)) !== null) {
13
+ const link = match[1];
14
+ if (link.startsWith('http') || link.startsWith('#'))
15
+ continue;
16
+ const linkedPath = join(context.entryPath, link);
17
+ if (!(await fs.pathExists(linkedPath))) {
18
+ issues.push({
19
+ level: 'error',
20
+ category: 'link',
21
+ message: `Cannot resolve: ${link}`,
22
+ });
23
+ }
24
+ }
25
+ return issues;
26
+ },
27
+ };
28
+ export default linter;
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,20 @@
1
+ import { getActiveEntry } from '../lib/session.js';
2
+ const linter = {
3
+ name: 'lockfile',
4
+ description: 'Checks if a logbook entry is still active',
5
+ async check(context) {
6
+ if (context.entryName)
7
+ return [];
8
+ const issues = [];
9
+ const active = await getActiveEntry();
10
+ if (active && active.source === 'lockfile') {
11
+ issues.push({
12
+ level: 'warning',
13
+ category: 'active',
14
+ message: `Entry '${active.slug}' is still active via lockfile. Run 'logbook release' after the current ticket is approved by prompter.`,
15
+ });
16
+ }
17
+ return issues;
18
+ },
19
+ };
20
+ export default linter;
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,62 @@
1
+ import fs from 'fs-extra';
2
+ import { join } from 'node:path';
3
+ const linter = {
4
+ name: 'placeholders',
5
+ description: 'Checks for unremoved template placeholders',
6
+ async check(context) {
7
+ if (!context.entryName || !context.content || !context.entryPath)
8
+ return [];
9
+ const issues = [];
10
+ // Read the raw file to extract the frontmatter block. context.content is body-only
11
+ // (gray-matter strips the frontmatter), so we cannot check it for frontmatter placeholders.
12
+ // Fall back to context.content if indexPath is unavailable (e.g. in unit tests).
13
+ let rawFile = context.content;
14
+ if (context.indexPath && (await fs.pathExists(context.indexPath))) {
15
+ rawFile = await fs.readFile(context.indexPath, 'utf8');
16
+ }
17
+ // Scan only the frontmatter block for bracket-style placeholders to avoid false
18
+ // positives when placeholder names are mentioned in the narrative body.
19
+ const frontmatterMatch = rawFile.match(/^---\n[\s\S]*?\n---/);
20
+ const frontmatterBlock = frontmatterMatch ? frontmatterMatch[0] : rawFile;
21
+ const frontmatterPlaceholders = [
22
+ '[PROMPTER]',
23
+ '[HARNESS]',
24
+ '[LLM]',
25
+ '[WRITE_SUMMARY_HERE]',
26
+ '[DATE_START]',
27
+ // '[DATE_END]' is automatically set by `logbook release`, so it is not
28
+ // checked here. It is expected to be present in draft tickets and will be
29
+ // replaced with the actual release timestamp when the ticket is finalized.
30
+ ];
31
+ for (const placeholder of frontmatterPlaceholders) {
32
+ if (frontmatterBlock.includes(placeholder)) {
33
+ issues.push({
34
+ level: 'error',
35
+ category: 'placeholder',
36
+ message: `Unremoved placeholder in index.md: ${placeholder}`,
37
+ });
38
+ }
39
+ }
40
+ // Check boilerplate body text against the full content (body only is fine here).
41
+ if (context.content.includes('TODO: ')) {
42
+ issues.push({
43
+ level: 'error',
44
+ category: 'placeholder',
45
+ message: 'Found "TODO: "\'s in index.md: Write a polished, highly readable "short story" of the change here. Use the hints provided inside the file.',
46
+ });
47
+ }
48
+ const ticketPath = join(context.entryPath, 'ticket.md');
49
+ if (await fs.pathExists(ticketPath)) {
50
+ const ticketContent = await fs.readFile(ticketPath, 'utf8');
51
+ if (ticketContent.includes('(Paste Jira/Linear issue description here)')) {
52
+ issues.push({
53
+ level: 'error',
54
+ category: 'placeholder',
55
+ message: 'Unremoved placeholder in ticket.md: (Paste Jira/Linear issue description here)',
56
+ });
57
+ }
58
+ }
59
+ return issues;
60
+ },
61
+ };
62
+ export default linter;
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,29 @@
1
+ const linter = {
2
+ name: 'project-integrity',
3
+ description: 'Checks if project files are missing or outdated',
4
+ async check(context) {
5
+ if (context.entryName)
6
+ return [];
7
+ const issues = [];
8
+ const { getProjectStatus } = await import('../lib/migrations.js');
9
+ const projectStatus = await getProjectStatus();
10
+ for (const file of projectStatus) {
11
+ if (file.status === 'OUTDATED') {
12
+ issues.push({
13
+ level: 'warning',
14
+ category: 'outdated',
15
+ message: `${file.name} is different from the latest template. Run 'logbook upgrade' to sync.`,
16
+ });
17
+ }
18
+ else if (file.status === 'MISSING') {
19
+ issues.push({
20
+ level: 'error',
21
+ category: 'missing',
22
+ message: `${file.name} is missing. Run 'logbook upgrade' to create it.`,
23
+ });
24
+ }
25
+ }
26
+ return issues;
27
+ },
28
+ };
29
+ export default linter;
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;
@@ -0,0 +1,41 @@
1
+ const linter = {
2
+ name: 'readability',
3
+ description: 'Checks for readability, headings usage, and length',
4
+ async check(context) {
5
+ if (!context.entryName || !context.content)
6
+ return [];
7
+ const issues = [];
8
+ const contentParts = context.content.split('---');
9
+ // Content is after the second '---' if frontmatter exists
10
+ const contentWithoutFrontmatter = (contentParts.length >= 3 ? contentParts.slice(2).join('---') : context.content).trim();
11
+ if (contentWithoutFrontmatter.length === 0)
12
+ return [];
13
+ // 1. Heading Check
14
+ const h2Match = contentWithoutFrontmatter.match(/^##\s+.+$/gm);
15
+ if (!h2Match || h2Match.length < 1) {
16
+ issues.push({
17
+ level: 'warning',
18
+ category: 'readability',
19
+ message: 'Narrative should use at least one H2 heading (##) to group content.',
20
+ });
21
+ }
22
+ // 2. Length Check
23
+ const words = contentWithoutFrontmatter.split(/\s+/).filter((w) => w.length > 0).length;
24
+ if (words < 50) {
25
+ issues.push({
26
+ level: 'warning',
27
+ category: 'readability',
28
+ message: `Narrative seems too short (${words} words). Aim for a professional "short story".`,
29
+ });
30
+ }
31
+ else if (words > 1000) {
32
+ issues.push({
33
+ level: 'warning',
34
+ category: 'readability',
35
+ message: `Narrative is quite long (${words} words). Consider being more concise.`,
36
+ });
37
+ }
38
+ return issues;
39
+ },
40
+ };
41
+ export default linter;
@@ -0,0 +1,3 @@
1
+ import { Linter } from '../lib/lint-types.js';
2
+ declare const linter: Linter;
3
+ export default linter;