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
10 changes: 6 additions & 4 deletions .changeset/scoped-generic-data-attribute.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
---
'@walkeros/web-source-browser': minor
'@walkeros/mcp-source-browser': patch
'@walkeros/mcp-source-browser': minor
---

Add the `data-elb_` scoped generic attribute. It carries the same `key:value`
properties as the blanket `data-elb-` generic, but only events whose triggered
element is nested below the `data-elb_` element receive them. Use `data-elb-`
for properties every trigger in an entity should carry, and `data-elb_` when
only triggers within a specific branch should.
element is nested below the `data-elb_` element receive them. The
`createTagger()` API gains a `scoped()` method and the `generate_tagging` MCP
tool gains a `scoped` input to produce it. Use `data-elb-` for properties every
trigger in an entity should carry, and `data-elb_` when only triggers within a
specific branch should.
17 changes: 17 additions & 0 deletions .changeset/transformer-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@walkeros/transformer-validate': minor
'@walkeros/core': minor
'@walkeros/cli': patch
---

New `@walkeros/transformer-validate` transformer validates events against JSON
Schema contracts. It runs in both web and server flows, supports strict and pass
modes, and writes the verdict and error list to configurable paths so you can
gate or observe event quality.

The declarative per-step `validate` field on sources, transformers, and
destinations is removed. Define event shapes in the top-level `contract` and
enforce them at runtime by adding a `transformer-validate` step that references
them via `$contract.<name>`; `format: true` still checks an event is a valid
`WalkerOS.PartialEvent`. Design-time validation now checks step examples against
the resolved contract.
7 changes: 7 additions & 0 deletions .changeset/validate-schema-only-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@walkeros/transformer-validate': patch
---

Fix schema-only contract rules being skipped during validation. A contract rule
that carries only a whole-event `schema` (no `events` block) is now enforced
instead of being treated as an inert inline schema.
40 changes: 40 additions & 0 deletions package-lock.json

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

