fluffy-context 0.7.6 → 0.8.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 (36) hide show
  1. package/README.md +115 -36
  2. package/dist/src/agent/api.d.ts +49 -4
  3. package/dist/src/agent/api.js +183 -131
  4. package/dist/src/agent/index.d.ts +7 -1
  5. package/dist/src/agent/index.js +1 -0
  6. package/dist/src/cli/main.js +301 -15
  7. package/dist/src/cognition/evidence-graph.d.ts +4 -0
  8. package/dist/src/cognition/evidence-graph.js +187 -0
  9. package/dist/src/cognition/evolution.d.ts +36 -0
  10. package/dist/src/cognition/evolution.js +221 -0
  11. package/dist/src/cognition/journal.js +143 -2
  12. package/dist/src/cognition/projections.js +124 -2
  13. package/dist/src/cognition/recipes.d.ts +25 -0
  14. package/dist/src/cognition/recipes.js +149 -0
  15. package/dist/src/cognition/skills.d.ts +45 -0
  16. package/dist/src/cognition/skills.js +184 -0
  17. package/dist/src/cognition/types.d.ts +196 -2
  18. package/dist/src/cognition/usage-report.d.ts +40 -0
  19. package/dist/src/cognition/usage-report.js +232 -0
  20. package/dist/src/cognition/usage.d.ts +5 -0
  21. package/dist/src/cognition/usage.js +48 -0
  22. package/dist/src/compiler/compile.js +73 -4
  23. package/dist/src/compiler/graph.d.ts +7 -0
  24. package/dist/src/compiler/graph.js +117 -0
  25. package/dist/src/compiler/identity.d.ts +1 -1
  26. package/dist/src/compiler/identity.js +3 -1
  27. package/dist/src/compiler/sources.d.ts +3 -1
  28. package/dist/src/compiler/sources.js +11 -2
  29. package/dist/src/compiler/types.d.ts +23 -2
  30. package/dist/src/mcp/server.js +228 -6
  31. package/dist/src/storage/layout.d.ts +5 -0
  32. package/dist/src/storage/layout.js +15 -0
  33. package/dist/src/version.d.ts +1 -1
  34. package/dist/src/version.js +1 -1
  35. package/package.json +1 -1
  36. package/skills/fluffy-context/SKILL.md +53 -5
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { checkpoint, resume } from '../runtime/runtime.js';
3
- import { contextOrient } from '../agent/api.js';
4
- import { compileContext } from '../compiler/compile.js';
5
- import { contextExpand } from '../agent/api.js';
2
+ import { checkpoint } from '../runtime/runtime.js';
3
+ import { contextCompile, contextExpand, contextOrient, contextUsageReport, inspectRecipe, inspectSkill, listRecipes, listSkills, loadContext, materializeContextRecipe, materializeContextSkill, recordContextSkillInvocation, recordContextSkillOutcome, transitionContextRecipe, transitionContextSkill, acceptContextEvolutionCandidate, inspectContextEvolutionProposal, listContextEvolutionProposals, proposeContextEvolution, transitionContextEvolutionProposal } from '../agent/api.js';
4
+ import { formatUsageReport } from '../cognition/usage-report.js';
6
5
  import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
7
6
  import { recordUse } from '../cognition/feedback.js';
8
7
  import { verifyJournal } from '../cognition/journal.js';
@@ -75,6 +74,16 @@ function enumOption(args, name, values) {
75
74
  throw new Error(`${name} must be one of: ${values.join(', ')}`);
76
75
  return value;
77
76
  }
