Skip to content

Commit 2caeb22

Browse files
Pigbibicodex
andcommitted
fix: accept canonical M0 publisher summaries
Co-Authored-By: Codex <noreply@openai.com>
1 parent fcf760a commit 2caeb22

3 files changed

Lines changed: 61 additions & 6 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"computed_at":"2026-08-29T07:29:43Z","data_status":"ready","errors":[],"generated_at":"2026-08-29T07:29:43Z","hypotheses":[{"artifact_type":"research_hypothesis","as_of":"2026-06-20","authority":"research_only","evidence":{"evidence_ref_count":3,"risk_note_count":1,"source_entry_digest":"5018b413542d087208c8e82e976da01a9bd105c3607b8d4d398dfd4139ddb9ea"},"expires_at":"2026-09-05T07:29:43Z","generated_at":"2026-08-29T07:29:43Z","hypothesis_id":"m0r-5232ec3d400c8d9a-evt1-5018b413542d0872","no_order":true,"permitted_next_step":"research_validation_only","provenance":{"source_contract_version":"model_recommendations.v5","source_input_digest":null,"source_project":"QuantAdvisorResearch","source_report_digest":"5232ec3d400c8d9a3538d438719f44a76cfafc64d5d7f0acc5a431841bbd261c","source_schema_version":"5"},"research_context":{"primary_horizon":"medium","source_confidence":"medium","source_style":"event_driven","state":"candidate","suitable_horizons":["short","medium"],"theme_ids":[]},"schema_version":"qsl.m0_research_hypothesis.v1","subject":{"identifier":"EVT1","kind":"asset_idea"}}],"schema_version":"qsl_m0_research_source_snapshot.v1","source_id":"quant-advisor-research","source_report_digest":"5232ec3d400c8d9a3538d438719f44a76cfafc64d5d7f0acc5a431841bbd261c"}

tests/strategy_switch_worker_validation.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
22
import { readFileSync } from "node:fs";
33
import { fileURLToPath } from "node:url";
44
import { dirname, resolve } from "node:path";
5+
import { spawnSync } from "node:child_process";
56

