fix: stop claiming a bare-true replacement narrows the contract

Copilot review round 8 (its one suppressed finding; no new inline comments).

The `boolean-schema` suggestion for a bare `true` said the replacement
"narrows the schema deliberately". That holds in an ordinary property
position and inverts under a negating parent: `{"not": true}` rejects
every value, so swapping in `{"type": "object", …}` leaves the parent
rejecting only objects and every non-object then passes — the enclosing
contract is widened. Under `if` it redirects which branch applies rather
than tightening anything.

- Parent-aware advice under `not`: suggest `{}`, which is the exact
  object form of `true` and is already exempt from `untyped-schema` in
  that position, with the message saying why a concrete type would be
  wrong there.
- Everywhere else, and in the `untyped-schema` suggestion too, describe
  the replacement as a deliberate CHANGE of contract rather than a
  narrowing. A suggestion has no business claiming an effect it cannot
  guarantee across the positions it can print from.

Tests pin the `not`-specific advice and assert no bare-`true` suggestion
in any of the three positions contains "narrow". The CLI README's sample
report is re-captured from a real run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-25 03:41:17 -04:00
commit 182c48f296

View file

@ -132,6 +132,45 @@ describe("lintToolSchemas — boolean-schema", () => {
expect(findings[0]!.suggestion).toContain("additionalProperties");
});
it("suggests `{}` for a bare true under not, where a typed schema would widen", () => {
// `{"not": true}` rejects every value. Swapping in `{"type": "object", …}`
// leaves the parent rejecting only objects, so every non-object would then
// pass — the enclosing contract is widened, not narrowed. `{}` is the
// exact replacement, and is exempt from `untyped-schema` in that position.
const findings = lintToolSchemas(
tool({
inputSchema: { type: "object", properties: { a: { not: true } } },
}),
);
expect(rules(findings)).toEqual(["boolean-schema"]);
expect(findings[0]!.suggestion).toContain("exact replacement");
expect(findings[0]!.suggestion).not.toContain('"additionalProperties"');
});
it("never claims a bare-true replacement narrows the contract", () => {
// It narrows in an ordinary property position and does not under `not` or
// `if`, so the wording must not assert an effect it cannot guarantee.
const findings = lintToolSchemas(
tool({
inputSchema: {
type: "object",
properties: { a: true, b: { not: true }, c: { if: true } },
},
}),
);
// Four: a bare `true` in each of the three positions, plus `c` itself
// being unconstrained — a lone `if` asserts nothing without a `then`.
expect(rules(findings)).toEqual([
"boolean-schema",
"boolean-schema",
"untyped-schema",
"boolean-schema",
]);
for (const finding of findings) {
expect(finding.suggestion).not.toMatch(/narrow/i);
}
});
it("suggests the always-false object form for a bare false", () => {
// `properties: {a: false}` forbids the property. Deleting the entry would
// *permit* it with any value under the default `additionalProperties`, so