@llblab/pi-kit 0.37.0 → 0.38.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 (152) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +30 -14
  7. package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
  70. package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
  72. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
  73. package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
  74. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
  75. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
  76. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
  77. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
  78. package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
  79. package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
  80. package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
  81. package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
  82. package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
  83. package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
  84. package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
  85. package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
  86. package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
  87. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
  88. package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
  89. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
  90. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
  91. package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
  92. package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
  93. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
  94. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
  95. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
  96. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
  97. package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
  98. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
  99. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
  100. package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
  101. package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
  102. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
  103. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
  104. package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
  105. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
  106. package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
  107. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
  108. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
  109. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
  110. package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
  111. package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
  112. package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
  113. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
  114. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
  115. package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
  116. package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
  117. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
  118. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
  119. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
  120. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
  121. package/node_modules/@llblab/pi-state-flow/package.json +6 -1
  122. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
  123. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
  124. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  125. package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
  126. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  127. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
  128. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
  129. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
  130. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
  131. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
  132. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
  133. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
  134. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
  135. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
  136. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
  137. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  138. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
  139. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  140. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
  141. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  142. package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
  143. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  144. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
  145. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
  146. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
  147. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
  148. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
  149. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
  150. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  151. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
  152. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { isJsonValue, isObject, sameJson, validatePatch, type JsonObject, type JsonValue } from "./json.ts";
3
- import type { ScopedSemanticStates, StateScope } from "./state.ts";
3
+ import type { ScopeStateValues, StateScope } from "./state.ts";
4
4
 
5
5
  export const DEFAULT_HISTORY_LIMIT = 7;
6
6
  export const MAX_HISTORY_LIMIT = 100;
