fluffy-context 0.7.7 → 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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { checkpoint } from '../runtime/runtime.js';
3
- import { contextCompile, contextExpand, contextOrient, contextUsageReport, loadContext } from '../agent/api.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
4
  import { formatUsageReport } from '../cognition/usage-report.js';
5
5
  import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
6
6
  import { recordUse } from '../cognition/feedback.js';
@@ -74,6 +74,16 @@ function enumOption(args, name, values) {
74
74
  throw new Error(`${name} must be one of: ${values.join(', ')}`);
75
75
  return value;
76
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]; }));
77
87
  const HELP = {
78
88
  init: `usage: ctx init [path] [--path <path>]
79
89
 
@@ -152,6 +162,8 @@ Compile bounded, provider-neutral context with source provenance and selection e
152
162
  Options:
153
163
  --path <path> Project path
154
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
155
167
  --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
156
168
  --scope <scope> Exact consensus scope
157
169
  --paths <paths> Comma-separated project-relative paths
@@ -173,6 +185,8 @@ Options:
173
185
  --max-chars <number> Maximum expanded characters
174
186
  --compile-max-chars <number> Original compile budget (default: 4000)
175
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
176
190
  --query <text> Original compile query
177
191
  --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
178
192
  --scope <scope> Exact consensus scope
@@ -286,6 +300,13 @@ Record explicit use of verified v2 retrieval material.`,
286
300
  'feedback use': `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
287
301
 
288
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>]`,
289
310
  journal: `usage: ctx journal verify [--path <path>]
290
311
 
291
312
  Validate the v2 journal hash chain and replay projections in memory.`,
@@ -352,9 +373,49 @@ Rebuild v2 projections and verify imported v1 parity.
352
373
 
353
374
  Options:
354
375
  --path <path> Project path`,
355
- 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>]
356
389
 
357
- Control the local event-driven Knowledge Runtime after v1 migration verification.`,
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.
403
+
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>]`,
358
419
  filesystem: `usage: ctx filesystem enable|disable|status [--path <path>]
359
420
 
360
421
  Control whether the Runtime records filtered file metadata changes.`,
@@ -367,6 +428,9 @@ Disable filesystem change observation and restart a running Runtime.`,
367
428
  'filesystem status': `usage: ctx filesystem status [--path <path>]
368
429
 
369
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.`,
370
434
  'runtime status': `usage: ctx runtime status [--path <path>]
371
435
 
372
436
  Show the read-only runtime policy, migration and authenticated lease status.
@@ -390,7 +454,7 @@ Stop the local runtime through authenticated loopback IPC.`,
390
454
  Internal runtime daemon entry point.`,
391
455
  };
392
456
  function usage() {
393
- 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|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]
394
458
 
395
459
  Run \"ctx <command> --help\" for command details.`;
396
460
  }
@@ -418,6 +482,8 @@ function printHelp(args) {
418
482
  || (command === 'journal' && args[1] === 'verify')
419
483
  || (command === 'note' && ['add', 'list'].includes(args[1]))
420
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]))
421
487
  || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
422
488
  || (command === 'usage' && args[1] === 'report');
423
489
  const key = nested ? `${command} ${args[1]}` : command;
@@ -497,12 +563,14 @@ async function run(args) {
497
563
  return;
498
564
  }
499
565
  case 'compile': {
500
- 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'];
501
567
  validateOptions(args.slice(1), valueOptions, valueOptions);
502
568
  const query = positionals(args.slice(1), valueOptions);
503
569
  validatePositionals(query, 1, HELP.compile);
504
570
  print(await contextCompile(target, {
505
571
  contextId: option(args, '--context'),
572
+ recipeId: option(args, '--recipe-id'),
573
+ graphHops: graphHopsOption(args),
506
574
  query: query[0],
507
575
  scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
508
576
  scope: option(args, '--scope'),
@@ -516,7 +584,7 @@ async function run(args) {
516
584
  return;
517
585
  }
518
586
  case 'expand': {
519
- 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'];
520
588
  validateOptions(args.slice(1), valueOptions, valueOptions);
521
589
  validatePositionals(positionals(args.slice(1), valueOptions), 0, HELP.expand);
522
590
  const level = enumOption(args, '--level', ['structured', 'evidence']);
@@ -535,6 +603,8 @@ async function run(args) {
535
603
  maxChars: option(args, '--max-chars') === undefined ? undefined : numericOption(args, '--max-chars', 0),
536
604
  compile: {
537
605
  contextId: option(args, '--context'),
606
+ recipeId: option(args, '--recipe-id'),
607
+ graphHops: graphHopsOption(args),
538
608
  query: option(args, '--query'),
539
609
  scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
540
610
  scope: option(args, '--scope'),
@@ -678,6 +748,174 @@ async function run(args) {
678
748
  }
679
749
  return;
680
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
+ }
681
919
  case 'journal': {
682
920
  if (args[1] !== 'verify')
683
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
+ }
@@ -0,0 +1,36 @@
1
+ import type { EvolutionArtifactKind, EvolutionArtifactStatus, EvolutionLineageReference, EvolutionProposalState } from './types.js';
2
+ type ProposalTargetKind = Extract<EvolutionArtifactKind, 'skill' | 'compiler-recipe'>;
3
+ export interface ProposeEvolutionInput {
4
+ eventIds?: string[];
5
+ recentLimit?: number;
6
+ targetKind?: ProposalTargetKind;
7
+ }
8
+ export interface ProposeEvolutionResult {
9
+ experienceId: string;
10
+ experienceEventId: string;
11
+ proposalId: string | null;
12
+ proposalEventId: string | null;
13
+ recorded: boolean;
14
+ }
15
+ export interface AcceptEvolutionCandidateInput {
16
+ proposalId: string;
17
+ rationale: string;
18
+ }
19
+ export interface TransitionEvolutionProposalInput {
20
+ proposalId: string;
21
+ to: Extract<EvolutionArtifactStatus, 'verified' | 'rejected' | 'deprecated' | 'superseded'>;
22
+ rationale: string;
23
+ supersedes?: EvolutionLineageReference;
24
+ }
25
+ export declare function proposeEvolution(startPath: string | undefined, input?: ProposeEvolutionInput): Promise<ProposeEvolutionResult>;
26
+ export declare function listEvolutionProposals(startPath?: string): Promise<EvolutionProposalState[]>;
27
+ export declare function inspectEvolutionProposal(startPath: string | undefined, proposalId: string): Promise<EvolutionProposalState>;
28
+ export declare function acceptEvolutionCandidate(startPath: string | undefined, input: AcceptEvolutionCandidateInput): Promise<{
29
+ accepted: boolean;
30
+ eventId: string;
31
+ }>;
32
+ export declare function transitionEvolutionProposal(startPath: string | undefined, input: TransitionEvolutionProposalInput): Promise<{
33
+ transitioned: boolean;
34
+ eventId: string;
35
+ }>;
36
+ export {};