@esoteric-logic/praxis-harness 2.14.0 → 2.15.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 (39) hide show
  1. package/base/skills/px-prompt/SKILL.md +373 -0
  2. package/bin/praxis.js +7 -0
  3. package/bin/prompt-blocks.js +145 -0
  4. package/bin/prompt-compile.js +313 -0
  5. package/lib/assemblers.js +249 -0
  6. package/lib/loader.js +148 -0
  7. package/package.json +10 -3
  8. package/prompts/blocks/behaviors/flag-confidence.md +13 -0
  9. package/prompts/blocks/behaviors/handle-uncertainty.md +13 -0
  10. package/prompts/blocks/behaviors/no-flattery.md +15 -0
  11. package/prompts/blocks/behaviors/recommend-with-reasons.md +13 -0
  12. package/prompts/blocks/behaviors/verify-before-reporting.md +13 -0
  13. package/prompts/blocks/context/mcp-servers.md +12 -0
  14. package/prompts/blocks/context/official-docs-first.md +16 -0
  15. package/prompts/blocks/context/praxis-workflow.md +20 -0
  16. package/prompts/blocks/context/vault-integration.md +13 -0
  17. package/prompts/blocks/domains/cloud-infrastructure.md +13 -0
  18. package/prompts/blocks/domains/govcon.md +13 -0
  19. package/prompts/blocks/domains/web-development.md +13 -0
  20. package/prompts/blocks/formats/concise-responses.md +13 -0
  21. package/prompts/blocks/formats/what-so-what-now-what.md +16 -0
  22. package/prompts/blocks/identity/research-partner.md +10 -0
  23. package/prompts/blocks/identity/senior-engineer.md +15 -0
  24. package/prompts/blocks/identity/solutions-architect.md +13 -0
  25. package/prompts/profiles/_base.yaml +15 -0
  26. package/prompts/profiles/federal-cloud.yaml +18 -0
  27. package/prompts/profiles/praxis.yaml +13 -0
  28. package/prompts/projects/_template/prompt-config.yaml +34 -0
  29. package/prompts/projects/maximus/prompt-config.yaml +13 -0
  30. package/prompts/projects/maximus/references/maturity-questions.md +634 -0
  31. package/prompts/projects/maximus/references/phase-maturity-matrix.md +188 -0
  32. package/prompts/projects/maximus/references/proposal-writing-standards.md +367 -0
  33. package/prompts/projects/maximus/space-instructions.md +67 -0
  34. package/prompts/projects/maximus/system-prompt.md +641 -0
  35. package/prompts/projects/praxis/CLAUDE.md +84 -0
  36. package/prompts/projects/praxis/project-instructions.md +24 -0
  37. package/prompts/projects/praxis/prompt-config.yaml +40 -0
  38. package/prompts/projects/praxis/space-instructions.md +28 -0
  39. package/scripts/lint-harness.sh +42 -0
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const yaml = require('js-yaml');
7
+
8
+ const {
9
+ TARGETS,
10
+ PROMPTS_DIR,
11
+ loadPraxisConfig,
12
+ loadProfile,
13
+ mergeProfiles,
14
+ loadBlocks,
15
+ applyOverrides,
16
+ } = require('../lib/loader');
17
+
18
+ const {
19
+ interpolate,
20
+ findUnresolved,
21
+ assembleClaudeCode,
22
+ assembleClaudeProject,
23
+ assemblePerplexitySpace,
24
+ } = require('../lib/assemblers');
25
+
26
+ const PROJECTS_DIR = path.join(PROMPTS_DIR, 'projects');
27
+
28
+ const CHAR_BUDGETS = {
29
+ 'claude-code': Infinity,
30
+ 'claude-project': 2500,
31
+ 'perplexity-space': 4000,
32
+ };
33
+
34
+ // Global flags set by CLI parser
35
+ let PREVIEW_MODE = false;
36
+ let DIFF_MODE = false;
37
+ let STRICT_MODE = false;
38
+
39
+ // ── Helpers ──────────────────────────────────────────────────
40
+
41
+ function fail(msg) {
42
+ console.error(`\x1b[31mERROR:\x1b[0m ${msg}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ function warn(msg) {
47
+ console.error(`\x1b[33mWARN:\x1b[0m ${msg}`);
48
+ }
49
+
50
+ function ok(msg) {
51
+ console.log(`\x1b[32m✓\x1b[0m ${msg}`);
52
+ }
53
+
54
+ // ── Main ─────────────────────────────────────────────────────
55
+
56
+ /** Validate a standalone project — check files exist, report char budgets. */
57
+ function validateStandalone(projectName, projectDir, projectConfig) {
58
+ console.log(`\nValidating standalone: ${projectName}`);
59
+
60
+ const inventory = [
61
+ { file: 'system-prompt.md', budget: Infinity, required: true, label: 'System Prompt (Claude Projects)' },
62
+ { file: 'CLAUDE.md', budget: Infinity, required: false, label: 'Claude Code' },
63
+ { file: 'space-instructions.md', budget: CHAR_BUDGETS['perplexity-space'], required: false, label: 'Perplexity Space' },
64
+ { file: 'project-instructions.md', budget: CHAR_BUDGETS['claude-project'], required: false, label: 'Claude Project' },
65
+ ];
66
+
67
+ let missingGenerable = [];
68
+
69
+ for (const item of inventory) {
70
+ const filePath = path.join(projectDir, item.file);
71
+ if (fs.existsSync(filePath)) {
72
+ const content = fs.readFileSync(filePath, 'utf8');
73
+ const charCount = content.length;
74
+ const lineCount = content.split('\n').length;
75
+ const sizeInfo = item.budget < Infinity
76
+ ? `${charCount} chars (budget: ${item.budget})`
77
+ : `${charCount} chars, ${lineCount} lines`;
78
+
79
+ if (charCount > item.budget) {
80
+ warn(`${item.file} exceeds budget: ${charCount} chars (limit: ${item.budget})`);
81
+ } else {
82
+ ok(`${item.file} — ${sizeInfo}`);
83
+ }
84
+ } else if (item.required) {
85
+ warn(`${item.file} MISSING — standalone projects require a system prompt`);
86
+ } else {
87
+ missingGenerable.push(item.file);
88
+ }
89
+ }
90
+
91
+ if (missingGenerable.length > 0) {
92
+ console.log(`\n Missing platform outputs: ${missingGenerable.join(', ')}`);
93
+ console.log(' Run /px-prompt ' + projectName + ' to auto-generate from system-prompt.md');
94
+ }
95
+
96
+ // Version consistency check
97
+ const systemPromptPath = path.join(projectDir, 'system-prompt.md');
98
+ if (fs.existsSync(systemPromptPath) && projectConfig.version) {
99
+ const spContent = fs.readFileSync(systemPromptPath, 'utf8');
100
+ const versionMatch = spContent.match(/^version:\s*["']?([^"'\n]+)/m);
101
+ if (versionMatch && versionMatch[1].trim() !== String(projectConfig.version).trim()) {
102
+ warn(`Version mismatch: prompt-config.yaml says "${projectConfig.version}" but system-prompt.md says "${versionMatch[1].trim()}"`);
103
+ }
104
+ }
105
+
106
+ // Check for reference files
107
+ const refsDir = path.join(projectDir, 'references');
108
+ if (fs.existsSync(refsDir)) {
109
+ const refs = fs.readdirSync(refsDir).filter((f) => f.endsWith('.md'));
110
+ if (refs.length > 0) {
111
+ ok(`${refs.length} reference file(s): ${refs.join(', ')}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ function compileProject(projectName, targets) {
117
+ const projectDir = path.join(PROJECTS_DIR, projectName);
118
+ const configPath = path.join(projectDir, 'prompt-config.yaml');
119
+
120
+ if (!fs.existsSync(configPath)) {
121
+ fail(`Project config not found: ${configPath}\nRun /px-prompt <project-name> to create one.`);
122
+ }
123
+
124
+ const projectConfig = yaml.load(fs.readFileSync(configPath, 'utf8'));
125
+
126
+ // Standalone mode: validate files, report budgets, skip compilation
127
+ if (projectConfig.mode === 'standalone') {
128
+ validateStandalone(projectName, projectDir, projectConfig);
129
+ return;
130
+ }
131
+
132
+ const praxisConfig = loadPraxisConfig();
133
+
134
+ // Build vars map: project vars + praxis config + project name
135
+ const vars = {
136
+ ...praxisConfig,
137
+ ...(projectConfig.vars || {}),
138
+ project: projectConfig.project || projectName,
139
+ };
140
+
141
+ // Load profile: from named profile, project-local blocks, or _base fallback
142
+ let profile;
143
+ if (projectConfig.profile) {
144
+ profile = loadProfile(projectConfig.profile, fail);
145
+ } else if (projectConfig.blocks) {
146
+ const base = loadProfile('_base', fail);
147
+ profile = mergeProfiles(base, { blocks: projectConfig.blocks });
148
+ } else {
149
+ profile = loadProfile('_base', fail);
150
+ }
151
+ profile = applyOverrides(profile, projectConfig.overrides);
152
+
153
+ const profileName = projectConfig.profile || 'project-local';
154
+ console.log(`\nCompiling: ${projectName} (profile: ${profileName})`);
155
+
156
+ const assemblers = {
157
+ 'claude-code': assembleClaudeCode,
158
+ 'claude-project': assembleClaudeProject,
159
+ 'perplexity-space': assemblePerplexitySpace,
160
+ };
161
+
162
+ const outputNames = {
163
+ 'claude-code': 'CLAUDE.md',
164
+ 'claude-project': 'project-instructions.md',
165
+ 'perplexity-space': 'space-instructions.md',
166
+ };
167
+
168
+ for (const target of targets) {
169
+ const blocks = loadBlocks(profile, target, warn);
170
+ let output = assemblers[target](blocks, projectConfig, vars);
171
+
172
+ // Interpolate variables
173
+ output = interpolate(output, vars);
174
+
175
+ // Validate no unresolved placeholders
176
+ const unresolved = findUnresolved(output);
177
+ if (unresolved.length > 0) {
178
+ if (STRICT_MODE) {
179
+ fail(`[strict] Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
180
+ }
181
+ warn(`Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
182
+ }
183
+
184
+ // Check character budget
185
+ const budget = CHAR_BUDGETS[target];
186
+ if (output.length > budget) {
187
+ if (STRICT_MODE) {
188
+ fail(`[strict] ${target} exceeds budget: ${output.length} chars (limit: ${budget})`);
189
+ }
190
+ warn(`${target} output exceeds budget: ${output.length} chars (limit: ${budget})`);
191
+ }
192
+
193
+ const outputPath = path.join(projectDir, outputNames[target]);
194
+
195
+ // Preview mode: print to stdout instead of writing
196
+ if (PREVIEW_MODE) {
197
+ console.log(`\n--- ${outputNames[target]} (${output.length} chars) ---`);
198
+ console.log(output);
199
+ continue;
200
+ }
201
+
202
+ // Diff mode: show diff against existing file before writing
203
+ if (DIFF_MODE && fs.existsSync(outputPath)) {
204
+ const existing = fs.readFileSync(outputPath, 'utf8');
205
+ if (existing === output) {
206
+ ok(`${outputNames[target]} — unchanged (${output.length} chars)`);
207
+ continue;
208
+ }
209
+ console.log(`\n--- ${outputNames[target]} changed ---`);
210
+ const existingLines = existing.split('\n');
211
+ const outputLines = output.split('\n');
212
+ const addedCount = outputLines.filter((l) => !existingLines.includes(l)).length;
213
+ const removedCount = existingLines.filter((l) => !outputLines.includes(l)).length;
214
+ console.log(` +${addedCount} lines added, -${removedCount} lines removed`);
215
+ }
216
+
217
+ fs.writeFileSync(outputPath, output, 'utf8');
218
+ ok(`${outputNames[target]} — ${output.length} chars → ${outputPath}`);
219
+ }
220
+ }
221
+
222
+ // ── CLI ──────────────────────────────────────────────────────
223
+
224
+ function main() {
225
+ const args = process.argv.slice(2);
226
+
227
+ if (args.length === 0 || args.includes('--help')) {
228
+ console.log('Usage: prompt-compile <project-name|--all> [options]');
229
+ console.log('Options:');
230
+ console.log(' --target <target> claude-code|claude-project|perplexity-space|all');
231
+ console.log(' --preview Print output to stdout without writing files');
232
+ console.log(' --diff Show what changed before writing');
233
+ console.log(' --strict Exit with error on budget overruns or unresolved vars');
234
+ console.log(' --list List all projects with mode and file status');
235
+ process.exit(0);
236
+ }
237
+
238
+ // --list mode: show all projects
239
+ if (args.includes('--list')) {
240
+ const projectDirs = fs.readdirSync(PROJECTS_DIR)
241
+ .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
242
+ if (projectDirs.length === 0) {
243
+ console.log('No projects found.');
244
+ process.exit(0);
245
+ }
246
+ console.log(`${'Project'.padEnd(20)} ${'Mode'.padEnd(12)} ${'System Prompt'.padEnd(15)} ${'Claude Proj'.padEnd(15)} ${'Perplexity'.padEnd(15)} ${'CLAUDE.md'.padEnd(12)} Refs`);
247
+ console.log('-'.repeat(95));
248
+ for (const name of projectDirs) {
249
+ const dir = path.join(PROJECTS_DIR, name);
250
+ const cfgPath = path.join(dir, 'prompt-config.yaml');
251
+ const cfg = fs.existsSync(cfgPath) ? yaml.load(fs.readFileSync(cfgPath, 'utf8')) : {};
252
+ const mode = cfg.mode || 'compiled';
253
+ const fileStatus = (f) => {
254
+ const p = path.join(dir, f);
255
+ if (!fs.existsSync(p)) return '—';
256
+ return `${fs.readFileSync(p, 'utf8').length} chars`;
257
+ };
258
+ const refsDir = path.join(dir, 'references');
259
+ const refCount = fs.existsSync(refsDir)
260
+ ? fs.readdirSync(refsDir).filter((f) => f.endsWith('.md')).length
261
+ : 0;
262
+ console.log(
263
+ `${name.padEnd(20)} ${mode.padEnd(12)} ${fileStatus('system-prompt.md').padEnd(15)} ${fileStatus('project-instructions.md').padEnd(15)} ${fileStatus('space-instructions.md').padEnd(15)} ${fileStatus('CLAUDE.md').padEnd(12)} ${refCount}`
264
+ );
265
+ }
266
+ process.exit(0);
267
+ }
268
+
269
+ // Parse global flags
270
+ PREVIEW_MODE = args.includes('--preview');
271
+ DIFF_MODE = args.includes('--diff');
272
+ STRICT_MODE = args.includes('--strict');
273
+
274
+ // Parse --target flag
275
+ const targetIdx = args.indexOf('--target');
276
+ let targets = TARGETS;
277
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
278
+ const targetArg = args[targetIdx + 1];
279
+ if (TARGETS.includes(targetArg)) {
280
+ targets = [targetArg];
281
+ } else if (targetArg !== 'all') {
282
+ fail(`Unknown target: ${targetArg}. Use: ${TARGETS.join(', ')}, all`);
283
+ }
284
+ }
285
+
286
+ // Determine project(s) to compile
287
+ const flagValues = new Set();
288
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
289
+ flagValues.add(args[targetIdx + 1]);
290
+ }
291
+ const projectArg = args.find((a) => !a.startsWith('--') && !flagValues.has(a));
292
+
293
+ if (args.includes('--all')) {
294
+ const projectDirs = fs.readdirSync(PROJECTS_DIR)
295
+ .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
296
+
297
+ if (projectDirs.length === 0) {
298
+ fail('No projects found in prompts/projects/');
299
+ }
300
+
301
+ for (const projectName of projectDirs) {
302
+ compileProject(projectName, targets);
303
+ }
304
+ } else if (projectArg) {
305
+ compileProject(projectArg, targets);
306
+ } else {
307
+ fail('Specify a project name or use --all');
308
+ }
309
+
310
+ console.log('\nDone.');
311
+ }
312
+
313
+ main();
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ /** Replace {{var}} placeholders with values from vars map. */
4
+ function interpolate(text, vars) {
5
+ return text.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
6
+ if (key in vars) return vars[key];
7
+ return `{{${key}}}`;
8
+ });
9
+ }
10
+
11
+ /** Check for unresolved {{...}} placeholders. */
12
+ function findUnresolved(text) {
13
+ const matches = text.match(/\{\{(\w+)\}\}/g);
14
+ return matches ? [...new Set(matches)] : [];
15
+ }
16
+
17
+ function assembleClaudeCode(blocks, projectConfig, vars) {
18
+ const lines = [];
19
+ const today = new Date().toISOString().slice(0, 10);
20
+ lines.push(`# ${vars.project || vars.repo_name || 'Project'}`);
21
+ lines.push(`<!-- Generated by Praxis prompt-compile | profile: ${projectConfig.profile} | ${today} -->`);
22
+ lines.push('');
23
+
24
+ // Identity
25
+ const identityBlocks = blocks.filter((b) => b.category === 'identity');
26
+ if (identityBlocks.length > 0) {
27
+ lines.push('## Identity');
28
+ for (const block of identityBlocks) lines.push(block.content, '');
29
+ }
30
+
31
+ // Global Rules reference
32
+ lines.push('## Global Rules');
33
+ lines.push('Inherits execution engine from `~/.claude/CLAUDE.md`.');
34
+ lines.push('');
35
+
36
+ // Git Identity
37
+ if (vars.git_email || vars.git_identity) {
38
+ lines.push('## Git Identity');
39
+ if (vars.git_identity) lines.push(`- **Type**: ${vars.git_identity}`);
40
+ if (vars.git_email) lines.push(`- **Email**: ${vars.git_email}`);
41
+ lines.push('');
42
+ }
43
+
44
+ // Behaviors
45
+ const behaviorBlocks = blocks.filter((b) => b.category === 'behaviors');
46
+ if (behaviorBlocks.length > 0) {
47
+ lines.push('## Behaviors');
48
+ for (const block of behaviorBlocks) lines.push(block.content, '');
49
+ }
50
+
51
+ // Domains
52
+ const domainBlocks = blocks.filter((b) => b.category === 'domains');
53
+ if (domainBlocks.length > 0) {
54
+ lines.push('## Domain Expertise');
55
+ for (const block of domainBlocks) lines.push(block.content, '');
56
+ }
57
+
58
+ // Formats
59
+ const formatBlocks = blocks.filter((b) => b.category === 'formats');
60
+ if (formatBlocks.length > 0) {
61
+ lines.push('## Output Format');
62
+ for (const block of formatBlocks) lines.push(block.content, '');
63
+ }
64
+
65
+ // Tech Stack + Commands (from claude_code_append)
66
+ const append = (projectConfig.overrides || {}).claude_code_append || {};
67
+ if (append.tech_stack) {
68
+ lines.push('## Tech Stack');
69
+ lines.push(append.tech_stack.trim(), '');
70
+ }
71
+ if (append.commands) {
72
+ lines.push('## Commands');
73
+ lines.push('```bash');
74
+ lines.push(append.commands.trim());
75
+ lines.push('```', '');
76
+ }
77
+
78
+ // Context
79
+ const contextBlocks = blocks.filter((b) => b.category === 'context');
80
+ if (contextBlocks.length > 0) {
81
+ for (const block of contextBlocks) lines.push(block.content, '');
82
+ }
83
+
84
+ // Extra notes
85
+ if (append.extra_notes) {
86
+ lines.push('## Important Notes');
87
+ lines.push(append.extra_notes.trim(), '');
88
+ }
89
+
90
+ // Vault Project
91
+ if (vars.vault_project_path) {
92
+ lines.push('## Vault Project');
93
+ lines.push(`- **Vault path**: ${vars.vault_project_path}`);
94
+ lines.push('');
95
+ }
96
+
97
+ // Standard footer
98
+ lines.push('## Verification');
99
+ lines.push('- Before marking any task complete, run the test suite');
100
+ lines.push('- Check logs before claiming a bug is fixed');
101
+ lines.push('');
102
+ lines.push('## Conventions');
103
+ lines.push('- **Commits**: conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)');
104
+ lines.push('- **Branches**: `feat/description` or `fix/description`');
105
+ lines.push('');
106
+ lines.push('## Error Learning');
107
+ lines.push('<!-- Add project-specific learnings below -->');
108
+ lines.push('');
109
+
110
+ return lines.join('\n');
111
+ }
112
+
113
+ function assembleClaudeProject(blocks, projectConfig, vars) {
114
+ const lines = [];
115
+
116
+ // Layer 1: Role
117
+ const identityBlocks = blocks.filter((b) => b.category === 'identity');
118
+ lines.push('## Role');
119
+ if (projectConfig.description) lines.push(projectConfig.description);
120
+ for (const block of identityBlocks) lines.push(block.content);
121
+ lines.push('');
122
+
123
+ // Layer 2: Behavioral Constraints
124
+ const behaviorBlocks = blocks.filter((b) => b.category === 'behaviors');
125
+ if (behaviorBlocks.length > 0) {
126
+ lines.push('## Behavioral Constraints');
127
+ for (const block of behaviorBlocks) lines.push('- ' + block.content);
128
+ lines.push('');
129
+ }
130
+
131
+ // Layer 3: Domain Expertise
132
+ const domainBlocks = blocks.filter((b) => b.category === 'domains');
133
+ if (domainBlocks.length > 0) {
134
+ lines.push('## Domain Expertise');
135
+ for (const block of domainBlocks) lines.push('- ' + block.content);
136
+ lines.push('');
137
+ }
138
+
139
+ // Layer 3b: Output Format
140
+ const formatBlocks = blocks.filter((b) => b.category === 'formats');
141
+ if (formatBlocks.length > 0) {
142
+ lines.push('## Output Format');
143
+ for (const block of formatBlocks) lines.push('- ' + block.content);
144
+ lines.push('');
145
+ }
146
+
147
+ // Additional context
148
+ const append = (projectConfig.overrides || {}).claude_project_append || {};
149
+ if (append.additional_context) {
150
+ lines.push(append.additional_context.trim(), '');
151
+ }
152
+
153
+ // Layer 4: Quality Gates
154
+ if (append.quality_gates) {
155
+ lines.push('## Quality Gates');
156
+ lines.push(append.quality_gates.trim(), '');
157
+ }
158
+
159
+ // Context blocks (workflow, etc.)
160
+ const contextBlocks = blocks.filter((b) => b.category === 'context');
161
+ if (contextBlocks.length > 0) {
162
+ for (const block of contextBlocks) lines.push(block.content);
163
+ lines.push('');
164
+ }
165
+
166
+ // Layer 4b: Knowledge Files (if project has them)
167
+ const knowledgeFiles = projectConfig.knowledge_files || [];
168
+ if (knowledgeFiles.length > 0) {
169
+ lines.push('## Knowledge Files');
170
+ lines.push('Upload these alongside this prompt:');
171
+ for (const kf of knowledgeFiles) {
172
+ lines.push(`- **${kf.file}** — ${kf.description}`);
173
+ }
174
+ lines.push('');
175
+ }
176
+
177
+ // Layer 5: Failure Handling (always present)
178
+ lines.push('## When Uncertain');
179
+ lines.push('State uncertainty explicitly. Ask one clarifying question rather than guessing.');
180
+ lines.push('');
181
+
182
+ return lines.join('\n');
183
+ }
184
+
185
+ function assemblePerplexitySpace(blocks, projectConfig, vars) {
186
+ const lines = [];
187
+
188
+ // Purpose
189
+ const identityBlocks = blocks.filter((b) => b.category === 'identity');
190
+ lines.push('## Purpose');
191
+ if (projectConfig.description) lines.push(projectConfig.description);
192
+ for (const block of identityBlocks) lines.push(block.content);
193
+ lines.push('');
194
+
195
+ // Source Priority
196
+ const sourceBlocks = blocks.filter(
197
+ (b) => b.category === 'context' && (b.meta.tags || []).includes('sources')
198
+ );
199
+ if (sourceBlocks.length > 0) {
200
+ lines.push('## Source Priority');
201
+ for (const block of sourceBlocks) lines.push(block.content);
202
+ lines.push('');
203
+ }
204
+
205
+ // Domain Expertise
206
+ const domainBlocks = blocks.filter((b) => b.category === 'domains');
207
+ if (domainBlocks.length > 0) {
208
+ lines.push('## Domain Expertise');
209
+ for (const block of domainBlocks) lines.push(block.content);
210
+ lines.push('');
211
+ }
212
+
213
+ // Research Domains
214
+ const append = (projectConfig.overrides || {}).perplexity_space_append || {};
215
+ if (append.research_domains) {
216
+ lines.push('## Research Domains');
217
+ lines.push(append.research_domains.trim(), '');
218
+ }
219
+
220
+ // How to Answer
221
+ const behaviorBlocks = blocks.filter((b) => b.category === 'behaviors');
222
+ const formatBlocks = blocks.filter((b) => b.category === 'formats');
223
+ if (behaviorBlocks.length > 0 || formatBlocks.length > 0) {
224
+ lines.push('## How to Answer');
225
+ for (const block of behaviorBlocks) lines.push(block.content);
226
+ for (const block of formatBlocks) lines.push(block.content);
227
+ lines.push('');
228
+ }
229
+
230
+ // Anti-hallucination layer (auto-injected for all Perplexity outputs)
231
+ lines.push('## Accuracy Standards');
232
+ lines.push('- Flag your confidence level when synthesizing across sources');
233
+ lines.push('- Distinguish verified facts from analytical inferences');
234
+ lines.push('- If sources disagree, cite both and explain the discrepancy');
235
+ lines.push('- Never fabricate version numbers, API signatures, URLs, or code examples');
236
+ lines.push('- When information may be outdated (>12 months), note the publication date');
237
+ lines.push('- If you cannot find reliable sources, state that clearly rather than speculating');
238
+ lines.push('');
239
+
240
+ return lines.join('\n');
241
+ }
242
+
243
+ module.exports = {
244
+ interpolate,
245
+ findUnresolved,
246
+ assembleClaudeCode,
247
+ assembleClaudeProject,
248
+ assemblePerplexitySpace,
249
+ };