77
+ function graphHopsOption(args) {
78
+ const value = option(args, '--graph-hops');
79
+ if (value === undefined)
80
+ return undefined;
81
+ if (value !== '0' && value !== '1' && value !== '2')
82
+ throw new Error('--graph-hops must be one of: 0, 1, 2');
83
+ return Number(value);
84
+ }
85
+ const recipeBudgets = (value) => value === undefined ? undefined : Object.fromEntries(value.split(',').map((item) => { const [section, budget] = item.split('=', 2); const parsed = Number(budget); if (!section || !Number.isSafeInteger(parsed) || parsed < 0 || parsed > 100_000)
86
+ throw new Error('--budgets must be section=non-negative-integer up to 100000'); return [section, parsed]; }));
78
87
  const HELP = {
79
88
  init: `usage: ctx init [path] [--path <path>]
80
89
 
@@ -82,6 +91,34 @@ Initialize the local .context runtime layout. Re-running init is safe.
82
91
 
83
92
  Options:
84
93
  --path <path> Project path`,
94
+ usage: `usage: ctx usage report [options]
95
+
96
+ Show read-only context asset, reuse, token investment, and operation metrics.
97
+
98
+ Options:
99
+ --path <path> Project path
100
+ --since <ISO> Window start
101
+ --until <ISO> Window end
102
+ --days <number> Rolling window length (default: 30)
103
+ --month <YYYY-MM> Calendar month window
104
+ --granularity <value> day|week|month
105
+ --format <value> json|ascii (default: json)
106
+ --baseline-tokens <number> Explicit comparison token count
107
+ --baseline-source <value> manual|recorded-control`,
108
+ 'usage report': `usage: ctx usage report [options]
109
+
110
+ Show read-only context asset, reuse, token investment, and operation metrics.
111
+
112
+ Options:
113
+ --path <path> Project path
114
+ --since <ISO> Window start
115
+ --until <ISO> Window end
116
+ --days <number> Rolling window length (default: 30)
117
+ --month <YYYY-MM> Calendar month window
118
+ --granularity <value> day|week|month
119
+ --format <value> json|ascii (default: json)
120
+ --baseline-tokens <number> Explicit comparison token count
121
+ --baseline-source <value> manual|recorded-control`,
85
122
  checkpoint: `usage: ctx checkpoint [options]
86
123
 
87
124
  Save structured work state as a baseline or incremental snapshot.
@@ -125,6 +162,8 @@ Compile bounded, provider-neutral context with source provenance and selection e
125
162
  Options:
126
163
  --path <path> Project path
127
164
  --context <id> Context ID
165
+ --recipe-id <id> Explicit verified or published Recipe
166
+ --graph-hops <0|1|2> Optional bounded evidence-graph traversal
128
167
  --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
129
168
  --scope <scope> Exact consensus scope
130
169
  --paths <paths> Comma-separated project-relative paths
@@ -146,6 +185,8 @@ Options:
146
185
  --max-chars <number> Maximum expanded characters
147
186
  --compile-max-chars <number> Original compile budget (default: 4000)
148
187
  --context <id> Context ID
188
+ --recipe-id <id> Explicit verified or published Recipe
189
+ --graph-hops <0|1|2> Optional bounded evidence-graph traversal
149
190
  --query <text> Original compile query
150
191
  --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
151
192
  --scope <scope> Exact consensus scope
@@ -161,7 +202,7 @@ Start an MCP stdio server for Agent integrations.
161
202
  Run "ctx agent serve --help" for details.`,
162
203
  'agent serve': `usage: ctx agent serve
163
204
 
164
- Start an MCP stdio server exposing context_orient, context_expand, context_compile, context_resume, context_note_list, context_note_add, and context_checkpoint tools.
205
+ Start an MCP stdio server exposing context_orient, context_expand, context_compile, context_resume, context_note_list, context_note_add, context_usage_report, and context_checkpoint tools.
165
206
 
166
207
  The server owns standard input/output; do not use it interactively.`,
167
208
  hook: `usage: ctx hook claude-code session-start|user-prompt
@@ -259,6 +300,13 @@ Record explicit use of verified v2 retrieval material.`,
259
300
  'feedback use': `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
260
301
 
261
302
  Record an idempotent use signal that affects deterministic rank only.`,
303
+ skill: `usage: ctx skill list [--path <path>]
304
+ ctx skill inspect <skill-id> [--path <path>]
305
+ ctx skill materialize <proposal-id> --title <title> --procedure <steps> [--path <path>]
306
+ ctx skill verify|publish|deprecate <skill-id> --rationale <text> [--path <path>]
307
+ ctx skill supersede <skill-id> --rationale <text> --supersedes <entity:id> [--path <path>]
308
+ ctx skill invoke <skill-id> --invocation-id <id> --caller-event <id> --input-hash <hash> [--path <path>]
309
+ ctx skill outcome <skill-id> --invocation-id <id> --outcome-id <id> --caller-event <id> --outcome <value> --summary <text> [--path <path>]`,
262
310
  journal: `usage: ctx journal verify [--path <path>]
263
311
 
264
312
  Validate the v2 journal hash chain and replay projections in memory.`,
@@ -325,9 +373,49 @@ Rebuild v2 projections and verify imported v1 parity.
325
373
 
326
374
  Options:
327
375
  --path <path> Project path`,
328
- runtime: `usage: ctx runtime enable|disable|start|stop|status [--path <path>]
376
+ recipe: `usage: ctx recipe list|inspect|materialize|verify|publish|deprecate|supersede [options]
377
+
378
+ List, inspect, materialize, or govern immutable compiler Recipes.
379
+
380
+ Options:
381
+ --path <path> Project path
382
+ --title <text> Recipe title for materialize
383
+ --sources <items> Comma-separated source preferences
384
+ --sections <items> Comma-separated section preferences
385
+ --budgets <items> Comma-separated section=characters budgets
386
+ --rationale <text> Governance rationale
387
+ --supersedes <ref> Superseding entity:id`,
388
+ 'recipe verify': `usage: ctx recipe verify <recipe-id> --rationale <text> [--path <path>]
389
+
390
+ Mark a candidate compiler Recipe as verified.`,
391
+ 'recipe publish': `usage: ctx recipe publish <recipe-id> --rationale <text> [--path <path>]
392
+
393
+ Publish a verified compiler Recipe.`,
394
+ 'recipe deprecate': `usage: ctx recipe deprecate <recipe-id> --rationale <text> [--path <path>]
395
+
396
+ Retire a compiler Recipe with an explicit governance reason.`,
397
+ 'recipe supersede': `usage: ctx recipe supersede <recipe-id> --rationale <text> --supersedes <entity:id> [--path <path>]
398
+
399
+ Supersede a compiler Recipe while retaining its history.`,
400
+ evolution: `usage: ctx evolution propose|list|inspect|accept|verify|reject|deprecate|supersede [options]
401
+
402
+ Create and govern bounded, metadata-only Evolution proposals.
329
403
 
330
- Control the local event-driven Knowledge Runtime after v1 migration verification.`,
404
+ Options:
405
+ --path <path> Project path
406
+ --events <items> Comma-separated source event IDs
407
+ --recent-limit <n> Number of recent source events
408
+ --target-kind <kind> skill|compiler-recipe
409
+ --rationale <text> Governance rationale
410
+ --supersedes <ref> Superseding entity:id`,
411
+ 'evolution propose': `usage: ctx evolution propose [--events <event-id,...>|--recent-limit <n>] [--target-kind skill|compiler-recipe] [--path <path>]`,
412
+ 'evolution list': `usage: ctx evolution list [--path <path>]`,
413
+ 'evolution inspect': `usage: ctx evolution inspect <proposal-id> [--path <path>]`,
414
+ 'evolution accept': `usage: ctx evolution accept <proposal-id> --rationale <text> [--path <path>]`,
415
+ 'evolution verify': `usage: ctx evolution verify <proposal-id> --rationale <text> [--path <path>]`,
416
+ 'evolution reject': `usage: ctx evolution reject <proposal-id> --rationale <text> [--path <path>]`,
417
+ 'evolution deprecate': `usage: ctx evolution deprecate <proposal-id> --rationale <text> [--path <path>]`,
418
+ 'evolution supersede': `usage: ctx evolution supersede <proposal-id> --rationale <text> --supersedes <entity:id> [--path <path>]`,
331
419
  filesystem: `usage: ctx filesystem enable|disable|status [--path <path>]
332
420
 
333
421
  Control whether the Runtime records filtered file metadata changes.`,
@@ -340,6 +428,9 @@ Disable filesystem change observation and restart a running Runtime.`,
340
428
  'filesystem status': `usage: ctx filesystem status [--path <path>]
341
429
 
342
430
  Show the filesystem observation policy and Runtime status.`,
431
+ runtime: `usage: ctx runtime enable|disable|start|stop|status [--path <path>]
432
+
433
+ Control the local Knowledge Runtime lifecycle.`,
343
434
  'runtime status': `usage: ctx runtime status [--path <path>]
344
435
 
345
436
  Show the read-only runtime policy, migration and authenticated lease status.
@@ -363,7 +454,7 @@ Stop the local runtime through authenticated loopback IPC.`,
363
454
  Internal runtime daemon entry point.`,
364
455
  };
365
456
  function usage() {
366
- return `usage: ctx init|checkpoint|resume|orient|compile|expand|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
457
+ return `usage: ctx init|checkpoint|resume|orient|compile|expand|usage|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|skill|recipe|evolution|journal [options]
367
458
 
368
459
  Run \"ctx <command> --help\" for command details.`;
369
460
  }
