@ngockhoale/ukit 3.0.2 → 3.0.4

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 (109) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/ukit +12 -4
  3. package/manifests/engineConformance.yaml +29 -0
  4. package/manifests/platform.full.yaml +13 -0
  5. package/package.json +2 -1
  6. package/scripts/audit/decision-coverage.mjs +295 -0
  7. package/scripts/bench/outline-savings.mjs +19 -4
  8. package/scripts/bench/parallel-agents.mjs +15 -4
  9. package/scripts/bench/runGold.mjs +22 -4
  10. package/scripts/bench/v3-ceremony.mjs +7 -1
  11. package/scripts/bug/triage.mjs +56 -17
  12. package/scripts/index/build-index.mjs +94 -28
  13. package/scripts/index/query-index.mjs +48 -14
  14. package/scripts/index/refresh-index.mjs +142 -62
  15. package/scripts/perf/audit-perf.mjs +8 -2
  16. package/scripts/skill/audit-skill.mjs +54 -25
  17. package/src/bug/triageBug.js +9 -6
  18. package/src/cli/adapters.js +6 -0
  19. package/src/cli/commands/code.js +7 -1
  20. package/src/cli/commands/indexArgs.js +4 -2
  21. package/src/cli/commands/indexTools.js +8 -1
  22. package/src/cli/commands/install.js +13 -0
  23. package/src/cli/commands/memory.js +10 -3
  24. package/src/cli/commands/status.js +17 -1
  25. package/src/cli/commands/update.js +7 -0
  26. package/src/context/detectProjectContext.js +3 -1
  27. package/src/core/codeintel/analogy.js +1 -1
  28. package/src/core/codeintel/diagnostics.js +9 -6
  29. package/src/core/codeintel/graph.js +14 -8
  30. package/src/core/codeintel/impact.js +0 -1
  31. package/src/core/codeintel/invalidation.js +5 -3
  32. package/src/core/codeintel/packet.js +11 -0
  33. package/src/core/codeintel/router.js +17 -3
  34. package/src/core/codeintel/semanticProvider.js +1 -1
  35. package/src/core/codeintel/summaries.js +11 -7
  36. package/src/core/compact/contextBudget.js +26 -12
  37. package/src/core/compact/index.js +15 -8
  38. package/src/core/docContracts.js +10 -2
  39. package/src/core/experiments/deliberation.js +321 -0
  40. package/src/core/experiments/dynamicWorkflow.js +492 -0
  41. package/src/core/fileOps.js +8 -1
  42. package/src/core/gatewayProbe.js +29 -1
  43. package/src/core/gatewayResilienceEnv.js +44 -3
  44. package/src/core/handoffDocValidator.js +3 -1
  45. package/src/core/hookChainDoctor.js +16 -1
  46. package/src/core/memory/deltaOverlays.js +448 -0
  47. package/src/core/memory/learningCandidates.js +302 -0
  48. package/src/core/memory/migrate.js +59 -32
  49. package/src/core/memory/recordStore.js +24 -1
  50. package/src/core/memory/store.js +44 -8
  51. package/src/core/memory/storeV2.js +48 -33
  52. package/src/core/memory/userMemory.js +11 -10
  53. package/src/core/output/index.js +15 -10
  54. package/src/core/permissionPolicy.js +8 -0
  55. package/src/core/runtimeConfig.js +224 -4
  56. package/src/core/sensitiveValueScanner.js +10 -2
  57. package/src/core/taskBudgetValidator.js +12 -17
  58. package/src/core/taskProgressGuard.js +59 -9
  59. package/src/core/unattendedDoctor.js +5 -2
  60. package/src/core/uninstall.js +37 -8
  61. package/src/decision/client.js +371 -0
  62. package/src/decision/lease.js +198 -0
  63. package/src/decision/preflight.js +492 -0
  64. package/src/decision/protocol.js +308 -0
  65. package/src/decision/registry.js +384 -0
  66. package/src/decision/shadow.js +281 -0
  67. package/src/decision/statePacket.js +165 -0
  68. package/src/diagnostics/failurePatterns.js +2 -1
  69. package/src/diagnostics/ledgerFiles.js +3 -1
  70. package/src/diagnostics/routeOutcomes.js +1 -29
  71. package/src/index/buildIndex.js +23 -11
  72. package/src/index/impactContext.js +21 -2
  73. package/src/index/importResolution.js +13 -7
  74. package/src/index/queryIndex.js +11 -5
  75. package/src/index/resolveContext.js +16 -6
  76. package/src/index/taskRouting.js +63 -2
  77. package/src/index/verificationPlan.js +12 -1
  78. package/src/learning/patternProposals.js +6 -0
  79. package/src/render/renderTemplate.js +1 -1
  80. package/src/skill/auditSkill.js +3 -1
  81. package/src/stack/detectStack.js +3 -1
  82. package/template_project/.claude/agents/handoff-planner.md +2 -5
  83. package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
  84. package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
  85. package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
  86. package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
  87. package/template_project/.claude/hooks/handoff-resume.sh +10 -1
  88. package/template_project/.claude/hooks/protect-files.sh +0 -1
  89. package/template_project/.claude/hooks/record-execution.mjs +13 -1
  90. package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
  91. package/template_project/.claude/hooks/session-episode.sh +9 -2
  92. package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
  93. package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
  94. package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
  95. package/template_project/.claude/ukit/index/route-task.mjs +444 -0
  96. package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
  97. package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
  98. package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
  99. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
  100. package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
  101. package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
  102. package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
  103. package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
  104. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
  105. package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
  106. package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
  107. package/template_project/ukit/storage/config.json +34 -1
  108. package/template_project/.claude/ukit/skill-router-state.json +0 -1
  109. package/template_project/.ukit/storage/cache/hook-latency/unknown.jsonl +0 -4
