opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8

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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/index.js +1836 -0
  4. package/package.json +70 -4
  5. package/skills/codebase-design/SKILL.md +114 -0
  6. package/skills/design-contract/SKILL.md +27 -0
  7. package/skills/design-critique/SKILL.md +19 -0
  8. package/skills/design-system/SKILL.md +17 -0
  9. package/skills/design-system/references/image-analysis.md +14 -0
  10. package/skills/design-system/references/tokens.md +32 -0
  11. package/skills/design-system/references/ui-rules.md +14 -0
  12. package/skills/diagnosing-bugs/SKILL.md +138 -0
  13. package/skills/frontend-design/SKILL.md +36 -0
  14. package/skills/handoff/SKILL.md +16 -0
  15. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  16. package/skills/linter-negative/SKILL.md +74 -0
  17. package/skills/no-slop-ui/SKILL.md +26 -0
  18. package/skills/prototype/SKILL.md +26 -0
  19. package/skills/registry.json +44087 -0
  20. package/skills/tastemaker/SKILL.md +8 -0
  21. package/skills/tastemaker/references/tastemaker.md +14 -0
  22. package/skills/tdd/SKILL.md +38 -0
  23. package/skills/visual-review/SKILL.md +103 -0
  24. package/skills/visual-review/references/review-gates.md +42 -0
  25. package/skills/visual-review/references/surface-archetypes.md +37 -0
  26. package/skills/wizard/SKILL.md +44 -0
  27. package/skills/writing-for-agents/SKILL.md +81 -0
  28. package/src/cognition/cognition-store.js +251 -0
  29. package/src/cognition/cognitive-state.js +126 -0
  30. package/src/cognition/hypothesis-manager.js +113 -0
  31. package/src/cognition/observation-engine.js +112 -0
  32. package/src/cognition/observation-provenance.js +268 -0
  33. package/src/context/active-context-boundary.js +68 -0
  34. package/src/context/assembly.js +494 -0
  35. package/src/context/assessment-rules.js +55 -0
  36. package/src/context/capsule-staleness.js +267 -0
  37. package/src/context/capsules.js +2 -0
  38. package/src/context/compare-array.js +98 -0
  39. package/src/context/compare-object.js +89 -0
  40. package/src/context/compare-values.js +42 -0
  41. package/src/context/context-assembly-contract.js +71 -0
  42. package/src/context/context-budget-manager.js +232 -0
  43. package/src/context/context-builder.js +49 -0
  44. package/src/context/context-cache.js +55 -0
  45. package/src/context/context-capture.js +243 -0
  46. package/src/context/context-classification.js +199 -0
  47. package/src/context/context-compaction.js +179 -0
  48. package/src/context/context-decision-audit.js +267 -0
  49. package/src/context/context-demotion.js +173 -0
  50. package/src/context/context-dictionary.js +106 -0
  51. package/src/context/context-drift-detection.js +225 -0
  52. package/src/context/context-evaluation.js +374 -0
  53. package/src/context/context-event-ingress.js +100 -0
  54. package/src/context/context-event-normalization.js +224 -0
  55. package/src/context/context-feedback-loop.js +216 -0
  56. package/src/context/context-freshness.js +21 -0
  57. package/src/context/context-graph-builder.js +71 -0
  58. package/src/context/context-graph.js +451 -0
  59. package/src/context/context-history-relevance.js +321 -0
  60. package/src/context/context-interception.js +145 -0
  61. package/src/context/context-invalidation-events.js +154 -0
  62. package/src/context/context-lifecycle.js +170 -0
  63. package/src/context/context-loop-prevention.js +347 -0
  64. package/src/context/context-manager.js +340 -0
  65. package/src/context/context-memory-layers.js +265 -0
  66. package/src/context/context-minimality.js +166 -0
  67. package/src/context/context-normalization.js +130 -0
  68. package/src/context/context-optimization-metrics.js +434 -0
  69. package/src/context/context-output-policy.js +135 -0
  70. package/src/context/context-pack-authority.js +157 -0
  71. package/src/context/context-promotion.js +172 -0
  72. package/src/context/context-query-contract.js +101 -0
  73. package/src/context/context-ranking.js +164 -0
  74. package/src/context/context-rate-distortion.js +261 -0
  75. package/src/context/context-recovery.js +209 -0
  76. package/src/context/context-retrieval-integration.js +60 -0
  77. package/src/context/context-retrieval.js +93 -0
  78. package/src/context/context-router.js +457 -0
  79. package/src/context/context-routing-evaluation.js +331 -0
  80. package/src/context/context-routing-observability.js +296 -0
  81. package/src/context/context-routing-shadow.js +237 -0
  82. package/src/context/context-scope.js +181 -0
  83. package/src/context/context-snapshot.js +226 -0
  84. package/src/context/context-source-registry.js +95 -0
  85. package/src/context/context-sufficiency-gate.js +120 -0
  86. package/src/context/context-sufficiency-oracle.js +167 -0
  87. package/src/context/context-telemetry.js +176 -0
  88. package/src/context/context.js +633 -0
  89. package/src/context/dependency-closure.js +246 -0
  90. package/src/context/page-fault-tracker.js +275 -0
  91. package/src/context/prompt-engine.js +74 -0
  92. package/src/context/runtime-context-graph.js +260 -0
  93. package/src/context/selection.js +2 -0
  94. package/src/context/session.js +2 -0
  95. package/src/context/sufficiency-contract.js +300 -0
  96. package/src/context/tokenizer.js +17 -0
  97. package/src/context-benchmark-router.mjs +125 -0
  98. package/src/evidence/evidence-engine.js +161 -0
  99. package/src/evidence/evidence-factory.js +3 -0
  100. package/src/evidence/evidence-freshness.js +463 -0
  101. package/src/evidence/evidence-gap.js +76 -0
  102. package/src/evidence/evidence-lineage.js +525 -0
  103. package/src/evidence/evidence.js +136 -0
  104. package/src/evidence/freshness-validation.js +187 -0
  105. package/src/evidence/lineage.js +4 -0
  106. package/src/execution/action-evaluation.js +179 -0
  107. package/src/execution/circuit-breaker.js +70 -0
  108. package/src/execution/claim-interception.js +63 -0
  109. package/src/execution/execution-engine.js +258 -0
  110. package/src/execution/execution-intent.js +69 -0
  111. package/src/execution/execution-state.js +89 -0
  112. package/src/execution/formatting.js +60 -0
  113. package/src/execution/l1-sanitize.js +16 -0
  114. package/src/execution/l2-resource-guards.js +19 -0
  115. package/src/execution/l3-invariant-checker.js +5 -0
  116. package/src/execution/l4-containment.js +6 -0
  117. package/src/execution/runtime-guards.js +106 -0
  118. package/src/execution/strategy-identity.js +162 -0
  119. package/src/execution/tool-interceptor.js +7 -0
  120. package/src/execution/tool-result-classifier.js +63 -0
  121. package/src/integration/canary-deploy.js +143 -0
  122. package/src/integration/collector.js +59 -0
  123. package/src/integration/error-recovery.js +6 -0
  124. package/src/integration/event-bus.js +66 -0
  125. package/src/integration/health-checker.js +57 -0
  126. package/src/integration/logger.js +39 -0
  127. package/src/integration/message-handler.js +611 -0
  128. package/src/integration/opencode-db.js +153 -0
  129. package/src/integration/orchestration.js +419 -0
  130. package/src/integration/part-provenance.js +65 -0
  131. package/src/integration/rate-limiter.js +55 -0
  132. package/src/integration/release-gate.js +336 -0
  133. package/src/integration/rollback-manager.js +164 -0
  134. package/src/integration/router-adapter.js +269 -0
  135. package/src/integration/telemetry.js +45 -0
  136. package/src/integration/update-checker.js +10 -0
  137. package/src/persistence/event-sourcing.js +75 -0
  138. package/src/persistence/memory.js +501 -0
  139. package/src/persistence/migration-runner.js +16 -0
  140. package/src/persistence/persistence-manager.js +39 -0
  141. package/src/persistence/transaction-log.js +157 -0
  142. package/src/policy/action-capabilities.js +116 -0
  143. package/src/policy/action-policy.js +4 -0
  144. package/src/policy/assessment-engine.js +104 -0
  145. package/src/policy/assumption-tracking.js +66 -0
  146. package/src/policy/budget-enforcer.js +7 -0
  147. package/src/policy/config-validator.js +102 -0
  148. package/src/policy/execution-assessment.js +142 -0
  149. package/src/policy/failure-classification.js +5 -0
  150. package/src/policy/feature-flags.js +65 -0
  151. package/src/policy/governance-enforcement.js +118 -0
  152. package/src/policy/locale.js +15 -0
  153. package/src/policy/multi-signer.js +151 -0
  154. package/src/policy/policy-state-machine.js +97 -0
  155. package/src/policy/ponytail-constraint.js +38 -0
  156. package/src/policy/project-inspector.js +93 -0
  157. package/src/policy/query-builder.js +14 -0
  158. package/src/policy/request-classifier.js +79 -0
  159. package/src/policy/risk-engine.js +266 -0
  160. package/src/policy/scope-enforcement.js +140 -0
  161. package/src/policy/secret-scanner.js +68 -0
  162. package/src/policy/skill-routing.js +104 -0
  163. package/src/policy/strategy-capabilities.js +211 -0
  164. package/src/policy/strategy.js +6 -0
  165. package/src/policy/uncertainty.js +117 -0
  166. package/src/shared/container.js +44 -0
  167. package/src/shared/decision-types.js +24 -0
  168. package/src/shared/simple-test.js +160 -0
  169. package/src/shared/wam-events.js +52 -0
  170. package/src/skills/engine.js +1705 -0
  171. package/src/skills/skill-registry.js +19 -0
  172. package/src/skills/skill-routing.js +340 -0
  173. package/src/state/lifecycle-manager.js +113 -0
  174. package/src/state/operational-state.js +198 -0
  175. package/src/state/projections.js +47 -0
  176. package/src/state/replay-engine.js +5 -0
  177. package/src/state/requirement-ownership.js +20 -0
  178. package/src/state/requirement-state.js +152 -0
  179. package/src/state/run-event-identity.js +172 -0
  180. package/src/state/state-engine.js +19 -0
  181. package/src/state/state-machine.js +119 -0
  182. package/src/state/state-schema.js +318 -0
  183. package/src/state/state-store.js +335 -0
  184. package/src/state/state-transition.js +84 -0
  185. package/src/state/task-dependencies.js +287 -0
  186. package/src/state/task-execution.js +217 -0
  187. package/src/state/task-lifecycle.js +59 -0
  188. package/src/state/task-runs.js +361 -0
  189. package/src/state/task-state-file.js +86 -0
  190. package/src/state/task-state.js +41 -0
  191. package/src/state/task-status.js +54 -0
  192. package/src/state/task-store.js +48 -0
  193. package/src/state/wam-state.js +148 -0
  194. package/src/verification/completion-gate-simple.js +8 -0
  195. package/src/verification/completion-gate.js +183 -0
  196. package/src/verification/contract.js +9 -0
  197. package/src/verification/contradiction-resolution.js +255 -0
  198. package/src/verification/diagnostic-engine.js +659 -0
  199. package/src/verification/false-completion-prevention.js +100 -0
  200. package/src/verification/hook-validator.js +6 -0
  201. package/src/verification/requirements.js +6 -0
  202. package/src/verification/verification-context.js +162 -0
  203. package/src/verification/verification-lifecycle.js +66 -0
  204. package/src/verification/verification-model.js +89 -0
  205. package/src/verification/verification-persistence.js +129 -0
  206. package/src/verification/verification-policy.js +127 -0
  207. package/src/verification/verification.js +415 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Context Router Dependency Closure — proper traversal without depth limits.