@@ -10,8 +10,8 @@ export interface RecentScopePatch {
10
10
  patch: {
11
11
  [key: string]: JsonValue | undefined;
12
12
  intents?: JsonObject | null;
13
- contract?: JsonObject | null;
14
- working?: JsonObject | null;
13
+ strategy?: JsonObject | null;
14
+ tactics?: JsonObject | null;
15
15
  artifacts?: JsonObject | null;
16
16
  lazy?: JsonObject | null;
17
17
  response?: string | null;
@@ -31,7 +31,7 @@ export interface RecentTransition extends AcceptedTransition {
31
31
 
32
32
  export type RecentTransitionWindow = RecentTransition[];
33
33
  const SCOPES = new Set<StateScope>(["global", "cwd", "session"]);
34
- const PATCH_KEYS = new Set(["artifacts", "contract", "working", "intents", "response", "lazy"]);
34
+ const PATCH_KEYS = new Set(["artifacts", "strategy", "tactics", "intents", "response", "lazy"]);
35
35
 
36
36
  /** Normalize accepted replacements into recursive-merge replay, including removals. */
37
37
  function replayPatch(before: JsonObject, after: JsonObject): JsonObject {
@@ -57,6 +57,7 @@ function validateScopedPatch(value: unknown): asserts value is RecentScopePatch
57
57
  throw new Error(`Recent State Flow transition has an unknown scope: ${String(value.scope)}`);
58
58
  }
59
59
  validatePatch(value.patch);
60
+ if (Object.hasOwn(value.patch, "contract") || Object.hasOwn(value.patch, "working")) throw new Error("Retired State Flow semantic plane in target patch");
60
61
  for (const [key, field] of Object.entries(value.patch)) {
61
62
  const valid = key === "response"
62
63
  ? value.scope === "session" && (field === null || typeof field === "string")
@@ -78,11 +79,19 @@ export function validateRecentTransition(value: unknown): asserts value is Recen
78
79
  }
79
80
  }
80
81
 
81
- export function createAcceptedTransition(currentStates: ScopedSemanticStates, nextStates: ScopedSemanticStates, id?: string): AcceptedTransition | undefined {
82
+ export function createAcceptedTransition(currentStates: ScopeStateValues, nextStates: ScopeStateValues, id?: string): AcceptedTransition | undefined {
83
+ const owners = Object.keys(currentStates) as StateScope[];
84
+ if (owners.length === 0 || owners.some((scope) => !SCOPES.has(scope))
85
+ || Object.keys(nextStates).length !== owners.length || owners.some((scope) => !Object.hasOwn(nextStates, scope))) {
86
+ throw new Error("Accepted State Flow replay requires matching nonempty owner sets");
87
+ }
82
88
  const transitions: RecentScopePatch[] = [];
83
89
  for (const scope of SCOPES) {
84
- const patch = replayPatch(currentStates[scope], nextStates[scope]);
85
- if ((currentStates[scope].response ?? "") === (nextStates[scope].response ?? "")) delete patch.response;
90
+ if (!Object.hasOwn(currentStates, scope)) continue;
91
+ const before = currentStates[scope], after = nextStates[scope];
92
+ if (!isObject(before) || !isObject(after)) throw new Error("Accepted State Flow replay requires present owner values");
93
+ const patch = replayPatch(before, after);
94
+ if ((before.response ?? "") === (after.response ?? "")) delete patch.response;
86
95
  if (Object.keys(patch).length > 0) transitions.push({ scope, patch });
87
96
  }
88
97
  if (transitions.length === 0) return undefined;
@@ -49,7 +49,8 @@ function applyOwnedArrayPatch(
49
49
  return next;
50
50
  }
51
51
 
52
- function applyOwnedPatch(
52
+ /** Internal path-copying step; callers must own the detached draft and collect failures privately. */
53
+ export function applyOwnedPatch(
53
54
  state: JsonObject, patch: JsonObject, missingDeletions?: MissingDeletion[], path = "", unavailablePath?: string,
54
55
  ): JsonObject {
55
56
  let next = state;
@@ -1,13 +1,14 @@
1
- import type { MaterializedState, ScopedStates, StateScope } from "./state.ts";
1
+ import { assertScopeStateValues, type SemanticState, type ScopeStateValues, type ScopedStates, type StateScope } from "./state.ts";
2
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION, type ScopeSelection } from "./selection.ts";
2
3
 
3
- function hasSemanticMemory(state: MaterializedState): boolean {
4
- return Object.keys(state.contract).length > 0 || Object.keys(state.working).length > 0;
4
+ function hasSemanticMemory(state: SemanticState): boolean {
5
+ return Object.keys(state.strategy ?? {}).length > 0 || Object.keys(state.tactics ?? {}).length > 0;
5
6
  }
6
7
 
7
- export function retainedMemoryScopes(states: ScopedStates): Record<StateScope, boolean> {
8
- return {
9
- global: hasSemanticMemory(states.global),
10
- cwd: hasSemanticMemory(states.cwd),
11
- session: hasSemanticMemory(states.session),
12
- };
8
+ export function retainedMemoryScopes(states: ScopedStates): Record<StateScope, boolean>;
9
+ export function retainedMemoryScopes(states: ScopeStateValues, selection: ScopeSelection): Partial<Record<StateScope, boolean>>;
10
+ export function retainedMemoryScopes(states: ScopeStateValues, selection: ScopeSelection = DEFAULT_SCOPE_SELECTION): Partial<Record<StateScope, boolean>> {
11
+ const selected = captureScopeSelection(selection);
12
+ assertScopeStateValues(states, selected);
13
+ return Object.fromEntries(selected.map(scope => [scope, hasSemanticMemory(states[scope]!)]));
13
14
  }
@@ -0,0 +1,189 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import { parseArtifactProvenanceRegistry } from "./artifact.ts";
4
+ import { MAX_HISTORY_LIMIT } from "./history.ts";
5
+ import { canonicalJson, isJsonValue, isObject, validatePatch, type JsonObject, type JsonValue } from "./json.ts";
6
+ import { parseStateReadPath, parseValuePath } from "./query.ts";
7
+ import { assertRetainedModules, validateRetainedModules, type RetainedModules } from "./semantic-module.ts";
8
+ import { assertNoRetiredSemanticPlanes, type StateScope } from "./state.ts";
9
+ import { validateScopeStream, type ScopeStream } from "./temporal.ts";
10
+
11
+ /** Owns temporary pure semantic-dialect conversion; excludes I/O, publication, backup and lifecycle authority. */
12
+ export interface SemanticMigrationCandidate {
13
+ sourceVersion: 1 | 2 | 3 | 4;
14
+ targetVersion: 3 | 4;
15
+ /** A format conversion is not an authored semantic transition or revision. */
16
+ requiresConversion: boolean;
17
+ stream: ScopeStream;
18
+ }
19
+
20
+ export type ScopeMigrationFile = "checkpoint" | "patches" | "meta";
21
+ export type ScopeMigrationSources = Partial<Record<ScopeMigrationFile, Uint8Array>>;
22
+
23
+ /** Detached evidence for later backup/CAS, not a publication capability or a filesystem observation. */
24
+ export interface MigrationByteReceipt {
25
+ bytes: Uint8Array;
26
+ identity: `sha256:${string}`;
27
+ }
28
+
29
+ export interface SerializedSemanticMigrationCandidate extends SemanticMigrationCandidate {
30
+ basis: Record<ScopeMigrationFile, MigrationByteReceipt>;
31
+ target: Record<ScopeMigrationFile, string>;
32
+ }
33
+
34
+ const LEGACY_PLANES = new Map([["contract", "strategy"], ["working", "tactics"]]);
35
+ const LEGACY_SEMANTIC_PLANES = new Set(["intents", "contract", "working", "artifacts", "response", "lazy"]);
36
+ const LEGACY_LOCATOR = /^(?:((?:effective|global|cwd|session)(?:\[\d+\])?(?:\.patches(?:\[\d+\])?)?)\.)?(contract|working)(?=$|\.|\[)/;
37
+ const MARKED_LOCATOR = /(?<![\p{L}\p{N}_$.[\]:/\\-])\$([\p{L}\p{N}_$.[\]:-]+)(?![\p{L}\p{N}_$.[\]:/\\-])/gu;
38
+
39
+ function translateLocator(path: string): string {
40
+ const match = LEGACY_LOCATOR.exec(path);
41
+ if (!match) return path;
42
+ const [, prefix, plane] = match;
43
+ const suffix = path.slice(match[0].length);
44
+ try {
45
+ if (prefix) parseStateReadPath(prefix, MAX_HISTORY_LIMIT);
46
+ // The unchanged lazy root lets the shared parser validate selectors after legacy names retire.
47
+ parseValuePath(`lazy${suffix}`);
48
+ } catch {
49
+ return path;
50
+ }
51
+ return `${prefix ? `${prefix}.` : ""}${LEGACY_PLANES.get(plane!)!}${suffix}`;
52
+ }
53
+
54
+ function translateReferences(value: JsonValue): JsonValue {
55
+ if (typeof value === "string") return value.replace(MARKED_LOCATOR, (_token, path: string) => `$${translateLocator(path)}`);
56
+ if (Array.isArray(value)) return value.map(translateReferences);
57
+ if (!isObject(value)) return value;
58
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key,
59
+ key === "$ref" && typeof child === "string" ? translateLocator(child) : translateReferences(child)]));
60
+ }
61
+
62
+ function translateArtifacts(value: JsonValue): JsonValue {
63
+ if (!isObject(value)) return translateReferences(value);
64
+ return Object.fromEntries(Object.entries(value).map(([path, card]) => [path, !isObject(card) ? translateReferences(card)
65
+ : Object.fromEntries(Object.entries(card).map(([key, child]) => [key,
66
+ // Retired embedded provenance is evidence, never a semantic reference namespace.
67
+ ["hash", "compiler", "compiled_at"].includes(key) ? structuredClone(child) : translateReferences(child)]))]));
68
+ }
69
+
70
+ /** Translate a raw legacy checkpoint or replay patch before any known-field filtering. */
71
+ export function translateLegacySemanticObject(value: unknown): JsonObject {
72
+ validatePatch(value);
73
+ for (const plane of LEGACY_PLANES.values()) {
74
+ if (Object.hasOwn(value, plane)) throw new Error(`Target State Flow semantic plane in legacy format: ${plane}`);
75
+ }
76
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [LEGACY_PLANES.get(key) ?? key,
77
+ key === "artifacts" ? translateArtifacts(child) : LEGACY_SEMANTIC_PLANES.has(key) ? translateReferences(child) : structuredClone(child)]));
78
+ }
79
+
80
+ /** Build a detached target candidate; publication still belongs to the guarded storage transaction. */
81
+ export function prepareSemanticMigration(
82
+ value: unknown, scope: StateScope, sourceVersion: number, historyLimit = MAX_HISTORY_LIMIT, targetModules?: RetainedModules,
83
+ ): SemanticMigrationCandidate {
84
+ if (sourceVersion !== 1 && sourceVersion !== 2 && sourceVersion !== 3 && sourceVersion !== 4) throw new Error("Unsupported State Flow migration source version");
85
+ validatePatch(value);
86
+ if (!isObject(value.checkpoint) || !Array.isArray(value.patches)) throw new Error("Invalid temporal checkpoint/tail envelope");
87
+ if (targetModules !== undefined) validateRetainedModules(targetModules);
88
+ if (sourceVersion === 4) {
89
+ validateRetainedModules(value.modules);
90
+ if (targetModules !== undefined) assertRetainedModules(value.modules, targetModules);
91
+ }
92
+ else if (Object.hasOwn(value, "modules")) throw new Error("Legacy State Flow stream cannot declare modules");
93
+ const translate = (state: unknown): JsonObject => {
94
+ if (sourceVersion !== 4 && targetModules !== undefined && isObject(state)
95
+ && Object.keys(state).some(key => key.startsWith("x_"))) throw new Error("Legacy State Flow external namespace collision");
96
+ if (sourceVersion < 3) return translateLegacySemanticObject(state);
97
+ validatePatch(state);
98
+ assertNoRetiredSemanticPlanes(state);
99
+ return structuredClone(state);
100
+ };
101
+ // Preserve envelope fields until strict target validation; malformed input must not be sanitized into acceptance.
102
+ const stream = {
103
+ ...structuredClone(value),
104
+ ...(targetModules === undefined ? {} : { modules: structuredClone(targetModules) }),
105
+ checkpoint: { ...structuredClone(value.checkpoint), state: translate(value.checkpoint.state) },
106
+ patches: value.patches.map((record) => {
107
+ if (!isObject(record)) throw new Error("Invalid temporal patch envelope");
108
+ return { ...structuredClone(record), patch: translate(record.patch) };
109
+ }),
110
+ };
111
+ validateScopeStream(stream, scope, historyLimit);
112
+ const targetVersion = sourceVersion === 4 || targetModules !== undefined ? 4 : 3;
113
+ return { sourceVersion, targetVersion, requiresConversion: sourceVersion !== targetVersion, stream };
114
+ }
115
+
116
+ function captureMigrationBytes(value: Uint8Array | undefined, label: string): MigrationByteReceipt {
117
+ if (!(value instanceof Uint8Array)) throw new Error(`Incomplete State Flow migration cohort: ${label} bytes are required`);
118
+ const bytes = new Uint8Array(value);
119
+ return { bytes, identity: `sha256:${createHash("sha256").update(bytes).digest("hex")}` };
120
+ }
121
+
122
+ function decodeMigrationText(bytes: Uint8Array, label: string): string {
123
+ try {
124
+ // Do not silently repair invalid UTF-8 or strip a BOM that ordinary JSON decoding would reject.
125
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
126
+ } catch {
127
+ throw new Error(`State Flow migration ${label} is not valid UTF-8`);
128
+ }
129
+ }
130
+
131
+ function parseMigrationJson(source: string, label: string): JsonValue {
132
+ let value: unknown;
133
+ try { value = JSON.parse(source); }
134
+ catch { throw new Error(`State Flow migration ${label} contains invalid JSON`); }
135
+ if (!isJsonValue(value)) throw new Error(`State Flow migration ${label} must contain finite JSON data`);
136
+ return value;
137
+ }
138
+
139
+ /** Decode a complete raw owner cohort before filtering; never load files, normalize authority or publish. */
140
+ export function prepareSerializedScopeMigration(
141
+ sources: ScopeMigrationSources, scope: StateScope, expectedCwd?: string, targetModules?: RetainedModules,
142
+ ): SerializedSemanticMigrationCandidate {
143
+ const basis = {
144
+ checkpoint: captureMigrationBytes(sources.checkpoint, "checkpoint"),
145
+ patches: captureMigrationBytes(sources.patches, "patches"),
146
+ meta: captureMigrationBytes(sources.meta, "metadata"),
147
+ };
148
+ const metaText = decodeMigrationText(basis.meta.bytes, "metadata");
149
+ const metadata = parseMigrationJson(metaText, "metadata");
150
+ if (!isObject(metadata)) throw new Error("Invalid State Flow migration metadata");
151
+ const version = metadata.version;
152
+ if (version !== 1 && version !== 2 && version !== 3 && version !== 4) throw new Error("Unsupported State Flow migration source version");
153
+ if (version === 4) validateRetainedModules(metadata.modules);
154
+ else if (targetModules !== undefined && Object.hasOwn(metadata, "modules")) throw new Error("Legacy State Flow modules metadata collision");
155
+ if (scope === "cwd") {
156
+ if (expectedCwd === undefined) throw new Error("State Flow CWD migration requires its canonical identity");
157
+ if (metadata.owner === undefined) throw new Error("State Flow CWD scope identity is missing");
158
+ if (!isObject(metadata.owner) || Object.keys(metadata.owner).join(",") !== "cwd" || metadata.owner.cwd !== resolve(expectedCwd)) {
159
+ throw new Error("State Flow CWD migration identity mismatch");
160
+ }
161
+ } else if (expectedCwd !== undefined) throw new Error("Only State Flow CWD migration accepts a CWD identity");
162
+ if (Object.hasOwn(metadata, "artifacts")) parseArtifactProvenanceRegistry(metadata.artifacts, "State Flow migration provenance");
163
+ const temporal = metadata.temporal;
164
+ if (!isObject(temporal) || !Object.hasOwn(temporal, "checkpoint") || !Array.isArray(temporal.patches)) {
165
+ throw new Error("Invalid State Flow migration temporal metadata");
166
+ }
167
+ if (version >= 3 && !Object.hasOwn(temporal, "revision")) throw new Error("State Flow target migration metadata requires a revision");
168
+ const checkpointText = decodeMigrationText(basis.checkpoint.bytes, "checkpoint");
169
+ const patchesText = decodeMigrationText(basis.patches.bytes, "tail");
170
+ const checkpoint = parseMigrationJson(checkpointText, "checkpoint");
171
+ const patches = patchesText.split(/\r?\n/).flatMap((line, index) => line.trim().length === 0 ? []
172
+ : [parseMigrationJson(line, `tail line ${index + 1}`)]);
173
+ const boundaries = temporal.patches;
174
+ if (boundaries.length !== patches.length) throw new Error("State Flow migration metadata does not match its semantic tail");
175
+ const candidate = prepareSemanticMigration({
176
+ ...(version === 4 ? { modules: metadata.modules } : {}),
177
+ revision: temporal.revision === undefined ? patches.length : temporal.revision,
178
+ checkpoint: { through: temporal.checkpoint, state: checkpoint },
179
+ patches: patches.map((patch, index) => ({ transition: boundaries[index], patch })),
180
+ }, scope, version, MAX_HISTORY_LIMIT, targetModules);
181
+ const target = !candidate.requiresConversion && targetModules === undefined ? { checkpoint: checkpointText, patches: patchesText, meta: metaText } : {
182
+ checkpoint: `${canonicalJson(candidate.stream.checkpoint.state)}\n`,
183
+ patches: candidate.stream.patches.map((record) => `${canonicalJson(record.patch)}\n`).join(""),
184
+ meta: `${canonicalJson({ ...metadata, version: candidate.targetVersion,
185
+ ...(candidate.stream.modules === undefined ? {} : { modules: candidate.stream.modules }),
186
+ temporal: { ...temporal, revision: candidate.stream.revision } })}\n`,
187
+ };
188
+ return { ...candidate, basis, target };
189
+ }
@@ -0,0 +1,136 @@
1
+ // Domain: detached model/canonical patch, locator and structured-reference translation.
2
+ // Excludes: SDK schemas, mode, acquisition, existence checks, storage and request/publication authority.
3
+ import { isJsonValue, isObject, type JsonObject, type JsonValue, type MissingDeletion } from "./json.ts";
4
+ import { DEFAULT_HISTORY_LIMIT } from "./history.ts";
5
+ import { parseStateReadPath, parseValuePath } from "./query.ts";
6
+ import { captureScopeSelection, type ScopeSelection } from "./selection.ts";
7
+ import type { AtomicScopePatches, StateScope } from "./state.ts";
8
+
9
+ import { MODEL_PLANE_NAMES } from "./planes.ts";
10
+
11
+ const PATCH_PLANES = MODEL_PLANE_NAMES;
12
+ const SEMANTIC_ROOT = /^(?:intents|strategy|tactics|artifacts|response|lazy|x_[a-z][a-z0-9_]*)(?=\.|\[|$)/;
13
+ const PATCH_PATH = /^(?:effective|global|cwd|session)\.patches(?:\[\d+\])?$/;
14
+
15
+ function dense(value: JsonValue): boolean {
16
+ if (Array.isArray(value)) {
17
+ for (let i = 0; i < value.length; i++) if (!Object.hasOwn(value, i) || !dense(value[i]!)) return false;
18
+ return true;
19
+ }
20
+ return !isObject(value) || Object.values(value).every(dense);
21
+ }
22
+
23
+ function capture(value: unknown): JsonValue {
24
+ if (!isJsonValue(value)) throw new Error("Model interface requires finite acyclic JSON data");
25
+ let copy: unknown;
26
+ try { copy = structuredClone(value); } catch { throw new Error("Model interface requires detached JSON data"); }
27
+ if (!isJsonValue(copy) || !dense(copy)) throw new Error("Model interface requires finite dense JSON data");
28
+ return copy;
29
+ }
30
+
31
+ function translateReferences(value: JsonValue, translate: (ref: string) => string): JsonValue {
32
+ if (Array.isArray(value)) return value.map(child => translateReferences(child, translate));
33
+ if (!isObject(value)) return value;
34
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key,
35
+ key === "$ref" && typeof child === "string" ? translate(child) : translateReferences(child, translate)]));
36
+ }
37
+
38
+ function semanticReference(path: string, external?: readonly string[]): boolean {
39
+ const root = SEMANTIC_ROOT.exec(path)?.[0];
40
+ if (!root || root.startsWith("x_") && external !== undefined && !external.includes(root)) return false;
41
+ try { parseValuePath(path); return true; } catch { return false; }
42
+ }
43
+
44
+ /** Anchor eligible flat current references, never text/resource/effective/historical references. */
45
+ export function canonicalizeModelReferences(value: unknown, scopes: ScopeSelection, external: readonly string[] = []): JsonValue {
46
+ const selected = captureScopeSelection(scopes), copy = capture(value);
47
+ return selected.length !== 1 ? copy : translateReferences(copy, ref => semanticReference(ref, external) ? `${selected[0]}.${ref}` : ref);
48
+ }
49
+
50
+ /** Strip only validated same-owner current semantic reference prefixes; canonical data stays untouched. */
51
+ export function projectModelReferences(value: unknown, scopes: ScopeSelection, external: readonly string[] = []): JsonValue {
52
+ const selected = captureScopeSelection(scopes), copy = capture(value), prefix = `${selected[0]}.`;
53
+ return selected.length !== 1 ? copy : translateReferences(copy, ref => ref.startsWith(prefix) && semanticReference(ref.slice(prefix.length), external) ? ref.slice(prefix.length) : ref);
54
+ }
55
+
56
+ function admitKeys(value: JsonObject, selected: ScopeSelection, external: readonly string[]): void {
57
+ const keys = Object.keys(value);
58
+ if (!keys.length) throw new Error("Model patch requires at least one supplied branch");
59
+ if (selected.length === 1) {
60
+ if (keys.some(key => !PATCH_PLANES.includes(key) && !external.includes(key))) throw new Error("Single-owner model patches require flat semantic planes");
61
+ } else for (const key of keys) {
62
+ if (!selected.includes(key as StateScope)) throw new Error(`Multi-owner model patches require participating explicit owners; does not accept field ${key}`);
63
+ }
64
+ }
65
+
66
+ function planePatch(value: unknown, external: readonly string[]): asserts value is JsonObject {
67
+ if (!isObject(value) || !Object.keys(value).length) throw new Error("Model scope patch must be a nonempty object");
68
+ for (const key of Object.keys(value)) {
69
+ if (!PATCH_PLANES.includes(key) && !external.includes(key)) throw new Error(`Model patch does not accept semantic plane ${key}`);
70
+ if (!isObject(value[key])) throw new Error(`Model patch plane ${key} must be a JSON object`);
71
+ }
72
+ }
73
+
74
+ /** Value normalization only; callers must validate their original request/owner before invoking it. */
75
+ export function normalizeModelPatches(value: unknown, scopes: ScopeSelection, external: readonly string[] = []): AtomicScopePatches {
76
+ const selected = captureScopeSelection(scopes);
77
+ if (!isObject(value)) throw new Error("Model patch must be an object");
78
+ // Reject shape/excluded owners before touching a supplied owner's payload or cloning it.
79
+ admitKeys(value, selected, external);
80
+ const copy = (selected.length === 1 ? canonicalizeModelReferences(value, selected, external) : capture(value)) as JsonObject;
81
+ admitKeys(copy, selected, external);
82
+ if (selected.length === 1) {
83
+ planePatch(copy, external);
84
+ return { [selected[0]]: copy };
85
+ }
86
+ for (const key of Object.keys(copy)) planePatch(copy[key], external);
87
+ return copy as AtomicScopePatches;
88
+ }
89
+
90
+ /** Compilation target syntax, not a query locator: preserve the exact opaque source key. */
91
+ export function projectModelArtifactTarget(scope: StateScope, path: string, scopes: ScopeSelection): string {
92
+ const selected = captureScopeSelection(scopes);
93
+ if (!selected.includes(scope)) throw new Error("Model artifact target scope is excluded");
94
+ if (typeof path !== "string") throw new Error("Model artifact target path must be a string");
95
+ return `${selected.length === 1 ? "" : scope + "."}artifacts[${JSON.stringify(path)}]`;
96
+ }
97
+
98
+ /** Known deletion-report JSON pointers only; do not parse source keys or rewrite diagnostics. */
99
+ export function projectModelMissingDeletion(deletion: MissingDeletion, scopes: ScopeSelection): MissingDeletion {
100
+ const selected = captureScopeSelection(scopes);
101
+ const project = (path: string) => {
102
+ if (typeof path !== "string") throw new Error("Model deletion locator must be a string");
103
+ const prefix = `/${selected[0]}`;
104
+ return selected.length === 1 && (path === prefix || path.startsWith(prefix + "/")) ? path.slice(prefix.length) : path;
105
+ };
106
+ return { path: project(deletion.path), unavailablePath: project(deletion.unavailablePath) };
107
+ }
108
+
109
+ function validateReadPath(path: string, selected: ScopeSelection, historyLimit: number): void {
110
+ const query = PATCH_PATH.test(path) ? parseStateReadPath(path, historyLimit) : parseStateReadPath(parseValuePath(path).root, historyLimit);
111
+ if (query.scope !== undefined && !selected.includes(query.scope)) throw new Error("Model read scope is excluded");
112
+ }
113
+
114
+ /** Sole-owner locators bind to that exact owner; multi-owner effective aliases keep their existing meaning. */
115
+ export function normalizeModelReadPath(path: string, scopes: ScopeSelection, historyLimit = DEFAULT_HISTORY_LIMIT): string {
116
+ const selected = captureScopeSelection(scopes);
117
+ if (typeof path !== "string") throw new Error("Model read path must be a string");
118
+ let canonical = path;
119
+ if (selected.length === 1) {
120
+ if (/^patches(?:\[\d+\])?$/.test(path)) canonical = `${selected[0]}.${path}`;
121
+ else if (SEMANTIC_ROOT.test(path)) canonical = `${selected[0]}.${path}`;
122
+ else if (/^effective(?=\.|\[|$)/.test(path)) canonical = selected[0] + path.slice("effective".length);
123
+ }
124
+ validateReadPath(canonical, selected, historyLimit);
125
+ return canonical;
126
+ }
127
+
128
+ /** Presentation locator only: history keeps an explicit projection root and does not become an array index. */
129
+ export function projectModelReadPath(path: string, scopes: ScopeSelection, historyLimit = DEFAULT_HISTORY_LIMIT): string {
130
+ const selected = captureScopeSelection(scopes), canonical = normalizeModelReadPath(path, selected, historyLimit);
131
+ if (selected.length !== 1) return canonical;
132
+ const prefix = `${selected[0]}.`;
133
+ if (canonical.startsWith(prefix) && /^patches(?:\[\d+\])?$/.test(canonical.slice(prefix.length))) return canonical.slice(prefix.length);
134
+ if (canonical.startsWith(prefix) && semanticReference(canonical.slice(prefix.length))) return canonical.slice(prefix.length);
135
+ return "effective" + canonical.slice(selected[0].length);
136
+ }
@@ -0,0 +1,119 @@
1
+ // Domain: public module registration, native-host ownership and revocable catalog snapshots.
2
+ // Excludes: storage, callback evaluation, inference and semantic publication.
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { JsonObject, JsonValue } from "./json.ts";
5
+ import type { StateFlowModule } from "./semantic-module.ts";
6
+
7
+ type SessionManager = ExtensionContext["sessionManager"];
8
+ type Definition = StateFlowModule<JsonObject, JsonValue>;
9
+ interface Binding {
10
+ readonly manager: SessionManager;
11
+ readonly checkOwner: () => void;
12
+ readonly register: (definition: Definition) => () => void;
13
+ readonly modules: Map<string, Definition>;
14
+ controller: AbortController;
15
+ }
16
+ interface Bridge { readonly version: 1; readonly hosts: WeakMap<SessionManager, Binding> }
17
+ const BRIDGE = Symbol.for("@llblab/pi-state-flow/modules/v1");
18
+
19
+ function bridge(create = false): Bridge | undefined {
20
+ const globals = globalThis as unknown as Record<symbol, unknown>;
21
+ let value = globals[BRIDGE];
22
+ if (value === undefined && create) globals[BRIDGE] = value = { version: 1, hosts: new WeakMap() } satisfies Bridge;
23
+ if (value === undefined) return undefined;
24
+ if (!value || typeof value !== "object") throw new Error("Incompatible State Flow module registration bridge");
25
+ const candidate = value as Partial<Bridge>;
26
+ if (candidate.version !== 1 || !(candidate.hosts instanceof WeakMap)) throw new Error("Incompatible State Flow module registration bridge");
27
+ return candidate as Bridge;
28
+ }
29
+
30
+ function captureDefinition(definition: Definition): Definition {
31
+ if (!definition || typeof definition !== "object") throw new Error("Invalid State Flow module registration");
32
+ const { apiVersion, id, schemaVersion, immutable, validate, context, captureInput, produce } = definition;
33
+ if (apiVersion !== 1) throw new Error("Unsupported State Flow module API version");
34
+ if (typeof id !== "string" || !/^x_[a-z][a-z0-9_]*$/.test(id)) throw new Error("External State Flow module id must match x_[a-z][a-z0-9_]*");
35
+ if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) throw new Error("Invalid State Flow module schema version");
36
+ if (typeof immutable !== "boolean" || typeof validate !== "function" || typeof context !== "function"
37
+ || (captureInput !== undefined && typeof captureInput !== "function") || (produce !== undefined && typeof produce !== "function")) {
38
+ throw new Error("Invalid State Flow module callbacks or authoring policy");
39
+ }
40
+ // Preserve class/constructor-injected services while capturing policy and callback identities.
41
+ return Object.freeze({ apiVersion, id, schemaVersion, immutable, validate: validate.bind(definition), context: context.bind(definition),
42
+ ...(captureInput ? { captureInput: captureInput.bind(definition) } : {}), ...(produce ? { produce: produce.bind(definition) } : {}) });
43
+ }
44
+
45
+ export interface StateFlowModuleCatalog {
46
+ readonly modules: readonly Definition[];
47
+ readonly signal: AbortSignal;
48
+ assertCurrent(): void;
49
+ }
50
+
51
+ /** Registry metadata alone grants no content, source, private or publication authority. */
52
+ export function registerStateFlowModule<Content extends JsonObject, Input extends JsonValue>(
53
+ manager: SessionManager, definition: StateFlowModule<Content, Input>,
54
+ ): () => void {
55
+ const host = bridge()?.hosts.get(manager);
56
+ if (!host) throw new Error("State Flow module host unavailable; load State Flow first and register at session_start");
57
+ return host.register(definition as unknown as Definition);
58
+ }
59
+
60
+ /** One composition-root owner; catalogs are revoked independently of memory mode or storage. */
61
+ export class StateFlowModuleHost {
62
+ #binding: Binding | undefined;
63
+ bind(manager: SessionManager, checkOwner: () => void): void {
64
+ checkOwner();
65
+ const hosts = bridge(true)!.hosts;
66
+ const existing = hosts.get(manager);
67
+ if (existing && existing !== this.#binding) throw new Error("State Flow module host already bound to this native session manager");
68
+ this.dispose();
69
+ if (this.#binding || hosts.has(manager)) throw new Error("State Flow module host changed during rebind");
70
+ checkOwner();
71
+ const binding: Binding = { manager, checkOwner, modules: new Map(), controller: new AbortController(), register: definition => this.#register(binding, definition) };
72
+ this.#binding = binding;
73
+ hosts.set(manager, binding);
74
+ }
75
+ #check(binding: Binding): void {
76
+ if (this.#binding !== binding || bridge()?.hosts.get(binding.manager) !== binding) throw new Error("State Flow module host ownership changed");
77
+ binding.checkOwner();
78
+ }
79
+ #changed(binding: Binding): void {
80
+ const previous = binding.controller;
81
+ binding.controller = new AbortController();
82
+ previous.abort(new Error("State Flow module catalog changed"));
83
+ }
84
+ #register(binding: Binding, definition: Definition): () => void {
85
+ this.#check(binding);
86
+ const captured = captureDefinition(definition);
87
+ this.#check(binding);
88
+ if (binding.modules.has(captured.id)) throw new Error(`State Flow module id already registered: ${captured.id}`);
89
+ binding.modules.set(captured.id, captured);
90
+ this.#changed(binding);
91
+ return () => {
92
+ if (this.#binding !== binding || binding.modules.get(captured.id) !== captured) return;
93
+ binding.modules.delete(captured.id);
94
+ this.#changed(binding);
95
+ };
96
+ }
97
+ captureIfBound(): StateFlowModuleCatalog | undefined {
98
+ return this.#binding ? this.capture() : undefined;
99
+ }
100
+ capture(): StateFlowModuleCatalog {
101
+ const binding = this.#binding;
102
+ if (!binding) throw new Error("State Flow module host unavailable");
103
+ this.#check(binding);
104
+ const signal = binding.controller.signal;
105
+ return Object.freeze({ modules: Object.freeze([...binding.modules.values()]), signal, assertCurrent: () => {
106
+ this.#check(binding);
107
+ signal.throwIfAborted();
108
+ } });
109
+ }
110
+ dispose(): void {
111
+ const binding = this.#binding;
112
+ if (!binding) return;
113
+ this.#binding = undefined;
114
+ const hosts = bridge()?.hosts;
115
+ if (hosts?.get(binding.manager) === binding) hosts.delete(binding.manager);
116
+ binding.modules.clear();
117
+ binding.controller.abort(new Error("State Flow module host retired"));
118
+ }
119
+ }
@@ -31,8 +31,8 @@ export class OwnedOperationSlot<E extends OwnedOperation> {
31
31
  /** Abort and forget the current owner; returns its operation for optional awaiting. */
32
32
  cancel(): E["operation"] | undefined {
33
33
  const entry = this.#current;
34
- entry?.controller.abort();
35
34
  this.#current = undefined;
35
+ entry?.controller.abort();
36
36
  return entry?.operation;
37
37
  }
38
38
 
@@ -66,8 +66,9 @@ export class RenewableLifetime {
66
66
 
67
67
  /** Abort current holders and open a fresh lifetime. */
68
68
  renew(reason?: unknown): void {
69
- this.#controller.abort(reason);
69
+ const previous = this.#controller;
70
70
  this.#controller = new AbortController();
71
+ previous.abort(reason);
71
72
  }
72
73
 
73
74
  /** Abort current holders without opening a new lifetime (shutdown). */
@@ -2,7 +2,7 @@ import { isObject, type JsonObject, type JsonValue } from "./json.ts";
2
2
  import type { SemanticState, StateScope } from "./state.ts";
3
3
 
4
4
  /** Planes whose object keys an intent may own by structured reference. */
5
- export type OwnedPlane = "working" | "lazy";
5
+ export type OwnedPlane = "strategy" | "tactics" | "lazy";
6
6
 
7
7
  /** One same-scope object-key target of an intent ownership reference. */
8
8
  export interface OwnedPath {
@@ -10,14 +10,14 @@ export interface OwnedPath {
10
10
  keys: string[];
11
11
  }
12
12
 
13
- const OWNED_PLANES = new Set<string>(["working", "lazy"]);
13
+ const OWNED_PLANES = new Set<string>(["strategy", "tactics", "lazy"]);
14
14
  // Same key grammar as read_state value paths; array selectors never parse.
15
15
  const OBJECT_KEY = /^[A-Za-z_$][A-Za-z0-9_$-]*$/;
16
16
 
17
17
  /**
18
18
  * Parse one structured `$ref` target as an ownership path of `scope`.
19
19
  * Cross-scope, unscoped, `effective`, historical, plane-root, array-element
20
- * and non-`working`/`lazy` targets are not ownership targets.
20
+ * and non-`strategy`/`tactics`/`lazy` targets are not ownership targets.
21
21
  */
22
22
  export function parseOwnedPath(scope: StateScope, ref: string): OwnedPath | undefined {
23
23
  const parts = ref.split(".");
@@ -68,7 +68,7 @@ function exists(state: SemanticState, path: OwnedPath): boolean {
68
68
 
69
69
  /**
70
70
  * Compute the same-scope ownership cascade for intent keys removed between
71
- * `before` and `after` (the state after authored operations). A target is
71
+ * `before` and `after` (the normalized intent-phase view, before other writes). A target is
72
72
  * kept while any remaining intent references it, an ancestor or a descendant;
73
73
  * missing targets are skipped. The result is minimal (no target below
74
74
  * another) and deterministically ordered.
@@ -111,9 +111,9 @@ export function formatOwnedPath(scope: StateScope, path: OwnedPath): string {
111
111
  return [scope, path.plane, ...path.keys].join(".");
112
112
  }
113
113
 
114
- /** Top-level `working`/`lazy` keys that at least one current intent of `scope` owns. */
114
+ /** Top-level ownership-eligible keys referenced by current intents of `scope`. */
115
115
  export function ownedTopLevelKeys(scope: StateScope, state: SemanticState): Record<OwnedPlane, Set<string>> {
116
- const owned: Record<OwnedPlane, Set<string>> = { working: new Set(), lazy: new Set() };
116
+ const owned: Record<OwnedPlane, Set<string>> = { strategy: new Set(), tactics: new Set(), lazy: new Set() };
117
117
  const intents = isObject(state.intents) ? state.intents : {};
118
118
  for (const value of Object.values(intents)) for (const path of extractOwnedPaths(scope, value)) owned[path.plane].add(path.keys[0]!);
119
119
  return owned;