diff --git a/scripts/sync-acr-contracts.mjs b/scripts/sync-acr-contracts.mjs index b500df2..15d3bfc 100644 --- a/scripts/sync-acr-contracts.mjs +++ b/scripts/sync-acr-contracts.mjs @@ -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 = "fe3fe467afb91323eaa8d122be7f78a5e6758e3b"; +export const SOURCE_COMMIT = "74d8fc4c32a288fcb5b20fbeac335e17cf642be9"; const PRETTIER_OPTIONS = Object.freeze({ parser: "typescript", diff --git a/src/components/DeterministicAnswerView.test.tsx b/src/components/DeterministicAnswerView.test.tsx index ae3340d..136aa95 100644 --- a/src/components/DeterministicAnswerView.test.tsx +++ b/src/components/DeterministicAnswerView.test.tsx @@ -382,3 +382,36 @@ describe("DeterministicAnswerView: a refused continuation still shows the servic expect(withText).toBe(withoutText); }); }); + +/** + * The user-facing wording of the organization-scope refusal is the service's + * own fixed sentence (ACR `ContextFabricOrganizationScopeUnsupportedLimitation`): + * it says organization-wide analysis of status, health or drivers is not + * supported, and that organization-wide counts are. The view renders it as + * visible text under Limitations; the basis itself is a machine field the view + * does not key off. + */ +describe("DeterministicAnswerView: an organization-scope refusal shows what is and is not supported", () => { + const SENTENCE = + "This question was read as being about the organization as a whole. Organization-wide analysis of status, health or drivers is not supported; organization-wide counts of one kind of subject, such as how many repositories or teams there are, are supported. No canonical facts were read."; + + function refusedOrganizationQuestion(): InvestigationResult { + const base = mockScenarios().find((s) => s.id === "complete")!.result; + return { + ...structuredClone(base), + status: "no_match", + refusal_basis: "organization_scope_unsupported", + limitations: [SENTENCE], + } as unknown as InvestigationResult; + } + + it("renders the sentence as visible text, including what IS supported", () => { + render(); + const article = screen.getByRole("article", { name: "Deterministic answer" }); + const text = visibleText(article); + expect(text).toContain(SENTENCE); + expect(text).toContain("organization-wide counts"); + expect(text).not.toContain("The server refused this question on the basis"); + expect(screen.getByRole("heading", { name: "Limitations" })).toBeInTheDocument(); + }); +}); diff --git a/src/contracts/generated/error.ts b/src/contracts/generated/error.ts index 3fb0569..9ba8348 100644 --- a/src/contracts/generated/error.ts +++ b/src/contracts/generated/error.ts @@ -1,7 +1,7 @@ /* eslint-disable */ /** * GENERATED by scripts/sync-acr-contracts.mjs. DO NOT EDIT. - * Source: full-chaos/dev-health-acr @ fe3fe467afb91323eaa8d122be7f78a5e6758e3b + * Source: full-chaos/dev-health-acr @ 74d8fc4c32a288fcb5b20fbeac335e17cf642be9 * Regenerate with `pnpm acr:contracts:generate --source `. */ diff --git a/src/contracts/generated/investigation-request.ts b/src/contracts/generated/investigation-request.ts index c17dbe2..9ebb4f0 100644 --- a/src/contracts/generated/investigation-request.ts +++ b/src/contracts/generated/investigation-request.ts @@ -1,7 +1,7 @@ /* eslint-disable */ /** * GENERATED by scripts/sync-acr-contracts.mjs. DO NOT EDIT. - * Source: full-chaos/dev-health-acr @ fe3fe467afb91323eaa8d122be7f78a5e6758e3b + * Source: full-chaos/dev-health-acr @ 74d8fc4c32a288fcb5b20fbeac335e17cf642be9 * Regenerate with `pnpm acr:contracts:generate --source `. */ diff --git a/src/contracts/generated/investigation-result.ts b/src/contracts/generated/investigation-result.ts index 4c022eb..e5ae491 100644 --- a/src/contracts/generated/investigation-result.ts +++ b/src/contracts/generated/investigation-result.ts @@ -1,7 +1,7 @@ /* eslint-disable */ /** * GENERATED by scripts/sync-acr-contracts.mjs. DO NOT EDIT. - * Source: full-chaos/dev-health-acr @ fe3fe467afb91323eaa8d122be7f78a5e6758e3b + * Source: full-chaos/dev-health-acr @ 74d8fc4c32a288fcb5b20fbeac335e17cf642be9 * Regenerate with `pnpm acr:contracts:generate --source `. */ @@ -146,7 +146,8 @@ export type ACRContextFabricInvestigationResultV1 = { | "frame_invariant_violated" | "unspecified" | "continuation_context_unverifiable" - | "declared_kind_unmatched"; + | "declared_kind_unmatched" + | "organization_scope_unsupported"; }; export type InterpretedQuestion = { [k: string]: unknown | undefined; @@ -2839,7 +2840,8 @@ export type AnswerCompleteness = { | "frame_invariant_violated" | "unspecified" | "continuation_context_unverifiable" - | "declared_kind_unmatched"; + | "declared_kind_unmatched" + | "organization_scope_unsupported"; }; export interface FactRequirement { diff --git a/src/contracts/manifest.json b/src/contracts/manifest.json index b123856..3b71d5c 100644 --- a/src/contracts/manifest.json +++ b/src/contracts/manifest.json @@ -1,5 +1,5 @@ { - "source_commit": "fe3fe467afb91323eaa8d122be7f78a5e6758e3b", + "source_commit": "74d8fc4c32a288fcb5b20fbeac335e17cf642be9", "source_repository": "full-chaos/dev-health-acr", "files": [ { @@ -28,7 +28,7 @@ }, { "path": "schemas/context_fabric_common.v1.schema.json", - "sha256": "695065e0da7c731d1ee2d8efcc3fed9a7c6b4a95c58b99d1c76025185706e803" + "sha256": "707acb7ee25e1dbd252847740e3383ec4b204b77e8f0276e33f6911693afcfb8" }, { "path": "schemas/context_fabric_investigation_request.v1.schema.json", @@ -36,7 +36,7 @@ }, { "path": "schemas/context_fabric_investigation_result.v1.schema.json", - "sha256": "e8ef8ce7a446e17684371ad0279de20783a0e207877d763d8557f20f1a57843f" + "sha256": "47d6dd017133b29db1d11775d99b8e6d08614c7d759c112f5899eb1a63b65c86" }, { "path": "schemas/error.v1.schema.json", diff --git a/src/contracts/schemas/context_fabric_common.v1.schema.json b/src/contracts/schemas/context_fabric_common.v1.schema.json index 0ee96e3..dcb69fa 100644 --- a/src/contracts/schemas/context_fabric_common.v1.schema.json +++ b/src/contracts/schemas/context_fabric_common.v1.schema.json @@ -3268,7 +3268,8 @@ "frame_invariant_violated", "unspecified", "continuation_context_unverifiable", - "declared_kind_unmatched" + "declared_kind_unmatched", + "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." } diff --git a/src/contracts/schemas/context_fabric_investigation_result.v1.schema.json b/src/contracts/schemas/context_fabric_investigation_result.v1.schema.json index ef6642f..6190435 100644 --- a/src/contracts/schemas/context_fabric_investigation_result.v1.schema.json +++ b/src/contracts/schemas/context_fabric_investigation_result.v1.schema.json @@ -219,7 +219,8 @@ "frame_invariant_violated", "unspecified", "continuation_context_unverifiable", - "declared_kind_unmatched" + "declared_kind_unmatched", + "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." } diff --git a/src/lib/acr/validate.test.ts b/src/lib/acr/validate.test.ts index 5274469..09c6ec8 100644 --- a/src/lib/acr/validate.test.ts +++ b/src/lib/acr/validate.test.ts @@ -1346,8 +1346,8 @@ describe("investigation result contract — the continuation-refusal basis (cons ).$defs.AnswerCompleteness.properties.refusal_basis.enum; expect(resultEnum).toContain("continuation_context_unverifiable"); expect(commonEnum).toContain("continuation_context_unverifiable"); - expect(resultEnum).toHaveLength(5); - expect(commonEnum).toHaveLength(5); + expect(resultEnum).toHaveLength(6); + expect(commonEnum).toHaveLength(6); expect([...resultEnum].sort()).toEqual([...commonEnum].sort()); }); @@ -1494,6 +1494,106 @@ describe("investigation result contract — the declared-kind terminal basis (co }); }); +/** + * `refusal_basis` gains a 6th member, `organization_scope_unsupported`: the + * question made the organization itself the subject and asked for something + * other than a count -- its state, health or drivers -- which is not a + * capability the service has. Organization-wide counts of one kind of subject + * are supported, and the document's own fixed sentence says so. + * + * It is NOT `declared_kind_unmatched`, which refuses a question because none of + * the options found could be chosen to answer it: for an organization-wide + * health question no option could ever be the answer, because its subject is + * the organization itself. + * It is not a frame refusal either. The field is carried at the same two places + * as every other member, and the prior pin rejects the value, so the bump is + * what turns this truthful refusal into an answer this workbench can render. + */ +describe("investigation result contract — the organization-scope refusal basis (consumer pin)", () => { + const RESULT = "context_fabric_investigation_result.v1.schema.json"; + const MEMBER = "organization_scope_unsupported"; + + function withRefusalBasis(value: string): Record { + return { ...structuredClone(canonicalResult), refusal_basis: value }; + } + + it("GREEN: organization_scope_unsupported validates at this pin", () => { + const validation = validateContract(RESULT, withRefusalBasis(MEMBER)); + expect(validation.errors).toEqual([]); + expect(validation.valid).toBe(true); + }); + + it("every earlier member still validates alongside it", () => { + for (const value of [ + "member_kind_unservable", + "frame_invariant_violated", + "unspecified", + "continuation_context_unverifiable", + "declared_kind_unmatched", + ]) { + const validation = validateContract(RESULT, withRefusalBasis(value)); + expect(validation.errors).toEqual([]); + expect(validation.valid).toBe(true); + } + }); + + it("the vocabulary stays CLOSED around the new member: near-misses are rejected", () => { + for (const value of [ + "organization_scope_unsupported_", + "ORGANIZATION_SCOPE_UNSUPPORTED", + "organization_scope", + ]) { + expect(validateContract(RESULT, withRefusalBasis(value)).valid).toBe(false); + } + }); + + it("both vocabularies carry the member — as enum values, not as description text", () => { + const resultEnum = ( + investigationResultSchema as unknown as { + properties: { refusal_basis: { enum: string[] } }; + } + ).properties.refusal_basis.enum; + const commonEnum = ( + commonSchema as unknown as { + $defs: { + AnswerCompleteness: { properties: { refusal_basis: { enum: string[] } } }; + }; + } + ).$defs.AnswerCompleteness.properties.refusal_basis.enum; + expect(resultEnum).toContain(MEMBER); + expect(commonEnum).toContain(MEMBER); + expect([...resultEnum].sort()).toEqual([...commonEnum].sort()); + }); + + it("BEHAVIOURAL: the new value validates at completeness.refusal_basis too", () => { + const doc = structuredClone(canonicalResult) as unknown as Record; + doc.completeness = { + ...(doc.completeness as Record), + refusal_basis: MEMBER, + }; + const validation = validateContract(RESULT, doc); + expect(validation.errors).toEqual([]); + expect(validation.valid).toBe(true); + }); + + it("EXECUTED repro: the prior pin's own schema rejects the new value", () => { + const priorResult = structuredClone(investigationResultSchema) as unknown as { + properties: { refusal_basis: { enum: string[] } }; + }; + expect(priorResult.properties.refusal_basis.enum).toContain(MEMBER); + priorResult.properties.refusal_basis.enum = + priorResult.properties.refusal_basis.enum.filter((value) => value !== MEMBER); + + 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(withRefusalBasis(MEMBER))).toBe(false); + // The stand-in still accepts the previous member, so it is faithful. + expect(validate(withRefusalBasis("declared_kind_unmatched"))).toBe(true); + }); +}); + /** * `CoverageDetail.code` gains a 17th closed-vocabulary member, * `fact_read_origin_state`: the code the service publishes when a requirement's