@williamthorsen/kb 0.0.0 → 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.
- package/LICENSE +7 -0
- package/README.md +236 -1
- package/bin/kb.js +26 -0
- package/dist/esm/check/check.d.ts +11 -0
- package/dist/esm/check/check.js +17 -0
- package/dist/esm/check/enumerate.d.ts +14 -0
- package/dist/esm/check/enumerate.js +71 -0
- package/dist/esm/check/glob-segments.d.ts +2 -0
- package/dist/esm/check/glob-segments.js +12 -0
- package/dist/esm/check/index.d.ts +2 -0
- package/dist/esm/check/index.js +2 -0
- package/dist/esm/cli/commands/check.d.ts +20 -0
- package/dist/esm/cli/commands/check.js +179 -0
- package/dist/esm/cli/commands/create.d.ts +16 -0
- package/dist/esm/cli/commands/create.js +111 -0
- package/dist/esm/cli/commands/set-default.d.ts +15 -0
- package/dist/esm/cli/commands/set-default.js +123 -0
- package/dist/esm/cli/format.d.ts +24 -0
- package/dist/esm/cli/format.js +55 -0
- package/dist/esm/cli/index.d.ts +1 -0
- package/dist/esm/cli/index.js +25 -0
- package/dist/esm/cli/run.d.ts +9 -0
- package/dist/esm/cli/run.js +37 -0
- package/dist/esm/cli/select-kb-prompt.d.ts +16 -0
- package/dist/esm/cli/select-kb-prompt.js +44 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.d.ts +11 -0
- package/dist/esm/cli/targeting/resolve-changed-paths.js +53 -0
- package/dist/esm/cli/targeting/select-notes.d.ts +10 -0
- package/dist/esm/cli/targeting/select-notes.js +60 -0
- package/dist/esm/config/config-schema.d.ts +10 -0
- package/dist/esm/config/config-schema.js +10 -0
- package/dist/esm/config/index.d.ts +4 -0
- package/dist/esm/config/index.js +4 -0
- package/dist/esm/config/kb-loader-error.d.ts +5 -0
- package/dist/esm/config/kb-loader-error.js +14 -0
- package/dist/esm/config/load-config.d.ts +5 -0
- package/dist/esm/config/load-config.js +36 -0
- package/dist/esm/config/note-scope.d.ts +7 -0
- package/dist/esm/config/note-scope.js +10 -0
- package/dist/esm/create/create.d.ts +26 -0
- package/dist/esm/create/create.js +51 -0
- package/dist/esm/create/index.d.ts +3 -0
- package/dist/esm/create/index.js +2 -0
- package/dist/esm/create/render-seeds.d.ts +2 -0
- package/dist/esm/create/render-seeds.js +29 -0
- package/dist/esm/discovery/find-kb-root.d.ts +4 -0
- package/dist/esm/discovery/find-kb-root.js +22 -0
- package/dist/esm/discovery/index.d.ts +9 -0
- package/dist/esm/discovery/index.js +5 -0
- package/dist/esm/discovery/kb-registry-schema.d.ts +16 -0
- package/dist/esm/discovery/kb-registry-schema.js +10 -0
- package/dist/esm/discovery/load-registry.d.ts +15 -0
- package/dist/esm/discovery/load-registry.js +114 -0
- package/dist/esm/discovery/register-store.d.ts +9 -0
- package/dist/esm/discovery/register-store.js +26 -0
- package/dist/esm/discovery/registry-document.d.ts +2 -0
- package/dist/esm/discovery/registry-document.js +17 -0
- package/dist/esm/discovery/set-default-kb.d.ts +7 -0
- package/dist/esm/discovery/set-default-kb.js +53 -0
- package/dist/esm/filesystem/exists.d.ts +6 -0
- package/dist/esm/filesystem/exists.js +24 -0
- package/dist/esm/filesystem/index.d.ts +1 -0
- package/dist/esm/filesystem/index.js +1 -0
- package/dist/esm/frontmatter/index.d.ts +2 -0
- package/dist/esm/frontmatter/index.js +1 -0
- package/dist/esm/frontmatter/parse-note.d.ts +8 -0
- package/dist/esm/frontmatter/parse-note.js +98 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/layout/index.d.ts +1 -0
- package/dist/esm/layout/index.js +1 -0
- package/dist/esm/layout/store-layout.d.ts +15 -0
- package/dist/esm/layout/store-layout.js +23 -0
- package/dist/esm/lints/index.d.ts +2 -0
- package/dist/esm/lints/index.js +2 -0
- package/dist/esm/lints/paths.d.ts +6 -0
- package/dist/esm/lints/paths.js +12 -0
- package/dist/esm/lints/tag-alias.d.ts +6 -0
- package/dist/esm/lints/tag-alias.js +21 -0
- package/dist/esm/note-io/field-validators.d.ts +2 -0
- package/dist/esm/note-io/field-validators.js +30 -0
- package/dist/esm/note-io/index.d.ts +4 -0
- package/dist/esm/note-io/index.js +4 -0
- package/dist/esm/note-io/read-note.d.ts +8 -0
- package/dist/esm/note-io/read-note.js +22 -0
- package/dist/esm/note-io/write-note.d.ts +2 -0
- package/dist/esm/note-io/write-note.js +21 -0
- package/dist/esm/note-io/yaml-fields.d.ts +5 -0
- package/dist/esm/note-io/yaml-fields.js +49 -0
- package/dist/esm/records/assertion.d.ts +25 -0
- package/dist/esm/records/assertion.js +136 -0
- package/dist/esm/records/event.d.ts +28 -0
- package/dist/esm/records/event.js +128 -0
- package/dist/esm/records/index.d.ts +2 -0
- package/dist/esm/records/index.js +2 -0
- package/dist/esm/tags/canonicalize.d.ts +3 -0
- package/dist/esm/tags/canonicalize.js +6 -0
- package/dist/esm/tags/index.d.ts +3 -0
- package/dist/esm/tags/index.js +2 -0
- package/dist/esm/tags/load-aliases.d.ts +5 -0
- package/dist/esm/tags/load-aliases.js +72 -0
- package/dist/esm/type-guards.d.ts +3 -0
- package/dist/esm/type-guards.js +12 -0
- package/dist/esm/types.d.ts +50 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/vault-integrity/build-vault-index.d.ts +4 -0
- package/dist/esm/vault-integrity/build-vault-index.js +14 -0
- package/dist/esm/vault-integrity/check-vault-integrity.d.ts +7 -0
- package/dist/esm/vault-integrity/check-vault-integrity.js +49 -0
- package/dist/esm/vault-integrity/index.d.ts +3 -0
- package/dist/esm/vault-integrity/index.js +3 -0
- package/dist/esm/vault-integrity/wikilink-parse.d.ts +7 -0
- package/dist/esm/vault-integrity/wikilink-parse.js +75 -0
- package/package.json +108 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { asStringList, isValidDate } from "../note-io/field-validators.js";
|
|
2
|
+
export const EVENT_IMPACT_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
3
|
+
const IMPACT_LEVEL_SET = new Set(EVENT_IMPACT_LEVELS);
|
|
4
|
+
export function isEventImpact(value) {
|
|
5
|
+
return typeof value === 'string' && IMPACT_LEVEL_SET.has(value);
|
|
6
|
+
}
|
|
7
|
+
const TYPED_FIELDS = new Set([
|
|
8
|
+
'recordType',
|
|
9
|
+
'id',
|
|
10
|
+
'captured-at',
|
|
11
|
+
'session',
|
|
12
|
+
'cwd',
|
|
13
|
+
'summary',
|
|
14
|
+
'tags',
|
|
15
|
+
'addressed-by',
|
|
16
|
+
'impact',
|
|
17
|
+
]);
|
|
18
|
+
export function parseEvent(fields, body) {
|
|
19
|
+
const errors = [];
|
|
20
|
+
if (fields.recordType !== 'event') {
|
|
21
|
+
errors.push('recordType: expected "event"');
|
|
22
|
+
}
|
|
23
|
+
const id = requireString(fields.id, 'id', errors);
|
|
24
|
+
const session = readOptionalNonEmptyString(fields.session, 'session', errors);
|
|
25
|
+
const cwd = requireString(fields.cwd, 'cwd', errors);
|
|
26
|
+
const summary = requireString(fields.summary, 'summary', errors);
|
|
27
|
+
let capturedAt;
|
|
28
|
+
const rawCapturedAt = fields['captured-at'];
|
|
29
|
+
if (typeof rawCapturedAt !== 'string') {
|
|
30
|
+
errors.push('missing required field: captured-at');
|
|
31
|
+
}
|
|
32
|
+
else if (!isValidDate(rawCapturedAt)) {
|
|
33
|
+
errors.push('captured-at: expected a YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ date');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
capturedAt = rawCapturedAt;
|
|
37
|
+
}
|
|
38
|
+
const tags = readListField(fields.tags, 'tags', errors);
|
|
39
|
+
const addressedBy = readListField(fields['addressed-by'], 'addressed-by', errors);
|
|
40
|
+
const impact = readImpactField(fields.impact, errors);
|
|
41
|
+
if (errors.length > 0 ||
|
|
42
|
+
id === undefined ||
|
|
43
|
+
capturedAt === undefined ||
|
|
44
|
+
cwd === undefined ||
|
|
45
|
+
summary === undefined ||
|
|
46
|
+
tags === undefined ||
|
|
47
|
+
addressedBy === undefined) {
|
|
48
|
+
return { ok: false, errors };
|
|
49
|
+
}
|
|
50
|
+
const extra = {};
|
|
51
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
52
|
+
if (!TYPED_FIELDS.has(key)) {
|
|
53
|
+
extra[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
record: {
|
|
59
|
+
recordType: 'event',
|
|
60
|
+
id,
|
|
61
|
+
capturedAt,
|
|
62
|
+
...(session !== undefined && { session }),
|
|
63
|
+
cwd,
|
|
64
|
+
summary,
|
|
65
|
+
tags,
|
|
66
|
+
addressedBy,
|
|
67
|
+
...(impact !== undefined && { impact }),
|
|
68
|
+
extra,
|
|
69
|
+
body,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function renderEvent(record) {
|
|
74
|
+
const fields = {
|
|
75
|
+
recordType: record.recordType,
|
|
76
|
+
id: record.id,
|
|
77
|
+
'captured-at': record.capturedAt,
|
|
78
|
+
...(record.session !== undefined && record.session.length > 0 && { session: record.session }),
|
|
79
|
+
cwd: record.cwd,
|
|
80
|
+
summary: record.summary,
|
|
81
|
+
};
|
|
82
|
+
if (record.tags.length > 0) {
|
|
83
|
+
fields.tags = record.tags;
|
|
84
|
+
}
|
|
85
|
+
if (record.addressedBy.length > 0) {
|
|
86
|
+
fields['addressed-by'] = record.addressedBy;
|
|
87
|
+
}
|
|
88
|
+
if (record.impact !== undefined) {
|
|
89
|
+
fields.impact = record.impact;
|
|
90
|
+
}
|
|
91
|
+
Object.assign(fields, record.extra);
|
|
92
|
+
return { fields, body: record.body };
|
|
93
|
+
}
|
|
94
|
+
function readImpactField(value, errors) {
|
|
95
|
+
if (value === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (isEventImpact(value)) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
errors.push(`impact: expected one of ${EVENT_IMPACT_LEVELS.join(', ')}`);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function readListField(value, field, errors) {
|
|
105
|
+
const list = asStringList(value);
|
|
106
|
+
if (list === null) {
|
|
107
|
+
errors.push(`${field}: must be a list`);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return list;
|
|
111
|
+
}
|
|
112
|
+
function readOptionalNonEmptyString(value, field, errors) {
|
|
113
|
+
if (value === undefined || value === null || value === '') {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
errors.push(`${field}: expected a string`);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
function requireString(value, field, errors) {
|
|
123
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
errors.push(`missing required field: ${field}`);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { KbLoaderError } from "../config/kb-loader-error.js";
|
|
5
|
+
import { ALIASES_FILE } from "../layout/index.js";
|
|
6
|
+
import { isEnoent, isRecord } from "../type-guards.js";
|
|
7
|
+
export async function loadAliases(input) {
|
|
8
|
+
const path = join(input.kbRoot.path, ALIASES_FILE);
|
|
9
|
+
let text;
|
|
10
|
+
try {
|
|
11
|
+
text = await readFile(path, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (isEnoent(error)) {
|
|
15
|
+
return new Map();
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return parseAliases(text, path);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
throw new KbLoaderError(message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function parseAliases(text, contextLabel = 'tag-aliases') {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(`${contextLabel}: malformed YAML — ${message}`);
|
|
35
|
+
}
|
|
36
|
+
if (!isRecord(parsed)) {
|
|
37
|
+
throw new Error(`${contextLabel}: top-level must be a mapping`);
|
|
38
|
+
}
|
|
39
|
+
if (!('aliases' in parsed)) {
|
|
40
|
+
throw new Error(`${contextLabel}: missing required "aliases" key`);
|
|
41
|
+
}
|
|
42
|
+
const aliasesBlock = parsed.aliases;
|
|
43
|
+
if (!isRecord(aliasesBlock)) {
|
|
44
|
+
throw new Error(`${contextLabel}: "aliases" must be a mapping of canonical to alias list`);
|
|
45
|
+
}
|
|
46
|
+
const map = new Map();
|
|
47
|
+
for (const [canonical, value] of Object.entries(aliasesBlock)) {
|
|
48
|
+
if (!Array.isArray(value)) {
|
|
49
|
+
throw new TypeError(`${contextLabel}: "${canonical}" must be a list of alias strings`);
|
|
50
|
+
}
|
|
51
|
+
const seenInCanonical = new Set();
|
|
52
|
+
for (const entry of value) {
|
|
53
|
+
if (typeof entry !== 'string') {
|
|
54
|
+
throw new TypeError(`${contextLabel}: alias entries under "${canonical}" must be string values`);
|
|
55
|
+
}
|
|
56
|
+
const alias = entry.toLowerCase();
|
|
57
|
+
if (alias === canonical.toLowerCase()) {
|
|
58
|
+
throw new Error(`${contextLabel}: alias "${entry}" equals its canonical "${canonical}"`);
|
|
59
|
+
}
|
|
60
|
+
if (seenInCanonical.has(alias)) {
|
|
61
|
+
throw new Error(`${contextLabel}: alias "${entry}" is listed twice under canonical "${canonical}"`);
|
|
62
|
+
}
|
|
63
|
+
seenInCanonical.add(alias);
|
|
64
|
+
const existing = map.get(alias);
|
|
65
|
+
if (existing !== undefined && existing !== canonical) {
|
|
66
|
+
throw new Error(`${contextLabel}: alias "${entry}" appears under multiple canonicals ("${existing}" and "${canonical}")`);
|
|
67
|
+
}
|
|
68
|
+
map.set(alias, canonical);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return map;
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function isEnoent(error) {
|
|
2
|
+
return isErrorCode(error, 'ENOENT');
|
|
3
|
+
}
|
|
4
|
+
export function isErrorCode(error, code) {
|
|
5
|
+
return isObject(error) && error.code === code;
|
|
6
|
+
}
|
|
7
|
+
export function isRecord(value) {
|
|
8
|
+
return isObject(value) && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function isObject(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface KbRoot {
|
|
2
|
+
path: string;
|
|
3
|
+
kbDir: string;
|
|
4
|
+
}
|
|
5
|
+
export interface KbRegistryEntry {
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
readonly?: boolean;
|
|
10
|
+
source: 'user' | 'project';
|
|
11
|
+
}
|
|
12
|
+
export interface KbRegistry {
|
|
13
|
+
entries: KbRegistryEntry[];
|
|
14
|
+
defaultKb?: KbRegistryEntry;
|
|
15
|
+
sources: {
|
|
16
|
+
user?: string;
|
|
17
|
+
project?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface Frontmatter {
|
|
21
|
+
title: string;
|
|
22
|
+
recordType: string;
|
|
23
|
+
created: string;
|
|
24
|
+
updated: string;
|
|
25
|
+
tags: string[];
|
|
26
|
+
extra: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
export interface FrontmatterRaw {
|
|
29
|
+
text: string;
|
|
30
|
+
startLine: number;
|
|
31
|
+
endLine: number;
|
|
32
|
+
parseError?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ParsedNote {
|
|
35
|
+
path: string;
|
|
36
|
+
content: string;
|
|
37
|
+
frontmatter: Frontmatter | null;
|
|
38
|
+
frontmatterRaw: FrontmatterRaw | null;
|
|
39
|
+
body: string;
|
|
40
|
+
bodyStartLine: number;
|
|
41
|
+
}
|
|
42
|
+
export type FindingSeverity = 'error' | 'warning';
|
|
43
|
+
export interface Finding {
|
|
44
|
+
path: string;
|
|
45
|
+
line?: number;
|
|
46
|
+
rule: string;
|
|
47
|
+
severity: FindingSeverity;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
export type AliasMap = ReadonlyMap<string, string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
export function buildVaultIndex(notes) {
|
|
3
|
+
const index = new Map();
|
|
4
|
+
for (const note of notes) {
|
|
5
|
+
const key = basename(note.path).replace(/\.md$/, '');
|
|
6
|
+
let set = index.get(key);
|
|
7
|
+
if (set === undefined) {
|
|
8
|
+
set = new Set();
|
|
9
|
+
index.set(key, set);
|
|
10
|
+
}
|
|
11
|
+
set.add(note.path);
|
|
12
|
+
}
|
|
13
|
+
return index;
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildVaultIndex } from "./build-vault-index.js";
|
|
2
|
+
import { countNewlines, extractTarget, hasNonMarkdownExtension, lookupKey, maskFencedCode, maskInlineCode, WIKILINK, } from "./wikilink-parse.js";
|
|
3
|
+
export function checkVaultIntegrity(notes) {
|
|
4
|
+
const vaultIndex = buildVaultIndex(notes);
|
|
5
|
+
return [...unresolvedLinkFindings(notes, vaultIndex), ...basenameFindings(vaultIndex)];
|
|
6
|
+
}
|
|
7
|
+
function basenameFindings(vaultIndex) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const key of [...vaultIndex.keys()].toSorted()) {
|
|
10
|
+
const paths = vaultIndex.get(key);
|
|
11
|
+
if (paths === undefined || paths.size < 2)
|
|
12
|
+
continue;
|
|
13
|
+
const sortedPaths = [...paths].toSorted();
|
|
14
|
+
findings.push({
|
|
15
|
+
path: sortedPaths[0] ?? key,
|
|
16
|
+
rule: 'wikilinks.basename',
|
|
17
|
+
severity: 'warning',
|
|
18
|
+
message: `basename "${key}" is shared by ${sortedPaths.length} notes: ${sortedPaths.join(', ')}`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return findings;
|
|
22
|
+
}
|
|
23
|
+
function unresolvedLinkFindings(notes, vaultIndex) {
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const note of notes) {
|
|
26
|
+
const body = maskInlineCode(maskFencedCode(note.body));
|
|
27
|
+
for (const match of body.matchAll(WIKILINK)) {
|
|
28
|
+
const inner = match[1];
|
|
29
|
+
if (inner === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const target = extractTarget(inner);
|
|
32
|
+
if (target === null)
|
|
33
|
+
continue;
|
|
34
|
+
if (hasNonMarkdownExtension(target))
|
|
35
|
+
continue;
|
|
36
|
+
const resolved = vaultIndex.get(lookupKey(target));
|
|
37
|
+
if (resolved !== undefined && resolved.size > 0)
|
|
38
|
+
continue;
|
|
39
|
+
findings.push({
|
|
40
|
+
path: note.path,
|
|
41
|
+
line: note.bodyStartLine + countNewlines(body, match.index),
|
|
42
|
+
rule: 'wikilinks.unresolved',
|
|
43
|
+
severity: 'error',
|
|
44
|
+
message: `[[${target}]] does not resolve to any vault note`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { buildVaultIndex, type VaultIndex } from './build-vault-index.js';
|
|
2
|
+
export { checkVaultIntegrity, type VaultIntegrityNote } from './check-vault-integrity.js';
|
|
3
|
+
export { countNewlines, extractTarget, hasNonMarkdownExtension, lookupKey, maskFencedCode, maskInlineCode, WIKILINK, } from './wikilink-parse.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const WIKILINK: RegExp;
|
|
2
|
+
export declare function countNewlines(text: string, upTo: number): number;
|
|
3
|
+
export declare function extractTarget(inner: string): string | null;
|
|
4
|
+
export declare function hasNonMarkdownExtension(target: string): boolean;
|
|
5
|
+
export declare function lookupKey(target: string): string;
|
|
6
|
+
export declare function maskFencedCode(body: string): string;
|
|
7
|
+
export declare function maskInlineCode(body: string): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const WIKILINK = /(?<!\\)!?\[\[([^\]\n]+?)\]\]/g;
|
|
2
|
+
const NON_MD_EXTENSIONS = new Set([
|
|
3
|
+
'.png',
|
|
4
|
+
'.jpg',
|
|
5
|
+
'.jpeg',
|
|
6
|
+
'.gif',
|
|
7
|
+
'.svg',
|
|
8
|
+
'.webp',
|
|
9
|
+
'.pdf',
|
|
10
|
+
'.mp3',
|
|
11
|
+
'.mp4',
|
|
12
|
+
'.mov',
|
|
13
|
+
'.wav',
|
|
14
|
+
]);
|
|
15
|
+
export function countNewlines(text, upTo) {
|
|
16
|
+
let count = 0;
|
|
17
|
+
for (let index = 0; index < upTo && index < text.length; index += 1) {
|
|
18
|
+
if (text[index] === '\n')
|
|
19
|
+
count += 1;
|
|
20
|
+
}
|
|
21
|
+
return count;
|
|
22
|
+
}
|
|
23
|
+
export function extractTarget(inner) {
|
|
24
|
+
const beforeAlias = inner.split('|', 1)[0] ?? '';
|
|
25
|
+
const beforeAnchor = beforeAlias.split('#', 1)[0] ?? '';
|
|
26
|
+
const trimmed = beforeAnchor.trim();
|
|
27
|
+
return trimmed === '' ? null : trimmed;
|
|
28
|
+
}
|
|
29
|
+
export function hasNonMarkdownExtension(target) {
|
|
30
|
+
const dotIndex = target.lastIndexOf('.');
|
|
31
|
+
if (dotIndex === -1)
|
|
32
|
+
return false;
|
|
33
|
+
const ext = target.slice(dotIndex).toLowerCase();
|
|
34
|
+
if (ext === '.md')
|
|
35
|
+
return false;
|
|
36
|
+
return NON_MD_EXTENSIONS.has(ext);
|
|
37
|
+
}
|
|
38
|
+
export function lookupKey(target) {
|
|
39
|
+
const withoutExtension = target.endsWith('.md') ? target.slice(0, -3) : target;
|
|
40
|
+
const segments = withoutExtension.split('/');
|
|
41
|
+
return segments.at(-1) ?? withoutExtension;
|
|
42
|
+
}
|
|
43
|
+
export function maskFencedCode(body) {
|
|
44
|
+
const lines = body.split('\n');
|
|
45
|
+
let inFence = false;
|
|
46
|
+
let fenceChar = '';
|
|
47
|
+
let fenceLength = 0;
|
|
48
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
49
|
+
const line = lines[index] ?? '';
|
|
50
|
+
const fenceMatch = line.match(FENCE_LINE);
|
|
51
|
+
if (fenceMatch) {
|
|
52
|
+
const marker = fenceMatch[1] ?? '';
|
|
53
|
+
const char = marker[0] ?? '';
|
|
54
|
+
if (!inFence) {
|
|
55
|
+
inFence = true;
|
|
56
|
+
fenceChar = char;
|
|
57
|
+
fenceLength = marker.length;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (char === fenceChar && marker.length >= fenceLength) {
|
|
61
|
+
inFence = false;
|
|
62
|
+
fenceChar = '';
|
|
63
|
+
fenceLength = 0;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (inFence)
|
|
68
|
+
lines[index] = ' '.repeat(line.length);
|
|
69
|
+
}
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
export function maskInlineCode(body) {
|
|
73
|
+
return body.replace(/`+[^`\n]+?`+/g, (match) => ' '.repeat(match.length));
|
|
74
|
+
}
|
|
75
|
+
const FENCE_LINE = /^\s{0,3}(`{3,}|~{3,})/;
|
package/package.json
CHANGED
|
@@ -1,4 +1,110 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/kb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Knowledge-base foundation: discovery, config, frontmatter parsing, records, tags, and vault-integrity checks",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"frontmatter",
|
|
7
|
+
"knowledge-base",
|
|
8
|
+
"markdown",
|
|
9
|
+
"notes"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/williamthorsen/codeassembly/tree/main/packages/kb#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/williamthorsen/codeassembly/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/williamthorsen/codeassembly.git",
|
|
18
|
+
"directory": "packages/kb"
|
|
19
|
+
},
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"source": "./src/index.ts",
|
|
26
|
+
"types": "./dist/esm/index.d.ts",
|
|
27
|
+
"import": "./dist/esm/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./check": {
|
|
30
|
+
"source": "./src/check/index.ts",
|
|
31
|
+
"types": "./dist/esm/check/index.d.ts",
|
|
32
|
+
"import": "./dist/esm/check/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./config": {
|
|
35
|
+
"source": "./src/config/index.ts",
|
|
36
|
+
"types": "./dist/esm/config/index.d.ts",
|
|
37
|
+
"import": "./dist/esm/config/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./create": {
|
|
40
|
+
"source": "./src/create/index.ts",
|
|
41
|
+
"types": "./dist/esm/create/index.d.ts",
|
|
42
|
+
"import": "./dist/esm/create/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./discovery": {
|
|
45
|
+
"source": "./src/discovery/index.ts",
|
|
46
|
+
"types": "./dist/esm/discovery/index.d.ts",
|
|
47
|
+
"import": "./dist/esm/discovery/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./filesystem": {
|
|
50
|
+
"source": "./src/filesystem/index.ts",
|
|
51
|
+
"types": "./dist/esm/filesystem/index.d.ts",
|
|
52
|
+
"import": "./dist/esm/filesystem/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./frontmatter": {
|
|
55
|
+
"source": "./src/frontmatter/index.ts",
|
|
56
|
+
"types": "./dist/esm/frontmatter/index.d.ts",
|
|
57
|
+
"import": "./dist/esm/frontmatter/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./layout": {
|
|
60
|
+
"source": "./src/layout/index.ts",
|
|
61
|
+
"types": "./dist/esm/layout/index.d.ts",
|
|
62
|
+
"import": "./dist/esm/layout/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./note-io": {
|
|
65
|
+
"source": "./src/note-io/index.ts",
|
|
66
|
+
"types": "./dist/esm/note-io/index.d.ts",
|
|
67
|
+
"import": "./dist/esm/note-io/index.js"
|
|
68
|
+
},
|
|
69
|
+
"./records": {
|
|
70
|
+
"source": "./src/records/index.ts",
|
|
71
|
+
"types": "./dist/esm/records/index.d.ts",
|
|
72
|
+
"import": "./dist/esm/records/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./tags": {
|
|
75
|
+
"source": "./src/tags/index.ts",
|
|
76
|
+
"types": "./dist/esm/tags/index.d.ts",
|
|
77
|
+
"import": "./dist/esm/tags/index.js"
|
|
78
|
+
},
|
|
79
|
+
"./vault-integrity": {
|
|
80
|
+
"source": "./src/vault-integrity/index.ts",
|
|
81
|
+
"types": "./dist/esm/vault-integrity/index.d.ts",
|
|
82
|
+
"import": "./dist/esm/vault-integrity/index.js"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"main": "./dist/esm/index.js",
|
|
86
|
+
"bin": {
|
|
87
|
+
"kb": "bin/kb.js"
|
|
88
|
+
},
|
|
89
|
+
"files": [
|
|
90
|
+
"bin",
|
|
91
|
+
"dist"
|
|
92
|
+
],
|
|
93
|
+
"dependencies": {
|
|
94
|
+
"picomatch": "4.0.5",
|
|
95
|
+
"yaml": "2.9.0",
|
|
96
|
+
"zod": "4.4.3"
|
|
97
|
+
},
|
|
98
|
+
"devDependencies": {
|
|
99
|
+
"@types/picomatch": "4.0.3"
|
|
100
|
+
},
|
|
101
|
+
"engines": {
|
|
102
|
+
"node": ">=24"
|
|
103
|
+
},
|
|
104
|
+
"publishConfig": {
|
|
105
|
+
"access": "public"
|
|
106
|
+
},
|
|
107
|
+
"scripts": {
|
|
108
|
+
"build": "nmr compile"
|
|
109
|
+
}
|
|
110
|
+
}
|