opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10
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.
- package/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- 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
|
+
}
|