Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
name: ci

# Content-audit pipeline: install (prose only) → audit (deterministic) → cache invariant
# → extract+coverage → optionally install the CAS backends and exercise the socket store.
# The core path stays JSR-free (npm ci --omit=optional); cas/anchored-chain are optional
# deps installed on demand only for the socket-store step, so npm.jsr.io flakiness can't
# break the default pipeline.
# Content-audit pipeline: install (core) → audit (deterministic) → cache invariant
# → extract+coverage → install the optional CAS backends and exercise the socket store.
# The core install (npm ci --omit=optional) pulls the runtime deps — prose tools +
# verbspec (the typed report-tool contract). Only the heavy cas/anchored-chain
# signing/CAS backends are optional, installed on demand for the socket-store step, so
# their npm.jsr.io fetch can't break the default pipeline.
on:
push:
branches: [main]
Expand All @@ -21,7 +22,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Install — prose deps only, no JSR/CAS backends (wordlist + write-good)
- name: Install — core deps (prose tools + verbspec), no CAS backends
run: npm ci --omit=optional
- name: Test — anthropic path (request shape + parsing)
run: node test.mjs
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ ANTHROPIC_API_KEY=… node audit.mjs # real audits — only on cache-misses
- **anthropic** (`anthropic.mjs`) — runs on a cache **miss** when `ANTHROPIC_API_KEY`
is set. Structured output via tool-use (`{score, findings}`), cost-aware default
model (`claude-haiku-4-5`, override with `AUDIT_MODEL`), grounding enforced in the
system prompt.
system prompt. The `report` tool is authored once as a
[`verbspec`](https://github.com/bounded-systems/verbspec) `VerbSpec` and projected to
the Anthropic tool surface (`toAnthropicTool`), so its schema can't drift from the
CLI / MCP projections of the same verb.

## Copy hygiene — deterministic prose checks
Run on every symbol, every run (cheap, never cached):
Expand Down Expand Up @@ -71,6 +74,7 @@ and grounding/overclaim checking — none of which a prose linter does. See the
| hash-keyed result cache | [`cas`](https://github.com/bounded-systems/cas) — bytes by SHA-256 |
| signed, lineage-tracked derivations | [`anchored-chain`](https://github.com/bounded-systems/anchored-chain) |
| typed symbol catalog + per-type assertions | [`brand/content`](https://github.com/bounded-systems/brand) |
| one typed verb → CLI / MCP / Anthropic surfaces | [`verbspec`](https://github.com/bounded-systems/verbspec) — author a verb once, project everywhere |
| budget awareness | [`prx`](https://github.com/bounded-systems/prx) |

The local `.cache/` (SHA-256 keyed) is already a valid CAS; the `cas` package +
Expand Down
44 changes: 30 additions & 14 deletions anthropic.mjs
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
// Real Anthropic auditor — runs on a cache MISS when ANTHROPIC_API_KEY is set.
// Structured output via tool-use (the model must return {score, findings}), a
// cost-aware default model, and grounding enforced in the system prompt so the
// auditor flags ungrounded claims instead of inventing facts. Zero-dep (fetch).
// auditor flags ungrounded claims instead of inventing facts. The HTTP call is
// dep-free (native fetch); the report-tool schema is projected from a verbspec VerbSpec.
import { z } from "zod";
import { defineVerb, toAnthropicTool } from "@bounded-systems/verbspec";

const MODEL = process.env.AUDIT_MODEL || "claude-haiku-4-5-20251001";

const REPORT_TOOL = {
name: "report",
description: "Report the audit result for one copy string.",
input_schema: {
type: "object",
required: ["score", "findings"],
additionalProperties: false,
properties: {
score: { type: "integer", minimum: 0, maximum: 10, description: "0 = unusable, 10 = excellent for its type." },
findings: { type: "array", items: { type: "string" }, description: "Concrete, actionable problems. Empty if none." },
},
},
};
// The `report` tool is authored ONCE as a VerbSpec and projected to the Anthropic
// tool surface (toAnthropicTool) — the same typed contract a CLI / MCP / OpenAPI
// projection reads, so the structured-output schema can't drift from them. The model
// fills `report` with {score, findings}; there is no server-side run (it's a tool-use
// surface), so output/run are vestigial. parseResponse reads back tool_use.input.
export const reportVerb = defineVerb({
id: "report",
summary: "Report the audit result for one copy string.",
actor: "audit",
input: z.object({
score: z.number().int().min(0).max(10).describe("0 = unusable, 10 = excellent for its type."),
findings: z.array(z.string()).describe("Concrete, actionable problems. Empty if none."),
}),
output: z.object({ ok: z.boolean() }).describe("unused — `report` is a tool-use surface, not a runnable verb"),
run: () => ({ ok: true }),
});

// Project to the Anthropic tool definition. z.toJSONSchema stamps a `$schema` draft
// pointer on the input schema; strip it so the wire payload stays byte-for-byte what
// the hand-written REPORT_TOOL sent (the Messages API ignores it either way).
const REPORT_TOOL = (() => {
const tool = toAnthropicTool(reportVerb);
const { $schema, ...input_schema } = tool.input_schema;
return { ...tool, input_schema };
})();

const system = (type, grounding) =>
`You audit a single "${type}" copy string for a product.
Expand Down
22 changes: 21 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
},
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
"@bounded-systems/verbspec": "npm:@jsr/bounded-systems__verbspec@^0.3.0",
"an-array-of-english-words": "^2.0.0",
"write-good": "^1.0.8"
"write-good": "^1.0.8",
"zod": "^4.0.0"
},
"optionalDependencies": {
"@bounded-systems/anchored-chain": "npm:@jsr/bounded-systems__anchored-chain@^0.2.1",
Expand Down
12 changes: 12 additions & 0 deletions test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ const req = buildRequest({ type: "headline", value: "Hello world", grounding: ["
assert.equal(req.tool_choice.name, "report", "forces the report tool (structured output)");
assert.ok(req.tools[0].input_schema.required.includes("score"), "schema requires score");
assert.ok(req.tools[0].input_schema.required.includes("findings"), "schema requires findings");
// The tool is single-sourced from a VerbSpec (anthropic.mjs, projected via toAnthropicTool);
// pin the exact projected schema so a verbspec/zod bump can't silently drift the contract
// (no $schema pointer; integer 0..10; string[]; additionalProperties:false).
assert.deepEqual(req.tools[0].input_schema, {
type: "object",
properties: {
score: { type: "integer", minimum: 0, maximum: 10, description: "0 = unusable, 10 = excellent for its type." },
findings: { type: "array", items: { type: "string" }, description: "Concrete, actionable problems. Empty if none." },
},
required: ["score", "findings"],
additionalProperties: false,
}, "report tool projects to the pinned Anthropic contract");
assert.ok(req.system.includes("fact-a") && req.system.includes("fact-b"), "grounding enforced in system prompt");
assert.equal(req.messages[0].content, "Hello world", "audits the given copy");

Expand Down
Loading