@dotdotgod/cli 0.2.14 → 0.2.17

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.
@@ -1,93 +0,0 @@
1
- const TRELLO_API_BASE = 'https://api.trello.com';
2
-
3
- function clean(value) {
4
- return typeof value === 'string' ? value.trim() : '';
5
- }
6
-
7
- function credentialError(message) {
8
- return {
9
- ok: false,
10
- error: {
11
- code: 'TRELLO_CREDENTIALS_MISSING',
12
- message: `${message} Fix: configure GitHub Actions secrets or environment variables TRELLO_API_KEY and TRELLO_TOKEN, then retry from the trusted Trello sync workflow.`,
13
- },
14
- };
15
- }
16
-
17
- export function resolveTrelloCredentials({ env = process.env } = {}) {
18
- const apiKey = clean(env.TRELLO_API_KEY);
19
- const token = clean(env.TRELLO_TOKEN);
20
- if (!apiKey || !token) return credentialError('Trello write mode credentials are missing or empty.');
21
- return { ok: true, credentials: { apiKey, token, source: 'env' } };
22
- }
23
-
24
- function trelloApiError({ status = 0, statusText = '', body = '', retryAfter = null, operation = 'Trello API request', idOrShortLink = '', redact = (value) => value } = {}) {
25
- const bodyText = redact(typeof body === 'string' ? body : JSON.stringify(body ?? ''));
26
- const code = bodyText.includes('API_TOKEN_LIMIT_EXCEEDED') ? 'API_TOKEN_LIMIT_EXCEEDED' : bodyText.includes('API_KEY_LIMIT_EXCEEDED') ? 'API_KEY_LIMIT_EXCEEDED' : 'TRELLO_API_FAILED';
27
- const rateLimitGuidance = status === 429 ? ` Wait and retry${retryAfter ? ` after ${retryAfter} seconds` : ''}.` : '';
28
- return {
29
- code,
30
- status,
31
- retryAfter,
32
- message: `${operation} failed${idOrShortLink ? ` for Trello card ${idOrShortLink}` : ''}${status ? ` with HTTP ${status}${statusText ? ` ${statusText}` : ''}` : ''}${bodyText ? `: ${bodyText}` : ''}. Fix:${rateLimitGuidance || ' check card access, credentials, and Trello response details, then retry.'}`,
33
- };
34
- }
35
-
36
- export function createTrelloClient({ credentials, fetchImpl = globalThis.fetch, baseUrl = TRELLO_API_BASE } = {}) {
37
- if (typeof fetchImpl !== 'function') throw new Error('Trello client requires a fetch implementation.');
38
- const auth = { key: credentials?.apiKey ?? '', token: credentials?.token ?? '' };
39
- const redact = (value = '') => String(value).split(auth.key).join('[REDACTED_API_KEY]').split(auth.token).join('[REDACTED_TOKEN]');
40
-
41
- async function request(path, { method = 'GET', params = {}, body = null, operation, idOrShortLink } = {}) {
42
- const url = new URL(path, baseUrl);
43
- for (const [key, value] of Object.entries({ ...params, ...auth })) url.searchParams.set(key, value);
44
- const init = { method };
45
- if (body !== null) {
46
- init.headers = { 'Content-Type': 'application/json' };
47
- init.body = JSON.stringify(body);
48
- }
49
- let response;
50
- try {
51
- response = await fetchImpl(url, init);
52
- } catch (error) {
53
- throw trelloApiError({ operation, idOrShortLink, body: error?.message ?? 'network error', redact });
54
- }
55
- const bodyText = await response.text();
56
- if (!response.ok) {
57
- throw trelloApiError({ status: response.status, statusText: response.statusText, body: bodyText, retryAfter: response.headers?.get?.('Retry-After') ?? null, operation, idOrShortLink, redact });
58
- }
59
- if (!bodyText) return null;
60
- try {
61
- return JSON.parse(bodyText);
62
- } catch {
63
- throw trelloApiError({ status: response.status, statusText: response.statusText, body: 'Malformed JSON response', operation, idOrShortLink, redact });
64
- }
65
- }
66
-
67
- return {
68
- async getCard(idOrShortLink) {
69
- const card = await request(`/1/cards/${encodeURIComponent(idOrShortLink)}`, { params: { fields: 'id,idBoard,desc', customFieldItems: 'true' }, operation: 'Card lookup', idOrShortLink });
70
- if (!card || typeof card.id !== 'string' || typeof card.idBoard !== 'string') throw trelloApiError({ body: 'Malformed card response', operation: 'Card lookup', idOrShortLink, redact });
71
- return card;
72
- },
73
- async listAttachments(idOrShortLink) {
74
- const attachments = await request(`/1/cards/${encodeURIComponent(idOrShortLink)}/attachments`, { operation: 'Attachment lookup', idOrShortLink });
75
- if (!Array.isArray(attachments)) throw trelloApiError({ body: 'Malformed attachments response', operation: 'Attachment lookup', idOrShortLink, redact });
76
- return attachments;
77
- },
78
- async createAttachment(idOrShortLink, githubUrl) {
79
- return request(`/1/cards/${encodeURIComponent(idOrShortLink)}/attachments`, { method: 'POST', params: { url: githubUrl }, operation: 'Attachment creation', idOrShortLink });
80
- },
81
- async listCustomFields(idBoard) {
82
- const fields = await request(`/1/boards/${encodeURIComponent(idBoard)}/customFields`, { operation: 'Custom field lookup' });
83
- if (!Array.isArray(fields)) throw trelloApiError({ body: 'Malformed custom fields response', operation: 'Custom field lookup', redact });
84
- return fields;
85
- },
86
- async createCustomField(idBoard, { name, type = 'text' } = {}) {
87
- return request('/1/customFields', { method: 'POST', params: { idModel: idBoard, modelType: 'board', name, type, pos: 'bottom', display_cardFront: 'false' }, operation: 'Custom field creation' });
88
- },
89
- async updateCardCustomFieldText(idCard, idCustomField, text) {
90
- return request(`/1/cards/${encodeURIComponent(idCard)}/customField/${encodeURIComponent(idCustomField)}/item`, { method: 'PUT', body: { value: { text } }, operation: 'Custom field update', idOrShortLink: idCard });
91
- },
92
- };
93
- }
@@ -1,102 +0,0 @@
1
- export const DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME = 'dotdotgod-view';
2
-
3
- function cleanString(value) {
4
- return typeof value === 'string' ? value.trim() : '';
5
- }
6
-
7
- export function dotdotgodCustomFieldName({ env = process.env, name = '' } = {}) {
8
- return cleanString(name) || cleanString(env.DOTDOTGOD_TRELLO_CUSTOM_FIELD_NAME) || DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME;
9
- }
10
-
11
- function buildLinkedDocsCustomFieldData({ docsPath, githubUrl, trelloUrl, repositoryKey, repositoryLabel, title, summary, traceabilityState, traceabilityDetails = [] } = {}) {
12
- return {
13
- repositoryKey: cleanString(repositoryKey),
14
- repositoryLabel: cleanString(repositoryLabel) || cleanString(repositoryKey).split('/').filter(Boolean).at(-1) || '',
15
- docsPath: cleanString(docsPath),
16
- githubUrl: cleanString(githubUrl),
17
- trelloUrl: cleanString(trelloUrl),
18
- title: cleanString(title),
19
- summary: cleanString(summary),
20
- traceabilityState: cleanString(traceabilityState) || 'not_required',
21
- traceabilityDetails: Array.isArray(traceabilityDetails) ? traceabilityDetails.map((item) => String(item)) : [],
22
- };
23
- }
24
-
25
- export function serializeLinkedDocsCustomFieldPayload(entries = []) {
26
- return JSON.stringify({ version: 1, entries: entries.map((entry) => buildLinkedDocsCustomFieldData(entry)) });
27
- }
28
-
29
- export function parseLinkedDocsCustomFieldPayload(text = '') {
30
- const value = cleanString(text);
31
- if (!value) return { ok: true, entries: [] };
32
- let parsed;
33
- try {
34
- parsed = JSON.parse(value);
35
- } catch (error) {
36
- return {
37
- ok: false,
38
- error: {
39
- code: 'TRELLO_CUSTOM_FIELD_INVALID',
40
- message: `dotdotgod custom field contains invalid JSON. Cause: ${error.message}. Fix: clear the custom field or restore a valid dotdotgod linked-docs payload, then retry sync.`,
41
- },
42
- };
43
- }
44
- if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
45
- return {
46
- ok: false,
47
- error: {
48
- code: 'TRELLO_CUSTOM_FIELD_INVALID',
49
- message: 'dotdotgod custom field has an unsupported shape. Fix: clear the custom field or restore a version 1 linked-docs payload with an entries array, then retry sync.',
50
- },
51
- };
52
- }
53
- const entries = parsed.entries.map((entry) => buildLinkedDocsCustomFieldData(entry));
54
- const keys = entries.map((entry) => entry.repositoryKey).filter(Boolean);
55
- if (keys.length !== new Set(keys).size) {
56
- return {
57
- ok: false,
58
- error: {
59
- code: 'TRELLO_CUSTOM_FIELD_DUPLICATE_REPOSITORY',
60
- message: 'dotdotgod custom field contains duplicate repository entries. Fix: keep one entry per repository key, then retry sync.',
61
- },
62
- };
63
- }
64
- return { ok: true, entries };
65
- }
66
-
67
- export function mergeLinkedDocsCustomFieldValue(existingText = '', entry = {}) {
68
- const current = buildLinkedDocsCustomFieldData(entry);
69
- if (!current.repositoryKey) {
70
- return {
71
- ok: false,
72
- error: {
73
- code: 'TRELLO_CUSTOM_FIELD_REPOSITORY_MISSING',
74
- message: 'Cannot update dotdotgod custom field without a repository key. Fix: run from a trusted GitHub Actions repository or configure repository metadata, then retry sync.',
75
- },
76
- };
77
- }
78
- const parsed = parseLinkedDocsCustomFieldPayload(existingText);
79
- if (!parsed.ok) return parsed;
80
- const entries = [...parsed.entries];
81
- const index = entries.findIndex((item) => item.repositoryKey === current.repositoryKey);
82
- let action = 'create-payload';
83
- if (index >= 0) {
84
- const before = JSON.stringify(entries[index]);
85
- const after = JSON.stringify(current);
86
- action = before === after ? 'unchanged-entry' : 'replace-entry';
87
- entries[index] = current;
88
- } else if (entries.length > 0) {
89
- action = 'append-entry';
90
- entries.push(current);
91
- } else {
92
- entries.push(current);
93
- }
94
- const text = serializeLinkedDocsCustomFieldPayload(entries);
95
- const status = cleanString(existingText) === text ? 'unchanged' : 'written';
96
- return { ok: true, status, action: status === 'unchanged' ? 'unchanged-entry' : action, text, entries, preservedRepositoryEntries: entries.filter((item) => item.repositoryKey !== current.repositoryKey).length };
97
- }
98
-
99
- export function customFieldTextValue(card = {}, idCustomField = '') {
100
- const item = Array.isArray(card.customFieldItems) ? card.customFieldItems.find((fieldItem) => fieldItem?.idCustomField === idCustomField) : null;
101
- return typeof item?.value?.text === 'string' ? item.value.text : '';
102
- }
@@ -1,107 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { spawnSync } from 'node:child_process';
3
- import { join } from 'node:path';
4
-
5
- function normalizeGitHubRepositoryKey(value = '') {
6
- const text = String(value).trim();
7
- const match = text.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/u);
8
- if (!match) return null;
9
- return `${match[1]}/${match[2]}`;
10
- }
11
-
12
- export function normalizeGitHubRemote(value = '') {
13
- const text = String(value).trim();
14
- if (!text) return null;
15
- let match = text.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/u);
16
- if (match) return { repositoryUrl: `https://github.com/${match[1]}/${match[2]}`, repositoryKey: `${match[1]}/${match[2]}` };
17
- match = text.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/u);
18
- if (match) return { repositoryUrl: `https://github.com/${match[1]}/${match[2]}`, repositoryKey: `${match[1]}/${match[2]}` };
19
- return null;
20
- }
21
-
22
- function encodePath(path = '') {
23
- return path.split('/').map((part) => encodeURIComponent(part)).join('/');
24
- }
25
-
26
- function repositoryFromPackage(root) {
27
- try {
28
- const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
29
- const raw = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
30
- return normalizeGitHubRemote(String(raw ?? '').replace(/^git\+/, ''));
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- function gitValue(root, args) {
37
- const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
38
- if (result.status !== 0) return '';
39
- return result.stdout.trim();
40
- }
41
-
42
- function repositoryResult(repositoryUrl, repositoryKey, source, warnings = []) {
43
- return { ok: true, repositoryUrl, repositoryKey, source, warnings, errors: [] };
44
- }
45
-
46
- export function resolveGitHubRepositoryIdentity({ root = '.', repository, repositoryUrl, remoteUrl, env = process.env, git = true, mode = 'dry-run' } = {}) {
47
- const warnings = [];
48
- const explicitKey = normalizeGitHubRepositoryKey(repository);
49
- if (explicitKey) return repositoryResult(`https://github.com/${explicitKey}`, explicitKey, 'explicit-repository', warnings);
50
-
51
- const explicitRemote = normalizeGitHubRemote(repositoryUrl);
52
- if (explicitRemote) return repositoryResult(explicitRemote.repositoryUrl, explicitRemote.repositoryKey, 'explicit-repository-url', warnings);
53
-
54
- const actionsKey = normalizeGitHubRepositoryKey(env?.GITHUB_REPOSITORY);
55
- if (actionsKey) return repositoryResult(`https://github.com/${actionsKey}`, actionsKey, 'github-actions', warnings);
56
- if (mode === 'write' && env?.GITHUB_REPOSITORY && !actionsKey) {
57
- return { ok: false, repositoryUrl: '', repositoryKey: '', source: '', warnings, errors: [{ code: 'GITHUB_REPOSITORY_KEY_INVALID', message: 'Could not resolve a valid GitHub repository key from GITHUB_REPOSITORY. Fix: run the trusted workflow with GITHUB_REPOSITORY in owner/name form, then retry sync.' }] };
58
- }
59
-
60
- const injectedRemote = normalizeGitHubRemote(remoteUrl);
61
- if (injectedRemote) {
62
- warnings.push({ code: 'GITHUB_URL_REMOTE_FALLBACK', message: 'GitHub URL resolved from injected git remote fallback.' });
63
- return repositoryResult(injectedRemote.repositoryUrl, injectedRemote.repositoryKey, 'remote-url', warnings);
64
- }
65
-
66
- if (mode !== 'write') {
67
- const packageRepo = repositoryFromPackage(root);
68
- if (packageRepo) {
69
- warnings.push({ code: 'GITHUB_URL_PACKAGE_FALLBACK', message: 'GitHub URL resolved from package repository metadata.' });
70
- return repositoryResult(packageRepo.repositoryUrl, packageRepo.repositoryKey, 'package', warnings);
71
- }
72
- if (git) {
73
- const remote = gitValue(root, ['remote', 'get-url', 'origin']);
74
- const gitRepo = normalizeGitHubRemote(remote);
75
- if (gitRepo) {
76
- warnings.push({ code: 'GITHUB_URL_GIT_REMOTE_FALLBACK', message: 'GitHub URL resolved from local git origin remote.' });
77
- return repositoryResult(gitRepo.repositoryUrl, gitRepo.repositoryKey, 'git-remote', warnings);
78
- }
79
- }
80
- }
81
-
82
- const code = mode === 'write' ? 'GITHUB_REPOSITORY_KEY_UNRESOLVED' : 'GITHUB_URL_UNRESOLVED_REPOSITORY';
83
- const message = mode === 'write'
84
- ? 'Could not resolve a trusted GitHub repository key for Trello custom field entry. Fix: run write mode from GitHub Actions with GITHUB_REPOSITORY or provide explicit GitHub repository metadata, then retry sync.'
85
- : 'Could not resolve a GitHub repository URL.';
86
- return { ok: false, repositoryUrl: '', repositoryKey: '', source: '', warnings, errors: [{ code, message }] };
87
- }
88
-
89
- export function resolveGitHubFileUrl({ root = '.', file, repository, repositoryUrl, branch, remoteUrl, env = process.env, git = true, mode = 'dry-run' } = {}) {
90
- const identity = resolveGitHubRepositoryIdentity({ root, repository, repositoryUrl, remoteUrl, env, git, mode });
91
- const warnings = [...(identity.warnings ?? [])];
92
- let resolvedBranch = branch;
93
- if (!resolvedBranch && mode === 'write') {
94
- if (env?.GITHUB_REF_NAME) resolvedBranch = env.GITHUB_REF_NAME;
95
- else if (typeof env?.GITHUB_REF === 'string' && env.GITHUB_REF.startsWith('refs/heads/')) resolvedBranch = env.GITHUB_REF.slice('refs/heads/'.length);
96
- else if (env?.GITHUB_DEFAULT_BRANCH) resolvedBranch = env.GITHUB_DEFAULT_BRANCH;
97
- }
98
- if (!resolvedBranch && git && mode !== 'write') {
99
- resolvedBranch = gitValue(root, ['branch', '--show-current']);
100
- if (resolvedBranch) warnings.push({ code: 'GITHUB_URL_GIT_BRANCH_FALLBACK', message: 'GitHub branch resolved from local git state.' });
101
- }
102
- const errors = [];
103
- if (!identity.ok) errors.push(...identity.errors);
104
- if (!resolvedBranch) errors.push({ code: 'GITHUB_URL_UNRESOLVED_BRANCH', message: 'Could not resolve a GitHub branch.' });
105
- if (errors.length > 0) return { ok: false, url: '', repositoryKey: identity.repositoryKey ?? '', repositoryUrl: identity.repositoryUrl ?? '', warnings, errors };
106
- return { ok: true, url: `${identity.repositoryUrl}/blob/${encodeURIComponent(resolvedBranch)}/${encodePath(file)}`, repositoryKey: identity.repositoryKey, repositoryUrl: identity.repositoryUrl, warnings, errors: [] };
107
- }
@@ -1,52 +0,0 @@
1
- const TRELLO_KEYS = new Set(['trelloUrl', 'trelloShortLink', 'trelloCardId']);
2
-
3
- export function parseFrontmatter(content = '') {
4
- if (!String(content).startsWith('---\n')) return { data: {}, errors: [] };
5
- const end = String(content).indexOf('\n---', 4);
6
- if (end === -1) return { data: {}, errors: [{ code: 'TRELLO_FRONTMATTER_INVALID', message: 'Frontmatter start marker is missing a closing marker.' }] };
7
- const raw = String(content).slice(4, end).split('\n');
8
- const data = {};
9
- const errors = [];
10
- for (const [index, line] of raw.entries()) {
11
- const trimmed = line.trim();
12
- if (!trimmed || trimmed.startsWith('#')) continue;
13
- const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
14
- if (!match) continue;
15
- const key = match[1];
16
- let value = match[2].trim();
17
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
18
- if (Object.hasOwn(data, key)) errors.push({ code: 'TRELLO_FRONTMATTER_DUPLICATE_KEY', message: `Duplicate frontmatter key: ${key}.`, line: index + 2 });
19
- data[key] = value;
20
- }
21
- return { data, errors };
22
- }
23
-
24
- export function extractTrelloShortLink(trelloUrl = '') {
25
- let url;
26
- try {
27
- url = new URL(trelloUrl);
28
- } catch {
29
- return null;
30
- }
31
- if (url.protocol !== 'https:' || url.hostname !== 'trello.com') return null;
32
- const parts = url.pathname.split('/').filter(Boolean);
33
- if (parts[0] !== 'c' || !parts[1] || parts.length > 3) return null;
34
- return parts[1];
35
- }
36
-
37
- export function parseTrelloMetadata(content = '') {
38
- const { data, errors } = parseFrontmatter(content);
39
- const warnings = [];
40
- const unsupported = [...TRELLO_KEYS].filter((key) => key !== 'trelloUrl' && Object.hasOwn(data, key));
41
- const trelloUrl = typeof data.trelloUrl === 'string' ? data.trelloUrl.trim() : '';
42
- if (unsupported.length > 0) {
43
- warnings.push({ code: 'TRELLO_UNSUPPORTED_METADATA', message: `Unsupported Trello metadata ignored in Phase 1: ${unsupported.join(', ')}. Use trelloUrl only.` });
44
- }
45
- if (!trelloUrl) {
46
- errors.push({ code: 'TRELLO_METADATA_MISSING_URL', message: 'Missing required frontmatter field: trelloUrl.' });
47
- return { ok: false, trelloUrl: '', shortLink: '', warnings, errors };
48
- }
49
- const shortLink = extractTrelloShortLink(trelloUrl);
50
- if (!shortLink) errors.push({ code: 'TRELLO_METADATA_INVALID_URL', message: 'trelloUrl must match https://trello.com/c/<shortLink>/ or https://trello.com/c/<shortLink>/<slug>.' });
51
- return { ok: errors.length === 0, trelloUrl, shortLink: shortLink ?? '', warnings, errors };
52
- }
@@ -1,60 +0,0 @@
1
- function plural(value, label) {
2
- return `${value} ${label}${value === 1 ? '' : 's'}`;
3
- }
4
-
5
- function pushMessages(lines, label, messages, indent = '') {
6
- if ((messages ?? []).length === 0) return;
7
- lines.push(`${indent}${label}:`);
8
- for (const message of messages) lines.push(`${indent} - ${message.code}: ${message.message}`);
9
- }
10
-
11
- export function formatTrelloDryRunReport(report) {
12
- const lines = ['Trello docs sync dry-run'];
13
- lines.push(`Root: ${report.root}`);
14
- lines.push(`Scan paths: ${report.scanPaths.join(', ')}`);
15
- lines.push(`Matched: ${plural(report.totals.matchedFiles, 'file')}; planned: ${plural(report.totals.plannedUpdates, 'update')}; warnings: ${report.totals.warnings}; errors: ${report.totals.errors}`);
16
- pushMessages(lines, 'Warnings', report.warnings);
17
- pushMessages(lines, 'Errors', report.errors);
18
- for (const file of report.files) {
19
- lines.push('');
20
- lines.push(`File: ${file.file}`);
21
- lines.push(` Trello URL: ${file.trelloUrl || '(missing)'}`);
22
- if (file.shortLink) lines.push(` Trello short link: ${file.shortLink}`);
23
- lines.push(` GitHub URL: ${file.githubUrl || '(unresolved)'}`);
24
- if (file.repositoryKey) lines.push(` Repository entry: ${file.repositoryKey}`);
25
- lines.push(` Planned attachment: ${file.plannedAttachmentAction}`);
26
- lines.push(` Planned custom field: ${file.plannedCustomFieldAction}`);
27
- lines.push(` Sync status: ${file.syncStatus}`);
28
- lines.push(` Traceability: ${file.traceability.state}${file.traceability.details.length ? ` (${file.traceability.details.join(', ')})` : ''}`);
29
- pushMessages(lines, 'Warnings', file.warnings, ' ');
30
- pushMessages(lines, 'Errors', file.errors, ' ');
31
- }
32
- return `${lines.join('\n')}\n`;
33
- }
34
-
35
- export function formatTrelloWriteReport(report) {
36
- const lines = ['Trello docs sync write'];
37
- lines.push(`Root: ${report.root}`);
38
- lines.push(`Scan paths: ${report.scanPaths.join(', ')}`);
39
- lines.push(`Matched: ${plural(report.totals.matchedFiles, 'file')}; written: ${report.totals.written}; unchanged: ${report.totals.unchanged}; blocked: ${report.totals.blocked}; conflict: ${report.totals.conflict}; api-failed: ${report.totals.apiFailed}; warnings: ${report.totals.warnings}; errors: ${report.totals.errors}`);
40
- pushMessages(lines, 'Warnings', report.warnings);
41
- pushMessages(lines, 'Errors', report.errors);
42
- for (const file of report.files) {
43
- lines.push('');
44
- lines.push(`File: ${file.file}`);
45
- lines.push(` Trello URL: ${file.trelloUrl || '(missing)'}`);
46
- if (file.shortLink) lines.push(` Trello short link: ${file.shortLink}`);
47
- lines.push(` GitHub URL: ${file.githubUrl || '(unresolved)'}`);
48
- if (file.repositoryKey) lines.push(` Repository entry: ${file.repositoryKey}`);
49
- lines.push(` Attachment: ${file.attachment?.status ?? 'not-run'}`);
50
- const customFieldAction = file.customField?.action ? ` (${file.customField.action})` : '';
51
- lines.push(` Custom field: ${file.customField?.status ?? 'not-run'}${customFieldAction}`);
52
- if (file.customField?.name) lines.push(` Custom field name: ${file.customField.name}`);
53
- if (file.customField?.preservedRepositoryEntries > 0) lines.push(` Preserved repository entries: ${file.customField.preservedRepositoryEntries}`);
54
- lines.push(` Final status: ${file.finalStatus ?? file.syncStatus}`);
55
- lines.push(` Traceability: ${file.traceability.state}${file.traceability.details.length ? ` (${file.traceability.details.join(', ')})` : ''}`);
56
- pushMessages(lines, 'Warnings', file.warnings, ' ');
57
- pushMessages(lines, 'Errors', file.errors, ' ');
58
- }
59
- return `${lines.join('\n')}\n`;
60
- }
@@ -1,60 +0,0 @@
1
- import { extractDotdotgodTraceabilityBlocks, validateTraceabilityBlock } from '../docs/traceability.mjs';
2
- import { requiresTraceability } from '../memory/config.mjs';
3
-
4
- export function summarizeTraceability({ content = '', root = '.', file = '', config } = {}) {
5
- const blocks = extractDotdotgodTraceabilityBlocks(content);
6
- if (blocks.some((block) => block.error)) {
7
- return { state: 'invalid', details: blocks.filter((block) => block.error).map((block) => `Invalid JSON at line ${block.line}: ${block.error}`), errors: [{ code: 'TRELLO_TRACEABILITY_INVALID', message: 'Malformed json dotdotgod traceability block.' }] };
8
- }
9
- if (blocks.length === 0) {
10
- const required = requiresTraceability(file, config);
11
- return { state: required ? 'missing' : 'not_required', details: [], errors: required ? [{ code: 'TRELLO_TRACEABILITY_MISSING', message: 'Missing required json dotdotgod traceability block.' }] : [] };
12
- }
13
- const errors = blocks.flatMap((block) => validateTraceabilityBlock(block.data, root, file, block.line, config));
14
- if (errors.length > 0) return { state: 'invalid', details: errors.map((error) => `${error.code}: ${error.message.split('\n')[0]}`), errors: [{ code: 'TRELLO_TRACEABILITY_INVALID', message: 'Invalid json dotdotgod traceability block.' }] };
15
- const latest = blocks.at(-1).data;
16
- const details = [];
17
- if (Array.isArray(latest.implementedBy) && latest.implementedBy.length > 0) details.push(`implementedBy: ${latest.implementedBy.length}`);
18
- if (Array.isArray(latest.verifiedBy) && latest.verifiedBy.length > 0) details.push(`verifiedBy: ${latest.verifiedBy.length}`);
19
- if (Array.isArray(latest.relatedDocs) && latest.relatedDocs.length > 0) details.push(`relatedDocs: ${latest.relatedDocs.length}`);
20
- return { state: 'present', details, errors: [] };
21
- }
22
-
23
- function stripFrontmatter(content = '') {
24
- return String(content).replace(/^---\n[\s\S]*?\n---\n?/u, '');
25
- }
26
-
27
- function stripCodeBlocks(content = '') {
28
- return String(content).replace(/```[\s\S]*?```/gu, '');
29
- }
30
-
31
- function compactText(value = '', maxLength = 280) {
32
- const compact = String(value).replace(/\s+/gu, ' ').trim();
33
- if (compact.length <= maxLength) return compact;
34
- return `${compact.slice(0, maxLength - 1).trimEnd()}…`;
35
- }
36
-
37
- export function summarizeMarkdownPreview({ content = '', file = '' } = {}) {
38
- const body = stripCodeBlocks(stripFrontmatter(content));
39
- const lines = body.split(/\r?\n/u).map((line) => line.trim());
40
- const title = lines.find((line) => /^#\s+\S/u.test(line))?.replace(/^#\s+/u, '').trim() || file.split('/').at(-1)?.replace(/\.md$/u, '') || '';
41
- const summary = compactText(lines.find((line) => line && !line.startsWith('#') && !line.startsWith('<!--') && !line.startsWith('- ') && !line.startsWith('|')) ?? '');
42
- return { title: compactText(title, 120), summary };
43
- }
44
-
45
- export function buildLinkedDocsData({ file, content = '', githubUrl, trelloUrl, syncStatus, traceability, repositoryKey } = {}) {
46
- const preview = summarizeMarkdownPreview({ content, file });
47
- return {
48
- docsPath: file,
49
- githubUrl,
50
- trelloUrl,
51
- syncStatus,
52
- repositoryKey,
53
- repositoryLabel: String(repositoryKey ?? '').split('/').filter(Boolean).at(-1) || '',
54
- title: preview.title,
55
- summary: preview.summary,
56
- traceabilityState: traceability?.state ?? 'not_required',
57
- traceabilityDetails: [...(traceability?.details ?? [])],
58
- };
59
- }
60
-