@equilateral_ai/mindmeld 3.5.0 → 3.5.2

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.
@@ -18,6 +18,56 @@ const path = require('path');
18
18
  const fs = require('fs').promises;
19
19
  const crypto = require('crypto');
20
20
 
21
+ /**
22
+ * Scrub sensitive data from text before sending to MindMeld API.
23
+ * Replaces AWS keys, API tokens, passwords, connection strings,
24
+ * private keys, and generic secrets with [REDACTED].
25
+ * @param {string} text - Text that may contain secrets
26
+ * @returns {string} Text with secrets replaced by [REDACTED]
27
+ */
28
+ function scrubSecrets(text) {
29
+ if (typeof text !== 'string') return text;
30
+
31
+ const patterns = [
32
+ // AWS Access Keys
33
+ /AKIA[0-9A-Z]{16}/g,
34
+ // AWS Secret Keys
35
+ /(?:aws_secret_access_key|secret_key|secretAccessKey)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/gi,
36
+ // Generic API tokens
37
+ /(?:api[_-]?key|api[_-]?token|auth[_-]?token|bearer)\s*[=:]\s*['"]?[A-Za-z0-9_\-\.]{20,}['"]?/gi,
38
+ // Passwords
39
+ /(?:password|passwd|pwd)\s*[=:]\s*['"]?[^\s'"]{4,}['"]?/gi,
40
+ // Connection strings
41
+ /(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+/gi,
42
+ // Private keys
43
+ /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/g,
44
+ // Generic secrets
45
+ /(?:secret|token|credential)\s*[=:]\s*['"]?[A-Za-z0-9_\-\.\/+=]{16,}['"]?/gi,
46
+ ];
47
+
48
+ let scrubbed = text;
49
+ for (const pattern of patterns) {
50
+ scrubbed = scrubbed.replace(pattern, '[REDACTED]');
51
+ }
52
+ return scrubbed;
53
+ }
54
+
55
+ /**
56
+ * Deep-scrub secrets from an object by traversing all string values.
57
+ * @param {*} obj - Object, array, or primitive to scrub
58
+ * @returns {*} Scrubbed copy (original is not mutated)
59
+ */
60
+ function scrubSecretsDeep(obj) {
61
+ if (typeof obj === 'string') return scrubSecrets(obj);
62
+ if (obj === null || obj === undefined || typeof obj !== 'object') return obj;
63
+ if (Array.isArray(obj)) return obj.map(scrubSecretsDeep);
64
+ const result = {};
65
+ for (const [key, value] of Object.entries(obj)) {
66
+ result[key] = scrubSecretsDeep(value);
67
+ }
68
+ return result;
69
+ }
70
+
21
71
  // LLM Pattern Detection (optional - requires ANTHROPIC_API_KEY)
22
72
  let LLMPatternDetector = null;
23
73
  try {
@@ -200,7 +250,7 @@ async function harvestPatterns(sessionTranscript) {
200
250
  ? sessionTranscript
201
251
  : sessionTranscript.transcript || JSON.stringify(sessionTranscript);
202
252
 
203
- llmAnalysis = await detector.analyzeSessionTranscript(transcriptText, {
253
+ llmAnalysis = await detector.analyzeSessionTranscript(scrubSecrets(transcriptText), {
204
254
  projectName: path.basename(process.cwd()),
205
255
  filesChanged: sessionTranscript.filesChanged || []
206
256
  });
@@ -238,23 +288,23 @@ async function harvestPatterns(sessionTranscript) {
238
288
  // 2. Validate against standards
239
289
  const validationResults = await validatePatterns(mindmeld, patterns);
240
290
 
241
- // 3. Record violations
291
+ // 3. Record violations (scrub secrets before sending to API)
242
292
  for (const result of validationResults.violations) {
243
- await mindmeld.recordViolation({
293
+ await mindmeld.recordViolation(scrubSecretsDeep({
244
294
  pattern: result.pattern,
245
295
  violations: result.violations,
246
296
  sessionId: sessionId,
247
297
  userId: userId
248
- });
298
+ }));
249
299
  }
250
300
 
251
- // 4. Reinforce valid patterns
301
+ // 4. Reinforce valid patterns (scrub secrets before sending to API)
252
302
  for (const result of validationResults.valid) {
253
- await mindmeld.reinforcePattern({
303
+ await mindmeld.reinforcePattern(scrubSecretsDeep({
254
304
  pattern: result.pattern,
255
305
  sessionId: sessionId,
256
306
  userId: userId
257
- });
307
+ }));
258
308
  }
259
309
 
260
310
  // 5. Check for promotion candidates
@@ -274,7 +324,26 @@ async function harvestPatterns(sessionTranscript) {
274
324
  console.error('[MindMeld] Plan harvesting failed (non-fatal):', error.message);
275
325
  }
276
326
 
277
- // 7. Log results
327
+ // 7. Detect and send corrections from conversation text
328
+ let correctionsDetected = 0;
329
+ try {
330
+ const transcriptText = typeof sessionTranscript === 'string'
331
+ ? sessionTranscript
332
+ : sessionTranscript.transcript || JSON.stringify(sessionTranscript);
333
+
334
+ const corrections = detectCorrections(transcriptText);
335
+ correctionsDetected = corrections.length;
336
+
337
+ if (corrections.length > 0) {
338
+ console.error(`[MindMeld] Detected ${corrections.length} correction(s) in session`);
339
+ await sendCorrections(corrections, authToken, apiConfig);
340
+ console.error(`[MindMeld] Sent ${corrections.length} correction(s) to API`);
341
+ }
342
+ } catch (error) {
343
+ console.error('[MindMeld] Correction harvesting failed (non-fatal):', error.message);
344
+ }
345
+
346
+ // 8. Log results
278
347
  const elapsed = Date.now() - startTime;
279
348
  const summary = {
280
349
  patternsDetected: patterns.length,
@@ -282,6 +351,7 @@ async function harvestPatterns(sessionTranscript) {
282
351
  reinforced: validationResults.valid.length,
283
352
  promotionCandidates: candidates.length,
284
353
  plansHarvested: harvestedPlans.length,
354
+ correctionsDetected: correctionsDetected,
285
355
  plans: harvestedPlans,
286
356
  readmeStale: null,
287
357
  readmeUpdateRecommended: false,
@@ -436,10 +506,10 @@ function parsePlanFile(filename, content, stat) {
436
506
  sizeBytes: stat.size,
437
507
  lineCount: lines.length,
438
508
  sections: Object.keys(sections),
439
- context: sections['context'] || null,
509
+ context: scrubSecrets(sections['context'] || null),
440
510
  filesReferenced: fileRefs.slice(0, 20),
441
511
  projectHints: projectHints,
442
- content: content
512
+ content: scrubSecrets(content)
443
513
  };
444
514
  }
445
515
 
@@ -502,6 +572,109 @@ async function checkPromotionCandidates(mindmeld, validPatterns) {
502
572
  return candidates;
503
573
  }
504
574
 
575
+ /**
576
+ * Detect correction language patterns in conversation text.
577
+ * Scans for phrases indicating the user corrected the AI's approach.
578
+ * @param {string} conversationText - Raw conversation text to scan
579
+ * @returns {Array<{correction_text: string, context_before: string, context_after: string, pattern_matched: string}>}
580
+ */
581
+ function detectCorrections(conversationText) {
582
+ if (typeof conversationText !== 'string' || conversationText.length === 0) return [];
583
+
584
+ const correctionPatterns = [
585
+ /no,? don'?t/gi,
586
+ /that'?s wrong/gi,
587
+ /instead,? do/gi,
588
+ /not like that/gi,
589
+ /revert that/gi,
590
+ /undo that/gi,
591
+ /shouldn'?t have/gi,
592
+ /wrong approach/gi,
593
+ /bad pattern/gi,
594
+ /don'?t use/gi,
595
+ /never do that/gi,
596
+ /stop doing/gi,
597
+ ];
598
+
599
+ const corrections = [];
600
+
601
+ for (const pattern of correctionPatterns) {
602
+ let match;
603
+ while ((match = pattern.exec(conversationText)) !== null) {
604
+ const matchStart = match.index;
605
+ const matchEnd = matchStart + match[0].length;
606
+
607
+ const contextStart = Math.max(0, matchStart - 100);
608
+ const contextEnd = Math.min(conversationText.length, matchEnd + 100);
609
+
610
+ corrections.push({
611
+ correction_text: match[0],
612
+ context_before: conversationText.slice(contextStart, matchStart),
613
+ context_after: conversationText.slice(matchEnd, contextEnd),
614
+ pattern_matched: pattern.source,
615
+ });
616
+ }
617
+ }
618
+
619
+ return corrections;
620
+ }
621
+
622
+ /**
623
+ * Send detected corrections to the MindMeld API.
624
+ * @param {Array} corrections - Array of correction objects from detectCorrections
625
+ * @param {string} authToken - Auth token for API calls
626
+ * @param {{apiUrl: string}} apiConfig - API configuration
627
+ * @returns {Promise<void>}
628
+ */
629
+ async function sendCorrections(corrections, authToken, apiConfig) {
630
+ if (!corrections || corrections.length === 0) return;
631
+
632
+ const url = `${apiConfig.apiUrl}/corrections`;
633
+ const body = JSON.stringify({
634
+ corrections: scrubSecretsDeep(corrections),
635
+ source: 'hook-harvest',
636
+ });
637
+
638
+ const https = require('https');
639
+ const http = require('http');
640
+ const parsedUrl = new URL(url);
641
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
642
+
643
+ const headers = {
644
+ 'Content-Type': 'application/json',
645
+ 'Content-Length': Buffer.byteLength(body),
646
+ };
647
+ if (authToken) {
648
+ headers['Authorization'] = `Bearer ${authToken}`;
649
+ }
650
+
651
+ return new Promise((resolve, reject) => {
652
+ const req = transport.request(
653
+ {
654
+ hostname: parsedUrl.hostname,
655
+ port: parsedUrl.port,
656
+ path: parsedUrl.pathname,
657
+ method: 'POST',
658
+ headers,
659
+ },
660
+ (res) => {
661
+ let data = '';
662
+ res.on('data', (chunk) => (data += chunk));
663
+ res.on('end', () => {
664
+ if (res.statusCode >= 200 && res.statusCode < 300) {
665
+ resolve(data);
666
+ } else {
667
+ reject(new Error(`Corrections API returned ${res.statusCode}: ${data}`));
668
+ }
669
+ });
670
+ }
671
+ );
672
+ req.on('error', reject);
673
+ req.write(body);
674
+ req.end();
675
+ });
676
+ }
677
+
505
678
  /**
506
679
  * Generate session ID using crypto for consistency
507
680
  */
@@ -650,20 +823,95 @@ async function generatePostCompactContext(summary, llmAnalysis) {
650
823
  return sections.join('\n');
651
824
  }
652
825
 
653
- // Execute if called directly
654
- if (require.main === module) {
655
- // Read session transcript from stdin or args
656
- const input = process.argv[2];
826
+ /**
827
+ * Read stdin with timeout.
828
+ * Claude Code hooks receive JSON input via stdin, not command-line arguments.
829
+ * @returns {Promise<string>} stdin content or empty string
830
+ */
831
+ function readStdin() {
832
+ return new Promise((resolve) => {
833
+ let data = '';
834
+ const timeout = setTimeout(() => resolve(data), 2000);
835
+
836
+ if (process.stdin.isTTY) {
837
+ clearTimeout(timeout);
838
+ resolve('');
839
+ return;
840
+ }
841
+
842
+ process.stdin.setEncoding('utf-8');
843
+ process.stdin.on('data', chunk => { data += chunk; });
844
+ process.stdin.on('end', () => {
845
+ clearTimeout(timeout);
846
+ resolve(data);
847
+ });
848
+ process.stdin.on('error', () => {
849
+ clearTimeout(timeout);
850
+ resolve('');
851
+ });
852
+ process.stdin.resume();
853
+ });
854
+ }
855
+
856
+ /**
857
+ * Read the session transcript from the best available source.
858
+ * Priority:
859
+ * 1. stdin JSON with transcript_path (Claude Code hooks protocol)
860
+ * 2. process.argv[2] (legacy / manual testing, limited by OS ARG_MAX)
861
+ * @returns {Promise<Object>} Parsed session transcript
862
+ */
863
+ async function readTranscriptInput() {
864
+ // 1. Try stdin — Claude Code hooks pass JSON with transcript_path
865
+ try {
866
+ const stdin = await readStdin();
867
+ if (stdin) {
868
+ const hookInput = JSON.parse(stdin);
869
+
870
+ // If we got a transcript_path, read the file (no ARG_MAX limit)
871
+ if (hookInput.transcript_path) {
872
+ console.error(`[MindMeld] Reading transcript from file: ${hookInput.transcript_path}`);
873
+ const content = await fs.readFile(hookInput.transcript_path, 'utf-8');
874
+ const transcript = parseSessionTranscript(content);
875
+ // Merge hook metadata (session_id, cwd) into transcript
876
+ if (hookInput.session_id && !transcript.sessionId) {
877
+ transcript.sessionId = hookInput.session_id;
878
+ }
879
+ return transcript;
880
+ }
881
+
882
+ // If stdin itself contains the transcript (inline JSON)
883
+ if (hookInput.transcript || hookInput.messages || hookInput.sessionId) {
884
+ return parseSessionTranscript(JSON.stringify(hookInput));
885
+ }
657
886
 
658
- if (!input) {
659
- console.error('[MindMeld] Usage: pre-compact.js <session-transcript-json>');
660
- process.exit(0);
887
+ // Stdin had JSON but no transcript — try parsing it as-is
888
+ return parseSessionTranscript(stdin);
889
+ }
890
+ } catch (e) {
891
+ console.error(`[MindMeld] stdin parse failed (falling back to argv): ${e.message}`);
892
+ }
893
+
894
+ // 2. Fallback: process.argv[2] (legacy, limited to ~1MB on macOS)
895
+ const argInput = process.argv[2];
896
+ if (argInput) {
897
+ console.error('[MindMeld] Reading transcript from argv (legacy mode)');
898
+ return parseSessionTranscript(argInput);
661
899
  }
662
900
 
663
- const sessionTranscript = parseSessionTranscript(input);
901
+ return null;
902
+ }
903
+
904
+ // Execute if called directly
905
+ if (require.main === module) {
906
+ readTranscriptInput()
907
+ .then(async (sessionTranscript) => {
908
+ if (!sessionTranscript) {
909
+ console.error('[MindMeld] No transcript input received (stdin or argv)');
910
+ process.exit(0);
911
+ }
912
+
913
+ const result = await harvestPatterns(sessionTranscript);
664
914
 
665
- harvestPatterns(sessionTranscript)
666
- .then(async (result) => {
667
915
  // Generate and output context summary for post-compaction injection
668
916
  const postCompactContext = await generatePostCompactContext(
669
917
  result,
@@ -680,4 +928,4 @@ if (require.main === module) {
680
928
  });
681
929
  }
682
930
 
683
- module.exports = { harvestPatterns, harvestPlans, parseSessionTranscript, generatePostCompactContext };
931
+ module.exports = { harvestPatterns, harvestPlans, parseSessionTranscript, generatePostCompactContext, scrubSecrets, scrubSecretsDeep, detectCorrections, sendCorrections, readStdin, readTranscriptInput };
@@ -427,7 +427,7 @@ async function loadAuthToken() {
427
427
  * @returns {Promise<Object>} Cognito constructor options or empty object (uses prod defaults)
428
428
  */
429
429
  async function loadCognitoConfig() {
430
- // 1. Check .mindmeld/config.json first (always points to prod)
430
+ // 1. Check .mindmeld/config.json first (explicit project config, always wins)
431
431
  try {
432
432
  const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
433
433
  const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
@@ -442,21 +442,28 @@ async function loadCognitoConfig() {
442
442
  // No .mindmeld/config.json or no auth section
443
443
  }
444
444
 
445
- // 2. Fallback to .myworld.json (may point to dev — for backwards compatibility)
446
- try {
447
- const configPath = path.join(process.cwd(), '.myworld.json');
448
- const content = await fs.readFile(configPath, 'utf-8');
449
- const config = JSON.parse(content);
450
- const auth = config.deployments?.backend?.auth;
451
- if (auth?.domain && auth?.client_id) {
452
- return {
453
- cognitoDomain: `${auth.domain}.auth.us-east-2.amazoncognito.com`,
454
- cognitoClientId: auth.client_id
455
- };
445
+ // 2. Fallback to .myworld.json but only for consumer projects.
446
+ // If this IS the MindMeld source repo, .myworld.json has dev Cognito
447
+ // config that would authenticate against the wrong user pool.
448
+ const isSourceRepo = await isMindMeldSourceRepo();
449
+ if (!isSourceRepo) {
450
+ try {
451
+ const configPath = path.join(process.cwd(), '.myworld.json');
452
+ const content = await fs.readFile(configPath, 'utf-8');
453
+ const config = JSON.parse(content);
454
+ const auth = config.deployments?.backend?.auth;
455
+ if (auth?.domain && auth?.client_id) {
456
+ return {
457
+ cognitoDomain: `${auth.domain}.auth.us-east-2.amazoncognito.com`,
458
+ cognitoClientId: auth.client_id
459
+ };
460
+ }
461
+ } catch (error) {
462
+ // No .myworld.json
456
463
  }
457
- } catch (error) {
458
- // No .myworld.json — use production defaults
459
464
  }
465
+
466
+ // 3. Production defaults (AuthManager has these built-in)
460
467
  return {};
461
468
  }
462
469
 
@@ -483,15 +490,40 @@ function spawnBackgroundLogin() {
483
490
  }
484
491
  }
485
492
 
493
+ /**
494
+ * Detect if we're running inside the MindMeld source repository (developer context)
495
+ * vs. a consumer project that uses MindMeld for standards injection.
496
+ *
497
+ * When developing MindMeld itself, .myworld.json contains deployment config pointing
498
+ * at dev/staging environments. The hook should still use production for its own
499
+ * standards and auth — dev URLs are for the product, not for the hook's API calls.
500
+ *
501
+ * @returns {Promise<boolean>} True if this is the MindMeld source repo
502
+ */
503
+ async function isMindMeldSourceRepo() {
504
+ try {
505
+ const configPath = path.join(process.cwd(), '.myworld.json');
506
+ const content = await fs.readFile(configPath, 'utf-8');
507
+ const config = JSON.parse(content);
508
+ const productName = (config.project?.product || '').toLowerCase();
509
+ return productName === 'mindmeld';
510
+ } catch (error) {
511
+ return false;
512
+ }
513
+ }
514
+
486
515
  /**
487
516
  * Load API configuration
488
- * Priority: .mindmeld/config.json → .myworld.json → env var → production default
489
- * The hook should always talk to the production MindMeld API for standards/patterns,
490
- * regardless of which environment the developer is building against.
517
+ * Priority: .mindmeld/config.json → .myworld.json (consumer only) → env var → production default
518
+ *
519
+ * When running inside the MindMeld source repo, .myworld.json is skipped because it
520
+ * contains dev/staging deployment config for the product — not config for the hook's
521
+ * own API calls, which should always target production.
522
+ *
491
523
  * @returns {Promise<{apiUrl: string}>}
492
524
  */
493
525
  async function loadApiConfig() {
494
- // 1. Check .mindmeld/config.json first (always points to prod)
526
+ // 1. Check .mindmeld/config.json first (explicit project config, always wins)
495
527
  try {
496
528
  const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
497
529
  const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
@@ -503,20 +535,28 @@ async function loadApiConfig() {
503
535
  // No .mindmeld/config.json or no apiUrl
504
536
  }
505
537
 
506
- // 2. Fallback to .myworld.json (may point to dev)
507
- try {
508
- const configPath = path.join(process.cwd(), '.myworld.json');
509
- const content = await fs.readFile(configPath, 'utf-8');
510
- const config = JSON.parse(content);
511
- const backend = config.deployments?.backend;
512
- return {
513
- apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev'
514
- };
515
- } catch (error) {
516
- return {
517
- apiUrl: process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev'
518
- };
538
+ // 2. Fallback to .myworld.json but only for consumer projects.
539
+ // If this IS the MindMeld source repo, .myworld.json has dev deployment
540
+ // URLs that would send hook API calls to the wrong environment.
541
+ const isSourceRepo = await isMindMeldSourceRepo();
542
+ if (!isSourceRepo) {
543
+ try {
544
+ const configPath = path.join(process.cwd(), '.myworld.json');
545
+ const content = await fs.readFile(configPath, 'utf-8');
546
+ const config = JSON.parse(content);
547
+ const backend = config.deployments?.backend;
548
+ if (backend?.api?.base_url) {
549
+ return { apiUrl: backend.api.base_url };
550
+ }
551
+ } catch (error) {
552
+ // No .myworld.json
553
+ }
519
554
  }
555
+
556
+ // 3. Environment variable or production default
557
+ return {
558
+ apiUrl: process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev'
559
+ };
520
560
  }
521
561
 
522
562
  /**
@@ -565,7 +605,9 @@ async function fetchRelevantStandardsFromAPI(apiUrl, authToken, characteristics,
565
605
  try {
566
606
  const parsed = JSON.parse(data);
567
607
  if (res.statusCode >= 400) {
568
- reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
608
+ const err = new Error(parsed.message || `HTTP ${res.statusCode}`);
609
+ err.statusCode = res.statusCode;
610
+ reject(err);
569
611
  } else {
570
612
  resolve(parsed.data?.standards || parsed.standards || []);
571
613
  }
@@ -767,6 +809,14 @@ async function injectContext() {
767
809
  // Subscription enforcement — do NOT fall through to file-based injection
768
810
  console.error('[MindMeld] Active subscription required. Subscribe at app.mindmeld.dev');
769
811
  return '';
812
+ } else if (standardsResult.status === 'rejected' &&
813
+ (standardsResult.reason.statusCode === 401 || standardsResult.reason.message === 'Unauthorized')) {
814
+ // Auth token expired or invalid — trigger re-auth and use file-based fallback
815
+ console.error('[MindMeld] Auth token expired. Triggering re-authentication...');
816
+ spawnBackgroundLogin();
817
+ const categories = mindmeld.relevanceDetector.mapCharacteristicsToCategories(characteristics);
818
+ relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories, characteristics);
819
+ console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback (scored)`);
770
820
  } else {
771
821
  if (standardsResult.status === 'rejected') {
772
822
  console.error(`[MindMeld] API fallback: ${standardsResult.reason.message}`);
@@ -893,6 +943,20 @@ async function cacheSubagentContext(standards, teamPatterns, projectName) {
893
943
  console.error(`[MindMeld] Cached subagent context (${sections.length} lines)`);
894
944
  }
895
945
 
946
+ /**
947
+ * Get maturity tier prefix for a standard
948
+ * @param {string|undefined} maturityTier - The maturity_tier from the standard
949
+ * @returns {string} Prefix string based on tier
950
+ */
951
+ function getMaturityPrefix(maturityTier) {
952
+ switch (maturityTier) {
953
+ case 'reinforced': return '**[MUST FOLLOW]** ';
954
+ case 'solidified': return '**[SHOULD FOLLOW]** ';
955
+ case 'provisional':
956
+ default: return '**[CONSIDER]** ';
957
+ }
958
+ }
959
+
896
960
  /**
897
961
  * Format context injection for Claude Code
898
962
  * @param {object} data - Context data to inject
@@ -968,8 +1032,15 @@ function formatContextInjection(data) {
968
1032
  const standards = [];
969
1033
  const workflows = [];
970
1034
 
1035
+ const businessInvariants = [];
971
1036
  if (relevantStandards && relevantStandards.length > 0) {
972
1037
  for (const item of relevantStandards) {
1038
+ // Separate business invariants from code standards
1039
+ if (item.content_type === 'business_invariant') {
1040
+ businessInvariants.push(item);
1041
+ continue;
1042
+ }
1043
+
973
1044
  // Detect workflows: check type flag, rule prefix, keywords, or structured examples
974
1045
  const isWorkflow = item.type === 'workflow' ||
975
1046
  (item.rule && item.rule.startsWith('WORKFLOW:')) ||
@@ -990,7 +1061,9 @@ function formatContextInjection(data) {
990
1061
  sections.push('');
991
1062
 
992
1063
  for (const standard of standards) {
993
- sections.push(`### ${standard.element}`);
1064
+ const tierPrefix = getMaturityPrefix(standard.maturity_tier);
1065
+ const lbPrefix = standard.load_bearing ? '[CRITICAL] ' : '';
1066
+ sections.push(`### ${tierPrefix}${lbPrefix}${standard.element}`);
994
1067
  sections.push(`**Category**: ${standard.category}`);
995
1068
  // Add fingerprint to rule text
996
1069
  sections.push(`**Rule**: ${standard.rule} ${fingerprintStr}`);
@@ -1018,6 +1091,11 @@ function formatContextInjection(data) {
1018
1091
  }
1019
1092
  }
1020
1093
 
1094
+ if (standard.consequence_tier === 'IRREVERSIBLE' || standard.consequence_tier === 'EXTERNAL_SIDE_EFFECT') {
1095
+ sections.push('');
1096
+ sections.push('⚠️ CONSEQUENCE: This standard involves irreversible/external actions. Verify before proceeding.');
1097
+ }
1098
+
1021
1099
  sections.push('');
1022
1100
  }
1023
1101
  }
@@ -1033,7 +1111,8 @@ function formatContextInjection(data) {
1033
1111
  ? workflow.examples[0]
1034
1112
  : null;
1035
1113
 
1036
- sections.push(`### ${workflow.element} ${fingerprintStr}`);
1114
+ const tierPrefix = getMaturityPrefix(workflow.maturity_tier);
1115
+ sections.push(`### ${tierPrefix}${workflow.element} ${fingerprintStr}`);
1037
1116
  sections.push(`**Category**: ${workflow.category}`);
1038
1117
 
1039
1118
  if (workflowData) {
@@ -1085,6 +1164,32 @@ function formatContextInjection(data) {
1085
1164
  }
1086
1165
  }
1087
1166
 
1167
+ // Business invariants (rendered with rationale and consequences)
1168
+ if (businessInvariants.length > 0) {
1169
+ sections.push('## Business Invariants');
1170
+ sections.push('');
1171
+
1172
+ for (const invariant of businessInvariants) {
1173
+ const tierPrefix = getMaturityPrefix(invariant.maturity_tier);
1174
+ sections.push(`### ${tierPrefix}${invariant.element}`);
1175
+ sections.push(`**Domain**: ${invariant.category}`);
1176
+ sections.push(`**Invariant**: ${invariant.rule} ${fingerprintStr}`);
1177
+ if (invariant.rationale) {
1178
+ sections.push(`**Rationale**: ${invariant.rationale}`);
1179
+ }
1180
+ if (invariant.consequences) {
1181
+ sections.push(`**If violated**: ${invariant.consequences}`);
1182
+ }
1183
+ if (invariant.exceptions && Array.isArray(invariant.exceptions) && invariant.exceptions.length > 0) {
1184
+ sections.push('**Exceptions**:');
1185
+ for (const ex of invariant.exceptions) {
1186
+ sections.push(`- ${ex}`);
1187
+ }
1188
+ }
1189
+ sections.push('');
1190
+ }
1191
+ }
1192
+
1088
1193
  // Team patterns (high correlation only)
1089
1194
  if (teamPatterns && teamPatterns.length > 0) {
1090
1195
  sections.push('## Team Patterns');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equilateral_ai/mindmeld",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -81,6 +81,7 @@
81
81
  "dependencies": {
82
82
  "@aws-sdk/client-bedrock-runtime": "^3.460.0",
83
83
  "@aws-sdk/client-ssm": "^3.985.0",
84
+ "js-yaml": "^4.1.1",
84
85
  "pg": "^8.18.0"
85
86
  },
86
87
  "devDependencies": {