Skip to content
2 changes: 1 addition & 1 deletion scripts/sync-acr-contracts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ const ARTIFACT_ROOT = path.join(ROOT, "src/contracts");
// WIDENING ONLY, and load-bearing: acr main mints this claim, so without the
// bump a response carrying it is rejected whole as `acr_contract_violation`
// instead of rendering.
export const SOURCE_COMMIT = "74d8fc4c32a288fcb5b20fbeac335e17cf642be9";
export const SOURCE_COMMIT = "9e35fe9405d70879ed41600f3e4f484aea6411b2";

const PRETTIER_OPTIONS = Object.freeze({
parser: "typescript",
Expand Down
39 changes: 39 additions & 0 deletions src/components/DeterministicAnswerView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -415,3 +415,42 @@ describe("DeterministicAnswerView: an organization-scope refusal shows what is a
expect(screen.getByRole("heading", { name: "Limitations" })).toBeInTheDocument();
});
});

/**
* CHAOS-5672: a stored clarification read back without its stored reading of
* the question carries `semantic_reading`, and the workbench says beside the
* options that the service could not check them. Without the field, no notice.
*/
describe("DeterministicAnswerView: a stored clarification whose reading was unavailable says so", () => {
function storedClarification(semanticReading?: unknown): InvestigationResult {
const base = structureMockScenarios()[0]!.result;
const clone = structuredClone(base) as unknown as Record<string, unknown>;
clone.status = "clarification_required";
if (semanticReading === undefined) delete clone.semantic_reading;
else clone.semantic_reading = semanticReading;
return clone as unknown as InvestigationResult;
}

it("renders a visible note for each reason", () => {
for (const [reason, phrase] of [
["semantic_state_absent", "no stored reading of the question is available"],
["semantic_state_unreadable", "its stored reading of the question could not be read"],
] as const) {
const view = render(
<DeterministicAnswerView
result={storedClarification({ status: "unavailable", reason })}
/>,
);
const note = view.container.querySelector(`[data-semantic-reading="${reason}"]`);
expect(note).not.toBeNull();
expect(visibleText(note!)).toContain(phrase);
expect(visibleText(note!)).toContain("Ask the question again");
view.unmount();
}
});

it("renders nothing without the field", () => {
const view = render(<DeterministicAnswerView result={storedClarification()} />);
expect(view.container.querySelector("[data-semantic-reading]")).toBeNull();
});
});
2 changes: 2 additions & 0 deletions src/components/DeterministicAnswerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { EvidenceReferences } from "@/components/EvidenceReferences";
import { FactRowsPanels } from "@/components/FactRowsPanel";
import { FindingsPanel } from "@/components/FindingsPanel";
import { LimitationsPanel } from "@/components/LimitationsPanel";
import { SemanticReadingNotice } from "@/components/SemanticReadingNotice";
import { PriorSubjectReceiptDisclosure } from "@/components/PriorSubjectReceiptDisclosure";
import { StructureConfirmationNotice } from "@/components/StructureConfirmationNotice";
import { StructureNeedsPanel } from "@/components/StructureNeedsPanel";
Expand Down Expand Up @@ -346,6 +347,7 @@ export function DeterministicAnswerView({
/>
);
})()}
<SemanticReadingNotice semanticReading={result.semantic_reading} />
<CoveragePanel coverage={result.coverage} />
<CompletenessPanel completeness={result.completeness} />
<AnswerPlanPanel answerPlan={result.answer_plan} />
Expand Down
31 changes: 31 additions & 0 deletions src/components/SemanticReadingNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { InvestigationResult } from "@/lib/contracts";

