akm-cli 0.6.1 → 0.7.0-rc1
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/CHANGELOG.md +66 -0
- package/dist/{cli.js → src/cli.js} +620 -26
- package/dist/{commands → src/commands}/config-cli.js +5 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/{commands → src/commands}/installed-stashes.js +1 -1
- package/dist/src/commands/proposal.js +119 -0
- package/dist/src/commands/propose.js +171 -0
- package/dist/src/commands/reflect.js +193 -0
- package/dist/{commands → src/commands}/registry-search.js +2 -1
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +74 -1
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +44 -0
- package/dist/{core → src/core}/asset-ref.js +5 -5
- package/dist/{core → src/core}/asset-spec.js +12 -0
- package/dist/{core → src/core}/common.js +1 -1
- package/dist/{core → src/core}/config.js +175 -121
- package/dist/{core → src/core}/errors.js +4 -0
- package/dist/src/core/events.js +239 -0
- package/dist/src/core/lesson-lint.js +86 -0
- package/dist/src/core/proposals.js +406 -0
- package/dist/src/core/warn.js +72 -0
- package/dist/{core → src/core}/write-source.js +80 -5
- package/dist/{indexer → src/indexer}/db-search.js +113 -24
- package/dist/{indexer → src/indexer}/db.js +76 -23
- package/dist/{indexer → src/indexer}/file-context.js +0 -3
- package/dist/src/indexer/graph-boost.js +179 -0
- package/dist/src/indexer/graph-extraction.js +212 -0
- package/dist/{indexer → src/indexer}/indexer.js +73 -6
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +111 -3
- package/dist/src/integrations/agent/config.js +292 -0
- package/dist/src/integrations/agent/detect.js +94 -0
- package/dist/src/integrations/agent/index.js +17 -0
- package/dist/src/integrations/agent/profiles.js +65 -0
- package/dist/src/integrations/agent/prompts.js +167 -0
- package/dist/src/integrations/agent/spawn.js +221 -0
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/src/llm/feature-gate.js +108 -0
- package/dist/src/llm/graph-extract.js +107 -0
- package/dist/src/llm/index-passes.js +35 -0
- package/dist/src/llm/memory-infer.js +86 -0
- package/dist/{output → src/output}/renderers.js +60 -1
- package/dist/src/output/shapes.js +516 -0
- package/dist/{output → src/output}/text.js +447 -4
- package/dist/{registry → src/registry}/build-index.js +14 -4
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +3 -2
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +43 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/tests/add-website-source.test.js +119 -0
- package/dist/tests/agent/agent-config-loader.test.js +70 -0
- package/dist/tests/agent/agent-config.test.js +221 -0
- package/dist/tests/agent/agent-detect.test.js +100 -0
- package/dist/tests/agent/agent-spawn.test.js +234 -0
- package/dist/tests/agent-output.test.js +186 -0
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
- package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
- package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
- package/dist/tests/asset-ref.test.js +192 -0
- package/dist/tests/asset-registry.test.js +103 -0
- package/dist/tests/asset-spec.test.js +241 -0
- package/dist/tests/bench/attribution.test.js +995 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +203 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +683 -0
- package/dist/tests/bench/cli.test.js +177 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +314 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/driver.js +346 -0
- package/dist/tests/bench/driver.test.js +443 -0
- package/dist/tests/bench/evolve-metrics.js +179 -0
- package/dist/tests/bench/evolve-metrics.test.js +187 -0
- package/dist/tests/bench/evolve.js +580 -0
- package/dist/tests/bench/evolve.test.js +616 -0
- package/dist/tests/bench/failure-modes.test.js +300 -0
- package/dist/tests/bench/feedback-integrity.test.js +456 -0
- package/dist/tests/bench/leakage.test.js +125 -0
- package/dist/tests/bench/learning-curve.test.js +133 -0
- package/dist/tests/bench/metrics.js +2319 -0
- package/dist/tests/bench/metrics.test.js +1144 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/report.js +1821 -0
- package/dist/tests/bench/report.test.js +989 -0
- package/dist/tests/bench/runner.js +536 -0
- package/dist/tests/bench/runner.test.js +958 -0
- package/dist/tests/bench/search-bridge.test.js +331 -0
- package/dist/tests/bench/tmp.js +41 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +109 -0
- package/dist/tests/bench/verifier.test.js +118 -0
- package/dist/tests/bench/workflow-evaluator.js +557 -0
- package/dist/tests/bench/workflow-evaluator.test.js +421 -0
- package/dist/tests/bench/workflow-spec.js +358 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +438 -0
- package/dist/tests/bench/workflow-trace.test.js +254 -0
- package/dist/tests/benchmark-search-quality.js +536 -0
- package/dist/tests/benchmark-suite.js +1441 -0
- package/dist/tests/capture-cli.test.js +112 -0
- package/dist/tests/cli-errors.test.js +203 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +223 -0
- package/dist/tests/commands/import.test.js +103 -0
- package/dist/tests/commands/proposal-cli.test.js +209 -0
- package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
- package/dist/tests/commands/remember.test.js +97 -0
- package/dist/tests/commands/scope-flags.test.js +300 -0
- package/dist/tests/commands/search.test.js +537 -0
- package/dist/tests/commands/show-indexer-parity.test.js +117 -0
- package/dist/tests/commands/show.test.js +294 -0
- package/dist/tests/common.test.js +266 -0
- package/dist/tests/completions.test.js +142 -0
- package/dist/tests/config-cli.test.js +193 -0
- package/dist/tests/config-llm-features.test.js +139 -0
- package/dist/tests/config.test.js +544 -0
- package/dist/tests/contracts/migration-baseline.test.js +43 -0
- package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
- package/dist/tests/contracts/spec-helpers.js +46 -0
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
- package/dist/tests/core/write-source.test.js +366 -0
- package/dist/tests/curate-command.test.js +87 -0
- package/dist/tests/db-scoring.test.js +201 -0
- package/dist/tests/db.test.js +654 -0
- package/dist/tests/distill-cli-flag.test.js +208 -0
- package/dist/tests/distill.test.js +515 -0
- package/dist/tests/docker-install.test.js +120 -0
- package/dist/tests/e2e.test.js +1398 -0
- package/dist/tests/embedder.test.js +340 -0
- package/dist/tests/embedding-model-config.test.js +379 -0
- package/dist/tests/feedback-command.test.js +172 -0
- package/dist/tests/file-context.test.js +552 -0
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
- package/dist/tests/fixtures/stashes/load.js +166 -0
- package/dist/tests/fixtures/stashes/load.test.js +88 -0
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
- package/dist/tests/frontmatter.test.js +190 -0
- package/dist/tests/fts-field-weighting.test.js +254 -0
- package/dist/tests/fuzzy-search.test.js +230 -0
- package/dist/tests/git-provider-clone.test.js +45 -0
- package/dist/tests/github.test.js +161 -0
- package/dist/tests/graph-boost-ranking.test.js +305 -0
- package/dist/tests/graph-extraction.test.js +282 -0
- package/dist/tests/helpers/usage-events.js +8 -0
- package/dist/tests/index-pass-llm.test.js +161 -0
- package/dist/tests/indexer.test.js +559 -0
- package/dist/tests/info-command.test.js +166 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/install-script.test.js +246 -0
- package/dist/tests/integration/agent-real-profile.test.js +94 -0
- package/dist/tests/issue-36-repro.test.js +304 -0
- package/dist/tests/issues-191-194.test.js +160 -0
- package/dist/tests/lesson-lint.test.js +111 -0
- package/dist/tests/llm-client.test.js +115 -0
- package/dist/tests/llm-feature-gate.test.js +151 -0
- package/dist/tests/llm.test.js +139 -0
- package/dist/tests/lockfile.test.js +216 -0
- package/dist/tests/manifest.test.js +205 -0
- package/dist/tests/markdown.test.js +126 -0
- package/dist/tests/matchers-unit.test.js +189 -0
- package/dist/tests/memory-inference.test.js +299 -0
- package/dist/tests/merge-scoring.test.js +136 -0
- package/dist/tests/metadata.test.js +313 -0
- package/dist/tests/migration-help.test.js +89 -0
- package/dist/tests/origin-resolve.test.js +124 -0
- package/dist/tests/output-baseline.test.js +217 -0
- package/dist/tests/output-shapes-unit.test.js +476 -0
- package/dist/tests/parallel-search.test.js +272 -0
- package/dist/tests/parameter-metadata.test.js +365 -0
- package/dist/tests/paths.test.js +177 -0
- package/dist/tests/progressive-disclosure.test.js +280 -0
- package/dist/tests/proposals.test.js +279 -0
- package/dist/tests/proposed-quality.test.js +271 -0
- package/dist/tests/provider-registry.test.js +32 -0
- package/dist/tests/ranking-regression.test.js +548 -0
- package/dist/tests/reflect-propose.test.js +455 -0
- package/dist/tests/registry-build-index.test.js +378 -0
- package/dist/tests/registry-cli.test.js +290 -0
- package/dist/tests/registry-index-v2.test.js +430 -0
- package/dist/tests/registry-install.test.js +728 -0
- package/dist/tests/registry-providers/parity.test.js +189 -0
- package/dist/tests/registry-providers/skills-sh.test.js +309 -0
- package/dist/tests/registry-providers/static-index.test.js +204 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +723 -0
- package/dist/tests/remember-frontmatter.test.js +380 -0
- package/dist/tests/remember-unit.test.js +123 -0
- package/dist/tests/ripgrep-install.test.js +251 -0
- package/dist/tests/ripgrep-resolve.test.js +108 -0
- package/dist/tests/ripgrep.test.js +163 -0
- package/dist/tests/save-command.test.js +94 -0
- package/dist/tests/save-trust-qa-fixes.test.js +270 -0
- package/dist/tests/scoring-pipeline.test.js +648 -0
- package/dist/tests/search-include-proposed-cli.test.js +118 -0
- package/dist/tests/self-update.test.js +442 -0
- package/dist/tests/semantic-search-e2e.test.js +512 -0
- package/dist/tests/semantic-status.test.js +471 -0
- package/dist/tests/setup-run.integration.js +877 -0
- package/dist/tests/setup-wizard.test.js +198 -0
- package/dist/tests/setup.test.js +131 -0
- package/dist/tests/source-add.test.js +11 -0
- package/dist/tests/source-clone.test.js +254 -0
- package/dist/tests/source-manage.test.js +366 -0
- package/dist/tests/source-providers/filesystem.test.js +82 -0
- package/dist/tests/source-providers/git.test.js +252 -0
- package/dist/tests/source-providers/website.test.js +128 -0
- package/dist/tests/source-qa-fixes.test.js +268 -0
- package/dist/tests/source-registry.test.js +350 -0
- package/dist/tests/source-resolve.test.js +100 -0
- package/dist/tests/source-source.test.js +221 -0
- package/dist/tests/source.test.js +533 -0
- package/dist/tests/tar-utils-scan.test.js +73 -0
- package/dist/tests/toggle-components.test.js +73 -0
- package/dist/tests/usage-telemetry.test.js +265 -0
- package/dist/tests/utility-scoring.test.js +558 -0
- package/dist/tests/vault-load-error.test.js +78 -0
- package/dist/tests/vault-qa-fixes.test.js +194 -0
- package/dist/tests/vault.test.js +429 -0
- package/dist/tests/vector-search.test.js +608 -0
- package/dist/tests/walker.test.js +252 -0
- package/dist/tests/wave2-cluster-bc.test.js +228 -0
- package/dist/tests/wave2-cluster-d.test.js +180 -0
- package/dist/tests/wave2-cluster-e.test.js +179 -0
- package/dist/tests/wiki-qa-fixes.test.js +270 -0
- package/dist/tests/wiki.test.js +529 -0
- package/dist/tests/workflow-cli.test.js +271 -0
- package/dist/tests/workflow-markdown.test.js +171 -0
- package/dist/tests/workflow-path-escape.test.js +132 -0
- package/dist/tests/workflow-qa-fixes.test.js +377 -0
- package/dist/tests/workflows/indexer-rejection.test.js +213 -0
- package/docs/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +244 -0
- package/package.json +2 -2
- package/dist/core/warn.js +0 -27
- package/dist/output/shapes.js +0 -212
- /package/dist/{commands → src/commands}/completions.js +0 -0
- /package/dist/{commands → src/commands}/curate.js +0 -0
- /package/dist/{commands → src/commands}/info.js +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/install-audit.js +0 -0
- /package/dist/{commands → src/commands}/migration-help.js +0 -0
- /package/dist/{commands → src/commands}/source-add.js +0 -0
- /package/dist/{commands → src/commands}/source-clone.js +0 -0
- /package/dist/{commands → src/commands}/source-manage.js +0 -0
- /package/dist/{commands → src/commands}/vault.js +0 -0
- /package/dist/{core → src/core}/asset-registry.js +0 -0
- /package/dist/{core → src/core}/frontmatter.js +0 -0
- /package/dist/{core → src/core}/markdown.js +0 -0
- /package/dist/{core → src/core}/paths.js +0 -0
- /package/dist/{indexer → src/indexer}/manifest.js +0 -0
- /package/dist/{indexer → src/indexer}/matchers.js +0 -0
- /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
- /package/dist/{indexer → src/indexer}/search-source.js +0 -0
- /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
- /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
- /package/dist/{indexer → src/indexer}/walker.js +0 -0
- /package/dist/{integrations → src/integrations}/github.js +0 -0
- /package/dist/{llm → src/llm}/embedder.js +0 -0
- /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
- /package/dist/{llm → src/llm}/embedders/local.js +0 -0
- /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
- /package/dist/{llm → src/llm}/embedders/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
- /package/dist/{output → src/output}/cli-hints.js +0 -0
- /package/dist/{output → src/output}/context.js +0 -0
- /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
- /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
- /package/dist/{registry → src/registry}/providers/index.js +0 -0
- /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
- /package/dist/{registry → src/registry}/providers/types.js +0 -0
- /package/dist/{registry → src/registry}/types.js +0 -0
- /package/dist/{setup → src/setup}/detect.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
- /package/dist/{setup → src/setup}/steps.js +0 -0
- /package/dist/{sources → src/sources}/include.js +0 -0
- /package/dist/{sources → src/sources}/provider-factory.js +0 -0
- /package/dist/{sources → src/sources}/provider.js +0 -0
- /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
- /package/dist/{sources → src/sources}/providers/index.js +0 -0
- /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
- /package/dist/{sources → src/sources}/providers/npm.js +0 -0
- /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/website.js +0 -0
- /package/dist/{sources → src/sources}/resolve.js +0 -0
- /package/dist/{sources → src/sources}/types.js +0 -0
- /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
- /package/dist/{version.js → src/version.js} +0 -0
- /package/dist/{wiki → src/wiki}/wiki.js +0 -0
- /package/dist/{workflows → src/workflows}/authoring.js +0 -0
- /package/dist/{workflows → src/workflows}/cli.js +0 -0
- /package/dist/{workflows → src/workflows}/db.js +0 -0
- /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
- /package/dist/{workflows → src/workflows}/parser.js +0 -0
- /package/dist/{workflows → src/workflows}/renderer.js +0 -0
- /package/dist/{workflows → src/workflows}/runs.js +0 -0
- /package/dist/{workflows → src/workflows}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for §6.8 feedback-signal integrity (#244).
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* • All four 2×2 quadrants (TP, FP, TN, FN).
|
|
6
|
+
* • Per-asset breakdown when an asset has mixed signals across runs.
|
|
7
|
+
* • `feedback_agreement < 0.80` triggers the warning marker (markdown +
|
|
8
|
+
* structured `warnings[]` JSON entry).
|
|
9
|
+
* • `feedback_coverage` correctly counts runs with feedback dispatched
|
|
10
|
+
* vs total Phase 1 runs.
|
|
11
|
+
* • NaN-safety: zero-feedback asset emits all rates as `null`, never
|
|
12
|
+
* `0` or `NaN`.
|
|
13
|
+
* • Attribution rule (§6.8): a feedback event is attributed to the run
|
|
14
|
+
* that produced it, not to a later run touching the same asset.
|
|
15
|
+
*
|
|
16
|
+
* The metric is a pure function over RunResult[] + feedbackLog[]; no spawn
|
|
17
|
+
* fakes are needed. We build small synthetic streams directly.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, expect, test } from "bun:test";
|
|
20
|
+
import { computeFeedbackIntegrity } from "./metrics";
|
|
21
|
+
import { FEEDBACK_AGREEMENT_WARNING_THRESHOLD, renderEvolveReport, renderFeedbackIntegrityTable } from "./report";
|
|
22
|
+
function fakeRun(overrides) {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
taskId: "t",
|
|
26
|
+
arm: "akm",
|
|
27
|
+
seed: 0,
|
|
28
|
+
model: "m",
|
|
29
|
+
outcome: "pass",
|
|
30
|
+
tokens: { input: 0, output: 0 },
|
|
31
|
+
wallclockMs: 0,
|
|
32
|
+
trajectory: { correctAssetLoaded: null, feedbackRecorded: null },
|
|
33
|
+
events: [],
|
|
34
|
+
verifierStdout: "",
|
|
35
|
+
verifierExitCode: 0,
|
|
36
|
+
assetsLoaded: [],
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function fb(overrides) {
|
|
41
|
+
return {
|
|
42
|
+
taskId: "t",
|
|
43
|
+
seed: 0,
|
|
44
|
+
goldRef: "skill:s",
|
|
45
|
+
signal: "positive",
|
|
46
|
+
ok: true,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
describe("computeFeedbackIntegrity — 2x2 quadrants", () => {
|
|
51
|
+
test("TP: feedback + on a passed run", () => {
|
|
52
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "pass" })] };
|
|
53
|
+
const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "positive" })];
|
|
54
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
55
|
+
expect(m.aggregate.truePositive).toBe(1);
|
|
56
|
+
expect(m.aggregate.falsePositive).toBe(0);
|
|
57
|
+
expect(m.aggregate.trueNegative).toBe(0);
|
|
58
|
+
expect(m.aggregate.falseNegative).toBe(0);
|
|
59
|
+
expect(m.aggregate.feedback_agreement).toBeCloseTo(1);
|
|
60
|
+
expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
|
|
61
|
+
expect(m.perAsset).toHaveLength(1);
|
|
62
|
+
expect(m.perAsset[0].ref).toBe("skill:a");
|
|
63
|
+
expect(m.perAsset[0].truePositive).toBe(1);
|
|
64
|
+
expect(m.perAsset[0].feedback_agreement).toBeCloseTo(1);
|
|
65
|
+
});
|
|
66
|
+
test("FP: feedback + on a failed run", () => {
|
|
67
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "fail" })] };
|
|
68
|
+
const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "positive" })];
|
|
69
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
70
|
+
expect(m.aggregate.truePositive).toBe(0);
|
|
71
|
+
expect(m.aggregate.falsePositive).toBe(1);
|
|
72
|
+
expect(m.aggregate.trueNegative).toBe(0);
|
|
73
|
+
expect(m.aggregate.falseNegative).toBe(0);
|
|
74
|
+
expect(m.aggregate.feedback_agreement).toBeCloseTo(0);
|
|
75
|
+
expect(m.aggregate.false_positive_rate).toBeCloseTo(1);
|
|
76
|
+
expect(m.perAsset[0].falsePositive).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
test("TN: feedback - on a failed run", () => {
|
|
79
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "fail" })] };
|
|
80
|
+
const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "negative" })];
|
|
81
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
82
|
+
expect(m.aggregate.trueNegative).toBe(1);
|
|
83
|
+
expect(m.aggregate.feedback_agreement).toBeCloseTo(1);
|
|
84
|
+
expect(m.aggregate.false_positive_rate).toBeCloseTo(0);
|
|
85
|
+
expect(m.perAsset[0].trueNegative).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
test("FN: feedback - on a passed run", () => {
|
|
88
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t1", seed: 0, outcome: "pass" })] };
|
|
89
|
+
const feedbackLog = [fb({ taskId: "t1", seed: 0, goldRef: "skill:a", signal: "negative" })];
|
|
90
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
91
|
+
expect(m.aggregate.falseNegative).toBe(1);
|
|
92
|
+
expect(m.aggregate.feedback_agreement).toBeCloseTo(0);
|
|
93
|
+
expect(m.aggregate.false_negative_rate).toBeCloseTo(1);
|
|
94
|
+
expect(m.perAsset[0].falseNegative).toBe(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe("computeFeedbackIntegrity — aggregate over mixed quadrants", () => {
|
|
98
|
+
test("computes feedback_agreement and rates correctly across mixed runs", () => {
|
|
99
|
+
// 4 runs covering all four quadrants — exactly one of each.
|
|
100
|
+
const phase1 = {
|
|
101
|
+
akmRuns: [
|
|
102
|
+
fakeRun({ taskId: "tp", seed: 0, outcome: "pass" }),
|
|
103
|
+
fakeRun({ taskId: "fp", seed: 0, outcome: "fail" }),
|
|
104
|
+
fakeRun({ taskId: "tn", seed: 0, outcome: "fail" }),
|
|
105
|
+
fakeRun({ taskId: "fn", seed: 0, outcome: "pass" }),
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
const feedbackLog = [
|
|
109
|
+
fb({ taskId: "tp", seed: 0, goldRef: "skill:tp", signal: "positive" }),
|
|
110
|
+
fb({ taskId: "fp", seed: 0, goldRef: "skill:fp", signal: "positive" }),
|
|
111
|
+
fb({ taskId: "tn", seed: 0, goldRef: "skill:tn", signal: "negative" }),
|
|
112
|
+
fb({ taskId: "fn", seed: 0, goldRef: "skill:fn", signal: "negative" }),
|
|
113
|
+
];
|
|
114
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
115
|
+
expect(m.aggregate.truePositive).toBe(1);
|
|
116
|
+
expect(m.aggregate.falsePositive).toBe(1);
|
|
117
|
+
expect(m.aggregate.trueNegative).toBe(1);
|
|
118
|
+
expect(m.aggregate.falseNegative).toBe(1);
|
|
119
|
+
expect(m.aggregate.feedback_agreement).toBeCloseTo(0.5); // 2/4
|
|
120
|
+
expect(m.aggregate.false_positive_rate).toBeCloseTo(0.5); // 1 / (1+1)
|
|
121
|
+
expect(m.aggregate.false_negative_rate).toBeCloseTo(0.5); // 1 / (1+1)
|
|
122
|
+
expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
|
|
123
|
+
expect(m.perAsset).toHaveLength(4);
|
|
124
|
+
// Per-asset rows should be sorted by ref
|
|
125
|
+
expect(m.perAsset.map((r) => r.ref)).toEqual(["skill:fn", "skill:fp", "skill:tn", "skill:tp"]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe("computeFeedbackIntegrity — per-asset mixed signals", () => {
|
|
129
|
+
test("aggregates correctly when one asset appears across multiple Phase 1 runs", () => {
|
|
130
|
+
// skill:shared has 2 TP, 1 FP, 1 TN, 1 FN across 5 runs.
|
|
131
|
+
const phase1 = {
|
|
132
|
+
akmRuns: [
|
|
133
|
+
fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
|
|
134
|
+
fakeRun({ taskId: "t", seed: 1, outcome: "pass" }),
|
|
135
|
+
fakeRun({ taskId: "t", seed: 2, outcome: "fail" }),
|
|
136
|
+
fakeRun({ taskId: "t", seed: 3, outcome: "fail" }),
|
|
137
|
+
fakeRun({ taskId: "t", seed: 4, outcome: "pass" }),
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
const feedbackLog = [
|
|
141
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:shared", signal: "positive" }), // TP
|
|
142
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:shared", signal: "positive" }), // TP
|
|
143
|
+
fb({ taskId: "t", seed: 2, goldRef: "skill:shared", signal: "positive" }), // FP
|
|
144
|
+
fb({ taskId: "t", seed: 3, goldRef: "skill:shared", signal: "negative" }), // TN
|
|
145
|
+
fb({ taskId: "t", seed: 4, goldRef: "skill:shared", signal: "negative" }), // FN
|
|
146
|
+
];
|
|
147
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
148
|
+
expect(m.perAsset).toHaveLength(1);
|
|
149
|
+
const row = m.perAsset[0];
|
|
150
|
+
expect(row.ref).toBe("skill:shared");
|
|
151
|
+
expect(row.truePositive).toBe(2);
|
|
152
|
+
expect(row.falsePositive).toBe(1);
|
|
153
|
+
expect(row.trueNegative).toBe(1);
|
|
154
|
+
expect(row.falseNegative).toBe(1);
|
|
155
|
+
expect(row.feedback_agreement).toBeCloseTo(3 / 5);
|
|
156
|
+
expect(row.false_positive_rate).toBeCloseTo(1 / 2); // FP / (FP+TN) = 1/2
|
|
157
|
+
expect(row.false_negative_rate).toBeCloseTo(1 / 3); // FN / (FN+TP) = 1/3
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("computeFeedbackIntegrity — attribution rule", () => {
|
|
161
|
+
test("attributes feedback to the run that produced it, not a later run touching the same asset", () => {
|
|
162
|
+
// skill:contested appears across two Phase 1 runs:
|
|
163
|
+
// run #0: passed, feedback + → TP
|
|
164
|
+
// run #1: failed, feedback + → FP
|
|
165
|
+
// The naive (wrong) implementation would conflate both events with
|
|
166
|
+
// run #1's outcome and label both as FP. The correct implementation
|
|
167
|
+
// joins each event to its own (taskId, seed) → gets one TP, one FP.
|
|
168
|
+
const phase1 = {
|
|
169
|
+
akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
|
|
170
|
+
};
|
|
171
|
+
const feedbackLog = [
|
|
172
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:contested", signal: "positive" }),
|
|
173
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:contested", signal: "positive" }),
|
|
174
|
+
];
|
|
175
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
176
|
+
expect(m.aggregate.truePositive).toBe(1);
|
|
177
|
+
expect(m.aggregate.falsePositive).toBe(1);
|
|
178
|
+
expect(m.aggregate.trueNegative).toBe(0);
|
|
179
|
+
expect(m.aggregate.falseNegative).toBe(0);
|
|
180
|
+
expect(m.perAsset[0].truePositive).toBe(1);
|
|
181
|
+
expect(m.perAsset[0].falsePositive).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe("computeFeedbackIntegrity — feedback_coverage", () => {
|
|
185
|
+
test("counts runs with feedback dispatched vs total Phase 1 runs", () => {
|
|
186
|
+
// 4 phase-1 runs, only 2 had feedback dispatched.
|
|
187
|
+
const phase1 = {
|
|
188
|
+
akmRuns: [
|
|
189
|
+
fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
|
|
190
|
+
fakeRun({ taskId: "t", seed: 1, outcome: "fail" }),
|
|
191
|
+
fakeRun({ taskId: "t", seed: 2, outcome: "harness_error" }),
|
|
192
|
+
fakeRun({ taskId: "t", seed: 3, outcome: "budget_exceeded" }),
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
const feedbackLog = [
|
|
196
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
|
|
197
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "negative" }),
|
|
198
|
+
];
|
|
199
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
200
|
+
expect(m.aggregate.feedback_coverage).toBeCloseTo(0.5); // 2 of 4
|
|
201
|
+
});
|
|
202
|
+
test("zero coverage when no feedback dispatched", () => {
|
|
203
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" })] };
|
|
204
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog: [] });
|
|
205
|
+
expect(m.aggregate.feedback_coverage).toBe(0);
|
|
206
|
+
expect(m.aggregate.feedback_agreement).toBe(0);
|
|
207
|
+
expect(m.perAsset).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
test("zero coverage and zero runs returns 0 (not NaN)", () => {
|
|
210
|
+
const m = computeFeedbackIntegrity({ phase1: { akmRuns: [] }, feedbackLog: [] });
|
|
211
|
+
expect(m.aggregate.feedback_coverage).toBe(0);
|
|
212
|
+
expect(m.aggregate.feedback_agreement).toBe(0);
|
|
213
|
+
expect(m.aggregate.false_positive_rate).toBe(0);
|
|
214
|
+
expect(m.aggregate.false_negative_rate).toBe(0);
|
|
215
|
+
expect(Number.isFinite(m.aggregate.feedback_coverage)).toBe(true);
|
|
216
|
+
expect(Number.isFinite(m.aggregate.feedback_agreement)).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe("computeFeedbackIntegrity — NaN safety", () => {
|
|
220
|
+
test("per-asset row with FP+TN === 0 emits null false_positive_rate (only positive feedback on passes)", () => {
|
|
221
|
+
const phase1 = {
|
|
222
|
+
akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "pass" })],
|
|
223
|
+
};
|
|
224
|
+
const feedbackLog = [
|
|
225
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:only-tp", signal: "positive" }),
|
|
226
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:only-tp", signal: "positive" }),
|
|
227
|
+
];
|
|
228
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
229
|
+
const row = m.perAsset[0];
|
|
230
|
+
expect(row.feedback_agreement).toBeCloseTo(1);
|
|
231
|
+
expect(row.false_positive_rate).toBeNull(); // FP+TN === 0
|
|
232
|
+
expect(row.false_negative_rate).toBeCloseTo(0); // FN/(FN+TP) = 0/2 = 0
|
|
233
|
+
});
|
|
234
|
+
test("per-asset row with FN+TP === 0 emits null false_negative_rate (only negative feedback on fails)", () => {
|
|
235
|
+
const phase1 = {
|
|
236
|
+
akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "fail" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
|
|
237
|
+
};
|
|
238
|
+
const feedbackLog = [
|
|
239
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:only-tn", signal: "negative" }),
|
|
240
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:only-tn", signal: "negative" }),
|
|
241
|
+
];
|
|
242
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
243
|
+
const row = m.perAsset[0];
|
|
244
|
+
expect(row.feedback_agreement).toBeCloseTo(1);
|
|
245
|
+
expect(row.false_negative_rate).toBeNull(); // FN+TP === 0
|
|
246
|
+
expect(row.false_positive_rate).toBeCloseTo(0); // FP/(FP+TN) = 0/2 = 0
|
|
247
|
+
});
|
|
248
|
+
test("ok=false feedback events are excluded from the matrix but still count toward coverage", () => {
|
|
249
|
+
const phase1 = {
|
|
250
|
+
akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
|
|
251
|
+
};
|
|
252
|
+
const feedbackLog = [
|
|
253
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive", ok: true }),
|
|
254
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "negative", ok: false }),
|
|
255
|
+
];
|
|
256
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
257
|
+
// Only the ok=true entry contributes to the matrix (TP=1).
|
|
258
|
+
expect(m.aggregate.truePositive).toBe(1);
|
|
259
|
+
expect(m.aggregate.trueNegative).toBe(0);
|
|
260
|
+
// But coverage counts both attempts.
|
|
261
|
+
expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
|
|
262
|
+
});
|
|
263
|
+
test("harness_error runs are excluded from the matrix even with a stamped feedback event", () => {
|
|
264
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "harness_error" })] };
|
|
265
|
+
const feedbackLog = [fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" })];
|
|
266
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
267
|
+
expect(m.aggregate.truePositive).toBe(0);
|
|
268
|
+
expect(m.aggregate.falsePositive).toBe(0);
|
|
269
|
+
expect(m.perAsset).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
test("feedback for a run not present in akmRuns is silently dropped", () => {
|
|
272
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "real", seed: 0, outcome: "pass" })] };
|
|
273
|
+
const feedbackLog = [fb({ taskId: "ghost", seed: 99, goldRef: "skill:a", signal: "positive" })];
|
|
274
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
275
|
+
expect(m.aggregate.truePositive).toBe(0);
|
|
276
|
+
expect(m.perAsset).toEqual([]);
|
|
277
|
+
// Coverage still records the dispatch attempt — operator wanted feedback.
|
|
278
|
+
expect(m.aggregate.feedback_coverage).toBeCloseTo(1);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
// ── Render-side coverage ───────────────────────────────────────────────────
|
|
282
|
+
function emptyUtilityReport() {
|
|
283
|
+
// Build a minimal §13.3-shaped utility report. The renderer reads
|
|
284
|
+
// many subfields; we stub them to safe zeros.
|
|
285
|
+
return {
|
|
286
|
+
timestamp: "2026-04-27T00:00:00Z",
|
|
287
|
+
branch: "test",
|
|
288
|
+
commit: "deadbee",
|
|
289
|
+
model: "m",
|
|
290
|
+
corpus: { domains: 0, tasks: 0, slice: "all", seedsPerArm: 1 },
|
|
291
|
+
aggregateNoakm: { passRate: 0, tokensPerPass: 0, wallclockMs: 0 },
|
|
292
|
+
aggregateAkm: { passRate: 0, tokensPerPass: 0, wallclockMs: 0 },
|
|
293
|
+
aggregateDelta: {
|
|
294
|
+
passRate: 0,
|
|
295
|
+
tokensPerPass: 0,
|
|
296
|
+
wallclockMs: 0,
|
|
297
|
+
},
|
|
298
|
+
trajectoryAkm: {
|
|
299
|
+
correctAssetLoaded: null,
|
|
300
|
+
feedbackRecorded: 0,
|
|
301
|
+
},
|
|
302
|
+
failureModes: { byLabel: {}, byTask: {} },
|
|
303
|
+
tasks: [],
|
|
304
|
+
warnings: [],
|
|
305
|
+
akmRuns: [],
|
|
306
|
+
taskMetadata: [],
|
|
307
|
+
goldRankRecords: [],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function evolveInputWith(metrics) {
|
|
311
|
+
return {
|
|
312
|
+
timestamp: "2026-04-27T00:00:00Z",
|
|
313
|
+
branch: "test",
|
|
314
|
+
commit: "deadbee",
|
|
315
|
+
model: "m",
|
|
316
|
+
domain: "test",
|
|
317
|
+
seedsPerArm: 1,
|
|
318
|
+
proposals: { rows: [], totalProposals: 0, totalAccepted: 0, acceptanceRate: 0, lintPassRate: 0 },
|
|
319
|
+
longitudinal: {
|
|
320
|
+
improvementSlope: 0.1,
|
|
321
|
+
overSyntheticLift: 0.05,
|
|
322
|
+
degradationCount: 0,
|
|
323
|
+
degradations: [],
|
|
324
|
+
prePassRate: 0.5,
|
|
325
|
+
postPassRate: 0.6,
|
|
326
|
+
syntheticPassRate: 0.55,
|
|
327
|
+
},
|
|
328
|
+
arms: { pre: emptyUtilityReport(), post: emptyUtilityReport(), synthetic: emptyUtilityReport() },
|
|
329
|
+
warnings: [],
|
|
330
|
+
...(metrics ? { feedbackIntegrity: metrics } : {}),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
describe("renderFeedbackIntegrityTable", () => {
|
|
334
|
+
test("emits aggregate matrix + per-asset rows", () => {
|
|
335
|
+
const phase1 = {
|
|
336
|
+
akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" }), fakeRun({ taskId: "t", seed: 1, outcome: "fail" })],
|
|
337
|
+
};
|
|
338
|
+
const feedbackLog = [
|
|
339
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
|
|
340
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "negative" }),
|
|
341
|
+
];
|
|
342
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
343
|
+
const md = renderFeedbackIntegrityTable(m);
|
|
344
|
+
expect(md).toContain("Feedback-signal integrity");
|
|
345
|
+
expect(md).toContain("feedback_agreement | 1.00");
|
|
346
|
+
expect(md).toContain("feedback_coverage | 1.00");
|
|
347
|
+
expect(md).toContain("`skill:a`");
|
|
348
|
+
});
|
|
349
|
+
test("renders n/a when a per-asset rate is null", () => {
|
|
350
|
+
const phase1 = { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" })] };
|
|
351
|
+
const feedbackLog = [fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" })];
|
|
352
|
+
const m = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
353
|
+
const md = renderFeedbackIntegrityTable(m);
|
|
354
|
+
// Only TP — false_positive_rate denom is 0 → null → "n/a".
|
|
355
|
+
expect(md).toContain("n/a");
|
|
356
|
+
});
|
|
357
|
+
test("renders 'No feedback events recorded' when perAsset is empty", () => {
|
|
358
|
+
const m = {
|
|
359
|
+
aggregate: {
|
|
360
|
+
truePositive: 0,
|
|
361
|
+
falsePositive: 0,
|
|
362
|
+
trueNegative: 0,
|
|
363
|
+
falseNegative: 0,
|
|
364
|
+
feedback_agreement: 0,
|
|
365
|
+
false_positive_rate: 0,
|
|
366
|
+
false_negative_rate: 0,
|
|
367
|
+
feedback_coverage: 0,
|
|
368
|
+
},
|
|
369
|
+
perAsset: [],
|
|
370
|
+
};
|
|
371
|
+
expect(renderFeedbackIntegrityTable(m)).toContain("No feedback events recorded");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
describe("renderEvolveReport — feedback_agreement headline + warning marker", () => {
|
|
375
|
+
test("places real feedback_agreement after improvement_slope when metrics provided", () => {
|
|
376
|
+
const metrics = computeFeedbackIntegrity({
|
|
377
|
+
phase1: { akmRuns: [fakeRun({ taskId: "t", seed: 0, outcome: "pass" })] },
|
|
378
|
+
feedbackLog: [fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" })],
|
|
379
|
+
});
|
|
380
|
+
const { markdown, json } = renderEvolveReport(evolveInputWith(metrics));
|
|
381
|
+
// feedback_agreement is on a line directly after improvement_slope.
|
|
382
|
+
const slopeIdx = markdown.indexOf("improvement_slope:");
|
|
383
|
+
const agreementIdx = markdown.indexOf("feedback_agreement:");
|
|
384
|
+
expect(slopeIdx).toBeGreaterThanOrEqual(0);
|
|
385
|
+
expect(agreementIdx).toBeGreaterThan(slopeIdx);
|
|
386
|
+
expect(markdown).toContain("feedback_agreement: 1.00");
|
|
387
|
+
expect(markdown).not.toContain("pending (#244)");
|
|
388
|
+
// JSON envelope carries `feedback_integrity` as a top-level key.
|
|
389
|
+
const parsed = json;
|
|
390
|
+
expect(parsed.feedback_integrity).toBeDefined();
|
|
391
|
+
expect(parsed.warnings.some((w) => w.startsWith("feedback_agreement_below_threshold"))).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
test("placeholder remains when metrics omitted (legacy path)", () => {
|
|
394
|
+
const { markdown, json } = renderEvolveReport(evolveInputWith(undefined));
|
|
395
|
+
expect(markdown).toContain("_feedback_agreement: pending (#244)_");
|
|
396
|
+
const parsed = json;
|
|
397
|
+
expect(parsed.feedback_integrity).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
test("agreement < 0.80 prepends warning marker to markdown and structured warnings[]", () => {
|
|
400
|
+
// 1 TP + 4 FP → agreement = 1/5 = 0.20.
|
|
401
|
+
const phase1 = {
|
|
402
|
+
akmRuns: [
|
|
403
|
+
fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
|
|
404
|
+
fakeRun({ taskId: "t", seed: 1, outcome: "fail" }),
|
|
405
|
+
fakeRun({ taskId: "t", seed: 2, outcome: "fail" }),
|
|
406
|
+
fakeRun({ taskId: "t", seed: 3, outcome: "fail" }),
|
|
407
|
+
fakeRun({ taskId: "t", seed: 4, outcome: "fail" }),
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
const feedbackLog = [
|
|
411
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
|
|
412
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "positive" }),
|
|
413
|
+
fb({ taskId: "t", seed: 2, goldRef: "skill:a", signal: "positive" }),
|
|
414
|
+
fb({ taskId: "t", seed: 3, goldRef: "skill:a", signal: "positive" }),
|
|
415
|
+
fb({ taskId: "t", seed: 4, goldRef: "skill:a", signal: "positive" }),
|
|
416
|
+
];
|
|
417
|
+
const metrics = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
418
|
+
expect(metrics.aggregate.feedback_agreement).toBeCloseTo(0.2);
|
|
419
|
+
expect(metrics.aggregate.feedback_agreement).toBeLessThan(FEEDBACK_AGREEMENT_WARNING_THRESHOLD);
|
|
420
|
+
const { markdown, json } = renderEvolveReport(evolveInputWith(metrics));
|
|
421
|
+
// Marker appears above the headline, not after it.
|
|
422
|
+
const warnIdx = markdown.indexOf("feedback_agreement = 0.20");
|
|
423
|
+
const slopeIdx = markdown.indexOf("**improvement_slope:");
|
|
424
|
+
expect(warnIdx).toBeGreaterThanOrEqual(0);
|
|
425
|
+
expect(warnIdx).toBeLessThan(slopeIdx);
|
|
426
|
+
expect(markdown).toContain("Track B headline numbers");
|
|
427
|
+
// Structured warning surfaces in the JSON envelope.
|
|
428
|
+
const parsed = json;
|
|
429
|
+
expect(parsed.warnings.some((w) => w.startsWith("feedback_agreement_below_threshold"))).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
test("agreement at exactly 0.80 does NOT trigger the warning marker", () => {
|
|
432
|
+
// 4 TP + 1 FP → agreement = 4/5 = 0.80 exactly.
|
|
433
|
+
const phase1 = {
|
|
434
|
+
akmRuns: [
|
|
435
|
+
fakeRun({ taskId: "t", seed: 0, outcome: "pass" }),
|
|
436
|
+
fakeRun({ taskId: "t", seed: 1, outcome: "pass" }),
|
|
437
|
+
fakeRun({ taskId: "t", seed: 2, outcome: "pass" }),
|
|
438
|
+
fakeRun({ taskId: "t", seed: 3, outcome: "pass" }),
|
|
439
|
+
fakeRun({ taskId: "t", seed: 4, outcome: "fail" }),
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
const feedbackLog = [
|
|
443
|
+
fb({ taskId: "t", seed: 0, goldRef: "skill:a", signal: "positive" }),
|
|
444
|
+
fb({ taskId: "t", seed: 1, goldRef: "skill:a", signal: "positive" }),
|
|
445
|
+
fb({ taskId: "t", seed: 2, goldRef: "skill:a", signal: "positive" }),
|
|
446
|
+
fb({ taskId: "t", seed: 3, goldRef: "skill:a", signal: "positive" }),
|
|
447
|
+
fb({ taskId: "t", seed: 4, goldRef: "skill:a", signal: "positive" }),
|
|
448
|
+
];
|
|
449
|
+
const metrics = computeFeedbackIntegrity({ phase1, feedbackLog });
|
|
450
|
+
expect(metrics.aggregate.feedback_agreement).toBeCloseTo(0.8);
|
|
451
|
+
const { markdown, json } = renderEvolveReport(evolveInputWith(metrics));
|
|
452
|
+
expect(markdown).not.toContain("Track B headline numbers");
|
|
453
|
+
const parsed = json;
|
|
454
|
+
expect(parsed.warnings.some((w) => w.startsWith("feedback_agreement_below_threshold"))).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leakage smoke test for the seeded bench corpus (spec §7.4).
|
|
3
|
+
*
|
|
4
|
+
* For every task that declares a `gold_ref` of the form `skill:<name>`,
|
|
5
|
+
* locate the SKILL.md inside the named fixture stash and assert that the
|
|
6
|
+
* verifier's *structural assertions* do not appear verbatim in the gold-ref
|
|
7
|
+
* content. The gold ref is allowed (and expected) to discuss the topic in
|
|
8
|
+
* general terms — what it must NOT do is hand the agent a copy-pasteable
|
|
9
|
+
* fragment that satisfies the verifier directly.
|
|
10
|
+
*
|
|
11
|
+
* The check extracts:
|
|
12
|
+
* • for `regex` verifiers — the literal segments of `expected_match`
|
|
13
|
+
* between regex meta-characters (these are the substrings the agent
|
|
14
|
+
* must produce);
|
|
15
|
+
* • for `pytest` verifiers — Python-style structural assertion paths and
|
|
16
|
+
* dictionary lookups (e.g., `services.redis.healthcheck.test`,
|
|
17
|
+
* `redis["healthcheck"]["test"]`);
|
|
18
|
+
* • for `script` (shell) verifiers — single-quoted `grep` patterns and
|
|
19
|
+
* `jq -e` expressions, which encode the exact assertion shape.
|
|
20
|
+
*
|
|
21
|
+
* Each fragment is checked individually. Lone tokens that legitimately
|
|
22
|
+
* appear in any reasonable description of the topic (e.g., `redis-cli`,
|
|
23
|
+
* `akm`, `bridge`, `feedback`) are filtered out by a minimum-length and
|
|
24
|
+
* minimum-token-count rule.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, expect, test } from "bun:test";
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { getTasksRoot, listTasks } from "./corpus";
|
|
30
|
+
const STASHES_ROOT = path.resolve(getTasksRoot(), "..", "..", "stashes");
|
|
31
|
+
/** Resolve `skill:<name>` against the named stash; returns SKILL.md path or `undefined`. */
|
|
32
|
+
function resolveGoldRefPath(stashName, goldRef) {
|
|
33
|
+
const match = /^skill:([a-z0-9][a-z0-9-]*)$/.exec(goldRef);
|
|
34
|
+
if (!match)
|
|
35
|
+
return undefined;
|
|
36
|
+
const skillDir = path.join(STASHES_ROOT, stashName, "skills", match[1]);
|
|
37
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
38
|
+
return fs.existsSync(skillFile) ? skillFile : undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Pull the literal segments out of a regex pattern. Splits on regex
|
|
42
|
+
* meta-characters and discards short fragments. The remaining strings are
|
|
43
|
+
* what the agent's stdout must contain — and therefore what the gold ref
|
|
44
|
+
* must NOT spell out verbatim.
|
|
45
|
+
*/
|
|
46
|
+
function regexLiterals(pattern) {
|
|
47
|
+
return pattern
|
|
48
|
+
.split(/[.*+?^${}()|[\]\\]/)
|
|
49
|
+
.map((s) => s.trim())
|
|
50
|
+
.filter((s) => s.length >= 6 && s.includes(" "));
|
|
51
|
+
}
|
|
52
|
+
/** Pull structural assertion fragments out of a pytest verifier file. */
|
|
53
|
+
function pytestStructuralFragments(text) {
|
|
54
|
+
const out = new Set();
|
|
55
|
+
// Subscript chains like compose["services"]["redis"]["healthcheck"]["test"].
|
|
56
|
+
const subscriptRe = /(?:\["[a-z0-9_]+"\]){2,}/g;
|
|
57
|
+
for (const m of text.matchAll(subscriptRe))
|
|
58
|
+
out.add(m[0]);
|
|
59
|
+
// Dotted attribute paths used in error messages, e.g. services.redis.healthcheck.test.
|
|
60
|
+
const dottedRe = /[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2,}/g;
|
|
61
|
+
for (const m of text.matchAll(dottedRe))
|
|
62
|
+
out.add(m[0]);
|
|
63
|
+
return [...out];
|
|
64
|
+
}
|
|
65
|
+
/** Pull shell-verifier assertions: single-quoted greps and jq -e expressions. */
|
|
66
|
+
function shellAssertionFragments(text) {
|
|
67
|
+
const out = new Set();
|
|
68
|
+
// grep -q '<pattern>' or grep -qi '<pattern>'.
|
|
69
|
+
const grepRe = /grep\s+-[a-zA-Z]+\s+'([^']{4,})'/g;
|
|
70
|
+
for (const m of text.matchAll(grepRe))
|
|
71
|
+
out.add(m[1]);
|
|
72
|
+
// jq -e '<expr>'.
|
|
73
|
+
const jqRe = /jq\s+-e\s+'([^']{4,})'/g;
|
|
74
|
+
for (const m of text.matchAll(jqRe))
|
|
75
|
+
out.add(m[1]);
|
|
76
|
+
return [...out];
|
|
77
|
+
}
|
|
78
|
+
function readVerifierFiles(task) {
|
|
79
|
+
let combined = "";
|
|
80
|
+
if (task.verifier === "pytest") {
|
|
81
|
+
const testsDir = path.join(task.taskDir, "tests");
|
|
82
|
+
if (fs.existsSync(testsDir)) {
|
|
83
|
+
for (const entry of fs.readdirSync(testsDir)) {
|
|
84
|
+
if (entry.endsWith(".py"))
|
|
85
|
+
combined += `${fs.readFileSync(path.join(testsDir, entry), "utf8")}\n`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (task.verifier === "script") {
|
|
90
|
+
const verify = path.join(task.taskDir, "verify.sh");
|
|
91
|
+
if (fs.existsSync(verify))
|
|
92
|
+
combined += fs.readFileSync(verify, "utf8");
|
|
93
|
+
}
|
|
94
|
+
return combined;
|
|
95
|
+
}
|
|
96
|
+
describe("gold-ref leakage check", () => {
|
|
97
|
+
const tasks = listTasks().filter((t) => t.goldRef);
|
|
98
|
+
test("at least one task ships with a gold_ref", () => {
|
|
99
|
+
expect(tasks.length).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
for (const task of tasks) {
|
|
102
|
+
test(`${task.id}: verifier text does not appear in gold-ref content`, () => {
|
|
103
|
+
const goldRef = task.goldRef;
|
|
104
|
+
const goldPath = resolveGoldRefPath(task.stash, goldRef);
|
|
105
|
+
// A declared gold_ref MUST resolve to an existing fixture asset. Silent
|
|
106
|
+
// skipping here previously masked typos and stash-name drift; we now
|
|
107
|
+
// fail loudly so the corpus author is forced to fix the reference.
|
|
108
|
+
if (!goldPath) {
|
|
109
|
+
throw new Error(`${task.id}: gold_ref "${goldRef}" against stash "${task.stash}" did not resolve to a SKILL.md under tests/fixtures/stashes/. Fix the gold_ref, fix the stash name, or remove the gold_ref.`);
|
|
110
|
+
}
|
|
111
|
+
const goldContent = fs.readFileSync(goldPath, "utf8");
|
|
112
|
+
const fragments = [];
|
|
113
|
+
if (task.verifier === "regex" && task.expectedMatch) {
|
|
114
|
+
fragments.push(...regexLiterals(task.expectedMatch));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const verifierText = readVerifierFiles(task);
|
|
118
|
+
fragments.push(...pytestStructuralFragments(verifierText));
|
|
119
|
+
fragments.push(...shellAssertionFragments(verifierText));
|
|
120
|
+
}
|
|
121
|
+
const leaked = fragments.filter((frag) => goldContent.includes(frag));
|
|
122
|
+
expect(leaked).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|