@@ -391,8 +482,10 @@ function printHelp(args) {
391
482
  || (command === 'journal' && args[1] === 'verify')
392
483
  || (command === 'note' && ['add', 'list'].includes(args[1]))
393
484
  || (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
485
+ || (command === 'recipe' && ['list', 'inspect', 'materialize', 'verify', 'publish', 'deprecate', 'supersede'].includes(args[1]))
486
+ || (command === 'evolution' && ['propose', 'list', 'inspect', 'accept', 'verify', 'reject', 'deprecate', 'supersede'].includes(args[1]))
394
487
  || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
395
- || (command === 'filesystem' && ['enable', 'disable', 'status'].includes(args[1]));
488
+ || (command === 'usage' && args[1] === 'report');
396
489
  const key = nested ? `${command} ${args[1]}` : command;
397
490
  const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
398
491
  const help = HELP[nestedKey];
@@ -450,7 +543,7 @@ async function run(args) {
450
543
  case 'resume':
451
544
  validateOptions(args.slice(1), ['--path', '--context', '--max-chars'], ['--path', '--context', '--max-chars']);
452
545
  validatePositionals(positionals(args.slice(1), ['--path', '--context', '--max-chars']), 0, HELP.resume);
453
- print(await resume(target, option(args, '--context'), numericOption(args, '--max-chars', 4000)));
546
+ print(await loadContext(target, { contextId: option(args, '--context'), maxChars: numericOption(args, '--max-chars', 4000) }, 'cli'));
454
547
  return;
455
548
  case 'orient': {
456
549
  const valueOptions = ['--path', '--context', '--scope', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit'];
@@ -466,16 +559,18 @@ async function run(args) {
466
559
  knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
467
560
  deadendLimit: numericOption(args, '--deadend-limit', 10),
468
561
  noteLimit: numericOption(args, '--note-limit', 20),
469
- }));
562
+ }, 'cli'));
470
563
  return;