@@ -0,0 +1,321 @@
1
+ /**
2
+ * deliberation.js (TASK-011 / M06.1)
3
+ *
4
+ * Read-only deliberation experiments — Arena (2–3 independent candidates +
5
+ * evidence-rubric judge), Swarm (≥3 independent read-only lanes), smart judge —
6
+ * behind `experiments.deliberation.enabled` (default false). Not wired into
7
+ * route-task; evaluated by fixtures (SPEC §5 FR-021, FR-023; §14 "M06 wiring").
8
+ *
9
+ * Disabled posture: activation predicates return `{activate:false}` and
10
+ * `runArena` returns `{status:'disabled'}` BEFORE any judge call — zero calls,
11
+ * zero prompt content.
12
+ *
13
+ * Judge contract: `judge({findings, rubric}) → verdict` where findings are
14
+ * frozen candidate descriptors `{candidateId, criterionScores?, violations?,
15
+ * evidence?}` and verdict is `{selected, rejected, uncertainty,
16
+ * requiredValidation}`. `selected`/`rejected[]` entries MUST carry `citations`
17
+ * drawn from candidate evidence — a majority-only verdict is rejected. Hard
18
+ * rubric constraints (`rubric.hardConstraints`) are floors: a verdict that
19
+ * selects a violator or declares `waivedConstraints` is rejected. `smartJudge`
20
+ * is the built-in deterministic judge implementing the same contract.
21
+ */
22
+
23
+ const MIN_ARENA_CANDIDATES = 2;
24
+ const MAX_ARENA_CANDIDATES = 3;
25
+ const MIN_SWARM_LANES = 3;
26
+
27
+ function deliberationEnabled(config) {
28
+ return config?.experiments?.deliberation?.enabled === true;
29
+ }
30
+
31
+ function isPlainObject(value) {
32
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+
35
+ function candidateIdOf(entry) {
36
+ return entry?.candidateId ?? entry?.id ?? null;
37
+ }
38
+
39
+ function citationsOf(entry) {
40
+ return Array.isArray(entry?.citations) ? entry.citations : [];
41
+ }
42
+
43
+ /**
44
+ * Arena activation predicate. Fires only when the route carries ≥2 materially
45
+ * different viable alternatives AND budget allows; suppressed on established
46
+ * precedent, reversible changes, or cosmetic-only alternatives.
47
+ *
48
+ * @param {object} route — route summary fields: `alternatives[]`
49
+ * ({id, viable, materiallyDifferent}), `budget.callsRemaining`,
50
+ * `precedent`, `reversible`.
51
+ * @param {object} [config] — runtime config; absent/disabled → suppress.
52
+ * @returns {{activate: boolean, reasons: string[]}}
53
+ */
54
+ export function evaluateArenaActivation(route = null, config = null) {
55
+ if (!deliberationEnabled(config)) {
56
+ return { activate: false, reasons: ['experiment-disabled'] };
57
+ }
58
+ const reasons = [];
59
+ if (route?.precedent) {
60
+ reasons.push('established-precedent');
61
+ }
62
+ if (route?.reversible === true) {
63
+ reasons.push('reversible-change');
64
+ }
65
+ const alternatives = Array.isArray(route?.alternatives) ? route.alternatives : [];
66
+ const material = alternatives.filter(
67
+ (alt) => alt?.viable === true && alt?.materiallyDifferent === true,
68
+ );
69
+ if (material.length < MIN_ARENA_CANDIDATES) {
70
+ reasons.push('insufficient-material-alternatives');
71
+ }
72
+ const callsRemaining = route?.budget?.callsRemaining;
73
+ const budgetAllows = !(typeof callsRemaining === 'number' && callsRemaining < 1);
74
+ if (!budgetAllows) {
75
+ reasons.push('budget-exhausted');
76
+ }
77
+ if (reasons.length > 0) {
78
+ return { activate: false, reasons };
79
+ }
80
+ return { activate: true, reasons: ['material-alternatives', 'budget-allows'] };
81
+ }
82
+
83
+ /**
84
+ * Swarm activation predicate. Fires only on ≥3 independent read-only lanes;
85
+ * any mutating or dependent lane suppresses the whole swarm.
86
+ *
87
+ * @param {object} route — route summary field `lanes[]`
88
+ * ({id, readOnly, independent}).
89
+ * @param {object} [config]
90
+ * @returns {{activate: boolean, reasons: string[], lanes?: string[]}}
91
+ */
92
+ export function evaluateSwarmActivation(route = null, config = null) {
93
+ if (!deliberationEnabled(config)) {
94
+ return { activate: false, reasons: ['experiment-disabled'], lanes: [] };
95
+ }
96
+ const lanes = Array.isArray(route?.lanes) ? route.lanes : [];
97
+ const reasons = [];
98
+ if (lanes.some((lane) => lane?.readOnly !== true)) {
99
+ reasons.push('lane-not-read-only');
100
+ }
101
+ const independent = lanes.filter(
102
+ (lane) => lane?.readOnly === true && lane?.independent === true,
103
+ );
104
+ if (independent.length < MIN_SWARM_LANES) {
105
+ reasons.push('insufficient-independent-lanes');
106
+ }
107
+ if (reasons.length > 0) {
108
+ return { activate: false, reasons, lanes: [] };
109
+ }
110
+ return {
111
+ activate: true,
112
+ reasons: ['independent-read-only-lanes'],
113
+ lanes: independent.map((lane) => lane.id),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Deterministic evidence-rubric judge. Scores each finding against weighted
119
+ * rubric criteria; findings violating a hard constraint are ineligible for
120
+ * selection regardless of score. Hard constraints are never waivable.
121
+ *
122
+ * @param {{findings: object[], rubric: {criteria?: {id, weight?}[],
123
+ * hardConstraints?: string[]}}} input
124
+ * @returns {{selected: object|null, rejected: object[], uncertainty: string[],
125
+ * requiredValidation: string[]}}
126
+ */
127
+ export function smartJudge({ findings = [], rubric = {} } = {}) {
128
+ const criteria = Array.isArray(rubric?.criteria) ? rubric.criteria : [];
129
+ const hardConstraints = new Set(
130
+ Array.isArray(rubric?.hardConstraints) ? rubric.hardConstraints : [],
131
+ );
132
+ const scored = (Array.isArray(findings) ? findings : []).map((finding) => {
133
+ const scores = isPlainObject(finding?.criterionScores) ? finding.criterionScores : {};
134
+ const score = criteria.reduce(
135
+ (sum, criterion) => sum + Number(scores[criterion.id] ?? 0) * Number(criterion.weight ?? 1),
136
+ 0,
137
+ );
138
+ const violations = (Array.isArray(finding?.violations) ? finding.violations : [])
139
+ .filter((id) => hardConstraints.has(id));
140
+ return { finding, score, violations };
141
+ });
142
+
143
+ const eligible = scored.filter((entry) => entry.violations.length === 0);
144
+ const rejected = [];
145
+ for (const entry of scored) {
146
+ if (entry.violations.length > 0) {
147
+ rejected.push({
148
+ candidateId: candidateIdOf(entry.finding),
149
+ reason: `hard-constraint:${entry.violations.join(',')}`,
150
+ citations: Array.isArray(entry.finding?.evidence) ? entry.finding.evidence : [],
151
+ });
152
+ }
153
+ }
154
+
155
+ eligible.sort((a, b) => b.score - a.score);
156
+ const top = eligible[0] ?? null;
157
+ const runnerUp = eligible[1] ?? null;
158
+ const uncertainty = [];
159
+ const requiredValidation = [];
160
+
161
+ let selected = null;
162
+ if (top) {
163
+ const citations = Array.isArray(top.finding?.evidence) ? top.finding.evidence : [];
164
+ if (citations.length === 0) {
165
+ uncertainty.push(`no-evidence:${candidateIdOf(top.finding)}`);
166
+ requiredValidation.push(`collect-evidence:${candidateIdOf(top.finding)}`);
167
+ } else {
168
+ selected = {
169
+ candidateId: candidateIdOf(top.finding),
170
+ score: top.score,
171
+ citations,
172
+ };
173
+ }
174
+ }
175
+ if (top && runnerUp && top.score - runnerUp.score <= 1) {
176
+ uncertainty.push(`close-call:${candidateIdOf(top.finding)}~${candidateIdOf(runnerUp.finding)}`);
177
+ requiredValidation.push(`validate-tiebreak:${candidateIdOf(top.finding)}`);
178
+ }
179
+ for (const entry of eligible) {
180
+ if (entry === top) continue;
181
+ rejected.push({
182
+ candidateId: candidateIdOf(entry.finding),
183
+ reason: 'lower-score',
184
+ citations: Array.isArray(entry.finding?.evidence) ? entry.finding.evidence : [],
185
+ });
186
+ }
187
+ if (!selected && !top) {
188
+ uncertainty.push('no-eligible-candidate');
189
+ requiredValidation.push('re-scope-candidates');
190
+ }
191
+
192
+ return { selected, rejected, uncertainty, requiredValidation };
193
+ }
194
+
195
+ /**
196
+ * Post-checks a judge verdict against the arena floors. Returns the list of
197
+ * rejection reasons (empty = verdict stands).
198
+ */
199
+ function verdictViolations(verdict, candidates, rubric) {
200
+ const reasons = [];
201
+ const hardConstraints = new Set(
202
+ Array.isArray(rubric?.hardConstraints) ? rubric.hardConstraints : [],
203
+ );
204
+ const waived = Array.isArray(verdict?.waivedConstraints) ? verdict.waivedConstraints : [];
205
+ if (waived.some((id) => hardConstraints.has(id)) || waived.length > 0) {
206
+ reasons.push('hard-constraint-waived');
207
+ }
208
+ const selectedId = candidateIdOf(verdict?.selected);
209
+ if (selectedId !== null) {
210
+ const selectedFinding = candidates.find((c) => candidateIdOf(c) === selectedId);
211
+ const violations = Array.isArray(selectedFinding?.violations) ? selectedFinding.violations : [];
212
+ if (violations.some((id) => hardConstraints.has(id))) {
213
+ reasons.push('hard-constraint-violation-selected');
214
+ }
215
+ }
216
+ const entries = [verdict?.selected, ...(Array.isArray(verdict?.rejected) ? verdict.rejected : [])]
217
+ .filter(Boolean);
218
+ if (entries.some((entry) => citationsOf(entry).length === 0)) {
219
+ reasons.push('majority-only-verdict');
220
+ }
221
+ return reasons;
222
+ }
223
+
224
+ // Deep-frozen structural copy: plain objects and arrays are cloned and frozen
225
+ // recursively so a judge cannot mutate nested state (violations, evidence)
226
+ // to defeat the verdict post-check. Non-plain values pass through frozen-safe.
227
+ function deepFreezeCopy(value) {
228
+ if (Array.isArray(value)) {
229
+ return Object.freeze(value.map(deepFreezeCopy));
230
+ }
231
+ if (isPlainObject(value)) {
232
+ const copy = {};
233
+ for (const [key, item] of Object.entries(value)) {
234
+ copy[key] = deepFreezeCopy(item);
235
+ }
236
+ return Object.freeze(copy);
237
+ }
238
+ return value;
239
+ }
240
+
241
+ /**
242
+ * Run a read-only arena over 2–3 frozen candidates with an evidence-rubric
243
+ * judge. The judge receives frozen copies — mutation attempts throw. The
244
+ * verdict is post-checked: hard-constraint waivers/violations and
245
+ * majority-only (citation-free) verdicts are rejected.
246
+ *
247
+ * @param {{candidates: object[], rubric: object, judge: Function,
248
+ * config?: object}} input
249
+ * @returns {{status: 'ok'|'disabled'|'rejected', selected?: object|null,
250
+ * rejected?: object[], uncertainty?: string[], requiredValidation?: string[],
251
+ * reasons?: string[]}}
252
+ */
253
+ export function runArena({ candidates = [], rubric = {}, judge = smartJudge, config = null } = {}) {
254
+ if (!deliberationEnabled(config)) {
255
+ return { status: 'disabled', selected: null, rejected: [], reasons: ['experiment-disabled'] };
256
+ }
257
+ const list = Array.isArray(candidates) ? candidates : [];
258
+ if (list.length < MIN_ARENA_CANDIDATES || list.length > MAX_ARENA_CANDIDATES) {
259
+ throw new TypeError(
260
+ `arena requires ${MIN_ARENA_CANDIDATES}–${MAX_ARENA_CANDIDATES} candidates.`,
261
+ );
262
+ }
263
+ if (typeof judge !== 'function') {
264
+ throw new TypeError('arena judge must be a function.');
265
+ }
266
+ // Read-only: the judge sees deep-frozen copies — mutation of any nested field
267
+ // (e.g. hiding a hard-constraint violation) throws in strict mode. A shallow
268
+ // freeze would leave nested arrays/objects mutable and defeat the post-check.
269
+ const findings = list.map((candidate) => deepFreezeCopy(candidate));
270
+ const verdict = judge({ findings, rubric }) ?? {};
271
+ const reasons = verdictViolations(verdict, findings, rubric);
272
+ if (reasons.length > 0) {
273
+ return { status: 'rejected', selected: null, rejected: [], reasons };
274
+ }
275
+ return {
276
+ status: 'ok',
277
+ selected: verdict.selected ?? null,
278
+ rejected: Array.isArray(verdict.rejected) ? verdict.rejected : [],
279
+ uncertainty: Array.isArray(verdict.uncertainty) ? verdict.uncertainty : [],
280
+ requiredValidation: Array.isArray(verdict.requiredValidation) ? verdict.requiredValidation : [],
281
+ };
282
+ }
283
+
284
+ const SCORECARD_VERDICTS = new Set(['net-gain', 'neutral', 'negative']);
285
+
286
+ /**
287
+ * Emit a normalized experiment scorecard (FR-023). Promotion requires
288
+ * `verdict:'net-gain'` + maintainer review; `neutral|negative` keeps the
289
+ * experiment disabled.
290
+ *
291
+ * @param {{experimentId: string, taskClass: string, baselineRef: string,
292
+ * deltas: {quality,rework,latency,calls}, verdict: string}} record
293
+ * @returns {object} scorecard with `promotionEligible` + `disposition`.
294
+ */
295
+ export function emitExperimentScorecard(record = {}) {
296
+ if (!isPlainObject(record) || typeof record.experimentId !== 'string' || !record.experimentId) {
297
+ throw new TypeError('scorecard record.experimentId must be a non-empty string.');
298
+ }
299
+ if (!SCORECARD_VERDICTS.has(record.verdict)) {
300
+ throw new TypeError(`scorecard verdict must be one of ${[...SCORECARD_VERDICTS].join('|')}.`);
301
+ }
302
+ const deltas = isPlainObject(record.deltas) ? record.deltas : {};
303
+ const scorecard = {
304
+ experimentId: record.experimentId,
305
+ taskClass: record.taskClass ?? null,
306
+ baselineRef: record.baselineRef ?? null,
307
+ deltas: {
308
+ quality: Number(deltas.quality ?? 0),
309
+ rework: Number(deltas.rework ?? 0),
310
+ latency: Number(deltas.latency ?? 0),
311
+ calls: Number(deltas.calls ?? 0),
312
+ },
313
+ verdict: record.verdict,
314
+ };
315
+ const promotionEligible = record.verdict === 'net-gain';
316
+ return {
317
+ ...scorecard,
318
+ promotionEligible,
319
+ disposition: promotionEligible ? 'maintainer-review' : 'stay-disabled',
320
+ };
321
+ }