67
import worker, { __test } from "../web/strategy-switch-console/worker.js";
78
import { DEFAULT_ACCOUNT_OPTIONS, RUNTIME_CATALOG_PROJECTION } from "../web/strategy-switch-console/config.js";
@@ -28,6 +29,28 @@ assert.deepEqual(
2829
["schema_version", "source_ledger_sha256", "source_generated_at", "source_computed_at", "viewed_at", "data_status", "summary", "subjects", "policy", "errors"],
2930
);
3031
assert.equal(m0ResearchPublisherEnvelopeSchema["x-qsl-canonical-utf8-max-bytes"], 256 * 1024);
32+
33+
function buildQrsCanonicalM0PublisherBody(sourceSnapshotPath) {
34+
// The fixture is a minimized, unchanged QAR #66 emitted source snapshot.
35+
// Execute QRS #309/#310's real offline builder and return its exact sorted-
36+
// key canonical UTF-8 request body, rather than duplicating it in JS.
37+
const publisherCode = [
38+
"import hashlib,json,sys",
39+
"sys.path.insert(0, sys.argv[2])",
40+
"from build_m0_research_publisher_envelope import build_m0_research_publisher_envelope, canonical_envelope_body",
41+
"raw=open(sys.argv[1], 'rb').read()",
42+
"source=json.loads(raw)",
43+
"envelope=build_m0_research_publisher_envelope(source_snapshot=source, source_artifact={'repository':'QuantStrategyLab/QuantAdvisorResearch','revision':'9e06f248fb60d1c995426e66468cb18454612e9b','run_id':'qar66-fixture-run','artifact_id':'QAR66:source/snapshot','sha256':hashlib.sha256(raw).hexdigest()}, producer_repository='QuantStrategyLab/QuantRuntimeSettings', producer_revision='451b11d0bf6ba2632ca2227c850e7236a40d12e5', now=source['generated_at'])",
44+
"sys.stdout.buffer.write(canonical_envelope_body(envelope))",
45+
].join("; ");
46+
const result = spawnSync(
47+
"python3",
48+
["-c", publisherCode, sourceSnapshotPath, resolve(root, "python/scripts")],
49+
{ cwd: root, encoding: "buffer" },
50+
);
51+
assert.equal(result.status, 0, Buffer.from(result.stderr || "").toString("utf8"));
52+
return Buffer.from(result.stdout);
53+
}
3154
assert.ok(__test.currentStrategiesTimeoutMs >= 8000);
3255
const renderPlatformsBody = indexHtml.match(/function renderPlatforms\(\) \{([\s\S]*?)\n \}/)?.[1] || "";
3356
assert.ok(!renderPlatformsBody.includes("syncStrategyForAccount("));
@@ -2815,6 +2838,25 @@ assert.match(m0ResearchPayload.viewed_at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z
28152838
assert.equal(m0ResearchPayload.data_status, "ready");
28162839
assert.equal(m0ResearchPayload.policy.no_order, true);
28172840
assert.equal(m0ResearchPayload.subjects[0].subject.identifier, "semiconductors");
2841+
const qar66SourceSnapshotPath = resolve(root, "tests/fixtures/qar66-m0-research-source-snapshot.json");
2842+
const qrsCanonicalM0PublisherBody = buildQrsCanonicalM0PublisherBody(qar66SourceSnapshotPath);
2843+
assert.ok(qrsCanonicalM0PublisherBody.byteLength <= m0ResearchPublisherEnvelopeSchema["x-qsl-canonical-utf8-max-bytes"]);
2844+
const qrsCanonicalM0Envelope = JSON.parse(qrsCanonicalM0PublisherBody.toString("utf8"));
2845+
assert.deepEqual(
2846+
Object.keys(qrsCanonicalM0Envelope.ledger.summary),
2847+
[...Object.keys(qrsCanonicalM0Envelope.ledger.summary)].sort(),
2848+
);
2849+
await __test.normalizeM0ResearchLedgerTransport(qrsCanonicalM0Envelope);
2850+
const qrsCanonicalM0Post = await worker.fetch(
2851+
new Request("https://switch.example/api/internal/sync-m0-research-ledger", {
2852+
method: "POST",
2853+
headers: { Authorization: `Bearer ${m0ResearchSyncValue}`, "Content-Type": "application/json" },
2854+
body: qrsCanonicalM0PublisherBody,
2855+
}),
2856+
m0ResearchEnv,
2857+
);
2858+
assert.equal(qrsCanonicalM0Post.status, 200);
2859+
assert.equal((await qrsCanonicalM0Post.json()).no_order, true);
28182860
const m0ExpiredReadProjection = __test.projectM0ResearchDashboardForRead(
28192861
m0ResearchLedger,
28202862
m0ResearchLedgerSha,

web/strategy-switch-console/worker.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ const M0_RESEARCH_HORIZONS = ["short", "medium", "long", "not_applicable"];
101101
const M0_RESEARCH_STATES = ["candidate", "source_verification_required", "deferred", "context_only"];
102102
const M0_RESEARCH_CONFIDENCE = ["high", "medium", "low", "mixed", "no_event", "unknown"];
103103
const M0_RESEARCH_STYLES = ["event_driven", "long_horizon_growth", "value_quality", "macro_context", "mixed_research"];
104+
const M0_RESEARCH_SUMMARY_FIELDS = [
105+
"subject_count",
106+
"observation_count",
107+
"fresh_observation_count",
108+
"stale_observation_count",
109+
"unknown_observation_count",
110+
"horizon_conflict_count",
111+
"historical_stale_horizon_drift_count",
112+
];
104113
// A console decision is intentionally an auditable owner intent, not an
105114
// execution permit. It remains separate from workflow dispatch credentials,
106115
// broker credentials, and any future deterministic execution gateway.
@@ -3101,7 +3110,7 @@ function normalizeM0ResearchLedger(payload, fieldName) {
31013110
assertM0ResearchSortedUnique(subjects, (subject) => `${subject.subject.kind}\u0000${subject.subject.identifier}`, `${fieldName}.subjects`);
31023111
const summary = normalizeM0ResearchLedgerSummary(ledger.summary, `${fieldName}.summary`);
31033112
const calculatedSummary = summarizeM0ResearchSubjects(subjects);
3104-
if (JSON.stringify(summary) !== JSON.stringify(calculatedSummary)) {
3113+
if (!m0ResearchSummariesEqual(summary, calculatedSummary)) {
31053114
throw new Error(`${fieldName}.summary does not match subjects`);
31063115
}
31073116
const expectedStatus = summary.fresh_observation_count > 0
@@ -3122,15 +3131,18 @@ function normalizeM0ResearchLedger(payload, fieldName) {
31223131
}
31233132

31243133
function normalizeM0ResearchLedgerSummary(value, fieldName) {
3125-
const summary = assertExactFields(value, [
3126-
"subject_count", "observation_count", "fresh_observation_count", "stale_observation_count", "unknown_observation_count",
3127-
"horizon_conflict_count", "historical_stale_horizon_drift_count",
3128-
], fieldName);
3134+
const summary = assertExactFields(value, M0_RESEARCH_SUMMARY_FIELDS, fieldName);
31293135
const result = {};
3130-
for (const key of Object.keys(summary)) result[key] = normalizeM0ResearchCount(summary[key], `${fieldName}.${key}`);
3136+
for (const key of M0_RESEARCH_SUMMARY_FIELDS) {
3137+
result[key] = normalizeM0ResearchCount(summary[key], `${fieldName}.${key}`);
3138+
}
31313139
return result;
31323140
}
31333141

3142+
function m0ResearchSummariesEqual(left, right) {
3143+
return M0_RESEARCH_SUMMARY_FIELDS.every((field) => left[field] === right[field]);
3144+
}
3145+
31343146
function normalizeM0ResearchLedgerPolicy(value, fieldName) {
31353147
const policy = assertExactFields(value, ["authority", "no_order", "permitted_next_step", "notice"], fieldName);
31363148
if (policy.authority !== "research_only" || policy.no_order !== true || policy.permitted_next_step !== "research_validation_only") {

0 commit comments

Comments
 (0)