471
564
  }
472
565
  case 'compile': {
473
- const valueOptions = ['--path', '--context', '--scenario', '--scope', '--paths', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
566
+ const valueOptions = ['--path', '--context', '--recipe-id', '--graph-hops', '--scenario', '--scope', '--paths', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
474
567
  validateOptions(args.slice(1), valueOptions, valueOptions);
475
568
  const query = positionals(args.slice(1), valueOptions);
476
569
  validatePositionals(query, 1, HELP.compile);
477
- print(await compileContext(target, {
570
+ print(await contextCompile(target, {
478
571
  contextId: option(args, '--context'),
572
+ recipeId: option(args, '--recipe-id'),
573
+ graphHops: graphHopsOption(args),
479
574
  query: query[0],
480
575
  scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
481
576
  scope: option(args, '--scope'),
@@ -485,11 +580,11 @@ async function run(args) {
485
580
  deadendLimit: numericOption(args, '--deadend-limit', 10),
486
581
  noteLimit: numericOption(args, '--note-limit', 20),
487
582
  activityLimit: numericOption(args, '--activity-limit', 20),
488
- }));
583
+ }, 'cli'));
489
584
  return;
490
585
  }
491
586
  case 'expand': {
492
- const valueOptions = ['--path', '--manifest-hash', '--candidate-id', '--item-hash', '--level', '--max-chars', '--compile-max-chars', '--context', '--query', '--scenario', '--scope', '--paths', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
587
+ const valueOptions = ['--path', '--manifest-hash', '--candidate-id', '--item-hash', '--level', '--max-chars', '--compile-max-chars', '--context', '--recipe-id', '--graph-hops', '--query', '--scenario', '--scope', '--paths', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
493
588
  validateOptions(args.slice(1), valueOptions, valueOptions);
494
589
  validatePositionals(positionals(args.slice(1), valueOptions), 0, HELP.expand);
495
590
  const level = enumOption(args, '--level', ['structured', 'evidence']);
@@ -508,6 +603,8 @@ async function run(args) {
508
603
  maxChars: option(args, '--max-chars') === undefined ? undefined : numericOption(args, '--max-chars', 0),
509
604
  compile: {
510
605
  contextId: option(args, '--context'),
606
+ recipeId: option(args, '--recipe-id'),
607
+ graphHops: graphHopsOption(args),
511
608
  query: option(args, '--query'),
512
609
  scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
513
610
  scope: option(args, '--scope'),
@@ -521,6 +618,27 @@ async function run(args) {
521
618
  }));
522
619
  return;
523
620
  }
621
+ case 'usage': {
622
+ if (args[1] !== 'report')
623
+ throw new Error(HELP.usage);
624
+ const valueOptions = ['--path', '--since', '--until', '--days', '--month', '--granularity', '--format', '--baseline-tokens', '--baseline-source'];
625
+ validateOptions(args.slice(2), valueOptions, valueOptions);
626
+ validatePositionals(positionals(args.slice(2), valueOptions), 0, HELP['usage report']);
627
+ const report = await contextUsageReport(target, {
628
+ since: option(args, '--since'),
629
+ until: option(args, '--until'),
630
+ days: option(args, '--days') === undefined ? undefined : numericOption(args, '--days', 30),
631
+ month: option(args, '--month'),
632
+ granularity: enumOption(args, '--granularity', ['day', 'week', 'month']),
633
+ baselineTokens: option(args, '--baseline-tokens') === undefined ? undefined : numericOption(args, '--baseline-tokens', 0),
634
+ baselineSource: enumOption(args, '--baseline-source', ['manual', 'recorded-control']),
635
+ });
636
+ if (enumOption(args, '--format', ['json', 'ascii']) === 'ascii')
637
+ process.stdout.write(formatUsageReport(report));
638
+ else
639
+ print(report);
640
+ return;
641
+ }
524
642
  case 'agent':
525
643
  if (args[1] !== 'serve')
526
644
  throw new Error(HELP.agent);
@@ -630,6 +748,174 @@ async function run(args) {
630
748
  }
631
749
  return;
632
750
  }
751
+ case 'skill': {
752
+ const action = args[1];
753
+ if (action === 'list') {
754
+ validateOptions(args.slice(2), ['--path'], ['--path']);
755
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP.skill);
756
+ print(await listSkills(target));
757
+ return;
758
+ }
759
+ if (!action)
760
+ throw new Error(HELP.skill);
761
+ const skillId = positionals(args.slice(2), ['--path', '--title', '--procedure', '--rationale', '--supersedes', '--invocation-id', '--caller-event', '--input-hash', '--outcome-id', '--outcome', '--summary'])[0];
762
+ if (action === 'inspect') {
763
+ validateOptions(args.slice(2), ['--path'], ['--path']);
764
+ const values = positionals(args.slice(2), ['--path']);
765
+ if (values.length !== 1)
766
+ throw new Error(HELP.skill);
767
+ print(await inspectSkill(target, values[0]));
768
+ return;
769
+ }
770
+ if (action === 'materialize') {
771
+ const options = ['--path', '--title', '--procedure'];
772
+ validateOptions(args.slice(2), options, options);
773
+ const values = positionals(args.slice(2), options);
774
+ if (values.length !== 1)
775
+ throw new Error(HELP.skill);
776
+ print(await materializeContextSkill(target, { proposalId: values[0], title: option(args, '--title') ?? '', procedure: listOption(args, '--procedure') ?? [] }));
777
+ return;
778
+ }
779
+ if (['verify', 'publish', 'deprecate', 'supersede'].includes(action)) {
780
+ const options = ['--path', '--rationale', '--supersedes'];
781
+ validateOptions(args.slice(2), options, options);
782
+ const values = positionals(args.slice(2), options);
783
+ if (values.length !== 1)
784
+ throw new Error(HELP.skill);
785
+ const supersedes = option(args, '--supersedes');
786
+ const [entity, entityId] = supersedes?.split(':', 2) ?? [];
787
+ if (action === 'supersede' && (!entity || !entityId || !['knowledge', 'deadend', 'skill', 'compiler-recipe', 'experience', 'proposal'].includes(entity)))
788
+ throw new Error('--supersedes must be <entity:id>');
789
+ print(await transitionContextSkill(target, {
790
+ skillId: values[0],
791
+ to: action === 'supersede' ? 'superseded' : action,
792
+ rationale: option(args, '--rationale') ?? '',
793
+ ...(supersedes ? { supersedes: { entity: entity, entityId: entityId, relation: 'supersedes' } } : {}),
794
+ }));
795
+ return;
796
+ }
797
+ if (action === 'invoke') {
798
+ const options = ['--path', '--invocation-id', '--caller-event', '--input-hash'];
799
+ validateOptions(args.slice(2), options, options);
800
+ const values = positionals(args.slice(2), options);
801
+ if (values.length !== 1)
802
+ throw new Error(HELP.skill);
803
+ print(await recordContextSkillInvocation(target, { skillId: values[0], invocationId: option(args, '--invocation-id') ?? '', callerEventId: option(args, '--caller-event') ?? '', inputHash: option(args, '--input-hash') ?? '' }));
804
+ return;
805
+ }
806
+ if (action === 'outcome') {
807
+ const options = ['--path', '--invocation-id', '--outcome-id', '--caller-event', '--outcome', '--summary'];
808
+ validateOptions(args.slice(2), options, options);
809
+ const values = positionals(args.slice(2), options);
810
+ if (values.length !== 1)
811
+ throw new Error(HELP.skill);
812
+ print(await recordContextSkillOutcome(target, { skillId: values[0], invocationId: option(args, '--invocation-id') ?? '', outcomeId: option(args, '--outcome-id') ?? '', callerEventId: option(args, '--caller-event') ?? '', outcome: enumOption(args, '--outcome', ['success', 'failure', 'partial', 'irrelevant', 'unknown']) ?? 'unknown', summary: option(args, '--summary') ?? '' }));
813
+ return;
814
+ }
815
+ void skillId;
816
+ throw new Error(HELP.skill);
817
+ }
818
+ case 'evolution': {
819
+ const action = args[1];
820
+ const valueOptions = ['--path', '--events', '--recent-limit', '--target-kind', '--rationale', '--supersedes'];
821
+ if (action === 'propose') {
822
+ validateOptions(args.slice(2), valueOptions.slice(0, 4), valueOptions.slice(0, 4));
823
+ validatePositionals(positionals(args.slice(2), valueOptions.slice(0, 4)), 0, HELP['evolution propose']);
824
+ if (option(args, '--events') !== undefined && option(args, '--recent-limit') !== undefined)
825
+ throw new Error('use either --events or --recent-limit');
826
+ const eventIds = listOption(args, '--events');
827
+ if (eventIds && (eventIds.length === 0 || eventIds.length > 32))
828
+ throw new Error('--events must contain 1 to 32 event IDs');
829
+ print(await proposeContextEvolution(target, { eventIds, recentLimit: option(args, '--recent-limit') === undefined ? undefined : numericOption(args, '--recent-limit', 32), targetKind: enumOption(args, '--target-kind', ['skill', 'compiler-recipe']) }));
830
+ return;
831
+ }
832
+ if (action === 'list') {
833
+ validateOptions(args.slice(2), ['--path'], ['--path']);
834
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['evolution list']);
835
+ print(await listContextEvolutionProposals(target));
836
+ return;
837
+ }
838
+ const values = positionals(args.slice(2), valueOptions);
839
+ if (action === 'inspect') {
840
+ validateOptions(args.slice(2), ['--path'], ['--path']);
841
+ if (values.length !== 1)
842
+ throw new Error(HELP['evolution inspect']);
843
+ print(await inspectContextEvolutionProposal(target, values[0]));
844
+ return;
845
+ }
846
+ if (['accept', 'verify', 'reject', 'deprecate', 'supersede'].includes(action ?? '')) {
847
+ validateOptions(args.slice(2), valueOptions, valueOptions);
848
+ if (values.length !== 1)
849
+ throw new Error(HELP[`evolution ${action}`]);
850
+ const rationale = option(args, '--rationale') ?? '';
851
+ if (!rationale.trim())
852
+ throw new Error('--rationale is required');
853
+ const supersedes = option(args, '--supersedes');
854
+ const [entity, entityId] = supersedes?.split(':', 2) ?? [];
855
+ if (action === 'supersede' && (!entity || !entityId || !['knowledge', 'deadend', 'skill', 'compiler-recipe', 'experience', 'proposal'].includes(entity)))
856
+ throw new Error('--supersedes must be <entity:id>');
857
+ if (action === 'accept')
858
+ print(await acceptContextEvolutionCandidate(target, { proposalId: values[0], rationale }));
859
+ else {
860
+ const to = action === 'verify' ? 'verified'
861
+ : action === 'supersede' ? 'superseded'
862
+ : action;
863
+ print(await transitionContextEvolutionProposal(target, { proposalId: values[0], to, rationale, ...(supersedes ? { supersedes: { entity: entity, entityId: entityId, relation: 'supersedes' } } : {}) }));
864
+ }
865
+ return;
866
+ }
867
+ throw new Error(HELP.evolution);
868
+ }
869
+ case 'recipe': {
870
+ const action = args[1];
871
+ const valueOptions = ['--path', '--title', '--sources', '--sections', '--budgets', '--rationale', '--supersedes'];
872
+ if (action === 'list') {
873
+ validateOptions(args.slice(2), ['--path'], ['--path']);
874
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP.recipe);
875
+ print(await listRecipes(target));
876
+ return;
877
+ }
878
+ if (action === 'inspect') {
879
+ validateOptions(args.slice(2), ['--path'], ['--path']);
880
+ const values = positionals(args.slice(2), ['--path']);
881
+ if (values.length !== 1)
882
+ throw new Error(HELP.recipe);
883
+ print(await inspectRecipe(target, values[0]));
884
+ return;
885
+ }
886
+ if (action === 'materialize') {
887
+ validateOptions(args.slice(2), valueOptions, valueOptions);
888
+ const values = positionals(args.slice(2), valueOptions);
889
+ if (values.length !== 1)
890
+ throw new Error(HELP.recipe);
891
+ print(await materializeContextRecipe(target, {
892
+ proposalId: values[0],
893
+ title: option(args, '--title') ?? '',
894
+ preferredSources: listOption(args, '--sources'),
895
+ preferredSections: listOption(args, '--sections'),
896
+ sectionBudgets: recipeBudgets(option(args, '--budgets')),
897
+ }));
898
+ return;
899
+ }
900
+ if (['verify', 'publish', 'deprecate', 'supersede'].includes(action ?? '')) {
901
+ validateOptions(args.slice(2), ['--path', '--rationale', '--supersedes'], ['--path', '--rationale', '--supersedes']);
902
+ const values = positionals(args.slice(2), ['--path', '--rationale', '--supersedes']);
903
+ if (values.length !== 1)
904
+ throw new Error(HELP.recipe);
905
+ const supersedes = option(args, '--supersedes');
906
+ const [entity, entityId] = supersedes?.split(':', 2) ?? [];
907
+ if (action === 'supersede' && (!entity || !entityId || !['knowledge', 'deadend', 'skill', 'compiler-recipe', 'experience', 'proposal'].includes(entity)))
908
+ throw new Error('--supersedes must be <entity:id>');
909
+ print(await transitionContextRecipe(target, {
910
+ recipeId: values[0],
911
+ to: action === 'supersede' ? 'superseded' : action,
912
+ rationale: option(args, '--rationale') ?? '',
913
+ ...(supersedes ? { supersedes: { entity: entity, entityId: entityId, relation: 'supersedes' } } : {}),
914
+ }));
915
+ return;
916
+ }
917
+ throw new Error(HELP.recipe);
918
+ }
633
919
  case 'journal': {
634
920
  if (args[1] !== 'verify')
635
921
  throw new Error(HELP.journal);
@@ -0,0 +1,4 @@
1
+ import type { CognitionEvent, CognitionGitAnchor, EvidenceGraphProjection } from './types.js';
2
+ export declare const EVIDENCE_GRAPH_DERIVATION_VERSION: "evidence-graph-v1";
3
+ export declare function workspaceNodeId(anchor: Pick<CognitionGitAnchor, 'worktreeId' | 'branch' | 'ref'>): string;
4
+ export declare function buildEvidenceGraph(events: CognitionEvent[]): EvidenceGraphProjection;
@@ -0,0 +1,187 @@
1
+ import crypto from 'node:crypto';
2
+ export const EVIDENCE_GRAPH_DERIVATION_VERSION = 'evidence-graph-v1';
3
+ function hash(value) {
4
+ return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
5
+ }
6
+ function record(event) {
7
+ const value = event.payload.record;
8
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
9
+ }
10
+ function normalizePath(value) {
11
+ const normalized = value.replaceAll('\\', '/');
12
+ return normalized.length > 0 && !normalized.startsWith('/') && !normalized.includes('\0') && !normalized.split('/').includes('..') ? normalized : null;
13
+ }
14
+ function entityKind(entity) {
15
+ if (entity === 'compiler-recipe')
16
+ return 'recipe';
17
+ if (entity === 'knowledge' || entity === 'deadend' || entity === 'skill' || entity === 'experience' || entity === 'proposal')
18
+ return entity;
19
+ return null;
20
+ }
21
+ function entityNodeId(entity, entityId) {
22
+ const kind = entityKind(entity);
23
+ return kind && entityId.trim().length > 0 ? `${kind}:${entityId}` : null;
24
+ }
25
+ export function workspaceNodeId(anchor) {
26
+ return `branch-worktree:${hash({ worktreeId: anchor.worktreeId, branch: anchor.branch, ref: anchor.ref }).slice(0, 16)}`;
27
+ }
28
+ function addNode(nodes, nodeId, kind) {
29
+ nodes.set(nodeId, { nodeId, kind });
30
+ }
31
+ function addEntityNode(nodes, entity, entityId) {
32
+ const nodeId = entityNodeId(entity, entityId);
33
+ const kind = entityKind(entity);
34
+ if (nodeId && kind)
35
+ addNode(nodes, nodeId, kind);
36
+ return nodeId;
37
+ }
38
+ function addEdge(edges, input) {
39
+ const sourceEventIds = [...new Set(input.sourceEventIds)].sort();
40
+ const basis = { from: input.from, to: input.to, type: input.type, sourceEventIds, occurredAt: input.occurredAt, confidence: input.confidence };
41
+ const edgeId = `edge:${hash(basis).slice(0, 24)}`;
42
+ edges.set(edgeId, { ...input, edgeId, sourceEventIds, derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION });
43
+ }
44
+ function anchorNodes(event, nodes) {
45
+ const workspace = workspaceNodeId(event.git);
46
+ addNode(nodes, workspace, 'branch-worktree');
47
+ if (event.git.head)
48
+ addNode(nodes, `commit:${event.git.head}`, 'commit');
49
+ return workspace;
50
+ }
51
+ function lineageEdge(edges, nodes, from, lineage, event) {
52
+ const target = addEntityNode(nodes, lineage.entity, lineage.entityId);
53
+ if (!target)
54
+ return;
55
+ addEdge(edges, { from, to: target, type: lineage.relation, sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
56
+ }
57
+ function sourceLinks(edges, nodes, from, contextIds, snapshotIds, event) {
58
+ if (Array.isArray(contextIds))
59
+ for (const contextId of contextIds)
60
+ if (typeof contextId === 'string' && contextId.trim()) {
61
+ const target = `context:${contextId}`;
62
+ addNode(nodes, target, 'context');
63
+ addEdge(edges, { from, to: target, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
64
+ }
65
+ if (Array.isArray(snapshotIds))
66
+ for (const snapshotId of snapshotIds)
67
+ if (typeof snapshotId === 'string' && snapshotId.trim()) {
68
+ const target = `snapshot:${snapshotId}`;
69
+ addNode(nodes, target, 'snapshot');
70
+ addEdge(edges, { from, to: target, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
71
+ }
72
+ }
73
+ export function buildEvidenceGraph(events) {
74
+ const nodes = new Map();
75
+ const edges = new Map();
76
+ for (const event of events) {
77
+ const workspace = anchorNodes(event, nodes);
78
+ if (event.git.head)
79
+ addEdge(edges, { from: workspace, to: `commit:${event.git.head}`, type: 'changed_in', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
80
+ const value = record(event);
81
+ if (event.type === 'context.checkpoint.imported' || event.type === 'context.state.recorded') {
82
+ const snapshot = value?.snapshot;
83
+ if (snapshot && typeof snapshot.contextId === 'string' && typeof snapshot.snapshotId === 'string') {
84
+ const context = `context:${snapshot.contextId}`;
85
+ const snap = `snapshot:${snapshot.snapshotId}`;
86
+ addNode(nodes, context, 'context');
87
+ addNode(nodes, snap, 'snapshot');
88
+ addEdge(edges, { from: context, to: snap, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
89
+ }
90
+ }
91
+ if (event.type === 'experience.recorded' && typeof value?.experienceId === 'string') {
92
+ const id = addEntityNode(nodes, 'experience', value.experienceId);
93
+ sourceLinks(edges, nodes, id, value.sourceContextIds, value.sourceSnapshotIds, event);
94
+ if (Array.isArray(value.paths))
95
+ for (const item of value.paths)
96
+ if (typeof item === 'string') {
97
+ const path = normalizePath(item);
98
+ if (!path)
99
+ continue;
100
+ const target = `path:${path}`;
101
+ addNode(nodes, target, 'path');
102
+ addEdge(edges, { from: id, to: target, type: 'observed_in', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
103
+ }
104
+ }
105
+ if (event.type === 'evolution.proposed' && typeof value?.proposalId === 'string') {
106
+ const id = addEntityNode(nodes, 'proposal', value.proposalId);
107
+ if (Array.isArray(value.sourceExperienceIds))
108
+ for (const source of value.sourceExperienceIds)
109
+ if (typeof source === 'string') {
110
+ const target = addEntityNode(nodes, 'experience', source);
111
+ if (target)
112
+ addEdge(edges, { from: id, to: target, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
113
+ }
114
+ if (Array.isArray(value.lineage))
115
+ for (const item of value.lineage)
116
+ if (item && typeof item === 'object')
117
+ lineageEdge(edges, nodes, id, item, event);
118
+ }
119
+ if (['knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected'].includes(event.type) && typeof value?.knowledgeId === 'string') {
120
+ const id = addEntityNode(nodes, 'knowledge', value.knowledgeId);
121
+ sourceLinks(edges, nodes, id, value.sourceContextIds, value.sourceSnapshotIds, event);
122
+ }
123
+ if (['deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected'].includes(event.type) && typeof value?.deadendId === 'string') {
124
+ const id = addEntityNode(nodes, 'deadend', value.deadendId);
125
+ sourceLinks(edges, nodes, id, value.sourceContextId === null ? [] : [value.sourceContextId], value.sourceSnapshotId === null ? [] : [value.sourceSnapshotId], event);
126
+ }
127
+ if (event.type === 'skill.materialized' && typeof value?.skillId === 'string') {
128
+ const id = addEntityNode(nodes, 'skill', value.skillId);
129
+ const proposal = typeof value.proposalId === 'string' ? addEntityNode(nodes, 'proposal', value.proposalId) : null;
130
+ if (proposal)
131
+ addEdge(edges, { from: id, to: proposal, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
132
+ if (Array.isArray(value.lineage))
133
+ for (const item of value.lineage)
134
+ if (item && typeof item === 'object')
135
+ lineageEdge(edges, nodes, id, item, event);
136
+ }
137
+ if (event.type === 'recipe.materialized' && typeof value?.recipeId === 'string') {
138
+ const id = addEntityNode(nodes, 'compiler-recipe', value.recipeId);
139
+ const proposal = typeof value.proposalId === 'string' ? addEntityNode(nodes, 'proposal', value.proposalId) : null;
140
+ if (proposal)
141
+ addEdge(edges, { from: id, to: proposal, type: 'derived_from', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
142
+ if (Array.isArray(value.lineage))
143
+ for (const item of value.lineage)
144
+ if (item && typeof item === 'object')
145
+ lineageEdge(edges, nodes, id, item, event);
146
+ }
147
+ if (event.type === 'evolution.superseded') {
148
+ const transition = event.payload.transition;
149
+ if (transition && typeof transition === 'object' && !Array.isArray(transition)) {
150
+ const value = transition;
151
+ const from = typeof value.entity === 'string' && typeof value.entityId === 'string' ? addEntityNode(nodes, value.entity, value.entityId) : null;
152
+ const supersedes = value.supersedes;
153
+ if (from && supersedes && typeof supersedes === 'object' && !Array.isArray(supersedes)) {
154
+ const reference = supersedes;
155
+ lineageEdge(edges, nodes, from, reference, event);
156
+ }
157
+ }
158
+ }
159
+ if (event.type === 'workspace.paths.changed') {
160
+ const payload = event.payload;
161
+ if (Array.isArray(payload.paths))
162
+ for (const item of payload.paths)
163
+ if (typeof item === 'string') {
164
+ const path = normalizePath(item);
165
+ if (!path)
166
+ continue;
167
+ const target = `path:${path}`;
168
+ addNode(nodes, target, 'path');
169
+ addEdge(edges, { from: workspace, to: target, type: 'observed_in', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
170
+ }
171
+ }
172
+ if (event.type === 'record.used') {
173
+ const used = event.payload;
174
+ if (typeof used.entity === 'string' && typeof used.entityId === 'string') {
175
+ const from = addEntityNode(nodes, used.entity, used.entityId);
176
+ if (from)
177
+ addEdge(edges, { from, to: workspace, type: 'used_for', sourceEventIds: [event.eventId], occurredAt: event.occurredAt });
178
+ }
179
+ }
180
+ }
181
+ return {
182
+ schemaVersion: 1,
183
+ derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION,
184
+ nodes: [...nodes.values()].sort((a, b) => a.nodeId.localeCompare(b.nodeId)),
185
+ edges: [...edges.values()].sort((a, b) => a.edgeId.localeCompare(b.edgeId)),
186
+ };
187
+ }