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,95 @@
1
+ /**
2
+ * Context Source Registry — Registration and lookup for context sources.
3
+ *
4
+ * Manages a collection of context sources (id → source mapping).
5
+ * Sources are plain objects with at least an `id` and `type` field.
6
+ *
7
+ * Keeps registration idempotent: re-registering the same id updates the source.
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} ContextSource
12
+ * @property {string} id - Unique identifier for the source
13
+ * @property {string} type - Source type/category
14
+ */
15
+
16
+ class ContextSourceRegistry {
17
+ constructor() {
18
+ /** @type {Map<string, ContextSource>} */
19
+ this._sources = new Map();
20
+ }
21
+
22
+ /**
23
+ * Register or update a context source.
24
+ * @param {ContextSource} source
25
+ * @returns {ContextSource} The registered source.
26
+ */
27
+ register(source) {
28
+ if (!source || typeof source.id !== "string" || !source.id) {
29
+ throw new TypeError("Source must have a non-empty string `id`");
30
+ }
31
+ this._sources.set(source.id, source);
32
+ return source;
33
+ }
34
+
35
+ /**
36
+ * Remove a source by id.
37
+ * @param {string} id
38
+ * @returns {boolean} true if removed, false if not found.
39
+ */
40
+ unregister(id) {
41
+ return this._sources.delete(id);
42
+ }
43
+
44
+ /**
45
+ * Retrieve a source by id.
46
+ * @param {string} id
47
+ * @returns {ContextSource | undefined}
48
+ */
49
+ get(id) {
50
+ return this._sources.get(id);
51
+ }
52
+
53
+ /**
54
+ * Check if a source is registered.
55
+ * @param {string} id
56
+ * @returns {boolean}
57
+ */
58
+ has(id) {
59
+ return this._sources.has(id);
60
+ }
61
+
62
+ /**
63
+ * List all registered sources.
64
+ * @returns {ContextSource[]}
65
+ */
66
+ list() {
67
+ return [...this._sources.values()];
68
+ }
69
+
70
+ /**
71
+ * List sources filtered by type.
72
+ * @param {string} type
73
+ * @returns {ContextSource[]}
74
+ */
75
+ listByType(type) {
76
+ return this.list().filter((s) => s.type === type);
77
+ }
78
+
79
+ /**
80
+ * Total number of registered sources.
81
+ * @returns {number}
82
+ */
83
+ get size() {
84
+ return this._sources.size;
85
+ }
86
+
87
+ /**
88
+ * Clear all sources.
89
+ */
90
+ clear() {
91
+ this._sources.clear();
92
+ }
93
+ }
94
+
95
+ export { ContextSourceRegistry };
@@ -0,0 +1,120 @@
1
+ import { TERMINAL_STATES } from "./context-lifecycle.js";
2
+ import { CONTEXT_PURPOSE } from "./context-query-contract.js";
3
+
4
+ export const SUFFICIENCY_LEVEL = Object.freeze({
5
+ INSUFFICIENT: "INSUFFICIENT",
6
+ PARTIAL: "PARTIAL",
7
+ SUFFICIENT: "SUFFICIENT",
8
+ COMPLETE: "COMPLETE",
9
+ });
10
+
11
+ export const SUFFICIENCY_DECISIONS = Object.freeze({
12
+ PROCEED: "PROCEED",
13
+ REQUEST_MORE: "REQUEST_MORE",
14
+ BLOCK: "BLOCK",
15
+ });
16
+
17
+ export const MIN_LAYER_COUNTS = Object.freeze({
18
+ [CONTEXT_PURPOSE.PLANNING]: 3,
19
+ [CONTEXT_PURPOSE.IMPLEMENTATION]: 2,
20
+ [CONTEXT_PURPOSE.INVESTIGATION]: 2,
21
+ [CONTEXT_PURPOSE.DEBUGGING]: 1,
22
+ [CONTEXT_PURPOSE.VALIDATION]: 1,
23
+ [CONTEXT_PURPOSE.REVIEW]: 2,
24
+ [CONTEXT_PURPOSE.RESUME]: 1,
25
+ });
26
+
27
+ const INCLUDE_HISTORY_PURPOSES = new Set([
28
+ CONTEXT_PURPOSE.DEBUGGING,
29
+ CONTEXT_PURPOSE.RESUME,
30
+ "REPLAN",
31
+ "AUDIT",
32
+ ]);
33
+
34
+ const _isTerminal = (lifecycle) => TERMINAL_STATES.includes(lifecycle);
35
+
36
+ export function checkSufficiency(items, purpose = CONTEXT_PURPOSE.PLANNING) {
37
+ if (!items || items.length === 0) {
38
+ return {
39
+ level: SUFFICIENCY_LEVEL.INSUFFICIENT,
40
+ missing: [{ reason: "NO_ACTIVE_CONTEXT", type: "context" }],
41
+ unresolvedCriticalUnknowns: [],
42
+ mandatoryIncluded: false,
43
+ activeItemCount: 0,
44
+ sufficient: false,
45
+ };
46
+ }
47
+
48
+ const includeHistory = INCLUDE_HISTORY_PURPOSES.has(purpose);
49
+ const activeItems = items.filter((item) => includeHistory || !_isTerminal(item.lifecycle));
50
+ const criticalUnknowns = items.flatMap((item) => item.unresolvedCriticalUnknowns || []);
51
+ const hasMandatory = items.some((item) => item.mandatoryIncluded === true);
52
+ const minCount = MIN_LAYER_COUNTS[purpose] || MIN_LAYER_COUNTS[CONTEXT_PURPOSE.PLANNING];
53
+
54
+ let level = SUFFICIENCY_LEVEL.INSUFFICIENT;
55
+
56
+ if (activeItems.length === 0) {
57
+ level = SUFFICIENCY_LEVEL.INSUFFICIENT;
58
+ } else if (criticalUnknowns.length > 0) {
59
+ level = SUFFICIENCY_LEVEL.PARTIAL;
60
+ } else if (activeItems.length >= minCount && hasMandatory) {
61
+ level = SUFFICIENCY_LEVEL.COMPLETE;
62
+ } else if (activeItems.length >= 1) {
63
+ level = SUFFICIENCY_LEVEL.SUFFICIENT;
64
+ }
65
+
66
+ return {
67
+ level,
68
+ missing: level === SUFFICIENCY_LEVEL.INSUFFICIENT
69
+ ? [{ reason: "INSUFFICIENT_ITEMS", type: "context", count: activeItems.length, required: minCount }]
70
+ : [],
71
+ unresolvedCriticalUnknowns: criticalUnknowns,
72
+ mandatoryIncluded: hasMandatory,
73
+ activeItemCount: activeItems.length,
74
+ sufficient: level !== SUFFICIENCY_LEVEL.INSUFFICIENT,
75
+ };
76
+ }
77
+
78
+ export class SufficiencyGate {
79
+ #purpose;
80
+ #minLevel;
81
+ #requireMandatory;
82
+
83
+ constructor(options = {}) {
84
+ this.#purpose = options.purpose || CONTEXT_PURPOSE.PLANNING;
85
+ this.#minLevel = options.minLevel || SUFFICIENCY_LEVEL.SUFFICIENT;
86
+ this.#requireMandatory = options.requireMandatory || false;
87
+ }
88
+
89
+ get purpose() { return this.#purpose; }
90
+ get minLevel() { return this.#minLevel; }
91
+ get requireMandatory() { return this.#requireMandatory; }
92
+
93
+ evaluate(items) {
94
+ const sufficiency = checkSufficiency(items, this.#purpose);
95
+ const levelOrder = [SUFFICIENCY_LEVEL.INSUFFICIENT, SUFFICIENCY_LEVEL.PARTIAL, SUFFICIENCY_LEVEL.SUFFICIENT, SUFFICIENCY_LEVEL.COMPLETE];
96
+ const actualRank = levelOrder.indexOf(sufficiency.level);
97
+ const minRank = levelOrder.indexOf(this.#minLevel);
98
+
99
+ let decision;
100
+ if (actualRank < levelOrder.indexOf(SUFFICIENCY_LEVEL.PARTIAL)) {
101
+ decision = SUFFICIENCY_DECISIONS.REQUEST_MORE;
102
+ } else if (actualRank < minRank) {
103
+ decision = this.#requireMandatory && !sufficiency.mandatoryIncluded
104
+ ? SUFFICIENCY_DECISIONS.BLOCK
105
+ : SUFFICIENCY_DECISIONS.REQUEST_MORE;
106
+ } else {
107
+ decision = SUFFICIENCY_DECISIONS.PROCEED;
108
+ }
109
+
110
+ return {
111
+ decision,
112
+ sufficiency,
113
+ metadata: {
114
+ gatePurpose: this.#purpose,
115
+ minLevel: this.#minLevel,
116
+ evaluatedAt: Date.now(),
117
+ },
118
+ };
119
+ }
120
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Context Sufficiency Oracle (C04 — context-sufficiency-oracle)
3
+ *
4
+ * Independent ground-truth verifier for context selection. Given a task and
5
+ * a selected set of node IDs, computes the *required closure graph* — the
6
+ * minimal set of nodes transitively required via REQUIRES / DEPENDS_ON edges
7
+ * starting from the task — and verifies `selected ⊇ requiredClosure`.
8
+ *
9
+ * Independence: this module does NOT consult router heuristics, scoring
10
+ * weights, or admission classes. It only walks the context-graph via edges
11
+ * labeled REQUIRES or DEPENDS_ON, starting at the task node. Its verdict
12
+ * is an objective ground-truth that the selection-router must satisfy.
13
+ */
14
+
15
+ import { EDGE_TYPES } from "./context-graph.js";
16
+
17
+ /**
18
+ * @typedef {Object} OracleNode
19
+ * @property {string} id
20
+ * @property {string} [type]
21
+ * @property {string} [content]
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} OracleEdge
26
+ * @property {string} from
27
+ * @property {string} to
28
+ * @property {string} type
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} OracleGraph
33
+ * @property {(id: string) => OracleNode|undefined} getNode
34
+ * @property {() => OracleEdge[]} getEdges
35
+ * @property {(id: string) => OracleEdge[]} getEdgesFrom
36
+ * @property {(id: string) => OracleEdge[]} getEdgesTo
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} OracleResult
41
+ * @property {string} taskId
42
+ * @property {string[]} requiredClosure - sorted node IDs that must be present
43
+ * @property {string[]} selected - input selected IDs (deduped, sorted)
44
+ * @property {string[]} missing - required ⊖ selected
45
+ * @property {string[]} unused - selected ⊖ required
46
+ * @property {boolean} sufficient - missing.length === 0
47
+ * @property {Object} stats
48
+ */
49
+
50
+ /**
51
+ * Default edge types that participate in the required closure walk.
52
+ * Walking includes both outgoing (node → its requirements) and incoming
53
+ * (other nodes that declare they require this node) so the closure is
54
+ * symmetric under graph direction.
55
+ */
56
+ const CLOSURE_EDGE_TYPES = new Set([
57
+ EDGE_TYPES.REQUIRES,
58
+ EDGE_TYPES.DEPENDS_ON,
59
+ "requires",
60
+ "depends_on",
61
+ "DEPENDENCY",
62
+ ]);
63
+
64
+ /**
65
+ * Compute the required closure graph for a task — the minimal set of node
66
+ * IDs that any sufficient context MUST include.
67
+ *
68
+ * @param {string} taskId
69
+ * @param {OracleGraph} graph
70
+ * @returns {string[]} sorted node IDs
71
+ */
72
+ export function computeRequiredClosure(taskId, graph) {
73
+ if (!taskId || typeof taskId !== "string") {
74
+ throw new TypeError("computeRequiredClosure: taskId must be a non-empty string");
75
+ }
76
+ if (!graph || typeof graph.getNode !== "function") {
77
+ throw new TypeError("computeRequiredClosure: graph with getNode() is required");
78
+ }
79
+ if (!graph.getNode(taskId)) {
80
+ throw new Error(`computeRequiredClosure: task '${taskId}' not in graph`);
81
+ }
82
+
83
+ const closure = new Set([taskId]);
84
+ const queue = [taskId];
85
+
86
+ while (queue.length > 0) {
87
+ const current = queue.shift();
88
+ const related = [
89
+ ...(graph.getEdgesFrom(current) || []),
90
+ ...(graph.getEdgesTo(current) || []),
91
+ ];
92
+
93
+ for (const edge of related) {
94
+ if (!CLOSURE_EDGE_TYPES.has(edge.type)) continue;
95
+ const nextId = edge.from === current ? edge.to : edge.from;
96
+ if (!nextId || closure.has(nextId)) continue;
97
+ closure.add(nextId);
98
+ queue.push(nextId);
99
+ }
100
+ }
101
+
102
+ return [...closure].sort();
103
+ }
104
+
105
+ /**
106
+ * Verify a selection against the oracle's required closure.
107
+ *
108
+ * @param {string} taskId
109
+ * @param {OracleGraph} graph
110
+ * @param {string[]|Set<string>} selected
111
+ * @returns {OracleResult}
112
+ */
113
+ export function verifySufficiency(taskId, graph, selected) {
114
+ const required = computeRequiredClosure(taskId, graph);
115
+ const selectedSet = new Set(selected || []);
116
+ const requiredSet = new Set(required);
117
+
118
+ const missing = required.filter((id) => !selectedSet.has(id));
119
+ const unused = [...selectedSet].filter((id) => !requiredSet.has(id)).sort();
120
+
121
+ const nodeCount = (graph.getEdges ? graph.getEdges().length : 0);
122
+
123
+ return {
124
+ taskId,
125
+ requiredClosure: required,
126
+ selected: [...selectedSet].sort(),
127
+ missing,
128
+ unused,
129
+ sufficient: missing.length === 0,
130
+ stats: {
131
+ requiredCount: required.length,
132
+ selectedCount: selectedSet.size,
133
+ missingCount: missing.length,
134
+ unusedCount: unused.length,
135
+ graphEdgeCount: nodeCount,
136
+ },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Convenience: derive sufficiency boolean without constructing the full
142
+ * result object. Useful in test assertions.
143
+ */
144
+ export function isSufficient(taskId, graph, selected) {
145
+ return verifySufficiency(taskId, graph, selected).sufficient;
146
+ }
147
+
148
+ /**
149
+ * Build a minimal oracle graph from a plain node/edge list. Helpful for tests
150
+ * and for callers that don't have a full graph instance.
151
+ *
152
+ * @param {{nodes: OracleNode[], edges: OracleEdge[]}} spec
153
+ * @returns {OracleGraph}
154
+ */
155
+ export function buildOracleGraph(spec) {
156
+ const nodes = new Map((spec.nodes || []).map((n) => [n.id, n]));
157
+ const edges = spec.edges || [];
158
+
159
+ return {
160
+ getNode: (id) => nodes.get(id),
161
+ getEdges: () => edges.slice(),
162
+ getEdgesFrom: (id) => edges.filter((e) => e.from === id),
163
+ getEdgesTo: (id) => edges.filter((e) => e.to === id),
164
+ };
165
+ }
166
+
167
+ export const ORACLE_CLOSURE_EDGE_TYPES = [...CLOSURE_EDGE_TYPES];
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Context Selection Telemetry (C03 — context-observability-v1)
3
+ *
4
+ * Emits one JSONL record per decision made by the context-router / builder.
5
+ * Each record is intended to be appended to `selection-log.jsonl` (or written
6
+ * via the writer injected through `setWriter`).
7
+ *
8
+ * Required fields per decision:
9
+ * - timestamp (ISO)
10
+ * - decisionId
11
+ * - taskId
12
+ * - per-node admission tier (MANDATORY | CONDITIONAL | OPTIONAL)
13
+ * - per-node token estimate
14
+ * - omitted vs selected counts
15
+ * - budget breakdown (mandatory / conditional / optional)
16
+ * - sufficiency status (level + decision)
17
+ */
18
+
19
+ import { ADMISSION } from "./context-router.js";
20
+ import { SUFFICIENCY_LEVEL, SUFFICIENCY_DECISIONS } from "./context-sufficiency-gate.js";
21
+
22
+ const ISO_NOW = () => new Date().toISOString();
23
+
24
+ let _writer = null;
25
+ let _idCounter = 0;
26
+
27
+ /**
28
+ * Inject a writer used to persist telemetry records.
29
+ * Should accept a complete serialized JSONL string (ending with '\n').
30
+ *
31
+ * @param {(line: string) => void} writer
32
+ */
33
+ export function setWriter(writer) {
34
+ _writer = writer;
35
+ }
36
+
37
+ /**
38
+ * Reset internal state. Used in tests.
39
+ */
40
+ export function _resetTelemetry() {
41
+ _writer = null;
42
+ _idCounter = 0;
43
+ }
44
+
45
+ /**
46
+ * @typedef {Object} TraceDecisionInput
47
+ * @property {string} taskId
48
+ * @property {string} [decision]
49
+ * @property {Array<{id:string, admission:string, tokenEstimate:number}>} [selected]
50
+ * @property {Array<{id:string, admission:string, reason:string}>} [omitted]
51
+ * @property {Array<{id:string, admission:string, tokenEstimate:number}>} [admitted] // all nodes considered for admission before budgeting
52
+ * @property {{tokenBudget:number, tokensUsed:number, tokensAvailable:number}} [budget]
53
+ * @property {{level:string, decision?:string, activeItemCount?:number, missing?:Array}} [sufficiency]
54
+ * @property {Object} [extra]
55
+ */
56
+
57
+ /**
58
+ * Record a selection decision. Emits a JSONL line and returns the structured record.
59
+ *
60
+ * @param {TraceDecisionInput} input
61
+ * @returns {Object} the telemetry record (also persisted to writer)
62
+ */
63
+ export function recordDecision(input) {
64
+ const selected = input.selected || [];
65
+ const omitted = input.omitted || [];
66
+
67
+ const budgetBreakdown = computeBudgetBreakdown(selected, omitted, input.admitted || []);
68
+
69
+ const sufficiencyStatus = {
70
+ level: input.sufficiency?.level || SUFFICIENCY_LEVEL.INSUFFICIENT,
71
+ decision: input.sufficiency?.decision || SUFFICIENCY_DECISIONS.REQUEST_MORE,
72
+ sufficient: input.sufficiency?.level === SUFFICIENCY_LEVEL.SUFFICIENT
73
+ || input.sufficiency?.level === SUFFICIENCY_LEVEL.COMPLETE,
74
+ };
75
+
76
+ const record = {
77
+ schema: "context-selection-telemetry/v1",
78
+ timestamp: ISO_NOW(),
79
+ decisionId: `d_${(++_idCounter).toString(36)}`,
80
+ taskId: input.taskId,
81
+ decision: input.decision || "SELECT",
82
+ counts: {
83
+ considered: (input.admitted || []).length,
84
+ selected: selected.length,
85
+ omitted: omitted.length,
86
+ },
87
+ perNodeAdmission: selected.map((n) => ({
88
+ id: n.id,
89
+ tier: n.admission || ADMISSION.OPTIONAL,
90
+ tokenEstimate: n.tokenEstimate || 0,
91
+ })),
92
+ omittedNodes: omitted.map((n) => ({
93
+ id: n.id,
94
+ tier: n.admission || ADMISSION.OPTIONAL,
95
+ reason: n.reason || "BUDGET",
96
+ })),
97
+ budget: {
98
+ tokenBudget: input.budget?.tokenBudget || 0,
99
+ tokensUsed: input.budget?.tokensUsed || 0,
100
+ tokensAvailable: input.budget?.tokensAvailable || 0,
101
+ breakdown: budgetBreakdown,
102
+ },
103
+ sufficiency: sufficiencyStatus,
104
+ ...(input.extra ? { extra: input.extra } : {}),
105
+ };
106
+
107
+ if (typeof _writer === "function") {
108
+ try {
109
+ _writer(JSON.stringify(record) + "\n");
110
+ } catch {
111
+ // writer errors must never break selection — telemetry is best-effort.
112
+ }
113
+ }
114
+
115
+ return record;
116
+ }
117
+
118
+ /**
119
+ * Compute per-tier token breakdown for a selection decision.
120
+ *
121
+ * Mandatory tokens are reported from `selected` (mandatory nodes are never
122
+ * dropped). Conditional and optional counts aggregate from both selected and
123
+ * omitted lists so we can observe what was cut.
124
+ */
125
+ export function computeBudgetBreakdown(selected, omitted, admitted) {
126
+ const init = () => ({
127
+ MANDATORY: { tokens: 0, count: 0 },
128
+ CONDITIONAL: { tokens: 0, count: 0 },
129
+ OPTIONAL: { tokens: 0, count: 0 },
130
+ });
131
+
132
+ const breakdown = init();
133
+
134
+ for (const n of selected || []) {
135
+ const tier = n.admission || ADMISSION.OPTIONAL;
136
+ breakdown[tier].tokens += n.tokenEstimate || 0;
137
+ breakdown[tier].count += 1;
138
+ }
139
+
140
+ for (const n of omitted || []) {
141
+ const tier = n.admission || ADMISSION.OPTIONAL;
142
+ breakdown[tier].count += 1;
143
+ }
144
+
145
+ // Record considered-but-either-included-or-cut counts to give observability
146
+ // over nodes that never made it through the admission stage.
147
+ const considered = { total: admitted.length };
148
+ breakdown.considered = considered;
149
+
150
+ return breakdown;
151
+ }
152
+
153
+ /**
154
+ * Convenience: persist a record + return it. Mirrors `recordDecision` but
155
+ * requires a writer to be set; throws otherwise. Used by hook-points where
156
+ * silent telemetry is undesirable (e.g. test fixtures asserting wiring).
157
+ */
158
+ export function requireRecordDecision(input) {
159
+ if (typeof _writer !== "function") {
160
+ throw new Error("No telemetry writer configured. Call setWriter() first.");
161
+ }
162
+ return recordDecision(input);
163
+ }
164
+
165
+ export const SCHEMA = "context-selection-telemetry/v1";
166
+
167
+ /**
168
+ * Read & parse a JSONL file. Exposed for tests / debugging. Always returns an
169
+ * array. Malformed lines are skipped (telemetry must never crash consumers).
170
+ */
171
+ export function parseLog(text) {
172
+ if (!text) return [];
173
+ return text.split("\n").filter(Boolean).map((line) => {
174
+ try { return JSON.parse(line); } catch { return null; }
175
+ }).filter(Boolean);
176
+ }