/**
* CHAOS-5672: renders ACR's `semantic_reading` disclosure on a STORED
* clarification read back by id.
*
* ACR checks a stored clarification against the reading of the question it was
* produced under. When that reading cannot be loaded, the row is served as it
* was stored and the check could not be made, so this workbench says so beside
* the options: they may not answer the question, and asking again starts a
* fresh investigation. Absent field, no notice -- ACR sets it only when the
* check was needed and could not run.
*/
export function SemanticReadingNotice({
semanticReading,
}: {
semanticReading: InvestigationResult["semantic_reading"];
}) {
if (semanticReading === undefined || semanticReading.status !== "unavailable") return null;
const cause =
semanticReading.reason === "semantic_state_absent"
? "no stored reading of the question is available for it"
: "its stored reading of the question could not be read";
return (
<p role="note" data-semantic-reading={semanticReading.reason}>
This clarification was stored earlier, and {cause}, so the service could not check
whether these options still answer the question. Ask the question again to start a fresh
investigation.
</p>
);
}
2 changes: 1 addition & 1 deletion src/contracts/generated/error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable */
/**
* GENERATED by scripts/sync-acr-contracts.mjs. DO NOT EDIT.
* Source: full-chaos/dev-health-acr @ 74d8fc4c32a288fcb5b20fbeac335e17cf642be9
* Source: full-chaos/dev-health-acr @ 9e35fe9405d70879ed41600f3e4f484aea6411b2
* Regenerate with `pnpm acr:contracts:generate --source <acr worktree>`.
*/

Expand Down
2 changes: 1 addition & 1 deletion src/contracts/generated/investigation-request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable */
/**
* GENERATED by scripts/sync-acr-contracts.mjs. DO NOT EDIT.
* Source: full-chaos/dev-health-acr @ 74d8fc4c32a288fcb5b20fbeac335e17cf642be9
* Source: full-chaos/dev-health-acr @ 9e35fe9405d70879ed41600f3e4f484aea6411b2
* Regenerate with `pnpm acr:contracts:generate --source <acr worktree>`.
*/

Expand Down
9 changes: 8 additions & 1 deletion src/contracts/generated/investigation-result.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable */
/**
* GENERATED by scripts/sync-acr-contracts.mjs. DO NOT EDIT.
* Source: full-chaos/dev-health-acr @ 74d8fc4c32a288fcb5b20fbeac335e17cf642be9
* Source: full-chaos/dev-health-acr @ 9e35fe9405d70879ed41600f3e4f484aea6411b2
* Regenerate with `pnpm acr:contracts:generate --source <acr worktree>`.
*/

Expand Down Expand Up @@ -148,6 +148,13 @@ export type ACRContextFabricInvestigationResultV1 = {
| "continuation_context_unverifiable"
| "declared_kind_unmatched"
| "organization_scope_unsupported";
/**
* CHAOS-5672: present only on a read of a stored clarification whose answerability needed the stored accepted reading and could not load it. Absent means the read did not need the reading or had it. semantic_state_absent: the row carries no reading; semantic_state_unreadable: the row carries a reading this build cannot read. Fresh composition never sets it.
*/
semantic_reading?: {
status: "unavailable";
reason: "semantic_state_absent" | "semantic_state_unreadable";
};
};
export type InterpretedQuestion = {
[k: string]: unknown | undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/contracts/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"source_commit": "74d8fc4c32a288fcb5b20fbeac335e17cf642be9",
"source_commit": "9e35fe9405d70879ed41600f3e4f484aea6411b2",
"source_repository": "full-chaos/dev-health-acr",
"files": [
{
Expand Down Expand Up @@ -36,7 +36,7 @@
},
{
"path": "schemas/context_fabric_investigation_result.v1.schema.json",
"sha256": "47d6dd017133b29db1d11775d99b8e6d08614c7d759c112f5899eb1a63b65c86"
"sha256": "fa6a4d159a0bd54ecf71ef1a58ad6a86206ee02ad56d0bfd1c2d371e6ce64499"
},
{
"path": "schemas/error.v1.schema.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,30 @@
"organization_scope_unsupported"
],
"description": "CHAOS-5442: names WHY the server refused to act on this question's frame, over a closed vocabulary. ABSENT on every turn that was not refused -- absence means \"not refused\", never \"refused for a reason nobody recorded\" (that state is the `unspecified` member). Orthogonal to terminal_reason, which names the CHANNEL an explanation travelled through rather than the decision taken: a refused frame carries both. `continuation_context_unverifiable` refuses a window-only continuation whose prior semantic context the server could not verify; it is not a frame refusal, and the document carries its own fixed limitation sentence."
},
"semantic_reading": {
"type": "object",
"additionalProperties": false,
"required": [
"status",
"reason"
],
"properties": {
"status": {
"type": "string",
"enum": [
"unavailable"
]
},
"reason": {
"type": "string",
"enum": [
"semantic_state_absent",
"semantic_state_unreadable"
]
}
},
"description": "CHAOS-5672: present only on a read of a stored clarification whose answerability needed the stored accepted reading and could not load it. Absent means the read did not need the reading or had it. semantic_state_absent: the row carries no reading; semantic_state_unreadable: the row carries a reading this build cannot read. Fresh composition never sets it."
}
},
"required": [
Expand Down
64 changes: 64 additions & 0 deletions src/lib/acr/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,70 @@ describe("investigation result contract — the organization-scope refusal basis
});
});

