@kontourai/flow-agents 3.3.0 → 3.4.1
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/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +48 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +56 -3
- package/build/src/lib/flow-resolver.js +151 -11
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +101 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +153 -10
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://kontourai.dev/schemas/kontour.learning-review-proposals/0.1.json",
|
|
4
|
+
"title": "kontour.learning-review-proposals",
|
|
5
|
+
"description": "Per-run kit/gate tuning proposal document + ledger-line shape (flow-agents #352). Advisory only, never auto-applied. evidence.cost and evidence.defect are co-required on every proposal (R7-style Goodhart guard, mirrors kontour.console.economics's cost+defects co-requirement).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": ["schema", "version", "window", "outcome", "aggregates", "proposals"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema": { "const": "kontour.learning-review-proposals" },
|
|
11
|
+
"version": { "const": "0.1" },
|
|
12
|
+
"window": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": true,
|
|
15
|
+
"required": ["since", "until"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"since": { "type": ["number", "string", "null"] },
|
|
18
|
+
"until": { "type": ["number", "string", "null"] }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"records_considered": { "type": "number" },
|
|
22
|
+
"outcome": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": ["ok", "insufficient-data"]
|
|
25
|
+
},
|
|
26
|
+
"aggregates": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": true,
|
|
29
|
+
"required": ["by_kit", "by_gate", "partial"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"by_kit": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": true,
|
|
36
|
+
"required": ["kit_id", "runs"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"kit_id": { "type": "string" },
|
|
39
|
+
"runs": { "type": "number" },
|
|
40
|
+
"first_half_avg_cost_usd": { "type": ["number", "null"] },
|
|
41
|
+
"second_half_avg_cost_usd": { "type": ["number", "null"] },
|
|
42
|
+
"cost_trend_pct": { "type": ["number", "null"] },
|
|
43
|
+
"first_half_findings_total": { "type": ["number", "null"] },
|
|
44
|
+
"second_half_findings_total": { "type": ["number", "null"] },
|
|
45
|
+
"findings_delta_pct": { "type": ["number", "null"] },
|
|
46
|
+
"avg_wall_clock_s": { "type": ["number", "null"] },
|
|
47
|
+
"avg_human_wait_s": { "type": ["number", "null"] },
|
|
48
|
+
"route_back_rate": { "type": ["number", "null"] },
|
|
49
|
+
"caught_false_completions_total": { "type": ["number", "null"] }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"by_gate": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": true,
|
|
58
|
+
"required": ["gate_id", "fire_count"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"gate_id": { "type": "string" },
|
|
61
|
+
"fire_count": { "type": "number" },
|
|
62
|
+
"correct_count": { "type": "number" },
|
|
63
|
+
"false_block_count": { "type": "number" },
|
|
64
|
+
"missed_block_count": { "type": "number" },
|
|
65
|
+
"false_block_rate": { "type": ["number", "null"] },
|
|
66
|
+
"avg_wall_clock_s_when_fired": { "type": ["number", "null"] },
|
|
67
|
+
"avg_human_wait_s_when_fired": { "type": ["number", "null"] }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"partial": { "type": "boolean" }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"proposals": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": { "$ref": "#/$defs/proposal" }
|
|
77
|
+
},
|
|
78
|
+
"notes": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"items": { "type": "string" }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"$defs": {
|
|
84
|
+
"proposal": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"additionalProperties": true,
|
|
87
|
+
"required": [
|
|
88
|
+
"proposal_id",
|
|
89
|
+
"target",
|
|
90
|
+
"pattern",
|
|
91
|
+
"proposed_change",
|
|
92
|
+
"evidence",
|
|
93
|
+
"expected_effect",
|
|
94
|
+
"decision"
|
|
95
|
+
],
|
|
96
|
+
"properties": {
|
|
97
|
+
"proposal_id": { "type": "string", "minLength": 1 },
|
|
98
|
+
"target": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"additionalProperties": true,
|
|
101
|
+
"required": ["kind", "id"],
|
|
102
|
+
"properties": {
|
|
103
|
+
"kind": { "type": "string", "enum": ["kit", "gate"] },
|
|
104
|
+
"id": { "type": "string" }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"pattern": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": ["kit-review-cost-inflation", "gate-false-block-review", "gate-well-calibrated"]
|
|
110
|
+
},
|
|
111
|
+
"proposed_change": { "type": "string", "minLength": 1 },
|
|
112
|
+
"severity": { "const": "advisory" },
|
|
113
|
+
"evidence": {
|
|
114
|
+
"type": "object",
|
|
115
|
+
"additionalProperties": true,
|
|
116
|
+
"required": ["cost", "defect"],
|
|
117
|
+
"description": "Goodhart guard (structural): cost and defect are co-required — no proposal may cite a cost/time signal without also pairing a defect signal, mirroring kontour.console.economics's own cost+defects co-requirement (R7).",
|
|
118
|
+
"properties": {
|
|
119
|
+
"cost": { "type": "object", "additionalProperties": true },
|
|
120
|
+
"defect": { "type": "object", "additionalProperties": true }
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"expected_effect": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"additionalProperties": true,
|
|
126
|
+
"required": ["metric", "direction", "description"],
|
|
127
|
+
"properties": {
|
|
128
|
+
"metric": { "type": "string", "minLength": 1 },
|
|
129
|
+
"direction": { "type": "string", "enum": ["decrease", "increase", "maintain"] },
|
|
130
|
+
"description": { "type": "string", "minLength": 1 }
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"decision": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": true,
|
|
136
|
+
"required": ["status", "decided_by", "decided_at", "rationale"],
|
|
137
|
+
"properties": {
|
|
138
|
+
"status": { "type": "string", "enum": ["proposed", "ratified", "rejected", "deferred"] },
|
|
139
|
+
"decided_by": { "type": ["string", "null"] },
|
|
140
|
+
"decided_at": { "type": ["string", "null"] },
|
|
141
|
+
"rationale": { "type": ["string", "null"] }
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"follow_on_ref": { "type": ["string", "null"] },
|
|
145
|
+
"effect_observed": {
|
|
146
|
+
"type": ["object", "null"],
|
|
147
|
+
"additionalProperties": true,
|
|
148
|
+
"required": ["measured_at", "metric", "before", "after", "moved"],
|
|
149
|
+
"properties": {
|
|
150
|
+
"measured_at": { "type": "string" },
|
|
151
|
+
"metric": { "type": "string" },
|
|
152
|
+
"before": { "type": ["number", "null"] },
|
|
153
|
+
"after": { "type": ["number", "null"] },
|
|
154
|
+
"moved": { "type": "string", "enum": ["improved", "worsened", "unchanged"] }
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"already_proposed": { "type": "boolean" }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# learning-review-proposals.sh — turn recorded kontour.console.economics outcomes into per-kit/per-gate
|
|
3
|
+
# tuning PROPOSALS (#352), generalizing the routing-efficiency.sh (#415) shape from per-(role,model) to
|
|
4
|
+
# per-kit/per-gate.
|
|
5
|
+
#
|
|
6
|
+
# Reads a window of economics.jsonl records, joins each record's kit identity (trust.bundle
|
|
7
|
+
# claims[].claimType prefix) and gate identity (gate-review.inquiries.json InquiryRecord calibration),
|
|
8
|
+
# computes per-kit/per-gate aggregates, and emits ADVISORY proposals into a durable, idempotent ledger.
|
|
9
|
+
# NEVER writes to kits/**, .datum/config.json, or any gate/flow config file (ADR 0002/0003 call 5,
|
|
10
|
+
# ADR 0008/0010 consume-never-fork) — this is a pure downstream consumer of fa#349's record and
|
|
11
|
+
# gate-review's InquiryRecord output; neither is ever invoked or modified by this script.
|
|
12
|
+
#
|
|
13
|
+
# See docs/specs/learning-review-proposals-contract.md for the full formula/ledger/effect-fill spec.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# learning-review-proposals.sh [economics.jsonl ...] # defaults to the local economics log
|
|
17
|
+
# cat economics.jsonl | learning-review-proposals.sh - # records on stdin
|
|
18
|
+
# learning-review-proposals.sh --since ISO_OR_EPOCH --until ISO_OR_EPOCH \
|
|
19
|
+
# --sessions-root DIR --ledger PATH
|
|
20
|
+
#
|
|
21
|
+
# Tunables (env): LR_MIN_WINDOW_SAMPLE (5), LR_MIN_KIT_SAMPLE (6), LR_MIN_GATE_SAMPLE (3),
|
|
22
|
+
# LR_COST_RISE_PCT (25), LR_FLAT_FINDINGS_PCT (10), LR_GATE_FALSE_BLOCK_RATE (0.5),
|
|
23
|
+
# LR_GATE_WELL_CALIBRATED_RATE (0.9).
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
cat <<'USAGE'
|
|
28
|
+
Usage: learning-review-proposals.sh [economics.jsonl ...] [--since VAL] [--until VAL]
|
|
29
|
+
[--sessions-root DIR] [--ledger PATH]
|
|
30
|
+
cat economics.jsonl | learning-review-proposals.sh -
|
|
31
|
+
|
|
32
|
+
--since VAL ISO-8601 (no fractional seconds) or epoch-ms lower bound on record .at
|
|
33
|
+
--until VAL ISO-8601 (no fractional seconds) or epoch-ms upper bound on record .at
|
|
34
|
+
--sessions-root DIR root for <task_slug>/trust.bundle + <task_slug>/gate-review.inquiries.json
|
|
35
|
+
joins (default: .kontourai/flow-agents)
|
|
36
|
+
--ledger PATH proposal ledger path (default: .kontourai/telemetry/learning-review-proposals.jsonl,
|
|
37
|
+
override via LEARNING_REVIEW_PROPOSALS_LEDGER)
|
|
38
|
+
-h, --help show this help
|
|
39
|
+
|
|
40
|
+
Advisory only — never writes kits/**, .datum/config.json, or any gate/flow config file.
|
|
41
|
+
See docs/specs/learning-review-proposals-contract.md for the full contract.
|
|
42
|
+
USAGE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
command -v jq >/dev/null 2>&1 || {
|
|
46
|
+
echo '{"schema":"kontour.learning-review-proposals","version":"0.1","window":{"since":null,"until":null},"records_considered":0,"outcome":"insufficient-data","aggregates":{"by_kit":[],"by_gate":[],"partial":true},"proposals":[],"notes":["jq unavailable — no analysis"]}'
|
|
47
|
+
exit 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 0
|
|
51
|
+
default_log="${TELEMETRY_ECONOMICS_LOG_FILE:-${TELEMETRY_DATA_DIR:-${SCRIPT_DIR}/../..}/.kontourai/telemetry/economics.jsonl}"
|
|
52
|
+
default_sessions_root="${SCRIPT_DIR}/../../.kontourai/flow-agents"
|
|
53
|
+
default_ledger="${LEARNING_REVIEW_PROPOSALS_LEDGER:-${TELEMETRY_DATA_DIR:-${SCRIPT_DIR}/../..}/.kontourai/telemetry/learning-review-proposals.jsonl}"
|
|
54
|
+
|
|
55
|
+
since_arg=""
|
|
56
|
+
until_arg=""
|
|
57
|
+
sessions_root="$default_sessions_root"
|
|
58
|
+
ledger_path="$default_ledger"
|
|
59
|
+
sources=()
|
|
60
|
+
|
|
61
|
+
while [[ "$#" -gt 0 ]]; do
|
|
62
|
+
case "$1" in
|
|
63
|
+
--since) since_arg="${2:-}"; shift 2 ;;
|
|
64
|
+
--until) until_arg="${2:-}"; shift 2 ;;
|
|
65
|
+
--sessions-root) sessions_root="${2:-}"; shift 2 ;;
|
|
66
|
+
--ledger) ledger_path="${2:-}"; shift 2 ;;
|
|
67
|
+
-h|--help) usage; exit 0 ;;
|
|
68
|
+
--) shift; while [[ "$#" -gt 0 ]]; do sources+=("$1"); shift; done ;;
|
|
69
|
+
*) sources+=("$1"); shift ;;
|
|
70
|
+
esac
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
min_window_sample="${LR_MIN_WINDOW_SAMPLE:-5}"
|
|
74
|
+
min_kit_sample="${LR_MIN_KIT_SAMPLE:-6}"
|
|
75
|
+
min_gate_sample="${LR_MIN_GATE_SAMPLE:-3}"
|
|
76
|
+
cost_rise_pct="${LR_COST_RISE_PCT:-25}"
|
|
77
|
+
flat_findings_pct="${LR_FLAT_FINDINGS_PCT:-10}"
|
|
78
|
+
gate_false_block_rate="${LR_GATE_FALSE_BLOCK_RATE:-0.5}"
|
|
79
|
+
gate_well_calibrated_rate="${LR_GATE_WELL_CALIBRATED_RATE:-0.9}"
|
|
80
|
+
|
|
81
|
+
# Shared to_epoch_ms jq def (single source of truth, reused by both the --since/--until
|
|
82
|
+
# pre-validation below and the main analysis pipeline, so the two can never drift out of sync).
|
|
83
|
+
# Parses (in order): epoch-ms number/digit-string, date-only (YYYY-MM-DD, normalized to midnight
|
|
84
|
+
# UTC), full ISO-8601 with optional fractional seconds and a literal "Z" offset, full ISO-8601
|
|
85
|
+
# with optional fractional seconds and an explicit "+00:00" UTC offset (fractional seconds and
|
|
86
|
+
# the offset are both stripped/converted before parsing, since fromdateiso8601 only understands
|
|
87
|
+
# a bare "%Y-%m-%dT%H:%M:%SZ" — never the other direction, which would silently turn a parseable
|
|
88
|
+
# value into an unparseable one). Any other string raises an explicit jq error (never a silent
|
|
89
|
+
# wrong epoch, never swallowed into a fabricated result).
|
|
90
|
+
to_epoch_ms_def="$(cat <<'JQDEF'
|
|
91
|
+
def to_epoch_ms:
|
|
92
|
+
if . == null or . == "" then null
|
|
93
|
+
elif (. | type) == "number" then .
|
|
94
|
+
elif (. | test("^[0-9]+$")) then (. | tonumber)
|
|
95
|
+
elif (. | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) then (. + "T00:00:00Z" | fromdateiso8601 * 1000)
|
|
96
|
+
elif (. | test("\\.[0-9]+Z$")) then (. | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601 * 1000)
|
|
97
|
+
elif (. | test("Z$")) then (. | fromdateiso8601 * 1000)
|
|
98
|
+
elif (. | test("\\.[0-9]+\\+00:00$")) then (. | sub("\\.[0-9]+\\+00:00$"; "+00:00") | sub("\\+00:00$"; "Z") | fromdateiso8601 * 1000)
|
|
99
|
+
elif (. | test("\\+00:00$")) then (. | sub("\\+00:00$"; "Z") | fromdateiso8601 * 1000)
|
|
100
|
+
else error("unrecognized timestamp format: \\(.)")
|
|
101
|
+
end;
|
|
102
|
+
JQDEF
|
|
103
|
+
)"
|
|
104
|
+
|
|
105
|
+
# FAIL LOUD on a malformed --since/--until BEFORE any analysis or ledger I/O: a bad CLI window
|
|
106
|
+
# argument is operator-correctable input and must never masquerade as "insufficient-data" (which
|
|
107
|
+
# would look like an honest thin-data result rather than a usage error).
|
|
108
|
+
validate_time_arg() {
|
|
109
|
+
local label="$1" val="$2"
|
|
110
|
+
[[ -z "$val" ]] && return 0
|
|
111
|
+
if ! jq -e -n --arg v "$val" "${to_epoch_ms_def}
|
|
112
|
+
(\$v | to_epoch_ms) != null" >/dev/null 2>&1; then
|
|
113
|
+
echo "learning-review-proposals: invalid --${label} value: '${val}' (expected epoch-ms, YYYY-MM-DD, or full ISO-8601 with a Z or +00:00 offset; fractional seconds optional)" >&2
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
validate_time_arg "since" "$since_arg"
|
|
118
|
+
validate_time_arg "until" "$until_arg"
|
|
119
|
+
|
|
120
|
+
# Gather records: explicit args (a '-' means stdin), else stdin when piped, else the default local log.
|
|
121
|
+
records=""
|
|
122
|
+
if [[ "${#sources[@]}" -gt 0 ]]; then
|
|
123
|
+
for src in "${sources[@]}"; do
|
|
124
|
+
if [[ "$src" == "-" ]]; then records+="$(cat 2>/dev/null)"$'\n'
|
|
125
|
+
elif [[ -f "$src" ]]; then records+="$(cat "$src" 2>/dev/null)"$'\n'; fi
|
|
126
|
+
done
|
|
127
|
+
elif [[ ! -t 0 ]]; then
|
|
128
|
+
records="$(cat 2>/dev/null)"
|
|
129
|
+
elif [[ -f "$default_log" ]]; then
|
|
130
|
+
records="$(cat "$default_log" 2>/dev/null)"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
fallback_envelope() {
|
|
134
|
+
echo '{"schema":"kontour.learning-review-proposals","version":"0.1","window":{"since":null,"until":null},"records_considered":0,"outcome":"insufficient-data","aggregates":{"by_kit":[],"by_gate":[],"partial":true},"proposals":[],"notes":["no parseable records"]}'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
records_json="$(printf '%s' "$records" | jq -s -c '[ .[] | select(type == "object" and .schema == "kontour.console.economics") ]' 2>/dev/null)"
|
|
138
|
+
[[ -n "$records_json" ]] || { fallback_envelope; exit 0; }
|
|
139
|
+
|
|
140
|
+
# Distinct, non-null task_slugs present in the gathered records — the join keys we need to resolve.
|
|
141
|
+
slugs="$(printf '%s' "$records_json" | jq -r '[ .[] | .task_slug | select(. != null) ] | unique[]' 2>/dev/null)"
|
|
142
|
+
|
|
143
|
+
# Build the session join array: one entry per distinct task_slug, {task_slug, kit_id, gate_rows}.
|
|
144
|
+
session_join="[]"
|
|
145
|
+
if [[ -n "$slugs" ]]; then
|
|
146
|
+
join_entries=()
|
|
147
|
+
while IFS= read -r slug; do
|
|
148
|
+
[[ -n "$slug" ]] || continue
|
|
149
|
+
bundle="${sessions_root}/${slug}/trust.bundle"
|
|
150
|
+
inquiries="${sessions_root}/${slug}/gate-review.inquiries.json"
|
|
151
|
+
kit_id="unattributed"
|
|
152
|
+
if [[ -f "$bundle" ]]; then
|
|
153
|
+
resolved="$(jq -r '
|
|
154
|
+
[ .claims[]?.claimType | select(. != null) | split(".")[0] ]
|
|
155
|
+
| if length == 0 then "unattributed"
|
|
156
|
+
else (group_by(.) | max_by(length) | .[0]) end
|
|
157
|
+
' "$bundle" 2>/dev/null)"
|
|
158
|
+
[[ -n "$resolved" && "$resolved" != "null" ]] && kit_id="$resolved"
|
|
159
|
+
fi
|
|
160
|
+
gate_rows="[]"
|
|
161
|
+
if [[ -f "$inquiries" ]]; then
|
|
162
|
+
resolved_rows="$(jq -c '
|
|
163
|
+
[ .[]? | select(.inquiry.target.fieldOrBehavior != null) |
|
|
164
|
+
{ gate_id: .inquiry.target.fieldOrBehavior,
|
|
165
|
+
outcome: .answer.value.calibration,
|
|
166
|
+
fired: (.answer.value.gateFired // false) } ]
|
|
167
|
+
' "$inquiries" 2>/dev/null)"
|
|
168
|
+
[[ -n "$resolved_rows" ]] && gate_rows="$resolved_rows"
|
|
169
|
+
fi
|
|
170
|
+
join_entries+=("$(jq -c -n --arg slug "$slug" --arg kit_id "$kit_id" --argjson gate_rows "$gate_rows" \
|
|
171
|
+
'{task_slug: $slug, kit_id: $kit_id, gate_rows: $gate_rows}')")
|
|
172
|
+
done <<< "$slugs"
|
|
173
|
+
if [[ "${#join_entries[@]}" -gt 0 ]]; then
|
|
174
|
+
session_join="$(printf '%s\n' "${join_entries[@]}" | jq -s -c '.')"
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
now_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
179
|
+
|
|
180
|
+
# Main analysis pass: window filter, kit/gate join, aggregates, proposal rules. The filter text is
|
|
181
|
+
# assembled from the shared $to_epoch_ms_def (single source of truth with the pre-validation
|
|
182
|
+
# above) concatenated with the rest of the program, then passed as ONE filter argument to jq
|
|
183
|
+
# (jq only accepts a single FILTER argument; passing the def as a separate positional argument
|
|
184
|
+
# would be mis-parsed as an input FILE, not appended to the filter).
|
|
185
|
+
main_jq_program="$to_epoch_ms_def"
|
|
186
|
+
# shellcheck disable=SC2016
|
|
187
|
+
# ^ this and every other single-quoted append below is a literal jq program fragment (its "$vars"
|
|
188
|
+
# are jq variables, bound later via --arg/--argjson) — never intended for bash expansion, the
|
|
189
|
+
# same deliberate idiom every other jq invocation in this file already uses as a direct argument.
|
|
190
|
+
main_jq_program+='
|
|
191
|
+
def round4: (. * 10000 | round) / 10000;
|
|
192
|
+
|
|
193
|
+
($since_raw | to_epoch_ms) as $since_ms
|
|
194
|
+
| ($until_raw | to_epoch_ms) as $until_ms
|
|
195
|
+
|
|
196
|
+
# window-filtered records, each carrying its kit_id + gate_rows join
|
|
197
|
+
| [ $recs[]
|
|
198
|
+
| select(
|
|
199
|
+
(try (.at | to_epoch_ms) catch null) as $at_ms
|
|
200
|
+
| ($since_ms == null or $at_ms == null or $at_ms >= $since_ms)
|
|
201
|
+
and ($until_ms == null or $at_ms == null or $at_ms <= $until_ms)
|
|
202
|
+
)
|
|
203
|
+
| . as $rec
|
|
204
|
+
| ( [ $sessions[] | select(.task_slug == $rec.task_slug) ] | first ) as $join
|
|
205
|
+
| {
|
|
206
|
+
rec: $rec,
|
|
207
|
+
at_ms: (try (.at | to_epoch_ms) catch null),
|
|
208
|
+
kit_id: ($join.kit_id // "unattributed"),
|
|
209
|
+
gate_rows: ($join.gate_rows // [])
|
|
210
|
+
}
|
|
211
|
+
] as $windowed
|
|
212
|
+
|
|
213
|
+
| ($windowed | length) as $records_considered
|
|
214
|
+
| (if $records_considered < $min_window_sample then "insufficient-data" else "ok" end) as $outcome
|
|
215
|
+
|
|
216
|
+
# effective window bounds: explicit arg else observed min/max .at
|
|
217
|
+
| ( [ $windowed[] | .at_ms | select(. != null) ] ) as $at_values
|
|
218
|
+
| (if $since_ms != null then $since_ms elif ($at_values | length) > 0 then ($at_values | min) else null end) as $eff_since
|
|
219
|
+
| (if $until_ms != null then $until_ms elif ($at_values | length) > 0 then ($at_values | max) else null end) as $eff_until
|
|
220
|
+
|
|
221
|
+
# ---- by_kit aggregates ----
|
|
222
|
+
| ( $windowed
|
|
223
|
+
| group_by(.kit_id)
|
|
224
|
+
| map(
|
|
225
|
+
. as $g
|
|
226
|
+
| ($g[0].kit_id) as $kit_id
|
|
227
|
+
| ($g | length) as $runs
|
|
228
|
+
| ($g | sort_by(.at_ms // 0)) as $sorted
|
|
229
|
+
| (($runs / 2) | floor) as $half_point
|
|
230
|
+
| ($sorted[0:$half_point]) as $first_half
|
|
231
|
+
| ($sorted[$half_point:$runs]) as $second_half
|
|
232
|
+
| (if ($first_half | length) > 0 then (($first_half | map(.rec.cost.estimated_cost_usd // 0) | add) / ($first_half | length)) else null end) as $fh_cost
|
|
233
|
+
| (if ($second_half | length) > 0 then (($second_half | map(.rec.cost.estimated_cost_usd // 0) | add) / ($second_half | length)) else null end) as $sh_cost
|
|
234
|
+
| (if $fh_cost == null or $fh_cost == 0 then null else (((($sh_cost // 0) - $fh_cost) / $fh_cost) * 100) end) as $cost_trend_pct
|
|
235
|
+
| ($first_half | map(.rec.defects.findings_by_severity as $f | ($f.critical // 0) + ($f.high // 0) + ($f.medium // 0) + ($f.low // 0)) | add // 0) as $fh_findings
|
|
236
|
+
| ($second_half | map(.rec.defects.findings_by_severity as $f | ($f.critical // 0) + ($f.high // 0) + ($f.medium // 0) + ($f.low // 0)) | add // 0) as $sh_findings
|
|
237
|
+
| (if $fh_findings == 0 then (if $sh_findings == 0 then 0 else null end) else ((($sh_findings - $fh_findings) / $fh_findings) * 100) end) as $findings_delta_pct
|
|
238
|
+
| ($g | map(.rec.time.wall_clock_s // 0) | add / $runs) as $avg_wall_clock_s
|
|
239
|
+
| ($g | map(.rec.time.human_wait_s // 0) | add / $runs) as $avg_human_wait_s
|
|
240
|
+
| ($g | map(.rec.iterations.count // 0) | add) as $count_sum
|
|
241
|
+
| ($g | map(.rec.iterations.route_backs // 0) | add) as $route_back_sum
|
|
242
|
+
| (if $count_sum == 0 then null else ($route_back_sum / $count_sum) end) as $route_back_rate
|
|
243
|
+
| ($g | map(.rec.defects.caught_false_completions // 0) | add) as $caught_false_completions_total
|
|
244
|
+
| {
|
|
245
|
+
kit_id: $kit_id,
|
|
246
|
+
runs: $runs,
|
|
247
|
+
first_half_avg_cost_usd: ($fh_cost | if . == null then null else round4 end),
|
|
248
|
+
second_half_avg_cost_usd: ($sh_cost | if . == null then null else round4 end),
|
|
249
|
+
cost_trend_pct: ($cost_trend_pct | if . == null then null else round4 end),
|
|
250
|
+
first_half_findings_total: $fh_findings,
|
|
251
|
+
second_half_findings_total: $sh_findings,
|
|
252
|
+
findings_delta_pct: ($findings_delta_pct | if . == null then null else round4 end),
|
|
253
|
+
avg_wall_clock_s: ($avg_wall_clock_s | round4),
|
|
254
|
+
avg_human_wait_s: ($avg_human_wait_s | round4),
|
|
255
|
+
route_back_rate: ($route_back_rate | if . == null then null else round4 end),
|
|
256
|
+
caught_false_completions_total: $caught_false_completions_total
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
| sort_by(.kit_id)
|
|
260
|
+
) as $by_kit
|
|
261
|
+
|
|
262
|
+
# ---- by_gate aggregates ----
|
|
263
|
+
# Each flattened gate row carries the ORIGINAL record'\''s index in $windowed (._idx) as a stable
|
|
264
|
+
# per-record identity key for the "when fired" dedupe below — never a (value-tuple) key, which
|
|
265
|
+
# would wrongly collapse distinct records that happen to share identical timing values.
|
|
266
|
+
| ( [ $windowed | to_entries[] | .key as $idx | .value as $w
|
|
267
|
+
| $w.gate_rows[] | . + { _idx: $idx, _wall_clock_s: $w.rec.time.wall_clock_s, _human_wait_s: $w.rec.time.human_wait_s } ]
|
|
268
|
+
) as $gate_rows_flat
|
|
269
|
+
| ( $gate_rows_flat
|
|
270
|
+
| group_by(.gate_id)
|
|
271
|
+
| map(
|
|
272
|
+
. as $g
|
|
273
|
+
| ($g[0].gate_id) as $gate_id
|
|
274
|
+
| ($g | length) as $fire_count
|
|
275
|
+
| ([ $g[] | select(.outcome == "correct") ] | length) as $correct_count
|
|
276
|
+
| ([ $g[] | select(.outcome == "false_block") ] | length) as $false_block_count
|
|
277
|
+
| ([ $g[] | select(.outcome == "missed_block") ] | length) as $missed_block_count
|
|
278
|
+
| (if $fire_count == 0 then null else ($false_block_count / $fire_count) end) as $false_block_rate
|
|
279
|
+
| ( [ $g[] | select(.fired == true) | { idx: ._idx, wall_clock_s: ._wall_clock_s, human_wait_s: ._human_wait_s } ] | unique_by(.idx) ) as $fired_records
|
|
280
|
+
| (if ($fired_records | length) > 0 then (($fired_records | map(.wall_clock_s // 0) | add) / ($fired_records | length)) else null end) as $avg_wall_clock_s_when_fired
|
|
281
|
+
| (if ($fired_records | length) > 0 then (($fired_records | map(.human_wait_s // 0) | add) / ($fired_records | length)) else null end) as $avg_human_wait_s_when_fired
|
|
282
|
+
| {
|
|
283
|
+
gate_id: $gate_id,
|
|
284
|
+
fire_count: $fire_count,
|
|
285
|
+
correct_count: $correct_count,
|
|
286
|
+
false_block_count: $false_block_count,
|
|
287
|
+
missed_block_count: $missed_block_count,
|
|
288
|
+
false_block_rate: ($false_block_rate | if . == null then null else round4 end),
|
|
289
|
+
avg_wall_clock_s_when_fired: ($avg_wall_clock_s_when_fired | if . == null then null else round4 end),
|
|
290
|
+
avg_human_wait_s_when_fired: ($avg_human_wait_s_when_fired | if . == null then null else round4 end)
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
| sort_by(.gate_id)
|
|
294
|
+
) as $by_gate
|
|
295
|
+
|
|
296
|
+
| (if $outcome == "insufficient-data" then [] else (
|
|
297
|
+
($by_kit | map(
|
|
298
|
+
select(.runs >= $min_kit_sample and .cost_trend_pct != null and .cost_trend_pct >= $cost_rise_pct
|
|
299
|
+
and .findings_delta_pct != null and .findings_delta_pct <= $flat_findings_pct)
|
|
300
|
+
| {
|
|
301
|
+
target: { kind: "kit", id: .kit_id },
|
|
302
|
+
pattern: "kit-review-cost-inflation",
|
|
303
|
+
proposed_change: ("Review " + .kit_id + "'\''s recent-run review depth/scope: cost per run rose " + (.cost_trend_pct | tostring) + "% while findings caught stayed flat/declined (" + (.findings_delta_pct | tostring) + "%)."),
|
|
304
|
+
severity: "advisory",
|
|
305
|
+
evidence: {
|
|
306
|
+
cost: { first_half_avg_cost_usd: .first_half_avg_cost_usd, second_half_avg_cost_usd: .second_half_avg_cost_usd, cost_trend_pct: .cost_trend_pct },
|
|
307
|
+
defect: { first_half_findings_total: .first_half_findings_total, second_half_findings_total: .second_half_findings_total, findings_delta_pct: .findings_delta_pct, caught_false_completions_total: .caught_false_completions_total }
|
|
308
|
+
},
|
|
309
|
+
expected_effect: { metric: "avg_cost_usd", direction: "decrease", description: "cost should come back down without a corresponding rise in escaped defects" }
|
|
310
|
+
}
|
|
311
|
+
))
|
|
312
|
+
+ ($by_gate | map(
|
|
313
|
+
select(.fire_count >= $min_gate_sample and .false_block_rate != null and .false_block_rate >= $gate_false_block_rate)
|
|
314
|
+
| {
|
|
315
|
+
target: { kind: "gate", id: .gate_id },
|
|
316
|
+
pattern: "gate-false-block-review",
|
|
317
|
+
proposed_change: ("Review gate \"" + .gate_id + "\": false_block_rate is " + (.false_block_rate | tostring) + " over " + (.fire_count | tostring) + " fires — it is blocking on already-passing claims more often than not."),
|
|
318
|
+
severity: "advisory",
|
|
319
|
+
evidence: {
|
|
320
|
+
cost: { avg_wall_clock_s_when_fired: .avg_wall_clock_s_when_fired, avg_human_wait_s_when_fired: .avg_human_wait_s_when_fired },
|
|
321
|
+
defect: { fire_count: .fire_count, correct_count: .correct_count, false_block_count: .false_block_count, missed_block_count: .missed_block_count }
|
|
322
|
+
},
|
|
323
|
+
expected_effect: { metric: "false_block_rate", direction: "decrease", description: "false_block_rate should fall toward correctly-calibrated blocking" }
|
|
324
|
+
}
|
|
325
|
+
))
|
|
326
|
+
+ ($by_gate | map(
|
|
327
|
+
select(.fire_count >= $min_gate_sample and .correct_count != null and .fire_count > 0
|
|
328
|
+
and (.correct_count / .fire_count) >= $gate_well_calibrated_rate
|
|
329
|
+
and .false_block_count == 0 and .missed_block_count == 0)
|
|
330
|
+
| {
|
|
331
|
+
target: { kind: "gate", id: .gate_id },
|
|
332
|
+
pattern: "gate-well-calibrated",
|
|
333
|
+
proposed_change: ("Gate \"" + .gate_id + "\" is well-calibrated over " + (.fire_count | tostring) + " fires (" + (.correct_count | tostring) + " correct, 0 false-block, 0 missed-block) — no change needed."),
|
|
334
|
+
severity: "advisory",
|
|
335
|
+
evidence: {
|
|
336
|
+
cost: { avg_wall_clock_s_when_fired: .avg_wall_clock_s_when_fired, avg_human_wait_s_when_fired: .avg_human_wait_s_when_fired },
|
|
337
|
+
defect: { fire_count: .fire_count, correct_count: .correct_count, false_block_count: .false_block_count, missed_block_count: .missed_block_count }
|
|
338
|
+
},
|
|
339
|
+
expected_effect: { metric: "false_block_rate", direction: "maintain", description: "gate is well-calibrated; no change expected or needed" }
|
|
340
|
+
}
|
|
341
|
+
))
|
|
342
|
+
) end) as $raw_proposals
|
|
343
|
+
|
|
344
|
+
| ($raw_proposals | map(
|
|
345
|
+
. as $p
|
|
346
|
+
| ("\($since_label)_\($until_label)_\($p.target.kind)-\($p.target.id)_\($p.pattern)"
|
|
347
|
+
| ascii_downcase
|
|
348
|
+
| gsub("[^a-z0-9]+"; "-")
|
|
349
|
+
| sub("^-"; "") | sub("-$"; "")) as $proposal_id
|
|
350
|
+
| $p + {
|
|
351
|
+
proposal_id: $proposal_id,
|
|
352
|
+
decision: { status: "proposed", decided_by: null, decided_at: null, rationale: null },
|
|
353
|
+
follow_on_ref: null,
|
|
354
|
+
effect_observed: null,
|
|
355
|
+
window: { since: $eff_since, until: $eff_until }
|
|
356
|
+
}
|
|
357
|
+
)) as $proposals
|
|
358
|
+
|
|
359
|
+
| ( [ $windowed[] | select((.rec.task_slug != null) and (.gate_rows | length) == 0) ] | length ) as $sessions_without_gate_join
|
|
360
|
+
|
|
361
|
+
| {
|
|
362
|
+
schema: "kontour.learning-review-proposals",
|
|
363
|
+
version: "0.1",
|
|
364
|
+
window: { since: $eff_since, until: $eff_until },
|
|
365
|
+
records_considered: $records_considered,
|
|
366
|
+
outcome: $outcome,
|
|
367
|
+
aggregates: { by_kit: $by_kit, by_gate: $by_gate, partial: ($outcome == "insufficient-data") },
|
|
368
|
+
proposals: $proposals,
|
|
369
|
+
notes: (
|
|
370
|
+
["Advisory only — human-ratified via learning-review-decide.sh; never auto-applied to kits/**, .datum/config.json, or any gate/flow config."]
|
|
371
|
+
+ (if $outcome == "insufficient-data" then ["records_considered (\($records_considered)) below LR_MIN_WINDOW_SAMPLE (\($min_window_sample)) — insufficient data for proposals; aggregates shown for transparency only."] else [] end)
|
|
372
|
+
+ (if $sessions_without_gate_join > 0 then ["\($sessions_without_gate_join) record(s) with a task_slug but no gate-review.inquiries.json join (sessions without a gate-review pass)."] else [] end)
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
'
|
|
376
|
+
|
|
377
|
+
current_output="$(jq -c -n \
|
|
378
|
+
--argjson recs "$records_json" \
|
|
379
|
+
--argjson sessions "$session_join" \
|
|
380
|
+
--arg since_raw "$since_arg" \
|
|
381
|
+
--arg until_raw "$until_arg" \
|
|
382
|
+
--argjson min_window_sample "$min_window_sample" \
|
|
383
|
+
--argjson min_kit_sample "$min_kit_sample" \
|
|
384
|
+
--argjson min_gate_sample "$min_gate_sample" \
|
|
385
|
+
--argjson cost_rise_pct "$cost_rise_pct" \
|
|
386
|
+
--argjson flat_findings_pct "$flat_findings_pct" \
|
|
387
|
+
--argjson gate_false_block_rate "$gate_false_block_rate" \
|
|
388
|
+
--argjson gate_well_calibrated_rate "$gate_well_calibrated_rate" \
|
|
389
|
+
--arg since_label "${since_arg:-all}" \
|
|
390
|
+
--arg until_label "${until_arg:-all}" \
|
|
391
|
+
--arg now_iso "$now_iso" \
|
|
392
|
+
"$main_jq_program" 2>/dev/null)"
|
|
393
|
+
|
|
394
|
+
[[ -n "$current_output" ]] || { fallback_envelope; exit 0; }
|
|
395
|
+
|
|
396
|
+
# ---- Ledger read + de-dupe + effect-fill (shell + jq per-line, not a single jq pass) ----
|
|
397
|
+
ledger_dir="$(dirname "$ledger_path")"
|
|
398
|
+
mkdir -p "$ledger_dir" 2>/dev/null || true
|
|
399
|
+
|
|
400
|
+
existing_ledger="[]"
|
|
401
|
+
if [[ -s "$ledger_path" ]]; then
|
|
402
|
+
# FAIL LOUD, NO WRITE on a corrupted ledger: a malformed line here must never be silently
|
|
403
|
+
# treated as "[]" (which would make the later mv overwrite and destroy the entire durable
|
|
404
|
+
# ledger, including recorded human decisions). Only a genuinely empty/absent ledger is "[]".
|
|
405
|
+
if ! existing_ledger="$(jq -s -c '.' "$ledger_path" 2>/dev/null)"; then
|
|
406
|
+
echo "learning-review-proposals: ledger at '$ledger_path' failed to parse (jq -s) — refusing to run to avoid destroying existing decisions. Inspect/repair the ledger file and retry; nothing was written." >&2
|
|
407
|
+
exit 1
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
new_proposals="$(printf '%s' "$current_output" | jq -c '.proposals')"
|
|
412
|
+
aggregates="$(printf '%s' "$current_output" | jq -c '.aggregates')"
|
|
413
|
+
eff_since="$(printf '%s' "$current_output" | jq -c '.window.since')"
|
|
414
|
+
|
|
415
|
+
ledger_result="$(jq -c -n \
|
|
416
|
+
--argjson existing "$existing_ledger" \
|
|
417
|
+
--argjson new_proposals "$new_proposals" \
|
|
418
|
+
--argjson aggregates "$aggregates" \
|
|
419
|
+
--argjson eff_since "$eff_since" \
|
|
420
|
+
--arg now_iso "$now_iso" \
|
|
421
|
+
'
|
|
422
|
+
def moved_dir($direction; $before; $after):
|
|
423
|
+
if $before == null or $after == null then null
|
|
424
|
+
elif $after == $before then "unchanged"
|
|
425
|
+
elif $direction == "decrease" then (if $after < $before then "improved" else "worsened" end)
|
|
426
|
+
elif $direction == "increase" then (if $after > $before then "improved" else "worsened" end)
|
|
427
|
+
else "worsened"
|
|
428
|
+
end;
|
|
429
|
+
|
|
430
|
+
($existing | map(.proposal_id)) as $existing_ids
|
|
431
|
+
|
|
432
|
+
| ($existing | map(
|
|
433
|
+
if (.decision.status == "ratified") and (.effect_observed == null)
|
|
434
|
+
and ($eff_since != null) and (.window.until != null) and (.window.until < $eff_since)
|
|
435
|
+
then
|
|
436
|
+
. as $entry
|
|
437
|
+
| ( if $entry.target.kind == "kit"
|
|
438
|
+
then ($aggregates.by_kit[]? | select(.kit_id == $entry.target.id) | .second_half_avg_cost_usd)
|
|
439
|
+
else ($aggregates.by_gate[]? | select(.gate_id == $entry.target.id) | .false_block_rate)
|
|
440
|
+
end ) as $after
|
|
441
|
+
| (if $after == null then $entry else (
|
|
442
|
+
( if $entry.target.kind == "kit"
|
|
443
|
+
then $entry.evidence.cost.second_half_avg_cost_usd
|
|
444
|
+
else (if ($entry.evidence.defect.fire_count // 0) > 0 then ($entry.evidence.defect.false_block_count / $entry.evidence.defect.fire_count) else null end)
|
|
445
|
+
end ) as $before
|
|
446
|
+
| $entry + {
|
|
447
|
+
effect_observed: {
|
|
448
|
+
measured_at: $now_iso,
|
|
449
|
+
metric: (if $entry.target.kind == "kit" then "avg_cost_usd" else "false_block_rate" end),
|
|
450
|
+
before: $before,
|
|
451
|
+
after: $after,
|
|
452
|
+
moved: moved_dir($entry.expected_effect.direction; $before; $after)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
) end)
|
|
456
|
+
else .
|
|
457
|
+
end
|
|
458
|
+
)) as $updated_existing
|
|
459
|
+
|
|
460
|
+
| ($new_proposals | map(. + { already_proposed: ((.proposal_id as $pid | ($existing_ids | index($pid))) != null) })) as $annotated_new
|
|
461
|
+
| ($annotated_new | map(select(.already_proposed == false) | del(.already_proposed))) as $to_append
|
|
462
|
+
|
|
463
|
+
| { ledger_lines: ($updated_existing + $to_append), final_proposals: $annotated_new }
|
|
464
|
+
' 2>/dev/null)"
|
|
465
|
+
|
|
466
|
+
if [[ -z "$ledger_result" ]]; then
|
|
467
|
+
# Ledger stage failed to parse (should not happen); fall back to the unledgered current output.
|
|
468
|
+
printf '%s\n' "$current_output"
|
|
469
|
+
exit 0
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
ledger_lines="$(printf '%s' "$ledger_result" | jq -c '.ledger_lines[]' 2>/dev/null)"
|
|
473
|
+
final_proposals="$(printf '%s' "$ledger_result" | jq -c '.final_proposals')"
|
|
474
|
+
|
|
475
|
+
if [[ -n "$ledger_lines" ]]; then
|
|
476
|
+
tmp_ledger="$(mktemp "${ledger_dir}/.learning-review-proposals.XXXXXX")" || tmp_ledger=""
|
|
477
|
+
if [[ -n "$tmp_ledger" ]]; then
|
|
478
|
+
printf '%s\n' "$ledger_lines" > "$tmp_ledger" && mv "$tmp_ledger" "$ledger_path"
|
|
479
|
+
fi
|
|
480
|
+
else
|
|
481
|
+
: > "$ledger_path"
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
printf '%s' "$current_output" | jq -c --argjson proposals "$final_proposals" '.proposals = $proposals'
|