11 changes: 0 additions & 11 deletions packages/cli/examples/flow-complete.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,17 +286,6 @@
"destinations": {
"ga4": {
"package": "@walkeros/web-destination-gtag",
"validate": {
"events": {
"order": {
"complete": {
"properties": {
"data": { "required": ["id", "total"] }
}
}
}
}
},
"config": {
"require": ["consent", "user"],
"consent": {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@walkeros/core": "4.1.2",
"@walkeros/server-core": "4.1.2",
"@walkeros/server-destination-api": "4.1.2",
"@walkeros/transformer-validate": "4.1.2",
"ajv": "^8.17.1",
"chalk": "^5.6.2",
"ci-info": "^4.4.0",
Expand Down
172 changes: 153 additions & 19 deletions packages/cli/src/__tests__/unit/validate/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,37 +608,47 @@ describe('validateFlow', () => {

expect(result.valid).toBe(true);
});
});

it('warns about contract compliance', () => {
const result = validateFlow({
version: 4,
contract: {
default: {
events: {
page: {
view: {
describe('contract compliance (example vs resolved contract)', () => {
const contractRequiringTotal = {
default: {
events: {
order: {
complete: {
type: 'object',
properties: {
data: {
type: 'object',
properties: { title: { type: 'string' } },
required: ['total'],
properties: { total: { type: 'number' } },
},
},
},
},
},
},
};

it('warns when a destination example violates the contract (non-strict)', () => {
const result = validateFlow({
version: 4,
contract: contractRequiringTotal,
flows: {
default: {
config: { platform: 'web' },
destinations: {
gtag: {
package: '@walkeros/web-destination-gtag',
api: {
package: '@walkeros/web-destination-api',
examples: {
pageview: {
order: {
in: {
name: 'page view',
entity: 'page',
action: 'view',
data: { title: 'Home' },
name: 'order complete',
entity: 'order',
action: 'complete',
data: { id: 'A1' }, // missing required `total`
},
out: ['event', 'page_view'],
out: ['event', 'purchase'],
},
},
},
Expand All @@ -647,13 +657,137 @@ describe('validateFlow', () => {
},
});

// Non-strict: violation is a warning, validation stays valid.
expect(result.valid).toBe(true);
expect(result.warnings).toContainEqual(
expect.objectContaining({
path: 'destination.gtag.examples.pageview',
message: expect.stringContaining('contract'),
path: 'destination.api.examples.order.in',
message: expect.stringContaining('violates contract'),
}),
);
});

it('errors when a destination example violates the contract (strict)', () => {
const result = validateFlow(
{
version: 4,
contract: contractRequiringTotal,
flows: {
default: {
config: { platform: 'web' },
destinations: {
api: {
package: '@walkeros/web-destination-api',
examples: {
order: {
in: {
name: 'order complete',
entity: 'order',
action: 'complete',
data: { id: 'A1' },
},
out: ['event', 'purchase'],
},
},
},
},
},
},
},
{ strict: true },
);

expect(result.valid).toBe(false);
expect(result.errors).toContainEqual(
expect.objectContaining({
path: 'destination.api.examples.order.in',
code: 'CONTRACT_VIOLATION',
}),
);
});

it('does not flag a compliant example', () => {
const result = validateFlow({
version: 4,
contract: contractRequiringTotal,
flows: {
default: {
config: { platform: 'web' },
destinations: {
api: {
package: '@walkeros/web-destination-api',
examples: {
order: {
in: {
name: 'order complete',
entity: 'order',
action: 'complete',
data: { total: 9.99 },
},
out: ['event', 'purchase'],
},
},
},
},
},
},
});

expect(result.valid).toBe(true);
expect(
result.warnings.some((w) => w.path.includes('destination.api')),
).toBe(false);
expect(result.errors.some((e) => e.code === 'CONTRACT_VIOLATION')).toBe(
false,
);
});

const uncoveredEventFlow = {
version: 4,
contract: contractRequiringTotal,
flows: {
default: {
config: { platform: 'web' },
destinations: {
api: {
package: '@walkeros/web-destination-api',
examples: {
page: {
in: {
name: 'page view',
entity: 'page',
action: 'view',
data: { title: 'Home' },
},
out: ['event', 'page_view'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use space-delimited event naming in the fixture.

Line 762 uses 'page_view', but this repo requires 'entity action' format with spaces.

Suggested fix
-                  out: ['event', 'page_view'],
+                  out: ['event', 'page view'],

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use event naming format: 'entity action' with space (e.g., 'page view', not 'page_view').

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
out: ['event', 'page_view'],
out: ['event', 'page view'],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/__tests__/unit/validate/flow.test.ts` at line 762, The test
fixture uses underscore-style event naming in the 'out' array (out: ['event',
'page_view']) but the repo requires space-delimited "entity action" names;
update the fixture value from 'page_view' to 'page view' so the 'out' array
becomes ['event', 'page view'] and ensure any assertions or snapshots referring
to that value are updated accordingly.

Source: Coding guidelines

},
},
},
},
},
},
} as const;

it('produces no diagnostic when an example matches no contract entry', () => {
const result = validateFlow(uncoveredEventFlow);

expect(result.valid).toBe(true);
expect(
result.warnings.some((w) => w.path.includes('destination.api')),
).toBe(false);
expect(result.errors.some((e) => e.code === 'CONTRACT_VIOLATION')).toBe(
false,
);
});

it('does not fail --strict on an uncovered event type', () => {
const result = validateFlow(uncoveredEventFlow, { strict: true });

expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(
result.warnings.some((w) => w.path.includes('destination.api')),
).toBe(false);
});
});
});
8 changes: 6 additions & 2 deletions packages/cli/src/commands/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
export async function validate(
type: ValidationType,
input: unknown,
options: { flow?: string; path?: string } = {},
options: { flow?: string; path?: string; strict?: boolean } = {},
): Promise<ValidateResult> {
// Resolve string inputs (file paths, URLs, JSON strings) to parsed objects
let resolved = input;
Expand All @@ -53,7 +53,10 @@ export async function validate(
case 'event':
return validateEvent(resolved);
case 'flow':
return validateFlow(resolved, { flow: options.flow });
return validateFlow(resolved, {
flow: options.flow,
strict: options.strict,
});
case 'mapping':
return validateMapping(resolved);
default:
Expand Down Expand Up @@ -141,6 +144,7 @@ export async function validateCommand(
const result = await validate(options.type, input, {
flow: options.flow,
path: options.path,
strict: options.strict,
});

// Format and write result
Expand Down
Loading
Loading