phewsh 0.11.15 → 0.11.16

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/bin/phewsh.js CHANGED
@@ -63,6 +63,8 @@ const COMMANDS = {
63
63
  watch: () => require('../commands/watch')(),
64
64
  mcp: () => require('../commands/mcp')(),
65
65
  serve: () => require('../commands/serve')(),
66
+ sequence: () => require('../commands/sequence')(),
67
+ seq: () => require('../commands/sequence')(),
66
68
  help: showHelp,
67
69
  version: showVersion,
68
70
  };
@@ -88,6 +90,7 @@ function showHelp() {
88
90
  console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}`);
89
91
  console.log('');
90
92
  console.log(` ${b(w('sync everywhere'))}`);
93
+ console.log(` ${cyan('seq')} ${g('Sequence all memory → optimal context for any agent')}`);
91
94
  console.log(` ${cyan('watch')} ${g('Auto-sync .intent/ → CLAUDE.md + cloud')}`);
92
95
  console.log(` ${cyan('push/pull')} ${g('Manual sync to/from phewsh.com/intent')}`);
93
96
  console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/intent')}`);
@@ -0,0 +1,136 @@
1
+ // phewsh sequence (phewsh seq)
2
+ // Universal Memory Transform — reads all AI memory files, emits optimal context.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { sequence } = require('../lib/sequencer');
7
+ const ui = require('../lib/ui');
8
+
9
+ const { b, w, sage, slate, teal, cream, green, ember } = ui;
10
+
11
+ const args = process.argv.slice(3);
12
+
13
+ const flags = {
14
+ target: getFlag('--target', '-t') || getPositionalTarget(),
15
+ budget: getFlag('--budget', '-b') || 'standard',
16
+ explain: args.includes('--explain') || args.includes('-e'),
17
+ write: args.includes('--write') || args.includes('-w'),
18
+ dryRun: args.includes('--dry-run'),
19
+ all: args.includes('--all'),
20
+ sources: getFlag('--sources', '-s'),
21
+ help: args.includes('--help') || args.includes('-h'),
22
+ };
23
+
24
+ function getFlag(long, short) {
25
+ for (let i = 0; i < args.length; i++) {
26
+ if ((args[i] === long || args[i] === short) && args[i + 1]) {
27
+ return args[i + 1];
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function getPositionalTarget() {
34
+ // phewsh seq claude → target claude-md
35
+ // phewsh seq cursor → target cursorrules
36
+ const aliases = {
37
+ claude: 'claude-md',
38
+ cursor: 'cursorrules',
39
+ agent: 'agent-md',
40
+ soul: 'soul-md',
41
+ json: 'json',
42
+ };
43
+ const first = args.find(a => !a.startsWith('-'));
44
+ return aliases[first] || null;
45
+ }
46
+
47
+ function showHelp() {
48
+ console.log('');
49
+ console.log(` ${b(cream('phewsh sequence'))} ${slate('(phewsh seq)')}`);
50
+ console.log(` ${sage('Universal Memory Transform — reads all AI memory files,')}`);
51
+ console.log(` ${sage('emits optimal context for any target agent.')}`);
52
+ console.log('');
53
+ console.log(` ${cream('usage')}`);
54
+ console.log(` ${teal('phewsh seq')} ${sage('Sequence → stdout summary')}`);
55
+ console.log(` ${teal('phewsh seq')} ${slate('claude')} ${sage('Sequence → CLAUDE.md section')}`);
56
+ console.log(` ${teal('phewsh seq')} ${slate('-w')} ${sage('Write to target file')}`);
57
+ console.log(` ${teal('phewsh seq')} ${slate('--explain')} ${sage('Show full ranking breakdown')}`);
58
+ console.log(` ${teal('phewsh seq')} ${slate('--dry-run')} ${sage('Show sources found, no output')}`);
59
+ console.log('');
60
+ console.log(` ${cream('targets')}`);
61
+ console.log(` ${teal('claude')} ${sage('CLAUDE.md section (between markers)')}`);
62
+ console.log('');
63
+ console.log(` ${cream('options')}`);
64
+ console.log(` ${teal('--budget')} ${slate('<level>')} ${sage('Token budget: minimal|standard|full|unlimited')}`);
65
+ console.log(` ${teal('--sources')} ${slate('<list>')} ${sage('Limit sources: intent,claude-md,claude-memory')}`);
66
+ console.log(` ${teal('--write, -w')} ${sage('Write output to target file')}`);
67
+ console.log(` ${teal('--explain, -e')} ${sage('Full ranking breakdown')}`);
68
+ console.log(` ${teal('--dry-run')} ${sage('Discover sources only')}`);
69
+ console.log('');
70
+ }
71
+
72
+ async function main() {
73
+ if (flags.help) { showHelp(); return; }
74
+
75
+ const sourceFilter = flags.sources ? flags.sources.split(',') : null;
76
+
77
+ // Dry run: just show what was found
78
+ if (flags.dryRun) {
79
+ const { discover } = require('../lib/sequencer/discover');
80
+ let sources = discover();
81
+ if (sourceFilter) {
82
+ sources = sources.filter(s => sourceFilter.some(f => s.type === f || s.type.startsWith(f)));
83
+ }
84
+
85
+ console.log('');
86
+ console.log(` ${b(cream('Sources discovered'))} ${slate(`(${sources.length})`)}`);
87
+ ui.divider('line');
88
+ if (sources.length === 0) {
89
+ console.log(` ${sage('No recognized memory files found in')} ${slate(process.cwd())}`);
90
+ } else {
91
+ for (const source of sources) {
92
+ console.log(` ${teal(source.type.padEnd(20))} ${sage(source.name)}`);
93
+ }
94
+ }
95
+ ui.divider('line');
96
+ console.log('');
97
+ return;
98
+ }
99
+
100
+ // Default behavior: no target = stdout summary
101
+ const target = flags.target || 'stdout';
102
+
103
+ try {
104
+ const result = sequence({
105
+ target,
106
+ budget: flags.budget,
107
+ sources: sourceFilter,
108
+ explain: flags.explain,
109
+ write: flags.write,
110
+ });
111
+
112
+ // If target is stdout, emit already printed
113
+ if (target === 'stdout') return;
114
+
115
+ // If writing to file
116
+ if (flags.write) {
117
+ if (result.writeResult === 'updated') {
118
+ console.log(`\n ${green('\u2713')} ${sage('Updated CLAUDE.md')}`);
119
+ } else if (result.writeResult === 'created') {
120
+ console.log(`\n ${green('\u2713')} ${sage('Created CLAUDE.md')}`);
121
+ }
122
+ console.log(` ${slate(`${result.chunks.length} chunks from ${result.sources.length} sources`)}`);
123
+ console.log('');
124
+ return;
125
+ }
126
+
127
+ // Otherwise print the output
128
+ console.log(result.output);
129
+
130
+ } catch (err) {
131
+ console.error(`\n ${ember('!')} ${sage(err.message)}\n`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+
136
+ module.exports = main;
@@ -307,6 +307,8 @@ async function main() {
307
307
  console.log(` ${teal('/reload')} ${sage('Reload .intent/ from disk')}`);
308
308
  console.log('');
309
309
  console.log(` ${cream('sync everywhere')}`);
310
+ console.log(` ${teal('/seq')} ${sage('Sequence all memory → optimal context')}`);
311
+ console.log(` ${teal('/seq claude')} ${sage('Sequence → write to CLAUDE.md')}`);
310
312
  console.log(` ${teal('/watch')} ${sage('Sync .intent/ → CLAUDE.md + cloud (background)')}`);
311
313
  console.log(` ${teal('/export')} ${sage('Export .intent/ for any AI tool')}`);
312
314
  console.log(` ${teal('/push')} ${sage('Push to phewsh.com/intent')}`);
@@ -468,6 +470,29 @@ async function main() {
468
470
  return;
469
471
  }
470
472
 
473
+ if (cmd === 'seq' || cmd === 'sequence') {
474
+ try {
475
+ const { sequence } = require('../lib/sequencer');
476
+ const target = cmdArg?.split(/\s+/)[0];
477
+ const explain = cmdArg?.includes('explain') || cmdArg?.includes('-e');
478
+ const write = cmdArg?.includes('-w') || cmdArg?.includes('write');
479
+
480
+ if (target === 'claude' || target === 'claude-md') {
481
+ const result = sequence({ target: 'claude-md', write: true });
482
+ if (result.writeResult) {
483
+ console.log(`\n ${teal('●')} ${sage('CLAUDE.md ' + result.writeResult)} ${slate('(' + result.chunks.length + ' chunks)')}\n`);
484
+ }
485
+ } else {
486
+ // Default: stdout summary
487
+ sequence({ target: 'stdout', explain });
488
+ }
489
+ } catch (err) {
490
+ console.error(` ${ember('!')} ${sage('Sequence failed:')} ${err.message}`);
491
+ }
492
+ rl.prompt();
493
+ return;
494
+ }
495
+
471
496
  if (cmd === 'export') {
472
497
  try {
473
498
  const { generateContext } = require('./context');
@@ -0,0 +1,51 @@
1
+ // Compress ranked chunks to fit a target token budget.
2
+ // Rough estimate: 1 token ~= 4 chars (conservative for markdown).
3
+
4
+ const TOKEN_BUDGETS = {
5
+ minimal: 500,
6
+ standard: 2000,
7
+ full: 5000,
8
+ unlimited: Infinity,
9
+ };
10
+
11
+ function estimateTokens(text) {
12
+ return Math.ceil(text.length / 4);
13
+ }
14
+
15
+ function compress(rankedChunks, budget = 'standard') {
16
+ const maxTokens = TOKEN_BUDGETS[budget] || TOKEN_BUDGETS.standard;
17
+ if (maxTokens === Infinity) return rankedChunks;
18
+
19
+ const result = [];
20
+ let totalTokens = 0;
21
+
22
+ for (const chunk of rankedChunks) {
23
+ const tokens = estimateTokens(chunk.content);
24
+
25
+ if (totalTokens + tokens <= maxTokens) {
26
+ result.push(chunk);
27
+ totalTokens += tokens;
28
+ } else {
29
+ // Partial inclusion: if chunk is large but high-weight, take first lines
30
+ const remainingTokens = maxTokens - totalTokens;
31
+ if (remainingTokens > 50 && chunk.weight > 0.3) {
32
+ const charBudget = remainingTokens * 4;
33
+ const lines = chunk.content.split('\n');
34
+ let partial = '';
35
+ for (const line of lines) {
36
+ if (partial.length + line.length + 1 > charBudget) break;
37
+ partial += line + '\n';
38
+ }
39
+ if (partial.trim()) {
40
+ result.push({ ...chunk, content: partial.trim(), _truncated: true });
41
+ totalTokens += estimateTokens(partial);
42
+ }
43
+ }
44
+ break; // Budget hit
45
+ }
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ module.exports = { compress, estimateTokens, TOKEN_BUDGETS };
@@ -0,0 +1,97 @@
1
+ // Discover all memory/context source files in the working directory.
2
+ // Returns a list of { path, type } for each recognized source.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const INTENT_FILES = ['vision.md', 'plan.md', 'status.md', 'narrative.md', 'next.md'];
9
+ const INTENT_JSON = ['project.json', 'pps.json', 'gate.json'];
10
+
11
+ function discover(cwd = process.cwd()) {
12
+ const sources = [];
13
+
14
+ // .intent/ artifacts
15
+ const intentDir = path.join(cwd, '.intent');
16
+ if (fs.existsSync(intentDir)) {
17
+ for (const file of [...INTENT_FILES, ...INTENT_JSON]) {
18
+ const p = path.join(intentDir, file);
19
+ if (fs.existsSync(p)) {
20
+ sources.push({ path: p, type: 'intent', name: file });
21
+ }
22
+ }
23
+ }
24
+
25
+ // CLAUDE.md — split into manual and generated sections later by parser
26
+ const claudeMd = path.join(cwd, 'CLAUDE.md');
27
+ if (fs.existsSync(claudeMd)) {
28
+ sources.push({ path: claudeMd, type: 'claude-md', name: 'CLAUDE.md' });
29
+ }
30
+
31
+ // Claude auto-memory — project-scoped
32
+ // .claude/projects/<encoded-cwd>/memory/MEMORY.md
33
+ const claudeDir = path.join(os.homedir(), '.claude');
34
+ if (fs.existsSync(claudeDir)) {
35
+ const projectsDir = path.join(claudeDir, 'projects');
36
+ if (fs.existsSync(projectsDir)) {
37
+ // Claude encodes cwd as path with - replacing /
38
+ const encoded = cwd.replace(/\//g, '-');
39
+ const memoryDir = path.join(projectsDir, encoded, 'memory');
40
+ if (fs.existsSync(memoryDir)) {
41
+ const memoryIndex = path.join(memoryDir, 'MEMORY.md');
42
+ if (fs.existsSync(memoryIndex)) {
43
+ sources.push({ path: memoryIndex, type: 'claude-memory', name: 'MEMORY.md' });
44
+
45
+ // Also discover linked memory files from MEMORY.md
46
+ try {
47
+ const content = fs.readFileSync(memoryIndex, 'utf-8');
48
+ const linkRegex = /\[([^\]]+\.md)\]\(([^)]+\.md)\)/g;
49
+ let match;
50
+ while ((match = linkRegex.exec(content)) !== null) {
51
+ const linked = path.join(memoryDir, match[2]);
52
+ if (fs.existsSync(linked)) {
53
+ sources.push({ path: linked, type: 'claude-memory-file', name: match[2] });
54
+ }
55
+ }
56
+ } catch { /* skip */ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ // .cursorrules
63
+ const cursorrules = path.join(cwd, '.cursorrules');
64
+ if (fs.existsSync(cursorrules)) {
65
+ sources.push({ path: cursorrules, type: 'cursor', name: '.cursorrules' });
66
+ }
67
+
68
+ // agent.md / AGENTS.md
69
+ for (const name of ['agent.md', 'AGENTS.md']) {
70
+ const p = path.join(cwd, name);
71
+ if (fs.existsSync(p)) {
72
+ sources.push({ path: p, type: 'agent', name });
73
+ }
74
+ }
75
+
76
+ // soul.md
77
+ const soulMd = path.join(cwd, 'soul.md');
78
+ if (fs.existsSync(soulMd)) {
79
+ sources.push({ path: soulMd, type: 'soul', name: 'soul.md' });
80
+ }
81
+
82
+ // .github/copilot-instructions.md
83
+ const copilot = path.join(cwd, '.github', 'copilot-instructions.md');
84
+ if (fs.existsSync(copilot)) {
85
+ sources.push({ path: copilot, type: 'copilot', name: 'copilot-instructions.md' });
86
+ }
87
+
88
+ // README.md (low priority but useful for identity)
89
+ const readme = path.join(cwd, 'README.md');
90
+ if (fs.existsSync(readme)) {
91
+ sources.push({ path: readme, type: 'readme', name: 'README.md' });
92
+ }
93
+
94
+ return sources;
95
+ }
96
+
97
+ module.exports = { discover };
@@ -0,0 +1,173 @@
1
+ // Emit a CLAUDE.md section from ranked, compressed chunks.
2
+ // This is the primary "continuity" output — what makes Claude Code
3
+ // instantly aware of everything across all sources.
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const START_MARKER = '<!-- PHEWSH:START -->';
9
+ const END_MARKER = '<!-- PHEWSH:END -->';
10
+
11
+ function emit(chunks, options = {}) {
12
+ const projectName = options.projectName || path.basename(process.cwd());
13
+ const sections = [];
14
+
15
+ sections.push(`# PHEWSH Adaptive Context — ${projectName}`);
16
+ sections.push(`> Auto-synced by \`phewsh seq\` | ${new Date().toISOString().split('T')[0]}`);
17
+ sections.push(`> This section is regenerated from ${countSources(chunks)} sources. Do not edit manually.`);
18
+ sections.push('');
19
+
20
+ // Group chunks by kind for structured output
21
+ const byKind = groupByKind(chunks);
22
+
23
+ // 1. Identity — what this project is
24
+ if (byKind.identity?.length > 0) {
25
+ sections.push('## Project');
26
+ for (const chunk of byKind.identity) {
27
+ sections.push(chunk.content);
28
+ }
29
+ sections.push('');
30
+ }
31
+
32
+ // 2. Constraints — operational reality
33
+ if (byKind.constraint?.length > 0) {
34
+ sections.push('## Operational Reality');
35
+ sections.push('These constraints MUST shape every suggestion and implementation decision:');
36
+ sections.push('');
37
+ for (const chunk of byKind.constraint) {
38
+ // If this has structured constraint metadata, emit rich format
39
+ if (chunk.metadata?.constraints) {
40
+ sections.push(formatConstraints(chunk.metadata.constraints));
41
+ } else {
42
+ sections.push(chunk.content);
43
+ }
44
+ }
45
+ sections.push('');
46
+ }
47
+
48
+ // 3. State — what's happening now
49
+ if (byKind.state?.length > 0) {
50
+ sections.push('## Current State');
51
+ for (const chunk of byKind.state) {
52
+ sections.push(chunk.content);
53
+ }
54
+ sections.push('');
55
+ }
56
+
57
+ // 4. Feedback — learned behaviors
58
+ if (byKind.feedback?.length > 0) {
59
+ sections.push('## Behavioral Rules');
60
+ sections.push('Learned from prior work:');
61
+ sections.push('');
62
+ for (const chunk of byKind.feedback) {
63
+ // Memory files are already concise — include as-is
64
+ const label = chunk.metadata?.name || chunk.source;
65
+ sections.push(`**${label}**: ${chunk.content.split('\n')[0]}`);
66
+ // If multi-line, include the rest indented
67
+ const rest = chunk.content.split('\n').slice(1).filter(l => l.trim());
68
+ if (rest.length > 0) {
69
+ rest.forEach(l => sections.push(` ${l}`));
70
+ }
71
+ }
72
+ sections.push('');
73
+ }
74
+
75
+ // 5. Context — architecture, systems, plan
76
+ if (byKind.context?.length > 0) {
77
+ sections.push('## Context');
78
+ for (const chunk of byKind.context) {
79
+ // Skip generated PHEWSH sections (they're derivative)
80
+ if (chunk.metadata?.generated) continue;
81
+ sections.push(chunk.content);
82
+ }
83
+ sections.push('');
84
+ }
85
+
86
+ // 6. Actions — current tasks
87
+ if (byKind.action?.length > 0) {
88
+ sections.push('## Active Actions');
89
+ for (const chunk of byKind.action) {
90
+ sections.push(chunk.content);
91
+ }
92
+ sections.push('');
93
+ }
94
+
95
+ // 7. References — pointers to external systems
96
+ if (byKind.reference?.length > 0) {
97
+ sections.push('## References');
98
+ for (const chunk of byKind.reference) {
99
+ const label = chunk.metadata?.name || chunk.source;
100
+ sections.push(`- **${label}**: ${chunk.content.split('\n')[0]}`);
101
+ }
102
+ sections.push('');
103
+ }
104
+
105
+ sections.push('---');
106
+ sections.push('*Auto-synced from [PHEWSH](https://phewsh.com/intent)*');
107
+
108
+ return sections.join('\n');
109
+ }
110
+
111
+ function formatConstraints(c) {
112
+ const lines = [];
113
+
114
+ if (c.budget > 0) {
115
+ lines.push(`- Budget: $${c.budget}. ${
116
+ c.budget < 100 ? 'Extremely tight — free/open-source only.'
117
+ : c.budget < 500 ? 'Limited — justify any paid tool.'
118
+ : c.budget < 2000 ? 'Moderate — strategic spending OK.'
119
+ : 'Substantial — professional tools welcome.'
120
+ }`);
121
+ }
122
+ if (c.timeHoursPerWeek > 0) {
123
+ lines.push(`- Time: ${c.timeHoursPerWeek} hrs/week. ${
124
+ c.timeHoursPerWeek <= 5 ? 'Micro-steps only.'
125
+ : c.timeHoursPerWeek <= 15 ? 'Part-time. Clear stopping points.'
126
+ : c.timeHoursPerWeek <= 30 ? 'Near full-time.'
127
+ : 'Full-time+.'
128
+ }`);
129
+ }
130
+ if (c.skillLevel) lines.push(`- Skill: ${c.skillLevel}`);
131
+ if (c.urgency) lines.push(`- Urgency: ${c.urgency}`);
132
+ if (c.autonomy) lines.push(`- Autonomy: ${c.autonomy}`);
133
+
134
+ return lines.join('\n');
135
+ }
136
+
137
+ function groupByKind(chunks) {
138
+ const groups = {};
139
+ for (const chunk of chunks) {
140
+ if (!groups[chunk.kind]) groups[chunk.kind] = [];
141
+ groups[chunk.kind].push(chunk);
142
+ }
143
+ return groups;
144
+ }
145
+
146
+ function countSources(chunks) {
147
+ return new Set(chunks.map(c => c.source.split(':')[0])).size;
148
+ }
149
+
150
+ // Write the emitted content into CLAUDE.md (between markers)
151
+ function writeToFile(content, cwd = process.cwd()) {
152
+ const claudePath = path.join(cwd, 'CLAUDE.md');
153
+ const wrapped = `${START_MARKER}\n${content}\n${END_MARKER}`;
154
+
155
+ if (fs.existsSync(claudePath)) {
156
+ let existing = fs.readFileSync(claudePath, 'utf-8');
157
+ const startIdx = existing.indexOf(START_MARKER);
158
+ const endIdx = existing.indexOf(END_MARKER);
159
+
160
+ if (startIdx !== -1 && endIdx !== -1) {
161
+ existing = existing.slice(0, startIdx) + wrapped + existing.slice(endIdx + END_MARKER.length);
162
+ } else {
163
+ existing = existing.trimEnd() + '\n\n' + wrapped + '\n';
164
+ }
165
+ fs.writeFileSync(claudePath, existing);
166
+ return 'updated';
167
+ } else {
168
+ fs.writeFileSync(claudePath, wrapped + '\n');
169
+ return 'created';
170
+ }
171
+ }
172
+
173
+ module.exports = { emit, writeToFile };
@@ -0,0 +1,89 @@
1
+ // Human-readable terminal output of the sequencing result.
2
+ // Shows what was found, how it was ranked, and the final output.
3
+
4
+ const ui = require('../../ui');
5
+
6
+ function emit(chunks, options = {}) {
7
+ const { explain = false, sources = [] } = options;
8
+ const { b, w, g, sage, slate, teal, cream, green, yellow, ember, peach } = ui;
9
+
10
+ console.log('');
11
+
12
+ if (explain) {
13
+ return emitExplain(chunks, sources, ui);
14
+ }
15
+
16
+ // Compact summary
17
+ const sourceCount = new Set(chunks.map(c => c.source.split(':')[0])).size;
18
+ const kindCounts = {};
19
+ for (const chunk of chunks) {
20
+ kindCounts[chunk.kind] = (kindCounts[chunk.kind] || 0) + 1;
21
+ }
22
+
23
+ console.log(` ${b(cream('Sequenced'))} ${teal(String(chunks.length))} ${sage('chunks from')} ${teal(String(sourceCount))} ${sage('sources')}`);
24
+ ui.divider('line');
25
+
26
+ // Show kind breakdown
27
+ const kindOrder = ['constraint', 'identity', 'feedback', 'state', 'action', 'context', 'reference'];
28
+ for (const kind of kindOrder) {
29
+ if (!kindCounts[kind]) continue;
30
+ const icon = KIND_ICONS[kind] || '?';
31
+ console.log(` ${icon} ${cream(kind.padEnd(12))} ${slate(String(kindCounts[kind]))}`);
32
+ }
33
+
34
+ ui.divider('line');
35
+
36
+ // Show top 5 chunks by weight
37
+ console.log(` ${sage('top signal:')}`);
38
+ const top = chunks.slice(0, 5);
39
+ for (const chunk of top) {
40
+ const weight = chunk.weight.toFixed(2);
41
+ const preview = chunk.content.split('\n')[0].slice(0, 60);
42
+ console.log(` ${slate(weight)} ${teal(chunk.kind.padEnd(10))} ${sage(preview)}`);
43
+ }
44
+
45
+ // Token estimate
46
+ const totalChars = chunks.reduce((sum, c) => sum + c.content.length, 0);
47
+ const estTokens = Math.ceil(totalChars / 4);
48
+ console.log('');
49
+ console.log(` ${slate(`~${estTokens} tokens`)}`);
50
+ console.log('');
51
+ }
52
+
53
+ function emitExplain(chunks, sources, { b, w, g, sage, slate, teal, cream, green, yellow, ember }) {
54
+ // Full ranking breakdown
55
+ console.log(` ${b(cream('Sequencer Explain'))}`);
56
+ console.log('');
57
+
58
+ // Sources discovered
59
+ console.log(` ${b(cream('Sources'))}`);
60
+ for (const source of sources) {
61
+ console.log(` ${teal(source.type.padEnd(16))} ${sage(source.name)}`);
62
+ }
63
+ console.log('');
64
+
65
+ // All chunks with full weight breakdown
66
+ console.log(` ${b(cream('Chunks'))} ${slate(`(${chunks.length} total, ranked by weight)`)}`);
67
+ console.log('');
68
+
69
+ for (const chunk of chunks) {
70
+ const weight = chunk.weight.toFixed(3);
71
+ const preview = chunk.content.split('\n')[0].slice(0, 50);
72
+ const truncated = chunk._truncated ? ` ${ember('[truncated]')}` : '';
73
+ console.log(` ${cream(weight)} ${teal(chunk.kind.padEnd(12))} ${sage(chunk.source)}`);
74
+ console.log(` ${slate(preview)}${truncated}`);
75
+ }
76
+ console.log('');
77
+ }
78
+
79
+ const KIND_ICONS = {
80
+ constraint: '\u2588', // solid block — constraints are load-bearing
81
+ identity: '\u25C6', // diamond — core
82
+ feedback: '\u25B6', // triangle — directional
83
+ state: '\u25CF', // circle — current
84
+ action: '\u25A0', // square — tasks
85
+ context: '\u25CB', // open circle — background
86
+ reference: '\u25B7', // open triangle — pointer
87
+ };
88
+
89
+ module.exports = { emit };
@@ -0,0 +1,116 @@
1
+ // Sequencer — Universal Memory Transform Layer
2
+ //
3
+ // The core pipeline: discover → parse → rank → compress → emit
4
+ // One function. N inputs. 1 optimal output per target format.
5
+
6
+ const { discover } = require('./discover');
7
+ const { rank } = require('./ranker');
8
+ const { compress } = require('./compressor');
9
+
10
+ // Parsers by source type
11
+ const parsers = {
12
+ intent: require('./parsers/intent'),
13
+ 'claude-md': require('./parsers/claude-md'),
14
+ 'claude-memory': require('./parsers/claude-memory'),
15
+ 'claude-memory-file': require('./parsers/claude-memory'),
16
+ cursor: require('./parsers/generic'),
17
+ agent: require('./parsers/generic'),
18
+ soul: require('./parsers/generic'),
19
+ copilot: require('./parsers/generic'),
20
+ readme: require('./parsers/generic'),
21
+ };
22
+
23
+ // Emitters by target format
24
+ const emitters = {
25
+ 'claude-md': require('./emitters/claude-md'),
26
+ stdout: require('./emitters/stdout'),
27
+ };
28
+
29
+ /**
30
+ * Run the full sequencing pipeline.
31
+ *
32
+ * @param {object} options
33
+ * @param {string} options.target - Output format: 'claude-md' | 'stdout' | 'json'
34
+ * @param {string} options.budget - Token budget: 'minimal' | 'standard' | 'full' | 'unlimited'
35
+ * @param {string[]} options.sources - Limit to specific source types (null = all)
36
+ * @param {boolean} options.explain - Show full ranking breakdown
37
+ * @param {boolean} options.write - Write to file (for claude-md target)
38
+ * @param {string} options.cwd - Working directory
39
+ * @returns {{ chunks: object[], output: string, sources: object[] }}
40
+ */
41
+ function sequence(options = {}) {
42
+ const {
43
+ target = 'claude-md',
44
+ budget = 'standard',
45
+ sources: sourceFilter = null,
46
+ explain = false,
47
+ write = false,
48
+ cwd = process.cwd(),
49
+ } = options;
50
+
51
+ // 1. Discover all source files
52
+ let sources = discover(cwd);
53
+
54
+ // Filter sources if requested
55
+ if (sourceFilter && sourceFilter.length > 0) {
56
+ sources = sources.filter(s =>
57
+ sourceFilter.some(f => s.type === f || s.type.startsWith(f))
58
+ );
59
+ }
60
+
61
+ // 2. Parse all sources into chunks
62
+ let chunks = [];
63
+ for (const source of sources) {
64
+ const parser = parsers[source.type];
65
+ if (!parser) continue;
66
+
67
+ const parsed = parser.parse(source);
68
+ chunks.push(...parsed);
69
+ }
70
+
71
+ // 3. Rank all chunks
72
+ chunks = rank(chunks);
73
+
74
+ // 4. Compress to budget
75
+ chunks = compress(chunks, budget);
76
+
77
+ // 5. Emit
78
+ const emitter = emitters[target];
79
+ if (!emitter) {
80
+ throw new Error(`Unknown target format: ${target}. Available: ${Object.keys(emitters).join(', ')}`);
81
+ }
82
+
83
+ if (target === 'stdout') {
84
+ emitter.emit(chunks, { explain, sources });
85
+ return { chunks, output: null, sources };
86
+ }
87
+
88
+ const output = emitter.emit(chunks, {
89
+ projectName: getProjectName(chunks, cwd),
90
+ });
91
+
92
+ // Write to file if requested
93
+ if (write && emitter.writeToFile) {
94
+ const result = emitter.writeToFile(output, cwd);
95
+ return { chunks, output, sources, writeResult: result };
96
+ }
97
+
98
+ return { chunks, output, sources };
99
+ }
100
+
101
+ /**
102
+ * Get project name from chunks or fall back to directory name.
103
+ */
104
+ function getProjectName(chunks, cwd) {
105
+ // Check identity chunks from intent source for a project name
106
+ for (const chunk of chunks) {
107
+ if (chunk.sourceType === 'intent' && chunk.kind === 'identity') {
108
+ const nameMatch = chunk.content.match(/\*\*Name\*\*:\s*(.+)/);
109
+ if (nameMatch) return nameMatch[1].trim();
110
+ }
111
+ }
112
+ const path = require('path');
113
+ return path.basename(cwd);
114
+ }
115
+
116
+ module.exports = { sequence };
@@ -0,0 +1,106 @@
1
+ // Parse CLAUDE.md into MemoryChunks.
2
+ // Critical distinction: manual sections (user-curated, high authority)
3
+ // vs generated sections (PHEWSH markers, lower authority — derivative).
4
+
5
+ const fs = require('fs');
6
+
7
+ const PHEWSH_START = '<!-- PHEWSH:START -->';
8
+ const PHEWSH_END = '<!-- PHEWSH:END -->';
9
+
10
+ function parse(source) {
11
+ const content = fs.readFileSync(source.path, 'utf-8');
12
+ const mtime = fs.statSync(source.path).mtime.toISOString();
13
+ const chunks = [];
14
+
15
+ const startIdx = content.indexOf(PHEWSH_START);
16
+ const endIdx = content.indexOf(PHEWSH_END);
17
+
18
+ let manualContent = content;
19
+ let generatedContent = null;
20
+
21
+ if (startIdx !== -1 && endIdx !== -1) {
22
+ // Split manual from generated
23
+ manualContent = (
24
+ content.slice(0, startIdx) +
25
+ content.slice(endIdx + PHEWSH_END.length)
26
+ ).trim();
27
+
28
+ generatedContent = content.slice(
29
+ startIdx + PHEWSH_START.length,
30
+ endIdx
31
+ ).trim();
32
+ }
33
+
34
+ // Parse manual sections
35
+ if (manualContent) {
36
+ const sections = splitSections(manualContent);
37
+ for (const section of sections) {
38
+ if (!section.body.trim()) continue;
39
+
40
+ const kind = classifySection(section.title);
41
+ chunks.push({
42
+ source: `CLAUDE.md:${section.title || 'root'}`,
43
+ sourceType: 'claude-md-manual',
44
+ kind,
45
+ content: section.body.trim(),
46
+ timestamp: mtime,
47
+ metadata: { section: section.title },
48
+ });
49
+ }
50
+ }
51
+
52
+ // Generated section — lower authority, will get deduped against .intent/
53
+ if (generatedContent) {
54
+ chunks.push({
55
+ source: 'CLAUDE.md:phewsh-generated',
56
+ sourceType: 'claude-md-generated',
57
+ kind: 'context',
58
+ content: generatedContent,
59
+ timestamp: mtime,
60
+ metadata: { generated: true },
61
+ });
62
+ }
63
+
64
+ return chunks;
65
+ }
66
+
67
+ function splitSections(content) {
68
+ const sections = [];
69
+ let current = { title: '', body: '' };
70
+
71
+ for (const line of content.split('\n')) {
72
+ const heading = line.match(/^(#{1,2})\s+(.+)/);
73
+ if (heading) {
74
+ if (current.body.trim() || current.title) {
75
+ sections.push(current);
76
+ }
77
+ current = { title: heading[2], body: '' };
78
+ } else {
79
+ current.body += line + '\n';
80
+ }
81
+ }
82
+ if (current.body.trim() || current.title) {
83
+ sections.push(current);
84
+ }
85
+
86
+ return sections;
87
+ }
88
+
89
+ function classifySection(title) {
90
+ if (!title) return 'context';
91
+ const t = title.toLowerCase();
92
+
93
+ if (/architect|structure|infrastructure|system|stack/.test(t)) return 'context';
94
+ if (/convention|rule|style|format|lint/.test(t)) return 'feedback';
95
+ if (/deploy|build|ship|ci|cd/.test(t)) return 'context';
96
+ if (/constraint|budget|time|limit/.test(t)) return 'constraint';
97
+ if (/product|about|what|mission|vision/.test(t)) return 'identity';
98
+ if (/command|usage|api|endpoint/.test(t)) return 'reference';
99
+ if (/status|progress|current|state/.test(t)) return 'state';
100
+ if (/key|secret|env|config/.test(t)) return 'reference';
101
+ if (/monetiz|payment|stripe|billing/.test(t)) return 'context';
102
+
103
+ return 'context';
104
+ }
105
+
106
+ module.exports = { parse };
@@ -0,0 +1,53 @@
1
+ // Parse Claude auto-memory files into MemoryChunks.
2
+ // Reads individual .md files with YAML-like frontmatter (name, description, type).
3
+
4
+ const fs = require('fs');
5
+
6
+ function parseFrontmatter(content) {
7
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
8
+ if (!match) return { meta: {}, body: content };
9
+
10
+ const meta = {};
11
+ for (const line of match[1].split('\n')) {
12
+ const kv = line.match(/^(\w+):\s*(.+)$/);
13
+ if (kv) meta[kv[1]] = kv[2].trim();
14
+ }
15
+ return { meta, body: match[2] };
16
+ }
17
+
18
+ // Map Claude memory types to our chunk kinds
19
+ const TYPE_TO_KIND = {
20
+ user: 'identity',
21
+ feedback: 'feedback',
22
+ project: 'context',
23
+ reference: 'reference',
24
+ };
25
+
26
+ function parse(source) {
27
+ const content = fs.readFileSync(source.path, 'utf-8');
28
+ const mtime = fs.statSync(source.path).mtime.toISOString();
29
+
30
+ // MEMORY.md index file — skip, we read the linked files directly
31
+ if (source.type === 'claude-memory') return [];
32
+
33
+ // Individual memory files
34
+ const { meta, body } = parseFrontmatter(content);
35
+ if (!body.trim()) return [];
36
+
37
+ const kind = TYPE_TO_KIND[meta.type] || 'context';
38
+
39
+ return [{
40
+ source: `claude-memory:${source.name}`,
41
+ sourceType: 'claude-memory-file',
42
+ kind,
43
+ content: body.trim(),
44
+ timestamp: mtime,
45
+ metadata: {
46
+ name: meta.name,
47
+ description: meta.description,
48
+ memoryType: meta.type,
49
+ },
50
+ }];
51
+ }
52
+
53
+ module.exports = { parse };
@@ -0,0 +1,31 @@
1
+ // Generic parser for .cursorrules, agent.md, soul.md, README.md, copilot-instructions.md
2
+ // These are all "read the whole file as one chunk" sources with type-appropriate kind.
3
+
4
+ const fs = require('fs');
5
+
6
+ const TYPE_TO_KIND = {
7
+ cursor: 'feedback', // .cursorrules are behavioral rules
8
+ agent: 'identity', // agent.md defines agent capabilities
9
+ soul: 'identity', // soul.md defines project soul
10
+ copilot: 'feedback', // copilot-instructions are behavioral rules
11
+ readme: 'context', // README is broad project context
12
+ };
13
+
14
+ function parse(source) {
15
+ const content = fs.readFileSync(source.path, 'utf-8');
16
+ if (!content.trim()) return [];
17
+
18
+ const mtime = fs.statSync(source.path).mtime.toISOString();
19
+ const kind = TYPE_TO_KIND[source.type] || 'context';
20
+
21
+ return [{
22
+ source: source.name,
23
+ sourceType: source.type,
24
+ kind,
25
+ content: content.trim(),
26
+ timestamp: mtime,
27
+ metadata: {},
28
+ }];
29
+ }
30
+
31
+ module.exports = { parse };
@@ -0,0 +1,237 @@
1
+ // Parse .intent/ artifacts into MemoryChunks.
2
+ // This is the highest-authority source — user-authored canonical intent.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ function parseFrontmatter(content) {
8
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
9
+ if (!match) return { meta: {}, body: content };
10
+
11
+ const meta = {};
12
+ for (const line of match[1].split('\n')) {
13
+ const kv = line.match(/^(\w+):\s*(.+)$/);
14
+ if (kv) meta[kv[1]] = kv[2].trim();
15
+ }
16
+ return { meta, body: match[2] };
17
+ }
18
+
19
+ function extractSections(body) {
20
+ const sections = [];
21
+ let current = null;
22
+
23
+ for (const line of body.split('\n')) {
24
+ const heading = line.match(/^##\s+(.+)/);
25
+ if (heading) {
26
+ if (current) sections.push(current);
27
+ current = { title: heading[1], lines: [] };
28
+ } else if (current) {
29
+ current.lines.push(line);
30
+ }
31
+ }
32
+ if (current) sections.push(current);
33
+ return sections;
34
+ }
35
+
36
+ function parseVision(filePath) {
37
+ const content = fs.readFileSync(filePath, 'utf-8');
38
+ const { meta, body } = parseFrontmatter(content);
39
+ const mtime = fs.statSync(filePath).mtime.toISOString();
40
+ const timestamp = meta.updated || meta.created || mtime;
41
+ const chunks = [];
42
+
43
+ // Whole vision as identity
44
+ chunks.push({
45
+ source: '.intent/vision.md',
46
+ sourceType: 'intent',
47
+ kind: 'identity',
48
+ content: body.trim(),
49
+ timestamp,
50
+ metadata: { frontmatter: meta },
51
+ });
52
+
53
+ return chunks;
54
+ }
55
+
56
+ function parsePlan(filePath) {
57
+ const content = fs.readFileSync(filePath, 'utf-8');
58
+ const { meta, body } = parseFrontmatter(content);
59
+ const mtime = fs.statSync(filePath).mtime.toISOString();
60
+ const timestamp = meta.updated || meta.created || mtime;
61
+
62
+ return [{
63
+ source: '.intent/plan.md',
64
+ sourceType: 'intent',
65
+ kind: 'context',
66
+ content: body.trim(),
67
+ timestamp,
68
+ metadata: { frontmatter: meta },
69
+ }];
70
+ }
71
+
72
+ function parseStatus(filePath) {
73
+ const content = fs.readFileSync(filePath, 'utf-8');
74
+ const { meta, body } = parseFrontmatter(content);
75
+ const mtime = fs.statSync(filePath).mtime.toISOString();
76
+ const timestamp = meta.updated || meta.created || mtime;
77
+
78
+ return [{
79
+ source: '.intent/status.md',
80
+ sourceType: 'intent',
81
+ kind: 'state',
82
+ content: body.trim(),
83
+ timestamp,
84
+ metadata: { frontmatter: meta },
85
+ }];
86
+ }
87
+
88
+ function parseNext(filePath) {
89
+ const content = fs.readFileSync(filePath, 'utf-8');
90
+ const { meta, body } = parseFrontmatter(content);
91
+ const mtime = fs.statSync(filePath).mtime.toISOString();
92
+ const timestamp = meta.updated || meta.created || mtime;
93
+
94
+ return [{
95
+ source: '.intent/next.md',
96
+ sourceType: 'intent',
97
+ kind: 'state',
98
+ content: body.trim(),
99
+ timestamp,
100
+ metadata: { frontmatter: meta },
101
+ }];
102
+ }
103
+
104
+ function parseNarrative(filePath) {
105
+ const content = fs.readFileSync(filePath, 'utf-8');
106
+ const { meta, body } = parseFrontmatter(content);
107
+ const mtime = fs.statSync(filePath).mtime.toISOString();
108
+ const timestamp = meta.updated || meta.created || mtime;
109
+
110
+ return [{
111
+ source: '.intent/narrative.md',
112
+ sourceType: 'intent',
113
+ kind: 'identity',
114
+ content: body.trim(),
115
+ timestamp,
116
+ metadata: { frontmatter: meta },
117
+ }];
118
+ }
119
+
120
+ function parseProjectJson(filePath) {
121
+ const raw = fs.readFileSync(filePath, 'utf-8');
122
+ const mtime = fs.statSync(filePath).mtime.toISOString();
123
+ let data;
124
+ try { data = JSON.parse(raw); } catch { return []; }
125
+
126
+ const chunks = [];
127
+
128
+ // Project identity
129
+ const identityParts = [];
130
+ if (data.name) identityParts.push(`**Name**: ${data.name}`);
131
+ if (data.tldr) identityParts.push(`**TLDR**: ${data.tldr}`);
132
+ if (data.archetype) identityParts.push(`**Type**: ${data.archetype}`);
133
+ if (identityParts.length > 0) {
134
+ chunks.push({
135
+ source: '.intent/project.json',
136
+ sourceType: 'intent',
137
+ kind: 'identity',
138
+ content: identityParts.join('\n'),
139
+ timestamp: mtime,
140
+ metadata: {},
141
+ });
142
+ }
143
+
144
+ // Decision gate constraints
145
+ const gate = data.decisionGate;
146
+ if (gate?.constraints) {
147
+ const c = gate.constraints;
148
+ const lines = [];
149
+ if (c.budget > 0) lines.push(`Budget: $${c.budget}`);
150
+ if (c.timeHoursPerWeek > 0) lines.push(`Time: ${c.timeHoursPerWeek} hrs/week`);
151
+ if (c.skillLevel) lines.push(`Skill: ${c.skillLevel}`);
152
+ if (c.urgency) lines.push(`Urgency: ${c.urgency}`);
153
+ if (c.autonomy) lines.push(`Autonomy: ${c.autonomy}`);
154
+
155
+ chunks.push({
156
+ source: '.intent/project.json:constraints',
157
+ sourceType: 'intent',
158
+ kind: 'constraint',
159
+ content: lines.join('\n'),
160
+ timestamp: gate.createdAt || mtime,
161
+ metadata: { constraints: c },
162
+ });
163
+ }
164
+
165
+ // Success criteria
166
+ if (gate?.successCriteria?.length > 0) {
167
+ chunks.push({
168
+ source: '.intent/project.json:success',
169
+ sourceType: 'intent',
170
+ kind: 'identity',
171
+ content: 'Success criteria:\n' + gate.successCriteria.map(c => `- ${c}`).join('\n'),
172
+ timestamp: gate.createdAt || mtime,
173
+ metadata: {},
174
+ });
175
+ }
176
+
177
+ // Responsibility split
178
+ if (gate?.responsibilitySplit) {
179
+ const lines = [];
180
+ if (gate.responsibilitySplit.ai?.length > 0) {
181
+ lines.push('AI can handle:');
182
+ gate.responsibilitySplit.ai.forEach(r => lines.push(`- ${r}`));
183
+ }
184
+ if (gate.responsibilitySplit.human?.length > 0) {
185
+ lines.push('Requires human:');
186
+ gate.responsibilitySplit.human.forEach(r => lines.push(`- ${r}`));
187
+ }
188
+ if (lines.length > 0) {
189
+ chunks.push({
190
+ source: '.intent/project.json:responsibilities',
191
+ sourceType: 'intent',
192
+ kind: 'constraint',
193
+ content: lines.join('\n'),
194
+ timestamp: gate.createdAt || mtime,
195
+ metadata: {},
196
+ });
197
+ }
198
+ }
199
+
200
+ // Actions as state
201
+ if (data.actions?.length > 0) {
202
+ const active = data.actions.filter(a => a.state !== 'reconciled');
203
+ if (active.length > 0) {
204
+ chunks.push({
205
+ source: '.intent/project.json:actions',
206
+ sourceType: 'intent',
207
+ kind: 'action',
208
+ content: active.map(a => `- [${a.state}] ${a.intent} (${a.category})`).join('\n'),
209
+ timestamp: mtime,
210
+ metadata: {},
211
+ });
212
+ }
213
+ }
214
+
215
+ return chunks;
216
+ }
217
+
218
+ const FILE_PARSERS = {
219
+ 'vision.md': parseVision,
220
+ 'plan.md': parsePlan,
221
+ 'status.md': parseStatus,
222
+ 'next.md': parseNext,
223
+ 'narrative.md': parseNarrative,
224
+ 'project.json': parseProjectJson,
225
+ };
226
+
227
+ function parse(source) {
228
+ const parser = FILE_PARSERS[source.name];
229
+ if (!parser) return [];
230
+ try {
231
+ return parser(source.path);
232
+ } catch {
233
+ return [];
234
+ }
235
+ }
236
+
237
+ module.exports = { parse };
@@ -0,0 +1,84 @@
1
+ // Rank memory chunks by recency, impact, source authority, and deduplication.
2
+ // Weight = recency * impact * sourceAuthority * dedupPenalty
3
+
4
+ const crypto = require('crypto');
5
+
6
+ // Impact by chunk kind — what matters most across all contexts
7
+ const KIND_IMPACT = {
8
+ constraint: 1.0,
9
+ identity: 0.9,
10
+ feedback: 0.8,
11
+ state: 0.7,
12
+ action: 0.6,
13
+ reference: 0.4,
14
+ context: 0.5,
15
+ };
16
+
17
+ // Source authority — how much to trust this origin
18
+ const SOURCE_AUTHORITY = {
19
+ 'intent': 1.0, // User-authored canonical intent
20
+ 'claude-md-manual': 0.9, // User-curated CLAUDE.md sections
21
+ 'claude-md-generated': 0.5, // Machine-generated CLAUDE.md sections (derivative)
22
+ 'agent': 0.8, // Deliberate agent identity docs
23
+ 'soul': 0.8, // Project soul/values
24
+ 'claude-memory-file': 0.7, // AI-observed memories (may be stale)
25
+ 'claude-memory': 0.6, // Memory index (pointers, not content)
26
+ 'cursor': 0.7, // Tool-specific rules
27
+ 'copilot': 0.7, // Tool-specific rules
28
+ 'readme': 0.4, // Often stale, broad
29
+ };
30
+
31
+ function recencyScore(timestamp) {
32
+ if (!timestamp) return 0.3;
33
+
34
+ const now = Date.now();
35
+ const then = new Date(timestamp).getTime();
36
+ if (isNaN(then)) return 0.3;
37
+
38
+ const daysAgo = (now - then) / (1000 * 60 * 60 * 24);
39
+
40
+ if (daysAgo < 1) return 1.0;
41
+ if (daysAgo < 7) return 0.8;
42
+ if (daysAgo < 30) return 0.6;
43
+ return 0.3;
44
+ }
45
+
46
+ function contentHash(content) {
47
+ return crypto.createHash('md5').update(content.trim().toLowerCase()).digest('hex');
48
+ }
49
+
50
+ function rank(chunks) {
51
+ // Assign raw weights
52
+ for (const chunk of chunks) {
53
+ const recency = recencyScore(chunk.timestamp);
54
+ const impact = KIND_IMPACT[chunk.kind] || 0.5;
55
+ const authority = SOURCE_AUTHORITY[chunk.sourceType] || 0.5;
56
+ chunk.weight = recency * impact * authority;
57
+ }
58
+
59
+ // Deduplication: penalize near-duplicate content across sources
60
+ const seen = new Map(); // hash → highest-weight chunk
61
+ for (const chunk of chunks) {
62
+ const hash = contentHash(chunk.content);
63
+ chunk._hash = hash;
64
+
65
+ if (seen.has(hash)) {
66
+ const existing = seen.get(hash);
67
+ if (chunk.weight > existing.weight) {
68
+ existing.weight *= 0.05; // near-zero the weaker duplicate
69
+ seen.set(hash, chunk);
70
+ } else {
71
+ chunk.weight *= 0.05;
72
+ }
73
+ } else {
74
+ seen.set(hash, chunk);
75
+ }
76
+ }
77
+
78
+ // Sort by weight descending
79
+ chunks.sort((a, b) => b.weight - a.weight);
80
+
81
+ return chunks;
82
+ }
83
+
84
+ module.exports = { rank, recencyScore, KIND_IMPACT, SOURCE_AUTHORITY };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.11.15",
3
+ "version": "0.11.16",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"