/**
* The investigation result gains an optional `semantic_reading` object: ACR
* sets it on a result-by-id read of a stored clarification whose check against
* its stored reading of the question could not run, because that reading could
* not be loaded. `status` is `unavailable`; `reason` is `semantic_state_absent`
* or `semantic_state_unreadable`. Additive: every document the prior pin
* accepted still validates, and the object stays closed.
*/
describe("investigation result contract — the semantic_reading disclosure (consumer pin)", () => {
const RESULT = "context_fabric_investigation_result.v1.schema.json";

function withSemanticReading(value: unknown): Record<string, unknown> {
return { ...structuredClone(canonicalResult), semantic_reading: value };
}

it("GREEN: both reasons validate at this pin", () => {
for (const reason of ["semantic_state_absent", "semantic_state_unreadable"]) {
const validation = validateContract(
RESULT,
withSemanticReading({ status: "unavailable", reason }),
);
expect(validation.errors).toEqual([]);
expect(validation.valid).toBe(true);
}
});

it("a result without the field still validates", () => {
expect(validateContract(RESULT, structuredClone(canonicalResult)).valid).toBe(true);
});

it("the object stays CLOSED: bad members, missing members and extra keys are rejected", () => {
for (const value of [
{ status: "available", reason: "semantic_state_absent" },
{ status: "unavailable", reason: "pre_semantic_state" },
{ status: "unavailable" },
{ reason: "semantic_state_absent" },
{ status: "unavailable", reason: "semantic_state_absent", note: "x" },
"unavailable",
]) {
expect(validateContract(RESULT, withSemanticReading(value)).valid).toBe(false);
}
});

it("EXECUTED repro: the prior pin's own schema rejects the field", () => {
const priorResult = structuredClone(investigationResultSchema) as unknown as {
properties: Record<string, unknown>;
};
expect(priorResult.properties).toHaveProperty("semantic_reading");
delete priorResult.properties.semantic_reading;

const ajv = new Ajv2020({ allErrors: true, strictSchema: false, strictTypes: false });
ajv.addSchema(commonSchema, "context_fabric_common.v1.schema.json");
const validate = ajv.compile(priorResult);

expect(
validate(
withSemanticReading({ status: "unavailable", reason: "semantic_state_absent" }),
),
).toBe(false);
// The stand-in still accepts a document without it, so it is faithful.
expect(validate(structuredClone(canonicalResult))).toBe(true);
});
});

/**
* `CoverageDetail.code` gains a 17th closed-vocabulary member,
* `fact_read_origin_state`: the code the service publishes when a requirement's
Expand Down