Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ node_modules/
pnpm-lock.yaml

# Environment files (secrets - never commit these!)
# Covers dev/stg/prod and any org slug (e.g. .env.roofr-production)
# Covers dev/stg/prod and any org slug (e.g. .env.acme-production)
.env
.env.*
!.env.example
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ server:
credentialId: my-server-credential

# State file (environment-specific)
# "my-server-credential": "2f6db611-ad08-4099-8bd8-74db37b0a07e"
# "my-server-credential": "11111111-1111-1111-1111-111111111111"
```

### State File
Expand Down
4 changes: 2 additions & 2 deletions src/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ function extractRemoteName(resource: VapiResource): string | undefined {
// Build the candidate resourceId(s) that a dashboard-orphan UUID would map to,
// so we can check them against `.vapi-ignore`. Two shapes are produced because
// real customer .vapi-ignore patterns target either form:
// - the bare name-slug (e.g. `assistants/iform-triage-classifier`)
// - the `<name>-<uuid8>` form pull.ts emits (`assistants/iform-...-d98136d9`)
// - the bare name-slug (e.g. `assistants/support-triage-classifier`)
// - the `<name>-<uuid8>` form pull.ts emits (`assistants/support-...-1a2b3c4d`)
function candidateResourceIdsForRemote(resource: VapiResource): string[] {
const name = extractRemoteName(resource);
const shortId = resource.id.slice(0, 8);
Expand Down
2 changes: 1 addition & 1 deletion src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { StateFile } from "./types.ts";
// Credential Resolution — resolve org-specific credential UUIDs across environments
//
// Credentials are pulled from the API and stored in state (name-slug → UUID).
// Resource files store credential NAMES (e.g., "roofr-server-credential").
// Resource files store credential NAMES (e.g., "acme-server-credential").
// Push resolves names → UUIDs. Pull resolves UUIDs → names.
//
// Replacement is scoped to `credentialId` / `credentialIds` fields only.
Expand Down
2 changes: 1 addition & 1 deletion src/new-file-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// (c) MOVED file — file copied without the state entry being rekeyed
//
// Silently treating every orphan as case (a) is what produced the duplicate
// fleet we surfaced during the gitops-mudflap working session 2026-05-13.
// fleet we surfaced in a customer working session on 2026-05-13.
// Flow F (`mv foo.md bar.md` + push), Flow G (dashboard rename → pull writes
// new file but leaves stale YAML), and Flow M (`apply` compresses Flow G into
// one click) all share this shape.
Expand Down
6 changes: 3 additions & 3 deletions src/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ export function listExistingResourceIds(resourceType: ResourceType): string[] {
}

// When pulling a new environment, a resource may already exist on disk under a
// different UUID suffix (e.g., `end-call-tool-8102e715` from dev). Match by
// different UUID suffix (e.g., `end-call-tool-9f8e7d6c` from dev). Match by
// name-slug so we reuse the existing file instead of creating a duplicate.
//
// State-awareness guard: if a name-matching file is already claimed in state
Expand Down Expand Up @@ -851,8 +851,8 @@ export async function pullResourceType(
// - `state[resourceType]` carries prior-pull claims loaded from
// disk. Without this, if the dashboard returns the new same-name
// twin BEFORE the tracked one, the new twin sees `newStateSection`
// empty and clobbers the tracked file. The customer's mudflap-prod
// 5-Rileys investigation surfaced this ordering dependency.
// empty and clobbers the tracked file. A customer-org investigation
// into five same-name assistants surfaced this ordering dependency.
// - `newStateSection` carries intra-pull claims from earlier
// iterations. Handles the converse (tracked-then-twin order).
// Spread `newStateSection` last so it wins when both have the same
Expand Down
4 changes: 2 additions & 2 deletions src/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1674,8 +1674,8 @@ async function main(): Promise<void> {
// Orphan-YAML pre-flight gate. Runs ONCE for ALL resource types after
// bootstrap (so state-recovery has a chance to rekey first) and BEFORE
// any apply phase. Halts push when local files exist with no state entry
// — the duplicate-creation pattern we surfaced during the gitops-mudflap
// working session 2026-05-13 (see src/new-file-gate.ts for context).
// — the duplicate-creation pattern we surfaced in a customer working
// session on 2026-05-13 (see src/new-file-gate.ts for context).
//
// Skipped during explicit `--bootstrap` runs: a bootstrap is supposed to
// populate state from scratch, so every local file legitimately lacks a
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export type ResourceType =
| "simulationSuites"
| "evals";

// Any slug-like string: "dev", "prod", "roofr-production", etc.
// Any slug-like string: "dev", "prod", "acme-production", etc.
export type Environment = string;

// Well-known names kept for backward-compatible npm scripts
Expand Down
12 changes: 6 additions & 6 deletions tests/apply-scoped-pull.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,31 @@ const { preserveExplicitOursPaths } = await import("../src/pull.ts");

test("parseResourceFilePath: long-form assistant path", () => {
const parsed = parseResourceFilePath(
"resources/test-fixture-org/assistants/call-transfer-test-c95f4c6b.md",
"resources/test-fixture-org/assistants/call-transfer-test-d3adb33f.md",
);
assert.deepEqual(parsed, {
type: "assistants",
resourceId: "call-transfer-test-c95f4c6b",
resourceId: "call-transfer-test-d3adb33f",
});
});

test("parseResourceFilePath: short-form assistant path", () => {
const parsed = parseResourceFilePath(
"assistants/call-transfer-test-c95f4c6b.md",
"assistants/call-transfer-test-d3adb33f.md",
);
assert.deepEqual(parsed, {
type: "assistants",
resourceId: "call-transfer-test-c95f4c6b",
resourceId: "call-transfer-test-d3adb33f",
});
});

test("resolvePullScopeFromFilePaths: maps file paths to dashboard UUIDs by state", () => {
const scope = resolvePullScopeFromFilePaths(
["resources/test-fixture-org/assistants/call-transfer-test-c95f4c6b.md"],
["resources/test-fixture-org/assistants/call-transfer-test-d3adb33f.md"],
{
credentials: {},
assistants: {
"call-transfer-test-c95f4c6b": {
"call-transfer-test-d3adb33f": {
uuid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
},
},
Expand Down
54 changes: 27 additions & 27 deletions tests/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,13 @@ test("state-uuid-collision: 1 slug per uuid → 0 findings", async () => {
// Rule: content-identical
// ─────────────────────────────────────────────────────────────────────────────

test("content-identical: 4 entries with same lastPulledHash (riley fixture) → 1 warn, 4 slugs", async () => {
test("content-identical: 4 entries with same lastPulledHash (taylor fixture) → 1 warn, 4 slugs", async () => {
const state = makeStateFile({
assistants: {
"riley-1": makeStateEntry("uuid-r1", "hash-shared"),
"riley-2": makeStateEntry("uuid-r2", "hash-shared"),
"riley-3": makeStateEntry("uuid-r3", "hash-shared"),
"riley-4": makeStateEntry("uuid-r4", "hash-shared"),
"taylor-1": makeStateEntry("uuid-r1", "hash-shared"),
"taylor-2": makeStateEntry("uuid-r2", "hash-shared"),
"taylor-3": makeStateEntry("uuid-r3", "hash-shared"),
"taylor-4": makeStateEntry("uuid-r4", "hash-shared"),
},
});
const findings = await runAudit(baseOpts(state));
Expand All @@ -252,10 +252,10 @@ test("content-identical: 4 entries with same lastPulledHash (riley fixture) →
assert.equal(identicals[0]!.severity, "warn");
assert.equal(identicals[0]!.resourceIds.length, 4);
assert.deepEqual(identicals[0]!.resourceIds, [
"riley-1",
"riley-2",
"riley-3",
"riley-4",
"taylor-1",
"taylor-2",
"taylor-3",
"taylor-4",
]);
});

Expand Down Expand Up @@ -294,19 +294,19 @@ test("content-identical: 2 entries share hash, 1 entry has distinct hash → 1 f
test("sibling-base-slug: bare + 2 suffixed entries cluster under same base → 1 finding, 3 slugs", async () => {
const state = makeStateFile({
assistants: {
"iform-barge": makeStateEntry("uuid-1"),
"iform-barge-d98136d9": makeStateEntry("uuid-2"),
"iform-barge-f6b53e27": makeStateEntry("uuid-3"),
"triage-bot": makeStateEntry("uuid-1"),
"triage-bot-1a2b3c4d": makeStateEntry("uuid-2"),
"triage-bot-5e6f7a8b": makeStateEntry("uuid-3"),
},
});
const findings = await runAudit(baseOpts(state));
const siblings = findings.filter((f) => f.rule === "sibling-base-slug");
assert.equal(siblings.length, 1);
assert.equal(siblings[0]!.resourceIds.length, 3);
assert.deepEqual(siblings[0]!.resourceIds, [
"iform-barge",
"iform-barge-d98136d9",
"iform-barge-f6b53e27",
"triage-bot",
"triage-bot-1a2b3c4d",
"triage-bot-5e6f7a8b",
]);
// No content-identical overlap here → message does NOT contain cross-ref.
assert.equal(
Expand All @@ -318,8 +318,8 @@ test("sibling-base-slug: bare + 2 suffixed entries cluster under same base → 1
test("sibling-base-slug: siblings that share a hash get cross-reference to content-identical", async () => {
const state = makeStateFile({
assistants: {
"iform-barge": makeStateEntry("uuid-1", "hash-shared"),
"iform-barge-d98136d9": makeStateEntry("uuid-2", "hash-shared"),
"triage-bot": makeStateEntry("uuid-1", "hash-shared"),
"triage-bot-1a2b3c4d": makeStateEntry("uuid-2", "hash-shared"),
},
});
const findings = await runAudit(baseOpts(state));
Expand All @@ -336,7 +336,7 @@ test("sibling-base-slug: siblings that share a hash get cross-reference to conte
test("sibling-base-slug: only one entry (no siblings) → 0 findings", async () => {
const state = makeStateFile({
assistants: {
"iform-barge": makeStateEntry("uuid-1"),
"triage-bot": makeStateEntry("uuid-1"),
},
});
const findings = await runAudit(baseOpts(state));
Expand Down Expand Up @@ -500,16 +500,16 @@ test("integration: orphan-yaml + collision + content-identical(4) + sibling-base
"coll-a": makeStateEntry("dup-uuid"),
"coll-b": makeStateEntry("dup-uuid"),
// 4 distinct slugs sharing hash H1 → 1 content-identical (warn, 4 slugs)
"riley-1": makeStateEntry("uuid-r1", "H1"),
"riley-2": makeStateEntry("uuid-r2", "H1"),
"riley-3": makeStateEntry("uuid-r3", "H1"),
"riley-4": makeStateEntry("uuid-r4", "H1"),
// 3 slugs sharing base "iform-barge"; 2 of them share hash H2 so
"taylor-1": makeStateEntry("uuid-r1", "H1"),
"taylor-2": makeStateEntry("uuid-r2", "H1"),
"taylor-3": makeStateEntry("uuid-r3", "H1"),
"taylor-4": makeStateEntry("uuid-r4", "H1"),
// 3 slugs sharing base "triage-bot"; 2 of them share hash H2 so
// sibling-base-slug message picks up the cross-ref AND we get one
// extra content-identical finding for those 2.
"iform-barge": makeStateEntry("uuid-s1", "H2"),
"iform-barge-d98136d9": makeStateEntry("uuid-s2", "H2"),
"iform-barge-f6b53e27": makeStateEntry("uuid-s3"),
"triage-bot": makeStateEntry("uuid-s1", "H2"),
"triage-bot-1a2b3c4d": makeStateEntry("uuid-s2", "H2"),
"triage-bot-5e6f7a8b": makeStateEntry("uuid-s3"),
},
});

Expand Down Expand Up @@ -544,7 +544,7 @@ test("integration: orphan-yaml + collision + content-identical(4) + sibling-base
]);

// The sibling finding must carry the cross-ref token because 2 of the 3
// siblings (iform-barge, iform-barge-d98136d9) also appear in a
// siblings (triage-bot, triage-bot-1a2b3c4d) also appear in a
// content-identical cluster.
const sibling = findings.find((f) => f.rule === "sibling-base-slug")!;
assert.ok(sibling.message.includes("overlaps with content-identical"));
Expand Down
4 changes: 2 additions & 2 deletions tests/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ function forwardMap(state: StateFile): Map<string, string> {

test("replaceCredentialRefs swaps at credentialId keys", () => {
const state = makeState({
"roofr-server-credential": "11111111-1111-1111-1111-111111111111",
"acme-server-credential": "11111111-1111-1111-1111-111111111111",
});
const input = {
server: {
url: "https://example.com",
credentialId: "roofr-server-credential",
credentialId: "acme-server-credential",
},
};
const out = replaceCredentialRefs(input, forwardMap(state));
Expand Down
4 changes: 2 additions & 2 deletions tests/dep-dedup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ test("findExistingResourceByName: assistant payload (top-level name only)", () =

test("findExistingResourceByName: state-only match", () => {
const m = findExistingResourceByName({
localResourceId: "b2b-invoice-end-call",
localResourceId: "billing-end-call",
localPayload: { function: { name: "end-call" } },
stateSection: {
"end-call-67aea057": { uuid: "uuid-aaa" },
Expand Down Expand Up @@ -157,7 +157,7 @@ test("findExistingResourceByName: ambiguous across state vs dashboard → lex-sm
// the SAME uuid appears in both — here the winner appears in only one,
// so source is whichever side it came from.
const m = findExistingResourceByName({
localResourceId: "b2b-invoice-end-call",
localResourceId: "billing-end-call",
localPayload: { function: { name: "end-call" } },
stateSection: { "end-call-67aea057": { uuid: "uuid-zzz" } },
remoteList: [{ id: "uuid-aaa", function: { name: "end-call" } }],
Expand Down
2 changes: 1 addition & 1 deletion tests/drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ test("canonicalizeForHash: strips server-managed fields (id, orgId, createdAt, u
// short-circuit baseline preservation.
//
// Regression coverage for a bug introduced by the drift-direction-classifier
// PR (#38) and caught by the E2E both-diverged smoke test on mudflap-iform-test:
// PR (#38) and caught by the E2E both-diverged smoke test on a customer test org:
// pull rebuilds each state section from EMPTY, and the classifier short-circuit
// branches wrote back a bare `{ uuid }` — dropping the baseline, so the next
// pull classified the resource as `no-baseline` and could never detect drift
Expand Down
Loading
Loading