style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a5154ee884
commit
ec27228a8e
@@ -55,7 +55,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'No active company' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Defense in depth alongside RLS — confirm membership before composing.
|
||||
// Defense in depth alongside RLS: confirm membership before composing.
|
||||
const { data: membership } = await supabase
|
||||
.from('company_members')
|
||||
.select('role')
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function GET(
|
||||
if (convErr) return NextResponse.json({ error: convErr.message }, { status: 500 })
|
||||
if (!conv) return NextResponse.json({ error: 'Conversation not found' }, { status: 404 })
|
||||
|
||||
// Defense in depth alongside RLS — verify caller is a member of the
|
||||
// Defense in depth alongside RLS: verify caller is a member of the
|
||||
// conversation's company AND owns the conversation row. Conversations are
|
||||
// user-scoped within a company; one team member should not see another's.
|
||||
if (conv.user_id !== user.id) {
|
||||
@@ -91,7 +91,7 @@ export async function PATCH(
|
||||
return NextResponse.json({ error: 'Nothing to update' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Defense in depth — verify ownership before update so a 404 is returned
|
||||
// Defense in depth: verify ownership before update so a 404 is returned
|
||||
// (instead of relying solely on RLS, which would silently 0-row).
|
||||
const { data: existing } = await supabase
|
||||
.from('agent_conversations')
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getActiveCompanyId } from '@/lib/company/context'
|
||||
// Query params:
|
||||
// archived: 'true' | 'false' (default 'false')
|
||||
// pinned: 'true' filters to pinned only
|
||||
// intent: 'general.help' (or any intent id) — filter
|
||||
// intent: 'general.help' (or any intent id), filter
|
||||
// q: case-insensitive substring match on title/context_ref
|
||||
// limit: 1..200, default 50
|
||||
//
|
||||
|
||||
@@ -10,7 +10,7 @@ import { guardSandbox } from '@/lib/sandbox/guard'
|
||||
import { requireCapability } from '@/lib/entitlements/has-capability'
|
||||
import { CAPABILITY } from '@/lib/entitlements/keys'
|
||||
|
||||
// Make sure extensions are loaded — the chat loop dispatches against the
|
||||
// Make sure extensions are loaded: the chat loop dispatches against the
|
||||
// agent tool registry which is populated by the mcp-server extension at load.
|
||||
ensureInitialized()
|
||||
|
||||
@@ -62,7 +62,7 @@ const BodySchema = z.object({
|
||||
// POST /api/agent/invoke
|
||||
//
|
||||
// Streams NDJSON events from the chat loop. Each line is a JSON object whose
|
||||
// `kind` identifies the event type — see lib/agent/chat/run-turn.ts StreamEvent.
|
||||
// `kind` identifies the event type: see lib/agent/chat/run-turn.ts StreamEvent.
|
||||
//
|
||||
// Auth: the user must be a member of the resolved company.
|
||||
//
|
||||
@@ -72,7 +72,7 @@ export async function POST(request: Request) {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
// Generous per-user rate limit — bounds runaway Bedrock spend (loop-firing
|
||||
// Generous per-user rate limit: bounds runaway Bedrock spend (loop-firing
|
||||
// sessions). Fails open on infra error.
|
||||
const rate = await checkAgentRateLimit(supabase, user.id)
|
||||
if (!rate.ok) {
|
||||
@@ -108,7 +108,7 @@ export async function POST(request: Request) {
|
||||
.maybeSingle()
|
||||
if (!membership) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
|
||||
// No Anthropic Bedrock calls in the sandbox — the demo runs entirely on
|
||||
// No Anthropic Bedrock calls in the sandbox: the demo runs entirely on
|
||||
// seed data and the assistant is gated to a "look, don't touch" preview.
|
||||
const blocked = await guardSandbox(supabase, companyId)
|
||||
if (blocked) return blocked
|
||||
@@ -116,12 +116,12 @@ export async function POST(request: Request) {
|
||||
const capBlocked = await requireCapability(supabase, companyId, CAPABILITY.ai)
|
||||
if (capBlocked) return capBlocked
|
||||
|
||||
// onboarding.intake completion signal — once the user has actually
|
||||
// onboarding.intake completion signal: once the user has actually
|
||||
// engaged (typed a real reply, not the auto-fired greeting prompt that
|
||||
// mounts the chat), stamp intake_completed_at on the profile so re-entry
|
||||
// logic and opportunistic follow-up logic in other intents can tell the
|
||||
// intake happened. Idempotent: the IS NULL guard ensures we never
|
||||
// overwrite the first engagement timestamp. Best-effort — failure here
|
||||
// overwrite the first engagement timestamp. Best-effort: failure here
|
||||
// doesn't break the chat; the next user turn retries.
|
||||
if (
|
||||
body.intent_id === 'onboarding.intake' &&
|
||||
@@ -136,7 +136,7 @@ export async function POST(request: Request) {
|
||||
.eq('company_id', companyId)
|
||||
.is('intake_completed_at', null)
|
||||
} catch {
|
||||
// ignored — see comment above
|
||||
// ignored: see comment above
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stream — NDJSON events from the chat loop.
|
||||
// Stream: NDJSON events from the chat loop.
|
||||
const encoder = new TextEncoder()
|
||||
// Conversation id is set above; capture into a non-null local for the
|
||||
// streaming closure's first emission.
|
||||
|
||||
@@ -8,13 +8,13 @@ import { requireWritePermission } from '@/lib/auth/require-write'
|
||||
// Mutate a single memory entry. Powers the list/edit/pin/dismiss affordances
|
||||
// on /settings/agent-memory (plan §11).
|
||||
//
|
||||
// content — edit the durable text (kind never changes; that would
|
||||
// content: edit the durable text (kind never changes; that would
|
||||
// muddle the audit lineage). Append-only is preserved by
|
||||
// superseded_by chains when an upstream caller wants it; the
|
||||
// transparency UI is allowed to overwrite in place because
|
||||
// the row's `updated_at` already documents the edit.
|
||||
// is_pinned — boost into the top-N prompt block regardless of score.
|
||||
// is_active — false = dismiss (removes from ranking pool); true = restore.
|
||||
// is_pinned: boost into the top-N prompt block regardless of score.
|
||||
// is_active: false = dismiss (removes from ranking pool); true = restore.
|
||||
//
|
||||
// RLS scopes to user_company_ids(); we ALSO re-verify membership for the
|
||||
// row's company_id as defense in depth before mutating.
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('GET /api/agent/memory', () => {
|
||||
{
|
||||
id: 'mem-1',
|
||||
kind: 'fact',
|
||||
content: 'Räkenskapsår jan–dec',
|
||||
content: 'Räkenskapsår jan-dec',
|
||||
source: 'composer',
|
||||
source_ref: null,
|
||||
relevance_score: 0.5,
|
||||
|
||||
@@ -20,7 +20,7 @@ const BodySchema = z.object({
|
||||
company_id: z.string().uuid().optional(),
|
||||
})
|
||||
|
||||
// 10s — the user is on a wait-screen with visible progress, so we can afford
|
||||
// 10s: the user is on a wait-screen with visible progress, so we can afford
|
||||
// the longer budget. The prior 5s clipped legitimate fetches (TIC fans out to
|
||||
// ~13 Lens calls upstream) into the fallback bucket while still burning the
|
||||
// in-flight upstream calls against quota. See actions.ts:182-189 for the
|
||||
@@ -58,17 +58,17 @@ interface ProfilePayload {
|
||||
// POST /api/agent/onboarding/stream
|
||||
//
|
||||
// Streams real-timed progress for the agent build sequence (plan §7 Phase A).
|
||||
// Each step runs on its actual latency — no artificial delays. On timeout or
|
||||
// Each step runs on its actual latency: no artificial delays. On timeout or
|
||||
// failure, the step emits `fallback` and the pipeline continues with a
|
||||
// deterministic default so the user always reaches Phase B.
|
||||
//
|
||||
// Response: application/x-ndjson — one JSON event per line.
|
||||
// Response: application/x-ndjson, one JSON event per line.
|
||||
export async function POST(request: Request) {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
// Generous per-user rate limit — bounds reload-spam of the onboarding build
|
||||
// Generous per-user rate limit: bounds reload-spam of the onboarding build
|
||||
// (each run fires 2 LLM calls). Fails open on infra error.
|
||||
const rate = await checkAgentRateLimit(supabase, user.id)
|
||||
if (!rate.ok) {
|
||||
@@ -103,7 +103,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Not a member of this company' }, { status: 403 })
|
||||
}
|
||||
|
||||
// No live composer run for sandbox companies — they ship with a pre-built
|
||||
// No live composer run for sandbox companies: they ship with a pre-built
|
||||
// verified agent_profile so the chrome is visible without burning Bedrock.
|
||||
const blocked = await guardSandbox(supabase, companyId)
|
||||
if (blocked) return blocked
|
||||
@@ -119,12 +119,12 @@ export async function POST(request: Request) {
|
||||
controller.enqueue(encoder.encode(JSON.stringify(event) + '\n'))
|
||||
} catch {
|
||||
// Stream was cancelled (user navigated away). Subsequent enqueues
|
||||
// would throw — we just stop emitting.
|
||||
// would throw: we just stop emitting.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1 — TIC: read companies.tic_snapshot; if missing or stale,
|
||||
// Step 1: TIC: read companies.tic_snapshot; if missing or stale,
|
||||
// live-fetch from the TIC extension (cookies forwarded from the
|
||||
// incoming request) and persist. Falls through gracefully when TIC is
|
||||
// disabled, the company has no org_number, or the request times out.
|
||||
@@ -146,7 +146,7 @@ export async function POST(request: Request) {
|
||||
// bounded to companies actively creating an agent, so the TIC
|
||||
// budget stays safe even when upgrading pre-v2 snapshots.
|
||||
// timeoutMs: lift the internal fetch signal to match the outer
|
||||
// budget — otherwise the 5s default fires first and we get the
|
||||
// budget: otherwise the 5s default fires first and we get the
|
||||
// pre-fix behavior even with the longer outer budget.
|
||||
ensureTicSnapshot({
|
||||
supabase,
|
||||
@@ -163,10 +163,10 @@ export async function POST(request: Request) {
|
||||
status: ticResult.snapshot ? 'success' : 'fallback',
|
||||
})
|
||||
|
||||
// Gather inputs once — used by select + narrative + persistence.
|
||||
// Gather inputs once: used by select + narrative + persistence.
|
||||
const inputs = await gatherComposerInputs(supabase, companyId)
|
||||
|
||||
// Step 2 — Opus atom selection with timeout + deterministic fallback.
|
||||
// Step 2: Opus atom selection with timeout + deterministic fallback.
|
||||
send({ step: 'select', status: 'in_progress' })
|
||||
let selection: AtomSelection
|
||||
try {
|
||||
@@ -181,14 +181,14 @@ export async function POST(request: Request) {
|
||||
// path produced the selection. fallbackAtomSelection generates a
|
||||
// generic template that doesn't know about KÄNDA FAKTA; the Opus
|
||||
// path is also re-filtered in case the model strayed. Cheap belt-
|
||||
// and-braces — same function used for both.
|
||||
// and-braces: same function used for both.
|
||||
selection.verification_questions = filterRedundantQuestions(
|
||||
selection.verification_questions,
|
||||
inputs,
|
||||
selection.modifier_atoms,
|
||||
)
|
||||
|
||||
// Step 3 — Sonnet narrative with timeout + plain fallback.
|
||||
// Step 3: Sonnet narrative with timeout + plain fallback.
|
||||
send({ step: 'narrative', status: 'in_progress' })
|
||||
let narrative: string
|
||||
try {
|
||||
@@ -199,7 +199,7 @@ export async function POST(request: Request) {
|
||||
send({ step: 'narrative', status: 'fallback', narrative })
|
||||
}
|
||||
|
||||
// Step 4 — Persist the profile so Phase B has a row to edit.
|
||||
// Step 4: Persist the profile so Phase B has a row to edit.
|
||||
const composedAt = new Date().toISOString()
|
||||
const sourceSignals = inputsToSourceSignals(inputs)
|
||||
const { error: upsertErr } = await supabase
|
||||
@@ -243,7 +243,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
})
|
||||
|
||||
// Step 5 — fire-and-forget cache pre-warm. The client renders the
|
||||
// Step 5: fire-and-forget cache pre-warm. The client renders the
|
||||
// review card already; pre-warm just buys a faster first chat turn.
|
||||
send({ step: 'prewarm', status: 'in_progress' })
|
||||
const allIds = [
|
||||
@@ -288,7 +288,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// Run a promise against a wall-clock budget. The underlying work continues to
|
||||
// completion on the server when the budget elapses — we just stop waiting for
|
||||
// completion on the server when the budget elapses: we just stop waiting for
|
||||
// it. For Anthropic calls that's fine: a slow Opus turn finishing later still
|
||||
// warms its own cache.
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getActiveCompanyId } from '@/lib/company/context'
|
||||
//
|
||||
// PATCH updates field_overrides (timestamped, merged with existing) and
|
||||
// optionally rewrites the atom arrays from the review UI. Does not touch
|
||||
// verified_at — that flows through /verify.
|
||||
// verified_at: that flows through /verify.
|
||||
|
||||
const AtomArrays = z.object({
|
||||
horizontal_atoms: z.array(z.string()).optional(),
|
||||
@@ -24,7 +24,7 @@ const PatchBody = z.object({
|
||||
field_overrides: z.record(z.string(), z.unknown()).optional(),
|
||||
atoms: AtomArrays.optional(),
|
||||
profile_summary: z.string().min(1).max(2000).optional(),
|
||||
// Agent personalization — name shown on the FAB and chat headers, and
|
||||
// Agent personalization: name shown on the FAB and chat headers, and
|
||||
// avatar key into the static AVATAR_OPTIONS registry. Both nullable so
|
||||
// the user can clear them back to defaults.
|
||||
display_name: z.string().min(1).max(60).nullable().optional(),
|
||||
@@ -41,7 +41,7 @@ export async function GET(request: Request) {
|
||||
url.searchParams.get('company_id') ?? (await getActiveCompanyId(supabase, user.id))
|
||||
if (!companyId) return NextResponse.json({ error: 'No active company' }, { status: 400 })
|
||||
|
||||
// Defense in depth alongside RLS — confirm membership before reading.
|
||||
// Defense in depth alongside RLS: confirm membership before reading.
|
||||
const { data: membership } = await supabase
|
||||
.from('company_members')
|
||||
.select('role')
|
||||
@@ -81,7 +81,7 @@ export async function PATCH(request: Request) {
|
||||
const companyId = body.company_id ?? (await getActiveCompanyId(supabase, user.id))
|
||||
if (!companyId) return NextResponse.json({ error: 'No active company' }, { status: 400 })
|
||||
|
||||
// RLS guards reads/updates by company_id; defense in depth — confirm membership.
|
||||
// RLS guards reads/updates by company_id; defense in depth: confirm membership.
|
||||
const { data: membership } = await supabase
|
||||
.from('company_members')
|
||||
.select('role')
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getActiveCompanyId } from '@/lib/company/context'
|
||||
// POST /api/agent/profile/verify
|
||||
//
|
||||
// Stamps verified_at + verified_by_user_id on agent_profiles when the user
|
||||
// clicks "Det här ser rätt ut — kör" in Phase B. Idempotent: re-verifying
|
||||
// clicks "Det här ser rätt ut, kör" in Phase B. Idempotent: re-verifying
|
||||
// updates the timestamp; this is desirable for "Bygg om" rebuild flows.
|
||||
|
||||
const BodySchema = z.object({
|
||||
@@ -31,7 +31,7 @@ export async function POST(request: Request) {
|
||||
const companyId = body.company_id ?? (await getActiveCompanyId(supabase, user.id))
|
||||
if (!companyId) return NextResponse.json({ error: 'No active company' }, { status: 400 })
|
||||
|
||||
// Defense in depth alongside RLS — confirm membership for the target
|
||||
// Defense in depth alongside RLS: confirm membership for the target
|
||||
// company; a non-viewer role is required to stamp verified_at.
|
||||
const { data: membership } = await supabase
|
||||
.from('company_members')
|
||||
|
||||
@@ -5,10 +5,10 @@ import { getActiveCompanyId } from '@/lib/company/context'
|
||||
// GET /api/agent/skills
|
||||
//
|
||||
// Read-only transparency surface for the in-app bookkeeping assistant's domain
|
||||
// knowledge ("atoms"). Powers /settings/agent-skills — the companion to
|
||||
// knowledge ("atoms"). Powers /settings/agent-skills: the companion to
|
||||
// /settings/agent-memory. Memory is what the assistant *learned* about this
|
||||
// company (user-editable); skills are the Swedish-accounting expertise it
|
||||
// *ships* with — authored in .claude/skills/**/SKILL.md, seeded into
|
||||
// *ships* with: authored in .claude/skills/**/SKILL.md, seeded into
|
||||
// agent_atom_registry, read-only for users, curated via mcp_exposed.
|
||||
//
|
||||
// Two shapes off one route:
|
||||
@@ -19,7 +19,7 @@ import { getActiveCompanyId } from '@/lib/company/context'
|
||||
//
|
||||
// "Active for this company": horizontal atoms are regulatory and shared by
|
||||
// every Swedish company, so always active. Vertical/modifier atoms are active
|
||||
// only when the composer selected them into this company's agent_profile —
|
||||
// only when the composer selected them into this company's agent_profile:
|
||||
// others are shown dormant so the user sees both the full library and what's
|
||||
// tuned for them. The profile arrays store full ids ("vertical/konsult-it"),
|
||||
// matched directly against agent_atom_registry.id (see lib/agent/chat/system-prompt.ts).
|
||||
|
||||
Reference in New Issue
Block a user