3
+ *
4
+ * Separates graph traversal from context admission.
5
+ * Traversal finds ALL dependencies; admission decides which enter the pack.
6
+ *
7
+ * Safety limits (for preventing explosion):
8
+ * - maxNodes: maximum nodes to collect
9
+ * - maxTraversalCost: maximum operations
10
+ * - cycle detection via visited set
11
+ *
12
+ * These are SAFETY limits, not semantic limits.
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} TraversalOptions
17
+ * @property {number} [maxNodes=100] - Safety limit for node count
18
+ * @property {number} [maxCost=1000] - Safety limit for traversal operations
19
+ * @property {string[]} [edgeTypes] - Edge types to follow (default: all)
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} TraversalResult
24
+ * @property {string[]} nodes - Collected node IDs
25
+ * @property {number} cost - Actual traversal cost
26
+ * @property {boolean} limitReached - Whether safety limit was hit
27
+ * @property {string} limitType - Which limit was reached (if any)
28
+ */
29
+
30
+ /**
31
+ * Traverse upstream dependencies from a node.
32
+ * Follows depends_on and produces edges until:
33
+ * - no more edges
34
+ * - cycle detected
35
+ * - safety limit reached
36
+ *
37
+ * @param {Object} graph - Context graph
38
+ * @param {string} startNodeId - Starting node
39
+ * @param {TraversalOptions} options
40
+ * @returns {TraversalResult}
41
+ */
42
+ export function traverseUpstream(graph, startNodeId, options = {}) {
43
+ const { maxNodes = 100, maxCost = 1000, edgeTypes } = options;
44
+
45
+ const visited = new Set();
46
+ const nodes = [];
47
+ let cost = 0;
48
+
49
+ const queue = [startNodeId];
50
+
51
+ while (queue.length > 0) {
52
+ cost++;
53
+ if (cost > maxCost) {
54
+ return {
55
+ nodes,
56
+ cost,
57
+ limitReached: true,
58
+ limitType: "maxCost",
59
+ };
60
+ }
61
+
62
+ if (nodes.length >= maxNodes) {
63
+ return {
64
+ nodes,
65
+ cost,
66
+ limitReached: true,
67
+ limitType: "maxNodes",
68
+ };
69
+ }
70
+
71
+ const nodeId = queue.shift();
72
+
73
+ if (visited.has(nodeId)) continue;
74
+ visited.add(nodeId);
75
+
76
+ const node = graph.getNode(nodeId);
77
+ if (!node) continue;
78
+
79
+ nodes.push(nodeId);
80
+
81
+ // Follow depends_on edges (upstream)
82
+ const dependsOn = graph.getEdgesFromByType
83
+ ? graph.getEdgesFromByType(nodeId, "depends_on")
84
+ : graph.getEdgesFrom(nodeId).filter((e) => e.type === "depends_on");
85
+
86
+ for (const edge of dependsOn) {
87
+ if (!visited.has(edge.to)) {
88
+ queue.push(edge.to);
89
+ }
90
+ }
91
+
92
+ // Follow produces edges (reverse direction - who produces this node)
93
+ const producesEdges = graph.getEdgesToByType
94
+ ? graph.getEdgesToByType(nodeId, "produces")
95
+ : graph.getEdgesTo(nodeId).filter((e) => e.type === "produces");
96
+
97
+ for (const edge of producesEdges) {
98
+ if (!visited.has(edge.from)) {
99
+ queue.push(edge.from);
100
+ }
101
+ }
102
+ }
103
+
104
+ return {
105
+ nodes,
106
+ cost,
107
+ limitReached: false,
108
+ limitType: null,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Traverse downstream effects from a node.
114
+ * Follows produces and depends_on edges in forward direction.
115
+ *
116
+ * @param {Object} graph - Context graph
117
+ * @param {string} startNodeId - Starting node
118
+ * @param {TraversalOptions} options
119
+ * @returns {TraversalResult}
120
+ */
121
+ export function traverseDownstream(graph, startNodeId, options = {}) {
122
+ const { maxNodes = 100, maxCost = 1000 } = options;
123
+
124
+ const visited = new Set();
125
+ const nodes = [];
126
+ let cost = 0;
127
+
128
+ const queue = [startNodeId];
129
+
130
+ while (queue.length > 0) {
131
+ cost++;
132
+ if (cost > maxCost) {
133
+ return { nodes, cost, limitReached: true, limitType: "maxCost" };
134
+ }
135
+
136
+ if (nodes.length >= maxNodes) {
137
+ return { nodes, cost, limitReached: true, limitType: "maxNodes" };
138
+ }
139
+
140
+ const nodeId = queue.shift();
141
+
142
+ if (visited.has(nodeId)) continue;
143
+ visited.add(nodeId);
144
+
145
+ const node = graph.getNode(nodeId);
146
+ if (!node) continue;
147
+
148
+ nodes.push(nodeId);
149
+
150
+ // Follow produces edges (downstream)
151
+ const produces = graph.getEdgesFromByType
152
+ ? graph.getEdgesFromByType(nodeId, "produces")
153
+ : graph.getEdgesFrom(nodeId).filter((e) => e.type === "produces");
154
+
155
+ for (const edge of produces) {
156
+ if (!visited.has(edge.to)) {
157
+ queue.push(edge.to);
158
+ }
159
+ }
160
+
161
+ // Follow depends_on edges (reverse - who depends on this)
162
+ const dependsOnEdges = graph.getEdgesToByType
163
+ ? graph.getEdgesToByType(nodeId, "depends_on")
164
+ : graph.getEdgesTo(nodeId).filter((e) => e.type === "depends_on");
165
+
166
+ for (const edge of dependsOnEdges) {
167
+ if (!visited.has(edge.from)) {
168
+ queue.push(edge.from);
169
+ }
170
+ }
171
+ }
172
+
173
+ return {
174
+ nodes,
175
+ cost,
176
+ limitReached: false,
177
+ limitType: null,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Compute full dependency closure for a set of nodes.
183
+ * Combines upstream and downstream traversal.
184
+ *
185
+ * @param {Object} graph - Context graph
186
+ * @param {string[]} nodeIds - Starting nodes
187
+ * @param {TraversalOptions} options
188
+ * @returns {TraversalResult}
189
+ */
190
+ export function computeDependencyClosure(graph, nodeIds, options = {}) {
191
+ const visited = new Set();
192
+ const allNodes = [];
193
+ let totalCost = 0;
194
+
195
+ for (const nodeId of nodeIds) {
196
+ const upstream = traverseUpstream(graph, nodeId, options);
197
+ const downstream = traverseDownstream(graph, nodeId, options);
198
+
199
+ for (const id of [...upstream.nodes, ...downstream.nodes]) {
200
+ if (!visited.has(id)) {
201
+ visited.add(id);
202
+ allNodes.push(id);
203
+ }
204
+ }
205
+
206
+ totalCost += upstream.cost + downstream.cost;
207
+
208
+ if (upstream.limitReached || downstream.limitReached) {
209
+ return {
210
+ nodes: allNodes,
211
+ cost: totalCost,
212
+ limitReached: true,
213
+ limitType: upstream.limitReached ? upstream.limitType : downstream.limitType,
214
+ };
215
+ }
216
+ }
217
+
218
+ return {
219
+ nodes: allNodes,
220
+ cost: totalCost,
221
+ limitReached: false,
222
+ limitType: null,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Check if traversal would hit safety limits.
228
+ * Useful for pre-flight checks.
229
+ *
230
+ * @param {Object} graph - Context graph
231
+ * @param {string} nodeId - Node to check
232
+ * @param {TraversalOptions} options
233
+ * @returns {{ safe: boolean, reason: string }}
234
+ */
235
+ export function checkTraversalSafety(graph, nodeId, options = {}) {
236
+ const result = traverseUpstream(graph, nodeId, { ...options, maxNodes: 10, maxCost: 20 });
237
+
238
+ if (result.limitReached) {
239
+ return {
240
+ safe: false,
241
+ reason: `Traversal hits ${result.limitType} limit early (${result.cost} ops, ${result.nodes.length} nodes)`,
242
+ };
243
+ }
244
+
245
+ return { safe: true, reason: "Traversal within safety bounds" };
246
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Page-Fault Tracker (C06 — page-fault-measurement).
3
+ *
4
+ * Tracks context page-faults: when omitted context is later requested via
5
+ * `retrieveContext()` (i.e. the system has to fetch what it had previously
6
+ * chosen NOT to inject). Provides per-fault metrics: fetch count, tokens
7
+ * fetched, latency, and reason classification.
8
+ *
9
+ * Design:
10
+ * - createPageFaultTracker() returns a tracker with:
11
+ * recordOmission({id, tokens, reason}) — log that context was dropped
12
+ * recordFetch({id, tokens, latencyMs, reason}) — log a subsequent fetch
13
+ * recordFault(id, ...) — record a fault (alias)
14
+ * snapshot() — current aggregate stats
15
+ * reset() — clear all state
16
+ * faults() — list of recorded faults
17
+ * - measurePageFaults(tracker, fetchFn) — runs an async fetch closure
18
+ * against the tracker and returns the snapshot.
19
+ *
20
+ * Page fault definition: a fetch of an id that was previously omitted
21
+ * (recorded via recordOmission). If the id was never omitted, it's a
22
+ * regular fetch and is NOT counted as a fault.
23
+ *
24
+ * Reason taxonomy (extensible):
25
+ * - "scope-overflow" — was trimmed because budget overflowed
26
+ * - "relevance-threshold" — failed admission scoring threshold
27
+ * - "explicit-omission" — manually dropped by selector
28
+ * - "ttl-expired" — context had aged out
29
+ * - "unknown" — no omission reason recorded
30
+ */
31
+
32
+ export const FAULT_REASONS = Object.freeze({
33
+ SCOPE_OVERFLOW: "scope-overflow",
34
+ RELEVANCE_THRESHOLD: "relevance-threshold",
35
+ EXPLICIT_OMISSION: "explicit-omission",
36
+ TTL_EXPIRED: "ttl-expired",
37
+ UNKNOWN: "unknown",
38
+ });
39
+
40
+ /**
41
+ * @typedef {Object} OmissionRecord
42
+ * @property {string} id
43
+ * @property {number} tokens
44
+ * @property {string} reason
45
+ * @property {number} timestamp
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} FaultRecord
50
+ * @property {string} id
51
+ * @property {number} tokens
52
+ * @property {number} latencyMs
53
+ * @property {string} reason
54
+ * @property {number} timestamp
55
+ * @property {string} omissionReason
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} PageFaultSnapshot
60
+ * @property {number} omissionCount
61
+ * @property {number} fetchCount
62
+ * @property {number} faultCount
63
+ * @property {number} tokensOmitted
64
+ * @property {number} tokensFetched
65
+ * @property {number} tokensRefetched - tokens fetched that were faults
66
+ * @property {number} totalLatencyMs
67
+ * @property {number} faultLatencyMs
68
+ * @property {Object} faultsByReason - {reason: count}
69
+ * @property {Object} omissionsByReason - {reason: count}
70
+ * @property {number} faultRate - faultCount / fetchCount (0 if no fetches)
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} PageFaultTracker
75
+ * @property {string[]} _ids
76
+ * @property {(record: OmissionRecord) => void} recordOmission
77
+ * @property {(record: {id: string, tokens?: number, latencyMs?: number, reason?: string}) => FaultRecord|null} recordFetch
78
+ * @property {(id: string, opts?: object) => FaultRecord|null} recordFault
79
+ * @property {() => OmissionRecord[]} omissions
80
+ * @property {() => FaultRecord[]} faults
81
+ * @property {() => PageFaultSnapshot} snapshot
82
+ * @property {() => void} reset
83
+ */
84
+
85
+ /**
86
+ * Create a new page-fault tracker.
87
+ *
88
+ * @param {Object} [opts]
89
+ * @param {number} [opts.clock=() => Date.now()] - injectable clock for tests
90
+ * @returns {PageFaultTracker}
91
+ */
92
+ export function createPageFaultTracker(opts = {}) {
93
+ const clock = typeof opts.clock === "function" ? opts.clock : () => Date.now();
94
+
95
+ /** @type {Map<string, OmissionRecord>} */
96
+ const omissions = new Map();
97
+ /** @type {FaultRecord[]} */
98
+ const faults = [];
99
+
100
+ /**
101
+ * Record that context `id` was omitted (not injected into the prompt).
102
+ * Calling recordOmission twice for the same id updates the prior record.
103
+ *
104
+ * @param {OmissionRecord} record
105
+ */
106
+ function recordOmission(record) {
107
+ if (!record || typeof record.id !== "string") {
108
+ throw new TypeError("recordOmission: record.id must be a string");
109
+ }
110
+ const entry = {
111
+ id: record.id,
112
+ tokens: typeof record.tokens === "number" ? record.tokens : 0,
113
+ reason: typeof record.reason === "string" ? record.reason : FAULT_REASONS.UNKNOWN,
114
+ timestamp: typeof record.timestamp === "number" ? record.timestamp : clock(),
115
+ };
116
+ omissions.set(entry.id, entry);
117
+ }
118
+
119
+ /**
120
+ * Record a fetch of context `id`. If `id` was previously omitted, this
121
+ * is a page fault and a FaultRecord is returned. Otherwise returns null.
122
+ *
123
+ * @param {{id: string, tokens?: number, latencyMs?: number, reason?: string, timestamp?: number}} record
124
+ * @returns {FaultRecord|null}
125
+ */
126
+ function recordFetch(record) {
127
+ if (!record || typeof record.id !== "string") {
128
+ throw new TypeError("recordFetch: record.id must be a string");
129
+ }
130
+ const id = record.id;
131
+ const omission = omissions.get(id);
132
+ if (!omission) return null;
133
+
134
+ const fault = {
135
+ id,
136
+ tokens: typeof record.tokens === "number" ? record.tokens : omission.tokens,
137
+ latencyMs: typeof record.latencyMs === "number" ? record.latencyMs : 0,
138
+ reason: typeof record.reason === "string" ? record.reason : FAULT_REASONS.UNKNOWN,
139
+ timestamp: typeof record.timestamp === "number" ? record.timestamp : clock(),
140
+ omissionReason: omission.reason,
141
+ };
142
+ faults.push(fault);
143
+ return fault;
144
+ }
145
+
146
+ /**
147
+ * Convenience alias for recordFetch.
148
+ *
149
+ * @param {string} id
150
+ * @param {{tokens?: number, latencyMs?: number, reason?: string}} [opts]
151
+ * @returns {FaultRecord|null}
152
+ */
153
+ function recordFault(id, opts = {}) {
154
+ return recordFetch({ id, ...(opts || {}) });
155
+ }
156
+
157
+ function snapshot() {
158
+ let tokensOmitted = 0;
159
+ let tokensFetched = 0;
160
+ let tokensRefetched = 0;
161
+ let totalLatencyMs = 0;
162
+ let faultLatencyMs = 0;
163
+ /** @type {Object<string, number>} */
164
+ const faultsByReason = {};
165
+ /** @type {Object<string, number>} */
166
+ const omissionsByReason = {};
167
+
168
+ for (const o of omissions.values()) {
169
+ tokensOmitted += o.tokens;
170
+ omissionsByReason[o.reason] = (omissionsByReason[o.reason] || 0) + 1;
171
+ }
172
+ for (const f of faults) {
173
+ tokensRefetched += f.tokens;
174
+ faultLatencyMs += f.latencyMs;
175
+ totalLatencyMs += f.latencyMs;
176
+ faultsByReason[f.reason] = (faultsByReason[f.reason] || 0) + 1;
177
+ }
178
+ const fetchCount = faults.length; // only faults count as fetches in this metric
179
+ const faultCount = faults.length;
180
+ const faultRate = fetchCount === 0 ? 0 : faultCount / fetchCount;
181
+
182
+ return {
183
+ omissionCount: omissions.size,
184
+ fetchCount,
185
+ faultCount,
186
+ tokensOmitted,
187
+ tokensFetched, // alias for tokensRefetched under fault-only mode
188
+ tokensRefetched,
189
+ totalLatencyMs,
190
+ faultLatencyMs,
191
+ faultsByReason,
192
+ omissionsByReason,
193
+ faultRate,
194
+ };
195
+ }
196
+
197
+ function reset() {
198
+ omissions.clear();
199
+ faults.length = 0;
200
+ }
201
+
202
+ return {
203
+ recordOmission,
204
+ recordFetch,
205
+ recordFault,
206
+ omissions: () => Array.from(omissions.values()),
207
+ faults: () => faults.slice(),
208
+ snapshot,
209
+ reset,
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Run `fetchFn` against the tracker and return the resulting snapshot.
215
+ * Useful for instrumenting an existing retrieveContext() call.
216
+ *
217
+ * The fetchFn receives a wrapped retrieveContext that automatically calls
218
+ * recordFetch on every retrieval, attributing tokens/latency to faults when
219
+ * applicable.
220
+ *
221
+ * @param {PageFaultTracker} tracker
222
+ * @param {(retrieve: (id: string, opts?: object) => any) => Promise<any>|any} fetchFn
223
+ * @returns {Promise<PageFaultSnapshot>|PageFaultSnapshot}
224
+ */
225
+ export function measurePageFaults(tracker, fetchFn) {
226
+ if (!tracker || typeof tracker.recordFetch !== "function") {
227
+ throw new TypeError("measurePageFaults: tracker required");
228
+ }
229
+ if (typeof fetchFn !== "function") {
230
+ throw new TypeError("measurePageFaults: fetchFn must be a function");
231
+ }
232
+ const wrapped = (id, opts = {}) => {
233
+ const start = (opts && typeof opts._start === "number") ? opts._start : Date.now();
234
+ const result = (opts && typeof opts._result === "object") ? opts._result : null;
235
+ const tokens = result && typeof result.tokens === "number"
236
+ ? result.tokens
237
+ : (opts && typeof opts.tokens === "number" ? opts.tokens : 0);
238
+ const latencyMs = result && typeof result.latencyMs === "number"
239
+ ? result.latencyMs
240
+ : Math.max(0, Date.now() - start);
241
+ return tracker.recordFetch({
242
+ id,
243
+ tokens,
244
+ latencyMs,
245
+ reason: (opts && opts.reason) || "unknown",
246
+ });
247
+ };
248
+ // NOTE: deliberately not async — returns snapshot directly.
249
+ // If fetchFn returns a Promise, the caller should await.
250
+ const out = fetchFn(wrapped);
251
+ if (out && typeof out.then === "function") {
252
+ return out.catch(() => {}).then(() => tracker.snapshot());
253
+ }
254
+ return tracker.snapshot();
255
+ }
256
+
257
+ /**
258
+ * Compute per-fault aggregates grouped by omission reason.
259
+ *
260
+ * @param {FaultRecord[]} records
261
+ * @returns {Object<string, {count: number, tokens: number, latencyMs: number}>}
262
+ */
263
+ export function aggregateByReason(records) {
264
+ /** @type {Object<string, {count: number, tokens: number, latencyMs: number}>} */
265
+ const out = {};
266
+ if (!Array.isArray(records)) return out;
267
+ for (const r of records) {
268
+ const key = (r && r.omissionReason) || FAULT_REASONS.UNKNOWN;
269
+ if (!out[key]) out[key] = { count: 0, tokens: 0, latencyMs: 0 };
270
+ out[key].count += 1;
271
+ out[key].tokens += typeof r.tokens === "number" ? r.tokens : 0;
272
+ out[key].latencyMs += typeof r.latencyMs === "number" ? r.latencyMs : 0;
273
+ }
274
+ return out;
275
+ }
@@ -0,0 +1,74 @@
1
+ import { countTokens } from './tokenizer.js';
2
+
3
+ export function buildPrompt({ system, context, user }, options = {}) {
4
+ const maxTokens = options.maxTokens ?? Infinity;
5
+ const reservedForUser = options.reservedForUser ?? 0;
6
+
7
+ const parts = [];
8
+ if (system) parts.push({ role: 'system', content: system });
9
+ if (context) parts.push({ role: 'context', content: context });
10
+ if (user) parts.push({ role: 'user', content: user });
11
+
12
+ const contentFor = (p) => (typeof p.content === 'string' ? p.content : '');
13
+
14
+ // Calculate per-part token counts.
15
+ const withTokens = parts.map((p) => ({ ...p, tokens: countTokens(contentFor(p)) }));
16
+ const totalTokens = withTokens.reduce((s, p) => s + p.tokens, 0);
17
+
18
+ if (totalTokens <= maxTokens) {
19
+ return {
20
+ parts: withTokens.map((p) => ({ role: p.role, content: p.content })),
21
+ totalTokens,
22
+ truncated: false,
23
+ };
24
+ }
25
+
26
+ // Truncate lowest-priority parts first; system has highest priority.
27
+ // Process in reverse priority order so system is preserved.
28
+ const priorities = ['user', 'context'];
29
+ const resultParts = withTokens.map((p) => ({
30
+ role: p.role,
31
+ content: p.content,
32
+ tokens: p.tokens,
33
+ }));
34
+
35
+ let currentTokens = totalTokens;
36
+ for (const role of priorities) {
37
+ if (currentTokens <= maxTokens) break;
38
+ const idx = resultParts.findIndex((p) => p.role === role);
39
+ if (idx === -1) continue;
40
+ const part = resultParts[idx];
41
+ const remainingBudget = maxTokens - (currentTokens - part.tokens);
42
+ const minForUser = role === 'user' ? reservedForUser : 0;
43
+ const allowed = Math.max(0, remainingBudget - minForUser);
44
+ if (allowed >= part.tokens) continue;
45
+ if (allowed === 0) {
46
+ part.content = '';
47
+ part.tokens = 0;
48
+ } else {
49
+ // Truncate content by approximate char ratio (chars ~= 4 chars per token).
50
+ const charBudget = allowed * 4;
51
+ const truncated = contentFor(part).slice(0, charBudget);
52
+ part.content = truncated + (contentFor(part).length > charBudget ? '...' : '');
53
+ part.tokens = countTokens(part.content);
54
+ }
55
+ currentTokens = resultParts.reduce((s, p) => s + p.tokens, 0);
56
+ }
57
+
58
+ // If still over budget after user/context truncation, drop user content entirely.
59
+ // System is preserved whenever possible (last resort: empty if it alone exceeds budget).
60
+ if (currentTokens > maxTokens) {
61
+ const userIdx = resultParts.findIndex((p) => p.role === 'user');
62
+ if (userIdx !== -1) {
63
+ resultParts[userIdx].content = '';
64
+ resultParts[userIdx].tokens = 0;
65
+ currentTokens = resultParts.reduce((s, p) => s + p.tokens, 0);
66
+ }
67
+ }
68
+
69
+ return {
70
+ parts: resultParts.map((p) => ({ role: p.role, content: p.content })),
71
+ totalTokens: currentTokens,
72
+ truncated: true,
73
+ };
74
+ }