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
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: ci

# Content-audit pipeline: install → audit (deterministic) → cache invariant → extract+coverage.
# 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.
on:
push:
branches: [main]
Expand All @@ -17,8 +21,8 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Install (wordlist + write-good)
run: npm ci
- name: Install — prose deps only, no JSR/CAS backends (wordlist + write-good)
run: npm ci --omit=optional
- name: Test — anthropic path (request shape + parsing)
run: node test.mjs
- name: Audit (deterministic — spell + grammar + grounding)
Expand All @@ -27,6 +31,8 @@ jobs:
run: node audit.mjs | grep -q "0 miss"
- name: Extract + coverage
run: node extract.mjs samples/page.html
- name: Install optional CAS backends (JSR) for the socket store
run: npm install --include=optional --no-audit --no-fund
- name: Room — mount the socket store + audit through it
run: |
node store-daemon.mjs &
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ Same `get/put/has` port, three backings:
home for the socket "door"; the CAS blobs/refs/lineage live under `<room>/cas`.

## Status
v0.3 — runnable. Deterministic + caching + grounding verified; the Anthropic path is
v0.3.1 — runnable. Deterministic + caching + grounding verified; the Anthropic path is
implemented (live-verify with a key). Copy-hygiene suite (ai-isms, overclaims, proofread,
readability) with data-driven [`ai-tells.json`](ai-tells.json) rules + first-class
severity. See open issues for productionization (`cas`/`anchored-chain` backing, real
`strings.json` catalog, the optional Vale provider — #6).
severity. The optional Vale provider now ships, gated on `AUDIT_VALE` (#6, #12); em-dash
voice tells (antithesis, cadence) are `suggestion`, not `warn`, so intentional voice
doesn't gate downstream. `cas`/`anchored-chain` are optional deps (the `STORE=cas`/socket
backings); the default run needs neither. See open issues for productionization (real
`strings.json` catalog).
25 changes: 15 additions & 10 deletions package-lock.json

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

19 changes: 15 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
{
"name": "@bounded-systems/string-audit",
"version": "0.0.1",
"private": true,
"version": "0.3.1",
"description": "Cost-aware, grounded content auditor — typed string symbols, type-scoped audits, CAS-memoized LLM calls.",
"type": "module",
"description": "Cost-aware, grounded content auditor — typed string symbols, type-scoped audits, CAS-memoized.",
"bin": { "string-audit": "./audit.mjs" },
"bin": {
"string-audit": "audit.mjs"
},
"scripts": {
"audit": "node audit.mjs",
"extract": "node extract.mjs",
"test": "node test.mjs"
},
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
"an-array-of-english-words": "^2.0.0",
"write-good": "^1.0.8"
},
"optionalDependencies": {
"@bounded-systems/anchored-chain": "npm:@jsr/bounded-systems__anchored-chain@^0.2.1",
"@bounded-systems/cas": "npm:@jsr/bounded-systems__cas@^0.1.2"
}
}
4 changes: 2 additions & 2 deletions store-daemon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import { createServer } from "node:net";
import { mkdirSync, rmSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { CasStore, socketPath } from "./store.mjs";
import { makeCasStore, socketPath } from "./store.mjs";

const sock = socketPath();
const room = dirname(sock);
mkdirSync(room, { recursive: true });
if (existsSync(sock)) rmSync(sock); // clear a stale socket
const store = new CasStore(join(room, "cas"));
const store = await makeCasStore(join(room, "cas")); // loads cas/anchored-chain backends

const server = createServer((c) => {
let buf = "";
Expand Down
39 changes: 27 additions & 12 deletions store.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,24 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } fr
import { join } from "node:path";
import { connect } from "node:net";
import { createPrivateKey, createPublicKey } from "node:crypto";
// CasStore backends are loaded ONLY when STORE=cas, so the default FsStore path
// (the documented "offline, free" run) needs no @bounded-systems packages installed.
const { sha256Hex, sha256BareHex } = process.env.STORE === "cas"
? await import("@bounded-systems/cas")
: {};
const {
digestManifest, manifestToStatement, canonicalJson,
assembleEnvelope, generateEd25519Keypair, ed25519Keyid, ed25519Signer,
} = process.env.STORE === "cas"
? await import("@bounded-systems/anchored-chain")
: {};
// CasStore backends (cas + anchored-chain) load lazily — only when a CasStore is
// actually built, never on the default FsStore / SocketStore-client paths. So the
// documented "offline, free" run needs no @bounded-systems packages installed. The
// load keys off CasStore *construction* (via makeCasStore), not the STORE env: the
// store daemon builds a CasStore with no STORE set, and must load them too.
let sha256Hex, sha256BareHex;
let digestManifest, manifestToStatement, canonicalJson,
assembleEnvelope, generateEd25519Keypair, ed25519Keyid, ed25519Signer;
let casBackendsLoaded = false;
async function loadCasBackends() {
if (casBackendsLoaded) return;
({ sha256Hex, sha256BareHex } = await import("@bounded-systems/cas"));
({
digestManifest, manifestToStatement, canonicalJson,
assembleEnvelope, generateEd25519Keypair, ed25519Keyid, ed25519Signer,
} = await import("@bounded-systems/anchored-chain"));
casBackendsLoaded = true;
}

// Where the store mounts when it's a socket "door" — inside a room (cf. guest-room).
export const socketPath = () => process.env.SOCK || join(process.env.ROOM || ".room", "store.sock");
Expand Down Expand Up @@ -115,8 +122,16 @@ export class SocketStore {
async put(key, value) { await this.#rpc({ op: "put", key, value }); }
}

// Build a CasStore, loading its cas/anchored-chain backends first. The one sanctioned
// way to construct one — both makeStore (STORE=cas) and the store daemon go through here,
// so the lazy backends are always loaded before the constructor reaches for them.
export async function makeCasStore(dir) {
await loadCasBackends();
return new CasStore(dir);
}

export async function makeStore(fsDir) {
if (process.env.STORE === "socket") return new SocketStore(socketPath()); // mounted door, in a room
if (process.env.STORE === "cas") return new CasStore(join(fsDir, "cas"));
if (process.env.STORE === "cas") return makeCasStore(join(fsDir, "cas"));
return new FsStore(fsDir);
}
Loading