pluribus-context 0.2.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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * pluribus.md parser
3
+ * Splits a markdown file into top-level sections (# Heading)
4
+ * Returns an object: { sectionName: sectionBody }
5
+ */
6
+
7
+ /**
8
+ * @param {string} content
9
+ * @returns {Record<string, string>}
10
+ */
11
+ export function parsePluribusFile(content) {
12
+ // Strip BOM if present
13
+ const cleaned = content.replace(/^\uFEFF/, '')
14
+
15
+ const sections = {}
16
+ const lines = cleaned.split(/\r?\n/)
17
+
18
+ let currentSection = null
19
+ let currentLines = []
20
+
21
+ for (const line of lines) {
22
+ if (line.startsWith('# ') && !line.startsWith('## ')) {
23
+ // Save previous section
24
+ if (currentSection !== null) {
25
+ sections[currentSection] = currentLines.join('\n').trim()
26
+ }
27
+ currentSection = line.slice(2).trim()
28
+ currentLines = []
29
+ } else {
30
+ if (currentSection !== null) {
31
+ currentLines.push(line)
32
+ }
33
+ }
34
+ }
35
+
36
+ // Save last section
37
+ if (currentSection !== null) {
38
+ sections[currentSection] = currentLines.join('\n').trim()
39
+ }
40
+
41
+ return sections
42
+ }
43
+
44
+ /**
45
+ * Returns the slug form of a section name (lowercase, spaces→hyphens)
46
+ * @param {string} name
47
+ * @returns {string}
48
+ */
49
+ export function slugify(name) {
50
+ return name.toLowerCase().replace(/\s+/g, '-')
51
+ }
52
+
53
+ /** Required sections per the spec */
54
+ export const REQUIRED_SECTIONS = ['Identity', 'Stack', 'Conventions', 'Goals', 'Constraints']
55
+
56
+ /** Optional sections per the spec */
57
+ export const OPTIONAL_SECTIONS = ['Examples', 'Anti-patterns', 'Workflow', 'Context', 'Team']
58
+
59
+ /**
60
+ * Validate a parsed pluribus.md sections object.
61
+ * @param {Record<string, string>} sections
62
+ * @returns {{ valid: boolean, errors: string[] }}
63
+ */
64
+ export function validateSections(sections) {
65
+ const errors = []
66
+ for (const required of REQUIRED_SECTIONS) {
67
+ if (!sections[required] || sections[required].trim() === '') {
68
+ errors.push(`Missing or empty required section: # ${required}`)
69
+ }
70
+ }
71
+
72
+ // Check for duplicates (can't happen with our parser since later overwrites earlier, but let's be safe)
73
+ return { valid: errors.length === 0, errors }
74
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Template renderer for Pluribus Template Syntax (PTS)
3
+ * Supports: {{variable}}, {{#if variable}} ... {{/if}}, {{pluribus.version}}, {{pluribus.date}}, {{pluribus.source}}
4
+ */
5
+
6
+ import { slugify } from './parser.js'
7
+ import { VERSION } from './version.js'
8
+
9
+ /**
10
+ * @param {string} template
11
+ * @param {Record<string, string>} sections — raw sections from parsePluribusFile
12
+ * @param {string} sourcePath
13
+ * @returns {string}
14
+ */
15
+ export function renderTemplate(template, sections, sourcePath) {
16
+ // Build a variables map: both exact name and slug → content
17
+ const vars = {}
18
+
19
+ for (const [name, body] of Object.entries(sections)) {
20
+ vars[name.toLowerCase()] = body
21
+ vars[slugify(name)] = body
22
+ }
23
+
24
+ // Built-in pluribus.* variables
25
+ const meta = {
26
+ 'pluribus.version': VERSION,
27
+ 'pluribus.date': new Date().toISOString().slice(0, 10),
28
+ 'pluribus.source': sourcePath,
29
+ }
30
+
31
+ let result = template
32
+
33
+ // 1. Process {{#if variable}} ... {{/if}} blocks
34
+ result = result.replace(/\{\{#if ([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, inner) => {
35
+ const val = vars[key.trim().toLowerCase()] ?? vars[key.trim()]
36
+ if (val && val.trim() !== '') {
37
+ // Render inner (still apply variable substitution later)
38
+ return inner
39
+ }
40
+ return ''
41
+ })
42
+
43
+ // 2. Replace {{pluribus.*}} meta vars
44
+ for (const [key, value] of Object.entries(meta)) {
45
+ result = result.replaceAll(`{{${key}}}`, value)
46
+ }
47
+
48
+ // 3. Replace {{variable}} with section content
49
+ result = result.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
50
+ const k = key.trim().toLowerCase()
51
+ if (vars[k] !== undefined) return vars[k]
52
+ if (vars[key.trim()] !== undefined) return vars[key.trim()]
53
+ // Unknown variable: emit warning but keep the placeholder commented out
54
+ return `<!-- pluribus: unknown variable "${key.trim()}" -->`
55
+ })
56
+
57
+ return result.trim() + '\n'
58
+ }
59
+
60
+ /**
61
+ * Parse a skill file into its sections.
62
+ * @param {string} content
63
+ * @returns {{ output: string[], template: string, sections: { required: string[], optional: string[] }, meta: Record<string, string> }}
64
+ */
65
+ export function parseSkillFile(content) {
66
+ // Split into top-level sections
67
+ const rawSections = {}
68
+ const lines = content.split(/\r?\n/)
69
+ let currentSection = null
70
+ let currentLines = []
71
+
72
+ for (const line of lines) {
73
+ if (line.startsWith('# ') && !line.startsWith('## ')) {
74
+ if (currentSection !== null) {
75
+ rawSections[currentSection] = currentLines.join('\n').trim()
76
+ }
77
+ currentSection = line.slice(2).trim()
78
+ currentLines = []
79
+ } else {
80
+ if (currentSection !== null) {
81
+ currentLines.push(line)
82
+ }
83
+ }
84
+ }
85
+ if (currentSection !== null) {
86
+ rawSections[currentSection] = currentLines.join('\n').trim()
87
+ }
88
+
89
+ // Parse # Output — list of file paths
90
+ const outputRaw = rawSections['Output'] || ''
91
+ const output = outputRaw
92
+ .split(/\r?\n/)
93
+ .map((l) => l.trim())
94
+ .filter((l) => l.length > 0 && !l.startsWith('#'))
95
+
96
+ // Parse # Sections — required: ..., optional: ...
97
+ const sectionsRaw = rawSections['Sections'] || ''
98
+ const requiredMatch = sectionsRaw.match(/required:\s*(.+)/)
99
+ const optionalMatch = sectionsRaw.match(/optional:\s*(.+)/)
100
+ const requiredSections = requiredMatch
101
+ ? requiredMatch[1].split(',').map((s) => s.trim())
102
+ : []
103
+ const optionalSections = optionalMatch
104
+ ? optionalMatch[1].split(',').map((s) => s.trim())
105
+ : []
106
+
107
+ // Parse # Meta
108
+ const metaRaw = rawSections['Meta'] || ''
109
+ const meta = {}
110
+ for (const line of metaRaw.split(/\r?\n/)) {
111
+ const colonIdx = line.indexOf(':')
112
+ if (colonIdx !== -1) {
113
+ meta[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim()
114
+ }
115
+ }
116
+
117
+ return {
118
+ output,
119
+ template: rawSections['Template'] || '',
120
+ sections: { required: requiredSections, optional: optionalSections },
121
+ meta,
122
+ }
123
+ }
@@ -0,0 +1 @@
1
+ export const VERSION = '0.2.0'