diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a86f46..84b12f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +## [v0.5.0] - 2026-09-03 + +### Added + +- `ai-brand-representation-snapshot@1.0.0`, a bounded agent-executed method for observing how ChatGPT, Gemini, and Perplexity represent one explicitly supplied brand under identical settled conditions. +- The `ai-brand-representation-snapshot/v1` result contract with closed framing, channel observation, comparison, and provider-neutral cost structures composed with the shared evidence envelope. +- Controlled `visibility`, `brand`, and `channel` vocabulary plus separate public capabilities for the three required answer channels. +- Deterministic complete, negative, failed, unavailable, insufficient-evidence, and rejected result fixtures with semantic checks for exact channel coverage, evidence links, identical conditions, comparison support, completion, disposition, and cost reconciliation. +- Fresh-context scenarios for snapshot selection, direct one-channel routing, pre-spend framing, current budget discovery, one observation per channel, valid negative handling, evidence separation, score limits, failure behavior, and buyer-prompt or trend boundaries. + +### Changed + +- Public runtime guidance now distinguishes direct one-channel brand analysis, the three-channel AI Brand Representation Snapshot, and future buyer-prompt presence or longitudinal methods. +- Catalog, package, plugin, and current-version identities advance to `0.5.0` without changing previously released recipe or result-contract versions. + ## [v0.4.1] - 2026-09-03 ### Changed diff --git a/README.md b/README.md index dc3bcb1..690490d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The skill does not write finished content, expose private server internals, or i - Answer narrow SEO questions with the smallest sufficient server evidence path. - Discover recipes progressively without loading an entire catalog into context. - Execute a selected recipe with bounded evidence, explicit stops, and typed results. +- Observe how ChatGPT, Gemini, and Perplexity currently represent one supplied brand under identical framing without inventing a universal visibility score. - Coordinate one selected content objective through bounded research, client-owned drafting, supplied-document analysis, approval, publication, and live verification without writing the finished copy. - Diagnose whether a page needs creation, refresh, consolidation, preservation, or a non-content intervention. - Hand implementation requirements to another agent without depending on shared conversation history. @@ -31,6 +32,7 @@ Invoke `$seo-agent-tools` when the agent has access to an SEO MCP server and the Example requests: - Use `$seo-agent-tools` to discover an available method for finding keyword opportunities for this service area. +- Use `$seo-agent-tools` to compare how ChatGPT, Gemini, and Perplexity currently represent this supplied brand under the same market framing. - Use `$seo-agent-tools` to diagnose why this page is underperforming before anyone rewrites it. - Use `$seo-agent-tools` to discover the content-production workflow for one approved article objective and coordinate its evidence, handoff, publication, and fresh verification without writing the copy itself. - Use `$seo-agent-tools` to convert this SEO diagnostic into a self-contained implementation handoff. @@ -60,8 +62,8 @@ Node.js 22 is used in CI. The repository has no runtime package dependency. ```bash npm ci npm run validate -npm run package -- v0.4.0 -npm run release:verify-assets -- v0.4.0 +npm run package -- v0.5.0 +npm run release:verify-assets -- v0.5.0 ``` `npm run validate` checks catalog structure, runtime skill structure, documentation links, behavioral fixtures, and maintenance contracts. Packaging creates deterministic standalone, Codex plugin, and Claude plugin archives plus versioned catalog and manifest assets under `dist/assets/`. Release verification checks all five assets and rebuilds them to prove byte identity. diff --git a/catalog/catalog.json b/catalog/catalog.json index b78ce72..70a4cb4 100644 --- a/catalog/catalog.json +++ b/catalog/catalog.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "catalog_version": "0.4.1", + "catalog_version": "0.5.0", "catalog_schema": "catalog/schemas/catalog.schema.json", "recipe_schema": "catalog/schemas/recipe.schema.json", "supported_recipe_schema_versions": [ @@ -14,7 +14,8 @@ "content", "keyword", "page", - "technical" + "technical", + "visibility" ], "operations": [ "audit", @@ -28,6 +29,7 @@ "verify" ], "targets": [ + "brand", "competitor-set", "content-handoff", "link-prospect-set", @@ -36,6 +38,7 @@ "site" ], "evidence_units": [ + "channel", "claim-set", "context-record", "document", @@ -47,6 +50,9 @@ "capabilities": [ "authority-metrics", "backlink-sets", + "brand-representation-chatgpt", + "brand-representation-gemini", + "brand-representation-perplexity", "canonical-content-document", "competitor-discovery", "competitor-domains", @@ -67,6 +73,7 @@ "serp-results" ], "result_contracts": { + "ai-brand-representation-snapshot/v1": "catalog/schemas/ai-brand-representation-snapshot.v1.schema.json", "content-question-review/v1": "catalog/schemas/content-question-review.v1.schema.json", "seo-diagnostic/v1": "catalog/schemas/seo-diagnostic.v1.schema.json", "seo-implementation-handoff/v1": "catalog/schemas/seo-implementation-handoff.v1.schema.json", diff --git a/catalog/recipes/ai-brand-representation-snapshot.json b/catalog/recipes/ai-brand-representation-snapshot.json new file mode 100644 index 0000000..8f8679e --- /dev/null +++ b/catalog/recipes/ai-brand-representation-snapshot.json @@ -0,0 +1,151 @@ +{ + "schema_version": 2, + "id": "ai-brand-representation-snapshot", + "version": "1.0.0", + "title": "AI Brand Representation Snapshot", + "summary": "Observe how ChatGPT, Gemini, and Perplexity represent one explicitly supplied brand under identical settled conditions, then return one evidence-linked current snapshot without cross-channel score normalization or buyer-prompt visibility claims.", + "primary_domain": "visibility", + "operations": [ + "compare", + "diagnose", + "validate" + ], + "target": "brand", + "required_inputs": [ + { + "id": "brand", + "description": "The brand name, bare public domain, or absolute HTTP/HTTPS public URL whose prompted brand representation should be observed." + } + ], + "evidence_plan": [ + { + "capability": "brand-representation-chatgpt", + "source": "server", + "use": "required", + "steps": [ + "observe-chatgpt", + "compare-representation", + "validate-snapshot" + ], + "scope": { + "unit": "channel", + "maximum": 1, + "guidance": "Collect no more than one ChatGPT observation under the settled framing." + }, + "completion_without": { + "allowed": false, + "limitation": "The recipe is not execution-ready when this required channel is unavailable before execution." + }, + "fallback": "Stop before channel execution when readiness is unavailable. If a planned invocation fails after execution begins, preserve the failed channel in a contract-valid incomplete snapshot and follow the current server result and failure guidance." + }, + { + "capability": "brand-representation-gemini", + "source": "server", + "use": "required", + "steps": [ + "observe-gemini", + "compare-representation", + "validate-snapshot" + ], + "scope": { + "unit": "channel", + "maximum": 1, + "guidance": "Collect no more than one Gemini observation under the settled framing." + }, + "completion_without": { + "allowed": false, + "limitation": "The recipe is not execution-ready when this required channel is unavailable before execution." + }, + "fallback": "Stop before channel execution when readiness is unavailable. If a planned invocation fails after execution begins, preserve the failed channel in a contract-valid incomplete snapshot and follow the current server result and failure guidance." + }, + { + "capability": "brand-representation-perplexity", + "source": "server", + "use": "required", + "steps": [ + "observe-perplexity", + "compare-representation", + "validate-snapshot" + ], + "scope": { + "unit": "channel", + "maximum": 1, + "guidance": "Collect no more than one Perplexity observation under the settled framing." + }, + "completion_without": { + "allowed": false, + "limitation": "The recipe is not execution-ready when this required channel is unavailable before execution." + }, + "fallback": "Stop before channel execution when readiness is unavailable. If a planned invocation fails after execution begins, preserve the failed channel in a contract-valid incomplete snapshot and follow the current server result and failure guidance." + } + ], + "steps": [ + { + "id": "settle-brand-framing", + "instruction": "Before spending, settle one supplied subject, intended brand name and canonical website when known, location or market, controlled competitor panel, supplied products or services, and unresolved identity limitations. Apply the same settled framing to every channel. Never invent competitors or products; omit a panel or product list that does not meet the current runtime's confirmed input requirements. Reject the snapshot before paid work when the intended brand remains materially ambiguous." + }, + { + "id": "budget-preflight", + "instruction": "Resolve this exact recipe through the connected server, inspect its current availability and result contract, obtain the current required, default, and maximum budget, and confirm authorization and headroom for all three required channel capabilities before any observation." + }, + { + "id": "observe-chatgpt", + "instruction": "Obtain exactly one ChatGPT observation under the settled conditions. Preserve a valid report or not-recognized result without rerunning it for a preferred outcome, and preserve an actual post-start failure instead of reconstructing the observation through another channel." + }, + { + "id": "observe-gemini", + "instruction": "Obtain exactly one Gemini observation under the settled conditions. Preserve a valid report or not-recognized result without rerunning it for a preferred outcome, and preserve an actual post-start failure instead of reconstructing the observation through another channel." + }, + { + "id": "observe-perplexity", + "instruction": "Obtain exactly one Perplexity observation under the settled conditions. Preserve a valid report or not-recognized result without rerunning it for a preferred outcome, and preserve an actual post-start failure instead of reconstructing the observation through another channel." + }, + { + "id": "compare-representation", + "instruction": "Compare recognition outcomes across valid channel observations. Compare report descriptions, categories, topics, associations, and competitors only when at least two structured reports are usable. Keep controlled-panel observations separate from generated competitors and supplied products or services separate from generated associations. Never average, normalize, or rank channel-native scores." + }, + { + "id": "validate-snapshot", + "instruction": "Return and validate ai-brand-representation-snapshot/v1. Include one row for each channel, exact conditions, evidence links, limitations, actual provider-neutral cost structure, deterministic disposition and completion state, and no complete raw provider response. Use connected-server result validation when that capability exists." + } + ], + "evidence_requirements": [ + "Every channel row records its outcome, invocation-scoped recognition status, observation time when attempted, exact conditions, evidence links, quality notes, and limitations.", + "A not-recognized result is valid normally billed evidence about that invocation, not a failed operation or proof of stable model knowledge.", + "Missing scores remain absent and numeric zero remains evidence; native scores do not share a calibrated cross-channel scale.", + "Generated source descriptors remain generated strings rather than verified citations.", + "Supplied competitor and product framing remains separate from generated competitor discoveries and associations.", + "The snapshot records actual quoted and charged amounts without embedding public static prices or private billing logic." + ], + "stop_conditions": [ + "Stop before paid work and use reject when the supplied subject cannot be settled to an unambiguous intended brand.", + "Stop before execution when current authorization, headroom, exact result-contract support, or any required channel capability is unavailable.", + "Never invent competitors or products to satisfy a current runtime minimum.", + "Never repeat a valid report or not-recognized observation to smooth or improve the result.", + "Do not initiate a hidden or preference-seeking retry; a server-directed retry for an actual retryable execution failure remains governed by the connected server.", + "Never infer stable model knowledge from one report or not-recognized outcome.", + "Never average, normalize, or rank native scores across channels.", + "Never present generated source descriptors as verified citations.", + "Never make report-content comparison claims from fewer than two usable reports.", + "Return a contract-valid incomplete result when a planned invocation fails after execution begins." + ], + "completion_criteria": [ + "The framing and identical invocation conditions are explicit for all three channels.", + "Exactly one row exists for ChatGPT, Gemini, and Perplexity, including unavailable, failed, or not-attempted rows when applicable.", + "Proceed is used only for three reports; conditional is used for at least two valid report or not-recognized outcomes when the snapshot is not a three-report comparison; defer is used for fewer than two valid outcomes; reject is used for invalid or unresolved framing before paid execution.", + "Completion is complete only when all three planned channels produced a valid report or not-recognized outcome.", + "Every report-content comparison claim links evidence from at least two report rows, while every recognition-status comparison claim links evidence from at least two valid report or not-recognized rows.", + "The cost summary reconciles channel quoted and charged amounts and preserves unknown quotes only when no quote was reached.", + "The result makes no universal visibility score, buyer-prompt presence, stable model knowledge, historical trend, or business-impact claim." + ], + "output_contracts": [ + { + "id": "ai-brand-representation-snapshot/v1", + "role": "primary" + } + ], + "bounds": { + "max_results": 1, + "sample_guidance": "Return one current non-persistent snapshot containing no more than one observation from each of the three required channels." + } +} diff --git a/catalog/schemas/ai-brand-representation-snapshot.v1.schema.json b/catalog/schemas/ai-brand-representation-snapshot.v1.schema.json new file mode 100644 index 0000000..9d437a7 --- /dev/null +++ b/catalog/schemas/ai-brand-representation-snapshot.v1.schema.json @@ -0,0 +1,694 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/TechSpokes/seo-agent-tools/catalog/schemas/ai-brand-representation-snapshot.v1.schema.json", + "title": "AI Brand Representation Snapshot v1", + "type": "object", + "allOf": [ + { + "$ref": "shared-evidence.schema.json" + }, + { + "type": "object", + "required": [ + "framing", + "channel_observations", + "comparison", + "cost_summary" + ], + "properties": { + "contract_id": { + "const": "ai-brand-representation-snapshot/v1" + }, + "framing": { + "$ref": "#/$defs/framing" + }, + "channel_observations": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "$ref": "#/$defs/channelObservation" + } + }, + "comparison": { + "$ref": "#/$defs/comparison" + }, + "cost_summary": { + "$ref": "#/$defs/costSummary" + } + } + } + ], + "unevaluatedProperties": false, + "$defs": { + "channel": { + "enum": [ + "chatgpt", + "gemini", + "perplexity" + ] + }, + "competitorInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "pattern": "^https?://" + } + }, + "anyOf": [ + { + "properties": { + "label": true + }, + "required": [ + "label" + ] + }, + { + "properties": { + "url": true + }, + "required": [ + "url" + ] + } + ] + }, + "framing": { + "type": "object", + "additionalProperties": false, + "required": [ + "suppliedSubject", + "settledBrandName", + "canonicalWebsite", + "location", + "suppliedCompetitorPanel", + "suppliedProductsOrServices", + "limitations" + ], + "properties": { + "suppliedSubject": { + "type": "string", + "minLength": 1 + }, + "settledBrandName": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "canonicalWebsite": { + "type": [ + "string", + "null" + ], + "pattern": "^https?://" + }, + "location": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "suppliedCompetitorPanel": { + "type": "array", + "items": { + "$ref": "#/$defs/competitorInput" + } + }, + "suppliedProductsOrServices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "limitations": { + "$ref": "#/$defs/stringArray" + } + } + }, + "invocationConditions": { + "type": "object", + "additionalProperties": false, + "required": [ + "brand", + "location", + "competitors", + "productsOrServices" + ], + "properties": { + "brand": { + "type": "string", + "minLength": 1 + }, + "location": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "competitors": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "productsOrServices": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "dimension": { + "type": "object", + "additionalProperties": false, + "required": [ + "category" + ], + "properties": { + "category": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "information": { + "type": "string", + "minLength": 1 + }, + "nativeScore": { + "type": "number" + } + } + }, + "controlledCompetitorObservation": { + "type": "object", + "additionalProperties": false, + "required": [ + "panelIndex", + "observedName", + "observedUrl", + "awareness", + "sentiment", + "credibility", + "limitations" + ], + "properties": { + "panelIndex": { + "type": "integer", + "minimum": 0 + }, + "observedName": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "observedUrl": { + "type": [ + "string", + "null" + ], + "pattern": "^https?://" + }, + "awareness": { + "$ref": "#/$defs/dimension" + }, + "sentiment": { + "$ref": "#/$defs/dimension" + }, + "credibility": { + "$ref": "#/$defs/dimension" + }, + "limitations": { + "$ref": "#/$defs/stringArray" + } + } + }, + "generatedCompetitor": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "url", + "awareness", + "sentiment", + "credibility", + "limitations" + ], + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "url": { + "type": [ + "string", + "null" + ], + "pattern": "^https?://" + }, + "awareness": { + "$ref": "#/$defs/dimension" + }, + "sentiment": { + "$ref": "#/$defs/dimension" + }, + "credibility": { + "$ref": "#/$defs/dimension" + }, + "limitations": { + "$ref": "#/$defs/stringArray" + } + }, + "anyOf": [ + { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + { + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + } + ] + }, + "report": { + "type": "object", + "additionalProperties": false, + "required": [ + "brandDescription", + "awareness", + "sentiment", + "credibility", + "generatedTopics", + "generatedAssociations", + "sourceDescriptors", + "controlledCompetitorObservations", + "generatedCompetitors" + ], + "properties": { + "brandDescription": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "awareness": { + "$ref": "#/$defs/dimension" + }, + "sentiment": { + "$ref": "#/$defs/dimension" + }, + "credibility": { + "$ref": "#/$defs/dimension" + }, + "generatedTopics": { + "$ref": "#/$defs/stringArray" + }, + "generatedAssociations": { + "$ref": "#/$defs/stringArray" + }, + "sourceDescriptors": { + "$ref": "#/$defs/stringArray" + }, + "controlledCompetitorObservations": { + "type": "array", + "items": { + "$ref": "#/$defs/controlledCompetitorObservation" + } + }, + "generatedCompetitors": { + "type": "array", + "items": { + "$ref": "#/$defs/generatedCompetitor" + } + } + } + }, + "channelObservation": { + "type": "object", + "additionalProperties": false, + "required": [ + "channel", + "outcome", + "recognitionStatus", + "observedAt", + "evidenceIds", + "conditions", + "qualityNotes", + "limitations" + ], + "properties": { + "channel": { + "$ref": "#/$defs/channel" + }, + "outcome": { + "enum": [ + "report", + "not_recognized", + "failed", + "unavailable", + "not_attempted" + ] + }, + "recognitionStatus": { + "enum": [ + "recognized", + "not_recognized", + "indeterminate" + ] + }, + "observedAt": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "evidenceIds": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "conditions": { + "$ref": "#/$defs/invocationConditions" + }, + "report": { + "$ref": "#/$defs/report" + }, + "qualityNotes": { + "$ref": "#/$defs/stringArray" + }, + "limitations": { + "$ref": "#/$defs/stringArray" + } + }, + "oneOf": [ + { + "properties": { + "outcome": { + "const": "report" + }, + "recognitionStatus": { + "const": "recognized" + }, + "observedAt": { + "type": "string" + }, + "evidenceIds": { + "type": "array", + "minItems": 1 + }, + "report": true + }, + "required": [ + "report" + ] + }, + { + "properties": { + "outcome": { + "const": "not_recognized" + }, + "recognitionStatus": { + "const": "not_recognized" + }, + "observedAt": { + "type": "string" + }, + "evidenceIds": { + "type": "array", + "minItems": 1 + } + }, + "not": { + "properties": { + "report": true + }, + "required": [ + "report" + ] + } + }, + { + "properties": { + "outcome": { + "const": "failed" + }, + "recognitionStatus": { + "const": "indeterminate" + }, + "observedAt": { + "type": "string" + }, + "evidenceIds": { + "type": "array", + "minItems": 1 + } + }, + "not": { + "properties": { + "report": true + }, + "required": [ + "report" + ] + } + }, + { + "properties": { + "outcome": { + "const": "unavailable" + }, + "recognitionStatus": { + "const": "indeterminate" + }, + "observedAt": { + "type": "null" + } + }, + "not": { + "properties": { + "report": true + }, + "required": [ + "report" + ] + } + }, + { + "properties": { + "outcome": { + "const": "not_attempted" + }, + "recognitionStatus": { + "const": "indeterminate" + }, + "observedAt": { + "type": "null" + } + }, + "not": { + "properties": { + "report": true + }, + "required": [ + "report" + ] + } + } + ] + }, + "comparisonClaim": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "statement", + "evidenceIds" + ], + "properties": { + "kind": { + "enum": [ + "recognition_status", + "category", + "description", + "topic", + "association", + "competitor", + "limitation" + ] + }, + "statement": { + "type": "string", + "minLength": 1 + }, + "evidenceIds": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "comparison": { + "type": "object", + "additionalProperties": false, + "required": [ + "channelsAttempted", + "usableReportChannels", + "completeness", + "supportedAgreements", + "supportedDifferences", + "excludedComparisonClaims", + "scoreComparisonPolicy" + ], + "properties": { + "channelsAttempted": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/channel" + } + }, + "usableReportChannels": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/channel" + } + }, + "completeness": { + "enum": [ + "complete", + "partial", + "unavailable" + ] + }, + "supportedAgreements": { + "type": "array", + "items": { + "$ref": "#/$defs/comparisonClaim" + } + }, + "supportedDifferences": { + "type": "array", + "items": { + "$ref": "#/$defs/comparisonClaim" + } + }, + "excludedComparisonClaims": { + "$ref": "#/$defs/stringArray" + }, + "scoreComparisonPolicy": { + "const": "Native scores are channel-specific and must not be averaged, normalized, or used to rank channels." + } + } + }, + "channelCost": { + "type": "object", + "additionalProperties": false, + "required": [ + "channel", + "quotedAmount", + "chargedAmount" + ], + "properties": { + "channel": { + "$ref": "#/$defs/channel" + }, + "quotedAmount": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "chargedAmount": { + "type": "number", + "minimum": 0 + } + } + }, + "costSummary": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "totalQuoted", + "totalCharged", + "channels" + ], + "properties": { + "unit": { + "type": "string", + "minLength": 1 + }, + "totalQuoted": { + "type": [ + "number", + "null" + ], + "minimum": 0 + }, + "totalCharged": { + "type": "number", + "minimum": 0 + }, + "channels": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "$ref": "#/$defs/channelCost" + } + } + } + }, + "stringArray": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a4a6e98..c1aa32b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -43,7 +43,7 @@ The projection direction is public repository to private server. This build neve ## Result Contracts -The opportunity, diagnostic, and implementation-handoff v1 contracts retain their shared evidence envelope and evidence-ID semantics. Focused contracts may instead reference additive shared primitives without inheriting that envelope. `content-question-review/v1` uses human-readable sources directly inside ordered question assessments and structured escalations so a reader does not need to reconstruct an internal ID graph. Verification continues to audit a source result rather than creating an unrelated generic result. +The opportunity, diagnostic, implementation-handoff, and AI Brand Representation Snapshot v1 contracts retain the shared evidence envelope and evidence-ID semantics. The snapshot adds closed framing, channel-observation, comparison, and cost structures plus generic semantic validation for exact channel coverage, identical conditions, preflight all-stop behavior, comparison evidence from multiple eligible channel rows, deterministic completion and disposition, and arithmetic cost reconciliation. Focused contracts may instead reference additive shared primitives without inheriting that envelope. `content-question-review/v1` uses human-readable sources directly inside ordered question assessments and structured escalations so a reader does not need to reconstruct an internal ID graph. Verification continues to audit a source result rather than creating an unrelated generic result. New result contracts are versioned. A recipe references an explicit contract version, and an incompatible contract change requires a new version instead of silently changing existing recipe meaning. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index d7e80bd..93de1bd 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -23,14 +23,14 @@ Result contracts use explicit identifiers such as `seo-diagnostic/v1`. Add `v2` ```bash npm run validate -npm run package -- v0.4.1 -npm run release:verify-assets -- v0.4.1 +npm run package -- v0.5.0 +npm run release:verify-assets -- v0.5.0 ``` 4. Run the repository preflight when the branch and remote state are ready for release. ```bash -npm run release:preflight -- v0.4.1 +npm run release:preflight -- v0.5.0 ``` 5. Review `dist/assets/SHA256SUMS`, the three ZIP inventories, and both versioned catalog files under `dist/catalog/`. diff --git a/docs/TESTING.md b/docs/TESTING.md index 86f3a4d..1163c7e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -17,8 +17,8 @@ npm run validate When runtime or release behavior changes, also build and verify the intended release candidate: ```bash -npm run package -- v0.4.1 -npm run release:verify-assets -- v0.4.1 +npm run package -- v0.5.0 +npm run release:verify-assets -- v0.5.0 ``` Use the intended version tag rather than retaining an older release example after the package version changes. @@ -45,7 +45,7 @@ Report a successful `npm run validate` result as structural or evaluation-regist `tests/evals/cases.json` is the machine-discoverable registry. Every scenario heading must be registered, and every required segment must have at least one case. -`tests/fixtures/contracts/cases.json` registers valid and intentionally invalid JSON instances assessed by catalog validation. The result cases cover a non-empty opportunity set, completed empty opportunity and diagnostic results, a diagnostic with issues, an incomplete diagnostic with a precise stop reason, a constrained incomplete diagnostic for structurally unavailable required evidence, a self-contained implementation handoff, and a content-question review with ordered supplied questions, separate supplemental questions, readable source variants, human escalations, and completion behavior. Negative cases preserve legacy unresolved-evidence checks and reject unsupported dispositions, missing handoff context, mismatched source payloads, missing source material, missing revision recommendations, blocking escalations in complete reviews, and incomplete reviews without stop reasons. Catalog and recipe cases prove rejection of unknown fields, unsupported schema versions, uncontrolled capability and scope values, unknown steps and result contracts, invalid evidence source and use classes, missing conditional predicates, invalid bounds, and ambiguous output composition. The registry and its source instances remain under `tests/` and are excluded from release archives and the server projection. +`tests/fixtures/contracts/cases.json` registers valid and intentionally invalid JSON instances assessed by catalog validation. The result cases cover a non-empty opportunity set, completed empty opportunity and diagnostic results, a diagnostic with issues, an incomplete diagnostic with a precise stop reason, a constrained incomplete diagnostic for structurally unavailable required evidence, a self-contained implementation handoff, a content-question review with ordered supplied questions, and AI Brand Representation Snapshot outcomes across reports, valid negatives, post-start failures, preflight unavailability, insufficient evidence, and rejected framing. Snapshot negative cases reject missing or duplicate channels, unresolved evidence, inconsistent conditions, universal or averaged scores, mismatched disposition or completion, invalid null quotes, and unreconciled cost totals. Other negative cases preserve legacy unresolved-evidence checks and reject unsupported dispositions, missing handoff context, mismatched source payloads, missing source material, missing revision recommendations, blocking escalations in complete reviews, and incomplete reviews without stop reasons. Catalog and recipe cases prove rejection of unknown fields, unsupported schema versions, uncontrolled capability and scope values, unknown steps and result contracts, invalid evidence source and use classes, missing conditional predicates, invalid bounds, and ambiguous output composition. The registry and its source instances remain under `tests/` and are excluded from release archives and the server projection. After a substantial instruction change, run fresh-context agent evaluations against representative fixtures. Give each evaluator only the installed runtime tree plus the prompt and test facts needed for its case. Record whether the output satisfied the invariants; do not count an evaluator's agreement with the prose as execution evidence. diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md index 7bc56ea..48c6ea7 100644 --- a/docs/THREAT-MODEL.md +++ b/docs/THREAT-MODEL.md @@ -40,6 +40,10 @@ An installed skill or public recipe can be newer than the connected server. The Expected list sizes can pressure an agent to invent opportunities or issues. Result contracts permit completed empty arrays, while incomplete execution requires a separate stop reason. +### Misclassified Brand Representation + +A prompted brand-conditioned report can be mislabeled as unprompted buyer-prompt presence, stable model knowledge, a calibrated cross-channel score, or a historical trend. The snapshot contract preserves invocation conditions, separates supplied framing from generated material, forbids universal and averaged score fields, requires exact channel outcomes, stops all channel execution when preflight availability fails, and requires every comparison claim to link evidence from at least two eligible channel rows. + ### Context-Dependent Handoffs Requirements that make sense only inside the analyst's conversation can cause unsafe or incorrect implementation. The handoff contract requires target, outcome, evidence, constraints, missing inputs, acceptance criteria, and verification in the artifact itself. diff --git a/docs/VERSION.md b/docs/VERSION.md index f4576e8..4f10370 100644 --- a/docs/VERSION.md +++ b/docs/VERSION.md @@ -1,5 +1,5 @@ # Version -Current version: `0.4.1`. +Current version: `0.5.0`. This version applies to the portable skill and plugin packages. Catalog and recipe definitions have independent versions described in [Releasing](RELEASING.md). diff --git a/docs/releases/v0.5.0.md b/docs/releases/v0.5.0.md new file mode 100644 index 0000000..209c2eb --- /dev/null +++ b/docs/releases/v0.5.0.md @@ -0,0 +1,28 @@ +# SEO Agent Tools v0.5.0 + +## [v0.5.0] + +This additive release publishes AI Brand Representation Snapshot, a bounded agent-executed recipe for observing how ChatGPT, Gemini, and Perplexity represent one explicitly supplied brand under identical settled conditions. + +The public contract versions are: + +- portable skill and plugin packages: `0.5.0`; +- catalog: `0.5.0`; +- recipe schema: `2`; +- AI Brand Representation Snapshot recipe: `ai-brand-representation-snapshot@1.0.0`; +- new result contract: `ai-brand-representation-snapshot/v1`; +- previously released recipe and result-contract versions remain unchanged. + +The catalog adds the controlled `visibility` domain, `brand` target, `channel` evidence unit, and three separate public brand-representation capabilities. The recipe settles one brand subject and optional market, competitor, and product framing before budget approval, confirms all three required capabilities, and permits no more than one observation per channel. + +The result contract preserves one row for each named channel, including valid reports, valid not-recognized outcomes, failed calls, preflight unavailability, and calls that were not attempted. It keeps supplied competitor and product framing separate from generated competitors and associations, treats generated source descriptors as unverified strings, preserves zero while leaving missing scores absent, and records provider-neutral quoted and charged amounts. + +Native scores remain channel-specific. The contract contains no universal score or averaged score field, and every comparison claim links evidence from at least two eligible channel rows. One snapshot does not measure unprompted buyer-prompt presence, stable model knowledge, a trend, or business impact. + +Deterministic semantic validation enforces exact channel coverage, identical invocation conditions, preflight all-stop behavior, evidence references, multi-row comparison support, disposition, completion, comparison completeness, and arithmetic cost reconciliation without imposing failed-call billing policy. Complete, valid-negative, partial, incomplete, unavailable, no-report, and rejected fixtures exercise those rules alongside invalid channel, evidence, score, condition, state, and cost cases. + +The release publishes three runtime archives plus `seo-agent-tools-catalog-v0.5.0.json` and `seo-agent-tools-catalog-manifest-v0.5.0.json`. `SHA256SUMS` covers all five assets. + +## Validation + +The exact release candidate must pass structural and evaluation-registry validation, deterministic two-build package verification, release preflight, three fresh exact-head reviews, protected merge, immutable release publication, and clean exact-tag installation and update verification before the release is declared complete. diff --git a/package-lock.json b/package-lock.json index 0c7d62c..1b11290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "seo-agent-tools", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "seo-agent-tools", - "version": "0.4.1", + "version": "0.5.0", "devDependencies": { "ajv": "^8.20.0", "ajv-formats": "^3.0.1" diff --git a/package.json b/package.json index 0c46c99..23c4ed3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "seo-agent-tools", - "version": "0.4.1", + "version": "0.5.0", "private": true, "type": "module", "scripts": { diff --git a/packaging/claude-plugin/.claude-plugin/plugin.json b/packaging/claude-plugin/.claude-plugin/plugin.json index 4eb52ff..3646cc9 100644 --- a/packaging/claude-plugin/.claude-plugin/plugin.json +++ b/packaging/claude-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "seo-agent-tools", "displayName": "SEO Agent Tools", - "version": "0.4.1", + "version": "0.5.0", "description": "Evidence-backed SEO analysis, content-production coordination, implementation handoffs, and verification through an SEO MCP server.", "author": { "name": "TechSpokes" diff --git a/packaging/codex-plugin/.codex-plugin/plugin.json b/packaging/codex-plugin/.codex-plugin/plugin.json index b6e3d04..e9cf539 100644 --- a/packaging/codex-plugin/.codex-plugin/plugin.json +++ b/packaging/codex-plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "seo-agent-tools", - "version": "0.4.1", + "version": "0.5.0", "description": "Evidence-backed SEO analysis, content-production coordination, implementation handoffs, and verification through an SEO MCP server.", "author": { "name": "TechSpokes" diff --git a/scripts/catalog.mjs b/scripts/catalog.mjs index 825ddc2..fc5c8ba 100644 --- a/scripts/catalog.mjs +++ b/scripts/catalog.mjs @@ -47,7 +47,8 @@ const catalogRoot = path.join(root, "catalog"); const recipeRoot = path.join(catalogRoot, "recipes"); const schemaRoot = path.join(catalogRoot, "schemas"); const contractFixtureRoot = path.join(root, "tests", "fixtures", "contracts"); -const legacyEnvelopeContractIds = new Set([ +const evidenceEnvelopeContractIds = new Set([ + "ai-brand-representation-snapshot/v1", "seo-diagnostic/v1", "seo-implementation-handoff/v1", "seo-opportunity-set/v1" @@ -344,7 +345,17 @@ function validateContractFixtures(authority, catalog, failures) { return registry.cases.length; } -/** Derive one negative instance from a checked-in valid baseline using a deliberately tiny mutation vocabulary. */ +/** + * Derive one negative instance from a checked-in valid baseline using a deliberately tiny mutation vocabulary. + * + * @param {object} source Valid checked-in baseline instance. + * @param {object[]} mutations Append, delete, or set operations using absolute JSON pointers. + * @param {string} label Fixture case label used in failures. + * @param {string[]} failures Accumulated validation failures. + * @returns {object} Cloned and mutated fixture instance. + * @sideEffects Adds mutation-shape failures to the supplied failure collection. + * @constraints Deleting an array index removes the item and closes the gap; deleting an object property preserves ordinary property-deletion behavior. + */ function applyFixtureMutations(source, mutations, label, failures) { const instance = structuredClone(source); if (!Array.isArray(mutations)) { @@ -370,7 +381,8 @@ function applyFixtureMutations(source, mutations, label, failures) { continue; } if (mutation.op === "delete") { - delete parent[property]; + if (Array.isArray(parent) && /^(?:0|[1-9][0-9]*)$/.test(property)) parent.splice(Number(property), 1); + else delete parent[property]; } else if (mutation.op === "append") { if (!Array.isArray(parent[property])) failures.push(`${mutationLabel} append target must be an array.`); else parent[property].push(structuredClone(mutation.value)); @@ -387,8 +399,8 @@ function applyFixtureMutations(source, mutations, label, failures) { * @param {object} result Structurally valid result contract instance. * @param {string} label Fixture path used in failures. * @param {string[]} failures Accumulated validation failures. - * @why Issue #17 adds a standalone result whose readable sources intentionally avoid the legacy evidence-ID graph. - * @constraints Legacy evidence-link checks apply only to the three released envelope-based v1 contracts; focused contracts must not acquire those fields implicitly. + * @why Issue #17 adds a standalone result whose readable sources intentionally avoid the evidence-ID graph, while issue #15 adds a focused contract composed with that graph. + * @constraints Evidence-link checks apply only to contracts composed with the shared envelope; standalone focused contracts must not acquire those fields implicitly. */ function validateResultSemantics(result, label, failures) { if (result.completion.status === "incomplete" && (typeof result.completion.stop_reason !== "string" || result.completion.stop_reason.trim() === "")) { @@ -401,14 +413,19 @@ function validateResultSemantics(result, label, failures) { failures.push(`${label} complete completion must not include a blocking human escalation.`); } - if (!legacyEnvelopeContractIds.has(result.contract_id)) return; + if (!evidenceEnvelopeContractIds.has(result.contract_id)) return; const evidenceIds = collectUniqueIds(result.evidence, `${label} evidence`, failures); collectUniqueIds(result.findings, `${label} findings`, failures); validateEvidenceLinks(result.findings, "evidence_ids", evidenceIds, `${label} findings`, failures, true); validateEvidenceLinks(result.verification, "evidence_ids", evidenceIds, `${label} verification`, failures, false); - if (result.contract_id === "seo-opportunity-set/v1") { + if (result.contract_id === "ai-brand-representation-snapshot/v1") { + validateEvidenceLinks(result.channel_observations, "evidenceIds", evidenceIds, `${label} channel_observations`, failures, false); + validateEvidenceLinks(result.comparison.supportedAgreements, "evidenceIds", evidenceIds, `${label} comparison.supportedAgreements`, failures, true); + validateEvidenceLinks(result.comparison.supportedDifferences, "evidenceIds", evidenceIds, `${label} comparison.supportedDifferences`, failures, true); + validateBrandSnapshotSemantics(result, label, failures); + } else if (result.contract_id === "seo-opportunity-set/v1") { collectUniqueIds(result.opportunities, `${label} opportunities`, failures); validateEvidenceLinks(result.opportunities, "evidence_ids", evidenceIds, `${label} opportunities`, failures, true); } else if (result.contract_id === "seo-diagnostic/v1") { @@ -420,6 +437,141 @@ function validateResultSemantics(result, label, failures) { } } +/** + * Enforce cross-collection snapshot invariants that JSON Schema cannot express. + * + * @param {object} result Structurally valid AI Brand Representation Snapshot instance. + * @param {string} label Fixture path used in failures. + * @param {string[]} failures Accumulated validation failures. + * @returns {void} + * @why Issue #15 requires exact channel coverage, deterministic disposition and completion, identical framing, and arithmetic cost reconciliation. + * @constraints The public validator reasons only from provider-neutral result fields and never embeds runtime mappings, prices, or billing policy. + */ +function validateBrandSnapshotSemantics(result, label, failures) { + const channels = ["chatgpt", "gemini", "perplexity"]; + const observations = result.channel_observations; + const costs = result.cost_summary.channels; + validateExactChannelRows(observations, `${label} channel_observations`, channels, failures); + validateExactChannelRows(costs, `${label} cost_summary.channels`, channels, failures); + + const baselineConditions = stableJson(observations[0].conditions); + for (const [index, observation] of observations.entries()) { + if (stableJson(observation.conditions) !== baselineConditions) { + failures.push(`${label} channel_observations[${index}] must use conditions identical to every other channel.`); + } + } + + const attemptedOutcomes = new Set(["report", "not_recognized", "failed"]); + const validOutcomes = new Set(["report", "not_recognized"]); + const attemptedChannels = observations.filter((item) => attemptedOutcomes.has(item.outcome)).map((item) => item.channel); + const reportChannels = observations.filter((item) => item.outcome === "report").map((item) => item.channel); + const validCount = observations.filter((item) => validOutcomes.has(item.outcome)).length; + validateExactChannelList(result.comparison.channelsAttempted, attemptedChannels, `${label} comparison.channelsAttempted`, failures); + validateExactChannelList(result.comparison.usableReportChannels, reportChannels, `${label} comparison.usableReportChannels`, failures); + + if (observations.some((item) => item.outcome === "unavailable") && attemptedChannels.length > 0) { + failures.push(`${label} unavailable preflight outcome requires zero attempted channel outcomes.`); + } + + if (result.disposition === "reject") { + if (observations.some((item) => item.outcome !== "not_attempted")) { + failures.push(`${label} reject disposition requires every channel outcome to be not_attempted.`); + } + } else { + const expectedDisposition = reportChannels.length === 3 ? "proceed" : validCount >= 2 ? "conditional" : "defer"; + if (result.disposition !== expectedDisposition) { + failures.push(`${label} disposition must be ${expectedDisposition} for the recorded channel outcomes.`); + } + } + + const expectedCompletion = validCount === 3 ? "complete" : "incomplete"; + if (result.completion.status !== expectedCompletion) { + failures.push(`${label} completion.status must be ${expectedCompletion} for the recorded channel outcomes.`); + } + + const expectedComparison = reportChannels.length === 3 ? "complete" : validCount >= 2 ? "partial" : "unavailable"; + if (result.comparison.completeness !== expectedComparison) { + failures.push(`${label} comparison.completeness must be ${expectedComparison} for the recorded channel outcomes.`); + } + + const claims = [...result.comparison.supportedAgreements, ...result.comparison.supportedDifferences]; + for (const claim of claims) { + const eligibleOutcomes = claim.kind === "recognition_status" ? validOutcomes : new Set(["report"]); + const claimEvidenceIds = new Set(claim.evidenceIds); + const evidencedChannels = observations.filter( + (item) => eligibleOutcomes.has(item.outcome) && item.evidenceIds.some((evidenceId) => claimEvidenceIds.has(evidenceId)), + ); + if (claim.kind === "recognition_status" && validCount < 2) { + failures.push(`${label} recognition-status comparison requires at least two valid channel outcomes.`); + } + if (claim.kind !== "recognition_status" && reportChannels.length < 2) { + failures.push(`${label} report-content comparison kind ${claim.kind} requires at least two usable reports.`); + } + if (evidencedChannels.length < 2) { + const evidenceClass = claim.kind === "recognition_status" ? "valid-outcome" : "report"; + failures.push(`${label} comparison kind ${claim.kind} must link evidence from at least two ${evidenceClass} channel rows.`); + } + } + + for (const [observationIndex, observation] of observations.entries()) { + if (observation.outcome !== "report") continue; + const panelIndexes = new Set(); + for (const competitor of observation.report.controlledCompetitorObservations) { + if (panelIndexes.has(competitor.panelIndex)) { + failures.push(`${label} channel_observations[${observationIndex}] duplicates controlled competitor panelIndex ${competitor.panelIndex}.`); + } + panelIndexes.add(competitor.panelIndex); + if (competitor.panelIndex >= result.framing.suppliedCompetitorPanel.length) { + failures.push(`${label} channel_observations[${observationIndex}] references controlled competitor panelIndex ${competitor.panelIndex} outside the supplied panel.`); + } + } + } + + const observationByChannel = new Map(observations.map((item) => [item.channel, item])); + for (const cost of costs) { + const outcome = observationByChannel.get(cost.channel)?.outcome; + if (cost.quotedAmount === null && !["unavailable", "not_attempted"].includes(outcome)) { + failures.push(`${label} cost_summary.channels quote for ${cost.channel} may be null only when no quote was reached.`); + } + if (["unavailable", "not_attempted"].includes(outcome) && cost.chargedAmount !== 0) { + failures.push(`${label} cost_summary.channels charge for ${cost.channel} must be zero for outcome ${outcome}.`); + } + } + + const chargedTotal = costs.reduce((sum, item) => sum + item.chargedAmount, 0); + if (!amountsEqual(result.cost_summary.totalCharged, chargedTotal)) { + failures.push(`${label} cost_summary.totalCharged must equal the sum of channel chargedAmount values.`); + } + const completeQuote = costs.every((item) => item.quotedAmount !== null); + if (completeQuote) { + const quotedTotal = costs.reduce((sum, item) => sum + item.quotedAmount, 0); + if (!amountsEqual(result.cost_summary.totalQuoted, quotedTotal)) { + failures.push(`${label} cost_summary.totalQuoted must equal the sum of channel quotedAmount values.`); + } + } else if (result.cost_summary.totalQuoted !== null) { + failures.push(`${label} cost_summary.totalQuoted must be null when any channel quote was not reached.`); + } +} + +function validateExactChannelRows(rows, label, expectedChannels, failures) { + const actual = rows.map((item) => item.channel); + validateExactChannelList(actual, expectedChannels, label, failures); +} + +function validateExactChannelList(actual, expected, label, failures) { + const actualCounts = new Map(actual.map((channel) => [channel, actual.filter((item) => item === channel).length])); + for (const channel of expected) { + if (actualCounts.get(channel) !== 1) failures.push(`${label} must contain channel ${channel} exactly once.`); + } + for (const channel of actualCounts.keys()) { + if (!expected.includes(channel)) failures.push(`${label} contains unexpected channel ${channel}.`); + } +} + +function amountsEqual(left, right) { + return typeof left === "number" && Math.abs(left - right) <= 1e-9; +} + function collectUniqueIds(items, label, failures) { const ids = new Set(); for (const [index, item] of items.entries()) { diff --git a/skills/seo-agent-tools/SKILL.md b/skills/seo-agent-tools/SKILL.md index 595285c..3de8715 100644 --- a/skills/seo-agent-tools/SKILL.md +++ b/skills/seo-agent-tools/SKILL.md @@ -1,6 +1,6 @@ --- name: seo-agent-tools -description: Use when an agent must investigate an SEO question with a connected SEO MCP server, discover and run an available SEO recipe, coordinate evidence-backed content production without writing the copy, diagnose why a page or site is underperforming, prepare an implementation handoff, or verify completed SEO work. Do not use it to write finished content, reveal private server internals, or invent unavailable tools, data, or persistent job behavior. +description: Use when an agent must investigate an SEO question with a connected SEO MCP server, observe current AI brand representation, discover and run an available SEO recipe, coordinate evidence-backed content production without writing the copy, diagnose why a page or site is underperforming, prepare an implementation handoff, or verify completed SEO work. Do not use it to write finished content, reveal private server internals, or invent unavailable tools, data, or persistent job behavior. --- # SEO Agent Tools @@ -13,7 +13,7 @@ The server is the authority for tools, recipes, availability, authorization, and ## Activation Boundaries -Activate this skill for SEO analysis that benefits from server evidence, including keyword and search-result investigation, page and site diagnosis, technical triage, competitor or backlink comparison, recipe discovery, content-production coordination, implementation planning, and verification of SEO changes. +Activate this skill for SEO analysis that benefits from server evidence, including keyword and search-result investigation, current AI brand-representation observation, page and site diagnosis, technical triage, competitor or backlink comparison, recipe discovery, content-production coordination, implementation planning, and verification of SEO changes. Use another skill or hand off when the primary task is writing finished page copy, administering the MCP server, implementing provider integrations, changing billing or access policy, or designing persistent jobs and scheduled workflows. @@ -25,6 +25,8 @@ When the user has already selected one content objective and wants to move from A direct analysis uses one or a few server capabilities when the question is narrow and the evidence path is obvious. +A direct one-channel brand analysis observes how one named answer channel represents a brand that the caller explicitly supplies. The three-channel AI Brand Representation Snapshot is a versioned recipe that applies identical settled framing to ChatGPT, Gemini, and Perplexity and returns one current, non-persistent result. Neither path measures unprompted buyer-prompt presence, stable model knowledge, or historical change. + A recipe is a versioned SEO method discovered from the connected server. It defines required inputs, stable steps, source-classed evidence requirements, evidence bounds, missing-capability behavior, stop conditions, completion criteria, and explicit result composition. The catalog can grow; never treat a remembered list of recipes as complete. A result contract makes the decision reusable by defining the exact subject, supporting material, decision fields, completion state, and any required verification or escalation. Some contracts use the legacy evidence-and-finding envelope, while focused human-facing contracts may place readable sources directly inside each assessment. A selected recipe may use a contract newer than this installed skill; use it only when the connected server supplies the exact versioned contract in an interpretable form. @@ -45,6 +47,8 @@ A result contract makes the decision reusable by defining the exact subject, sup Use direct analysis when one bounded evidence lookup can answer the question and a multi-step method would add no decision value. +Use a current direct capability when the user asks about one answer channel. Use recipe discovery for a comparable ChatGPT, Gemini, and Perplexity brand-representation snapshot. Do not use either path to answer whether unbranded buyer prompts mention the brand or whether visibility changed over time; those questions require separately supported methods and evidence. + Use recipe discovery when the request requires several dependent evidence steps, a repeatable methodology, or a typed result that another agent or system will consume. Filter compact recipe cards by goal facets, compare a small candidate set, then retrieve one full recipe definition. Treat a card's capability list as a discovery summary; the selected full recipe's evidence plan and output composition govern execution. An unfamiliar recipe name or facet is acceptable when the server describes it; an uninterpretable result contract is not. Use content-quality diagnosis when the user assumes a page needs new or rewritten content. Determine whether the actual intervention is to create, refresh, consolidate, preserve, fix a technical blocker, change site strategy, improve authority, or obtain first-party operational evidence. diff --git a/skills/seo-agent-tools/references/mcp-routing-and-evidence.md b/skills/seo-agent-tools/references/mcp-routing-and-evidence.md index 20e637c..558a408 100644 --- a/skills/seo-agent-tools/references/mcp-routing-and-evidence.md +++ b/skills/seo-agent-tools/references/mcp-routing-and-evidence.md @@ -25,6 +25,20 @@ Recipe discovery should prefer executable candidates. An availability state of ` Treat result-contract compatibility as a separate check from availability and approval. An unfamiliar contract is usable only when the server supplies its exact versioned fields, meanings, and validation rules in an interpretable form. Do not map it to a locally known contract because their names or purposes appear similar. If the exact contract is missing or incompatible, stop before its evidence-collection calls and report `unsupported-result-contract` with the observed skill, recipe, and server versions. +## Route Brand Representation Work + +Use a current direct capability when the user asks how one named answer channel represents a brand that the caller explicitly supplies. Preserve that invocation's recognition status, conditions, generated report material, source-descriptor limitations, and native scores without calling the result a three-channel snapshot. + +Use `ai-brand-representation-snapshot` when the decision requires a comparable current observation across ChatGPT, Gemini, and Perplexity. Settle one subject, intended brand, website, location, controlled competitor panel, supplied products or services, and identity limitations before preflight. Preserve supplied lists even when they cannot be sent, and omit an optional invocation list that does not satisfy the runtime's current requirements instead of inventing padding. + +Resolve the exact recipe and `ai-brand-representation-snapshot/v1` contract from the connected server. Obtain current budget, authorization, headroom, and availability for all three separately required channel capabilities before execution. Plan no more than one observation per channel under identical settled conditions; parallel execution is optional, not required. + +A valid report and a valid `not_recognized` result are both invocation-scoped domain outcomes. Do not rerun either outcome to seek a preferred answer, and do not initiate a hidden retry. A server-directed retry for an actual retryable execution failure remains governed by the connected server. Preserve failed, unavailable, and not-attempted channels explicitly, keep controlled-panel competitor observations separate from generated competitors, and keep supplied products or services separate from generated associations. + +Compare report descriptions, categories, topics, associations, and competitors only when at least two structured reports are usable, and link each comparison claim to evidence from at least two eligible channel rows. Recognition-status comparison may use valid report and not-recognized outcomes. Preserve native finite scores, including zero, but never average, normalize, or rank channel scores; missing scores remain absent, and generated source descriptors are not verified citations. + +Buyer-prompt presence asks whether an unbranded buyer question names the brand. Trend analysis compares methodologically compatible observations over time. Neither question is answered by a direct brand-conditioned report or one AI Brand Representation Snapshot, so route it to a separately supported method or report the current capability gap. + ## Resolve the Evidence Plan The selected full recipe, not its compact discovery card, defines execution. For every evidence-plan entry: @@ -79,7 +93,7 @@ Never place credentials, private identities, customer data, account balances, or Classify evidence outcomes by when and how they occur. - A readiness failure means authorization, availability, input, parameter, budget, or exact-contract requirements are not satisfied before evidence execution. Do not start the affected paid or protected step. Report the precise prerequisite through the selected recipe's stop behavior. -- An execution-time evidence failure means the capability was authorized and available with valid parameters, but it could not produce required evidence for the relevant input. Return the primary contract as an incomplete result. Preserve only valid observations and supported findings, identify the exact missing or structurally unavailable evidence and failure class, mark unsupported layers unknown, state the unresolved question and prohibited conclusions or interventions, give an appropriate client-side, user-supplied, connected, or manual recovery route plus the condition for resuming, use `defer`, and omit every conditional implementation handoff. +- An execution-time evidence failure means the capability was authorized and available with valid parameters, but it could not produce required evidence for the relevant input. Return the primary contract as an incomplete result. Preserve only valid observations and supported findings, identify the exact missing or structurally unavailable evidence and failure class, mark unsupported layers unknown, state the unresolved question and prohibited conclusions or interventions, give an appropriate client-side, user-supplied, connected, or manual recovery route plus the condition for resuming, and omit every conditional implementation handoff. Use the primary contract's deterministic disposition when it defines one; otherwise use `defer`. - Completed empty evidence means the capability completed successfully and returned no observations. Preserve it as legitimate completed evidence rather than relabeling it as an execution failure or padding the result. The server or tool being unavailable, caller authorization being absent, cost approval being missing, a required input being unresolved, result-contract skew, or a required source capability being absent are readiness failures when detected before execution. Weak, contradictory, or structurally unavailable evidence discovered after a ready step begins follows the recipe's execution-time failure or constrained-completion behavior. diff --git a/skills/seo-agent-tools/references/result-contracts.md b/skills/seo-agent-tools/references/result-contracts.md index 0920ab1..6800bed 100644 --- a/skills/seo-agent-tools/references/result-contracts.md +++ b/skills/seo-agent-tools/references/result-contracts.md @@ -51,6 +51,34 @@ Before creating a human escalation, inspect the brief and draft, retrieve availa An incomplete question review requires a precise `stop_reason`. A complete review omits `stop_reason`. A blocking unresolved escalation prevents a complete review; nonblocking escalations may remain when they do not affect publication readiness. Do not add fields outside the exact contract. +## AI Brand Representation Snapshot v1 + +Use `ai-brand-representation-snapshot/v1` for one current brand-conditioned observation across ChatGPT, Gemini, and Perplexity under identical settled conditions. The contract composes the shared evidence envelope with `framing`, `channel_observations`, `comparison`, and `cost_summary`; it does not measure buyer-prompt presence, stable model knowledge, historical change, or business impact. + +`framing` preserves the supplied subject, settled brand name and canonical website when known, optional location, supplied competitor panel in caller order, supplied products or services in caller order, and identity limitations. A supplied competitor entry may contain a label, a public URL, or both. Do not invent a label from a hostname, and do not pad supplied competitor or product lists to satisfy current runtime input requirements. + +`channel_observations` contains exactly one row for each named channel. Every row records `channel`, `outcome`, `recognitionStatus`, `observedAt`, `evidenceIds`, exact `conditions`, `qualityNotes`, and `limitations`. A `report` row additionally contains generated description, awareness, sentiment, and credibility dimensions, topics, associations, source descriptors, controlled-panel observations, and generated competitors. + +Use channel outcomes consistently: + +- `report` means the invocation returned a structured report and uses `recognized` recognition status. +- `not_recognized` means the invocation returned the exact valid no-report domain outcome and uses the same recognition status. +- `failed` means an attempted invocation returned no valid domain outcome and uses `indeterminate` recognition status. +- `unavailable` means the required capability was unavailable or unauthorized at preflight and uses `indeterminate` recognition status. +- `not_attempted` means no call occurred because framing was rejected or execution stopped before that channel and uses `indeterminate` recognition status. + +`observedAt` is required for `report`, `not_recognized`, and `failed` outcomes and is null for `unavailable` or `not_attempted`. A report or not-recognized result is valid normally billed invocation evidence. A failed, unavailable, or not-attempted outcome is not a substitute for an observation and remains visible in an incomplete result. + +Every report dimension requires a generated category or explicit null when the category is unavailable. `nativeScore` is optional and numeric when supplied. Preserve zero as evidence and represent a missing score by omitting `nativeScore`, not by converting it to zero or adding a fabricated null. + +`comparison` records attempted channels, usable report channels, completeness, evidence-linked supported agreements and differences, excluded claims, and the fixed score-comparison policy. A report-content claim identifies its comparison kind and links evidence from at least two usable report rows. A recognition-status claim links evidence from at least two valid report or not-recognized rows. Never add a universal score, cross-channel average, native-score normalization, or score-derived channel rank. + +`cost_summary` records the provider-neutral unit, total quoted amount when fully known, total charged amount, and exactly one cost row per channel. Channel rows contain quoted and charged amounts. All known totals must equal their channel sums; a null quote is valid only when no quote was reached. An unavailable or not-attempted channel records zero actual charge because no invocation occurred. A failed invocation preserves the connected server's actual charge, including zero when that is what the server reports. + +Use disposition and completion deterministically. `proceed` requires three report outcomes. `conditional` requires at least two valid report or not-recognized outcomes when the result is not a three-report comparison. `defer` means fewer than two valid outcomes remain. `reject` is reserved for invalid or unresolved framing before paid execution and requires all channels to remain not attempted. An unavailable preflight outcome also stops before every channel invocation, so the other channel outcomes must be unavailable or not attempted. Completion is `complete` only when all three planned channels produced a valid report or not-recognized outcome; every other state is `incomplete` with a precise stop reason. + +Do not copy complete raw channel responses into the result. Preserve concise evidence records and references, observation conditions, generated material required by the contract, quality notes, limitations, and verification status. + ## SEO Opportunity Set v1 Use `seo-opportunity-set/v1` for ranked keyword, competitor, content-gap, or link-prospect opportunities. Include: diff --git a/tests/evals/cases.json b/tests/evals/cases.json index 83d20b8..2d30c63 100644 --- a/tests/evals/cases.json +++ b/tests/evals/cases.json @@ -274,6 +274,105 @@ "conflicting observations ignored" ] }, + { + "id": "brand-snapshot-recipe-selection", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "routing", + "heading": "Multi-Channel Brand Request Selects the Snapshot Recipe", + "baseline_risks": [ + "unversioned direct-tool orchestration", + "snapshot scope misrepresented" + ] + }, + { + "id": "brand-direct-single-channel-routing", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "routing", + "heading": "One-Channel Brand Request Uses Direct Analysis", + "baseline_risks": [ + "unnecessary three-channel spend", + "direct observation mislabeled as snapshot" + ] + }, + { + "id": "brand-framing-before-spend", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "cost-authority", + "heading": "Brand Framing Settles Before Paid Evidence", + "baseline_risks": [ + "invented competitor or product padding", + "inconsistent channel conditions" + ] + }, + { + "id": "brand-snapshot-current-budget-capabilities", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "cost-authority", + "heading": "Snapshot Preflight Uses Current Budget and Three Capabilities", + "baseline_risks": [ + "stale public price assumption", + "missing required channel readiness" + ] + }, + { + "id": "brand-valid-negative-no-rerun", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "evidence-integrity", + "heading": "Valid Negative Observation Is Not Rerun", + "baseline_risks": [ + "valid negative treated as failure", + "paid outcome smoothing" + ] + }, + { + "id": "brand-channel-specific-scores", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "evidence-integrity", + "heading": "Snapshot Scores Remain Channel Specific", + "baseline_risks": [ + "cross-channel score averaging", + "zero and missing score confusion" + ] + }, + { + "id": "brand-supplied-generated-separation", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "evidence-integrity", + "heading": "Supplied and Generated Brand Evidence Remains Separate", + "baseline_risks": [ + "controlled and generated competitors conflated", + "source descriptors presented as citations" + ] + }, + { + "id": "brand-channel-failure-incomplete", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "failure", + "heading": "Channel Failure Produces an Incomplete Snapshot", + "baseline_risks": [ + "failed channel silently omitted", + "cost and completion totals inconsistent" + ] + }, + { + "id": "brand-snapshot-scope-boundary", + "kind": "scenario", + "fixture": "behavior-scenarios.md", + "segment": "runtime-authority", + "heading": "Snapshot Does Not Become Buyer-Prompt or Trend Analysis", + "baseline_risks": [ + "buyer-prompt presence inferred", + "trend fabricated from one snapshot" + ] + }, { "id": "retrieval-injection", "kind": "scenario", diff --git a/tests/fixtures/behavior-scenarios.md b/tests/fixtures/behavior-scenarios.md index 72e86aa..4a2825e 100644 --- a/tests/fixtures/behavior-scenarios.md +++ b/tests/fixtures/behavior-scenarios.md @@ -169,3 +169,94 @@ Expected invariants: - The agent inspects the observations underlying the score and the independent page evidence instead of treating the numeric or categorical score as an editorial verdict. - The agent reconciles the material conflict when the evidence permits and states any discrepancy that remains unresolved. - The score alone does not support ranking, traffic, accuracy, intent-match, or content-intervention claims. + +## Multi-Channel Brand Request Selects the Snapshot Recipe + +Input: The user supplies one brand and asks how ChatGPT, Gemini, and Perplexity currently represent it under comparable conditions. + +Expected invariants: + +- The agent discovers and selects `ai-brand-representation-snapshot` rather than assembling an unversioned comparison from remembered direct tools. +- The agent confirms the exact recipe version, three required channel capabilities, and `ai-brand-representation-snapshot/v1` before evidence collection. +- The agent describes the result as a prompted brand-conditioned snapshot rather than buyer-prompt visibility, stable model knowledge, or a trend. + +## One-Channel Brand Request Uses Direct Analysis + +Input: The user asks only how Gemini currently represents a supplied brand and does not request a three-channel comparison or reusable snapshot. + +Expected invariants: + +- The agent uses the current authorized direct one-channel capability when available instead of expanding the request into the three-channel recipe. +- The agent preserves invocation-scoped recognition status, current conditions, source descriptors, and score limitations for that one observation. +- The agent does not label the direct observation as the AI Brand Representation Snapshot or infer cross-channel conclusions. + +## Brand Framing Settles Before Paid Evidence + +Input: The user supplies a bare domain, two confirmed competitors, and two products, but the connected runtime requires larger optional lists when those fields are sent. + +Expected invariants: + +- The agent settles the intended brand, website, location, supplied competitor panel, supplied products, and unresolved identity limitations before budget approval or channel execution. +- The agent preserves the two supplied competitors and products in framing but omits undersized optional invocation lists rather than inventing padding values. +- The agent applies identical settled conditions to all planned channels and stops before paid work if the intended customer-facing brand remains materially ambiguous. + +## Snapshot Preflight Uses Current Budget and Three Capabilities + +Input: The connected server exposes the snapshot recipe, current quotes, authorization, balance information, and three separately mapped required channel capabilities. + +Expected invariants: + +- The agent uses the server's current required, default, and maximum budget rather than a public static or remembered price. +- The agent confirms authorization, headroom, exact result-contract support, and availability for all three required capabilities before the first observation. +- If any required capability is unavailable, the agent stops before every channel invocation and records the remaining channels as unavailable or not attempted. +- The agent plans no more than one observation for each channel and does not require parallel execution. + +## Valid Negative Observation Is Not Rerun + +Input: ChatGPT returns a report, Gemini returns a valid `not_recognized` outcome, and Perplexity returns a report under the settled conditions. + +Expected invariants: + +- The agent treats Gemini's result as valid normally billed invocation evidence rather than an execution failure or stable absence claim. +- The agent does not rerun Gemini or another valid channel to seek a preferred recognition outcome. +- The result remains complete because all planned channels produced valid domain outcomes, while its disposition and comparison limits reflect that it is not a three-report comparison. + +## Snapshot Scores Remain Channel Specific + +Input: All three channel reports provide awareness, sentiment, and credibility categories with different native numeric scores. + +Expected invariants: + +- The agent preserves each finite native score, including zero, and represents a missing score by absence rather than zero or a fabricated null. +- The agent may place generated categories side by side but does not average, normalize, or rank channels by native score. +- The result contains no universal visibility score and explains that categories and scores are not calibrated cross-channel measurements. + +## Supplied and Generated Brand Evidence Remains Separate + +Input: The settled framing includes a controlled competitor panel and supplied products, while the channel reports generate additional competitors, topics, source descriptors, and associations. + +Expected invariants: + +- The agent preserves caller-supplied competitor order and keeps controlled-panel observations separate from generated competitor discoveries. +- The agent keeps supplied products and services in framing and labels report associations as generated even when wording overlaps. +- The agent treats generated source descriptors as unverified strings and flags mixed-language or placeholder-like content without silently rewriting the evidence. + +## Channel Failure Produces an Incomplete Snapshot + +Input: All snapshot preflight checks pass, two channels return valid reports, and the third planned invocation fails without a valid domain outcome. + +Expected invariants: + +- The agent preserves one row for the failed channel with indeterminate recognition status, its attempt time, failure evidence, quality notes, and limitations. +- The result uses incomplete completion with a precise stop reason, conditional disposition for the two valid outcomes, and a partial comparison limited to supported dimensions. +- The agent records the connected server's actual charge for the failed channel, including zero only when that is what the server reports, reconciles total quoted and charged amounts, and does not reconstruct or silently omit the failed observation. + +## Snapshot Does Not Become Buyer-Prompt or Trend Analysis + +Input: After receiving one three-channel brand-conditioned snapshot, the user asks which channel is most likely to mention the brand in unprompted buyer questions and whether visibility improved over last month. + +Expected invariants: + +- The agent states that the snapshot does not measure unprompted buyer-prompt presence or methodologically comparable historical change. +- The agent does not rank channels, invent prior observations, or convert native scores into a visibility trend. +- The agent routes those questions to future separately supported methods or reports the current capability gap without expanding this recipe. diff --git a/tests/fixtures/contracts/brand-snapshot-rejected.json b/tests/fixtures/contracts/brand-snapshot-rejected.json new file mode 100644 index 0000000..42e2ad6 --- /dev/null +++ b/tests/fixtures/contracts/brand-snapshot-rejected.json @@ -0,0 +1,132 @@ +{ + "analysis_id": "analysis-brand-snapshot-rejected", + "contract_id": "ai-brand-representation-snapshot/v1", + "recipe": { + "id": "ai-brand-representation-snapshot", + "version": "1.0.0" + }, + "subject": { + "supplied_value": "example.test" + }, + "observed_at": "2026-09-03T12:00:00Z", + "evidence": [], + "findings": [], + "disposition": "reject", + "confidence": { + "level": "low", + "reasons": [ + "The website was identified, but the intended customer-facing brand remained unresolved." + ] + }, + "constraints": [ + "No paid channel observation was attempted." + ], + "completion": { + "status": "incomplete", + "stop_reason": "Brand framing was rejected before spending because the intended customer-facing brand remained materially ambiguous." + }, + "verification": [ + { + "check": "Brand identity framing", + "status": "unavailable", + "note": "Resume only after the intended customer-facing brand is confirmed." + } + ], + "framing": { + "suppliedSubject": "example.test", + "settledBrandName": null, + "canonicalWebsite": "https://example.test", + "location": null, + "suppliedCompetitorPanel": [], + "suppliedProductsOrServices": [], + "limitations": [ + "A website alone did not prove which customer-facing brand the caller intended." + ] + }, + "channel_observations": [ + { + "channel": "chatgpt", + "outcome": "not_attempted", + "recognitionStatus": "indeterminate", + "observedAt": null, + "evidenceIds": [], + "conditions": { + "brand": "example.test", + "location": null, + "competitors": [], + "productsOrServices": [] + }, + "qualityNotes": [], + "limitations": [ + "The call was not attempted because framing was rejected." + ] + }, + { + "channel": "gemini", + "outcome": "not_attempted", + "recognitionStatus": "indeterminate", + "observedAt": null, + "evidenceIds": [], + "conditions": { + "brand": "example.test", + "location": null, + "competitors": [], + "productsOrServices": [] + }, + "qualityNotes": [], + "limitations": [ + "The call was not attempted because framing was rejected." + ] + }, + { + "channel": "perplexity", + "outcome": "not_attempted", + "recognitionStatus": "indeterminate", + "observedAt": null, + "evidenceIds": [], + "conditions": { + "brand": "example.test", + "location": null, + "competitors": [], + "productsOrServices": [] + }, + "qualityNotes": [], + "limitations": [ + "The call was not attempted because framing was rejected." + ] + } + ], + "comparison": { + "channelsAttempted": [], + "usableReportChannels": [], + "completeness": "unavailable", + "supportedAgreements": [], + "supportedDifferences": [], + "excludedComparisonClaims": [ + "No representation comparison was possible before the subject was settled." + ], + "scoreComparisonPolicy": "Native scores are channel-specific and must not be averaged, normalized, or used to rank channels." + }, + "cost_summary": { + "unit": "test-unit", + "totalQuoted": null, + "totalCharged": 0, + "channels": [ + { + "channel": "chatgpt", + "quotedAmount": null, + "chargedAmount": 0 + }, + { + "channel": "gemini", + "quotedAmount": null, + "chargedAmount": 0 + }, + { + "channel": "perplexity", + "quotedAmount": null, + "chargedAmount": 0 + } + ] + } +} diff --git a/tests/fixtures/contracts/brand-snapshot-three-reports.json b/tests/fixtures/contracts/brand-snapshot-three-reports.json new file mode 100644 index 0000000..9cc7f27 --- /dev/null +++ b/tests/fixtures/contracts/brand-snapshot-three-reports.json @@ -0,0 +1,359 @@ +{ + "analysis_id": "analysis-brand-snapshot-three-reports", + "contract_id": "ai-brand-representation-snapshot/v1", + "recipe": { + "id": "ai-brand-representation-snapshot", + "version": "1.0.0" + }, + "subject": { + "brand": "Example Brand", + "website": "https://example.test" + }, + "observed_at": "2026-09-03T12:03:00Z", + "evidence": [ + { + "id": "evidence-chatgpt", + "source": "authorized ChatGPT brand-representation observation", + "subject": "Example Brand", + "observed_at": "2026-09-03T12:01:00Z", + "conditions": { + "location": "France", + "competitor_count": 3, + "product_count": 3 + }, + "observation": "A structured brand-conditioned report was returned.", + "limitations": [ + "This prompted observation does not measure buyer-prompt presence or stable model knowledge." + ] + }, + { + "id": "evidence-gemini", + "source": "authorized Gemini brand-representation observation", + "subject": "Example Brand", + "observed_at": "2026-09-03T12:02:00Z", + "conditions": { + "location": "France", + "competitor_count": 3, + "product_count": 3 + }, + "observation": "A structured brand-conditioned report was returned.", + "limitations": [ + "This prompted observation does not measure buyer-prompt presence or stable model knowledge." + ] + }, + { + "id": "evidence-perplexity", + "source": "authorized Perplexity brand-representation observation", + "subject": "Example Brand", + "observed_at": "2026-09-03T12:03:00Z", + "conditions": { + "location": "France", + "competitor_count": 3, + "product_count": 3 + }, + "observation": "A structured brand-conditioned report was returned.", + "limitations": [ + "This prompted observation does not measure buyer-prompt presence or stable model knowledge." + ] + } + ], + "findings": [ + { + "id": "finding-three-reports", + "statement": "All three channels returned usable structured reports under identical settled conditions.", + "evidence_ids": [ + "evidence-chatgpt", + "evidence-gemini", + "evidence-perplexity" + ], + "limitations": [ + "The native scores are channel-specific and cannot be compared as one calibrated scale." + ] + } + ], + "disposition": "proceed", + "confidence": { + "level": "high", + "reasons": [ + "Each required channel produced one evidence-linked structured report under identical conditions." + ] + }, + "constraints": [ + "Generated source descriptors are not verified citations.", + "The snapshot does not measure buyer-prompt presence, trends, or business impact." + ], + "completion": { + "status": "complete" + }, + "verification": [ + { + "check": "Snapshot contract validation", + "status": "pass", + "evidence_ids": [ + "evidence-chatgpt", + "evidence-gemini", + "evidence-perplexity" + ] + } + ], + "framing": { + "suppliedSubject": "https://example.test", + "settledBrandName": "Example Brand", + "canonicalWebsite": "https://example.test", + "location": "France", + "suppliedCompetitorPanel": [ + { + "label": "Alpha", + "url": "https://alpha.example" + }, + { + "url": "https://beta.example" + }, + { + "label": "Gamma" + } + ], + "suppliedProductsOrServices": [ + "Analytics", + "Reporting", + "Monitoring" + ], + "limitations": [] + }, + "channel_observations": [ + { + "channel": "chatgpt", + "outcome": "report", + "recognitionStatus": "recognized", + "observedAt": "2026-09-03T12:01:00Z", + "evidenceIds": [ + "evidence-chatgpt" + ], + "conditions": { + "brand": "https://example.test", + "location": "France", + "competitors": [ + "https://alpha.example", + "https://beta.example", + "Gamma" + ], + "productsOrServices": [ + "Analytics", + "Reporting", + "Monitoring" + ] + }, + "report": { + "brandDescription": "A reporting and monitoring service.", + "awareness": { + "category": "emerging", + "information": "The generated report describes limited category awareness.", + "nativeScore": 0 + }, + "sentiment": { + "category": "neutral", + "information": "The generated report uses neutral language." + }, + "credibility": { + "category": "moderate", + "nativeScore": 0.5 + }, + "generatedTopics": [ + "reporting workflows" + ], + "generatedAssociations": [ + "operational dashboards" + ], + "sourceDescriptors": [ + "industry publications" + ], + "controlledCompetitorObservations": [ + { + "panelIndex": 0, + "observedName": "Alpha", + "observedUrl": "https://alpha.example", + "awareness": { + "category": "established", + "nativeScore": 0.7 + }, + "sentiment": { + "category": "positive", + "nativeScore": 0.6 + }, + "credibility": { + "category": "established", + "nativeScore": 0.8 + }, + "limitations": [] + } + ], + "generatedCompetitors": [ + { + "name": "Delta", + "url": "https://delta.example", + "awareness": { + "category": "moderate" + }, + "sentiment": { + "category": "neutral" + }, + "credibility": { + "category": "moderate" + }, + "limitations": [ + "This competitor was generated by the channel and was not part of the supplied panel." + ] + } + ] + }, + "qualityNotes": [], + "limitations": [ + "The missing sentiment score is unavailable rather than zero." + ] + }, + { + "channel": "gemini", + "outcome": "report", + "recognitionStatus": "recognized", + "observedAt": "2026-09-03T12:02:00Z", + "evidenceIds": [ + "evidence-gemini" + ], + "conditions": { + "brand": "https://example.test", + "location": "France", + "competitors": [ + "https://alpha.example", + "https://beta.example", + "Gamma" + ], + "productsOrServices": [ + "Analytics", + "Reporting", + "Monitoring" + ] + }, + "report": { + "brandDescription": "A reporting and monitoring service.", + "awareness": { + "category": "emerging", + "nativeScore": 0.2 + }, + "sentiment": { + "category": "neutral", + "nativeScore": 0.1 + }, + "credibility": { + "category": "moderate", + "nativeScore": 0.4 + }, + "generatedTopics": [ + "reporting workflows" + ], + "generatedAssociations": [ + "operational dashboards" + ], + "sourceDescriptors": [ + "industry publications" + ], + "controlledCompetitorObservations": [], + "generatedCompetitors": [] + }, + "qualityNotes": [], + "limitations": [] + }, + { + "channel": "perplexity", + "outcome": "report", + "recognitionStatus": "recognized", + "observedAt": "2026-09-03T12:03:00Z", + "evidenceIds": [ + "evidence-perplexity" + ], + "conditions": { + "brand": "https://example.test", + "location": "France", + "competitors": [ + "https://alpha.example", + "https://beta.example", + "Gamma" + ], + "productsOrServices": [ + "Analytics", + "Reporting", + "Monitoring" + ] + }, + "report": { + "brandDescription": "A reporting and monitoring service.", + "awareness": { + "category": "emerging", + "nativeScore": 0.3 + }, + "sentiment": { + "category": "neutral", + "nativeScore": 0.2 + }, + "credibility": { + "category": "moderate", + "nativeScore": 0.6 + }, + "generatedTopics": [ + "reporting workflows" + ], + "generatedAssociations": [ + "operational dashboards" + ], + "sourceDescriptors": [ + "unknown listing" + ], + "controlledCompetitorObservations": [], + "generatedCompetitors": [] + }, + "qualityNotes": [], + "limitations": [] + } + ], + "comparison": { + "channelsAttempted": [ + "chatgpt", + "gemini", + "perplexity" + ], + "usableReportChannels": [ + "chatgpt", + "gemini", + "perplexity" + ], + "completeness": "complete", + "supportedAgreements": [], + "supportedDifferences": [], + "excludedComparisonClaims": [ + "No universal visibility score or cross-channel score average was calculated.", + "No buyer-prompt presence, trend, or business-impact claim was made." + ], + "scoreComparisonPolicy": "Native scores are channel-specific and must not be averaged, normalized, or used to rank channels." + }, + "cost_summary": { + "unit": "test-unit", + "totalQuoted": 3, + "totalCharged": 3, + "channels": [ + { + "channel": "chatgpt", + "quotedAmount": 1, + "chargedAmount": 1 + }, + { + "channel": "gemini", + "quotedAmount": 1, + "chargedAmount": 1 + }, + { + "channel": "perplexity", + "quotedAmount": 1, + "chargedAmount": 1 + } + ] + } +} diff --git a/tests/fixtures/contracts/cases.json b/tests/fixtures/contracts/cases.json index a18772c..9bf385a 100644 --- a/tests/fixtures/contracts/cases.json +++ b/tests/fixtures/contracts/cases.json @@ -1,6 +1,1258 @@ { "schema_version": 1, "cases": [ + { + "id": "brand-snapshot-three-reports-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid" + }, + { + "id": "brand-snapshot-two-reports-one-not-recognized-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt", + "gemini" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + } + ] + }, + { + "id": "brand-snapshot-one-report-two-not-recognized-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + } + ] + }, + { + "id": "brand-snapshot-one-failed-channel-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "failed" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt", + "gemini" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + }, + { + "op": "set", + "path": "/completion/status", + "value": "incomplete" + }, + { + "op": "set", + "path": "/completion/stop_reason", + "value": "The planned Perplexity invocation failed after execution began and did not produce a valid domain outcome." + }, + { + "op": "set", + "path": "/cost_summary/channels/2/chargedAmount", + "value": 0.25 + }, + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 2.25 + } + ] + }, + { + "id": "brand-snapshot-only-one-valid-outcome-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "failed" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "failed" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "unavailable" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "defer" + }, + { + "op": "set", + "path": "/completion/status", + "value": "incomplete" + }, + { + "op": "set", + "path": "/completion/stop_reason", + "value": "Only one valid domain outcome remained after two planned channel invocations failed." + }, + { + "op": "set", + "path": "/cost_summary/channels/1/chargedAmount", + "value": 0 + }, + { + "op": "set", + "path": "/cost_summary/channels/2/chargedAmount", + "value": 0 + }, + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 1 + } + ] + }, + { + "id": "brand-snapshot-no-usable-reports-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/0/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/0/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/0/report" + }, + { + "op": "set", + "path": "/channel_observations/0/limitations", + "value": [ + "This valid negative outcome supplies no report categories or scores." + ] + }, + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + } + ] + }, + { + "id": "brand-snapshot-preflight-unavailable-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/evidence", + "value": [] + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/verification/0/status", + "value": "unavailable" + }, + { + "op": "delete", + "path": "/verification/0/evidence_ids" + }, + { + "op": "set", + "path": "/channel_observations/0/outcome", + "value": "unavailable" + }, + { + "op": "set", + "path": "/channel_observations/0/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "set", + "path": "/channel_observations/0/observedAt", + "value": null + }, + { + "op": "set", + "path": "/channel_observations/0/evidenceIds", + "value": [] + }, + { + "op": "delete", + "path": "/channel_observations/0/report" + }, + { + "op": "set", + "path": "/channel_observations/0/limitations", + "value": [ + "The required channel capability was unavailable before execution." + ] + }, + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_attempted" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "set", + "path": "/channel_observations/1/observedAt", + "value": null + }, + { + "op": "set", + "path": "/channel_observations/1/evidenceIds", + "value": [] + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "not_attempted" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "set", + "path": "/channel_observations/2/observedAt", + "value": null + }, + { + "op": "set", + "path": "/channel_observations/2/evidenceIds", + "value": [] + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/channelsAttempted", + "value": [] + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "unavailable" + }, + { + "op": "set", + "path": "/disposition", + "value": "defer" + }, + { + "op": "set", + "path": "/completion/status", + "value": "incomplete" + }, + { + "op": "set", + "path": "/completion/stop_reason", + "value": "A required channel capability was unavailable at preflight, so no channel execution began." + }, + { + "op": "set", + "path": "/cost_summary/totalQuoted", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 0 + }, + { + "op": "set", + "path": "/cost_summary/channels/0/quotedAmount", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/channels/0/chargedAmount", + "value": 0 + }, + { + "op": "set", + "path": "/cost_summary/channels/1/quotedAmount", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/channels/1/chargedAmount", + "value": 0 + }, + { + "op": "set", + "path": "/cost_summary/channels/2/quotedAmount", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/channels/2/chargedAmount", + "value": 0 + } + ] + }, + { + "id": "brand-snapshot-unavailable-mixed-with-attempt-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "unavailable preflight outcome requires zero attempted channel outcomes", + "mutations": [ + { + "op": "delete", + "path": "/evidence/0" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/verification/0/evidence_ids", + "value": [ + "evidence-gemini", + "evidence-perplexity" + ] + }, + { + "op": "set", + "path": "/channel_observations/0/outcome", + "value": "unavailable" + }, + { + "op": "set", + "path": "/channel_observations/0/recognitionStatus", + "value": "indeterminate" + }, + { + "op": "set", + "path": "/channel_observations/0/observedAt", + "value": null + }, + { + "op": "set", + "path": "/channel_observations/0/evidenceIds", + "value": [] + }, + { + "op": "delete", + "path": "/channel_observations/0/report" + }, + { + "op": "set", + "path": "/channel_observations/0/limitations", + "value": [ + "The required channel capability was unavailable at preflight." + ] + }, + { + "op": "set", + "path": "/comparison/channelsAttempted", + "value": [ + "gemini", + "perplexity" + ] + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "gemini", + "perplexity" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + }, + { + "op": "set", + "path": "/completion/status", + "value": "incomplete" + }, + { + "op": "set", + "path": "/completion/stop_reason", + "value": "A required capability was unavailable at preflight, but channel observations were incorrectly attempted." + }, + { + "op": "set", + "path": "/cost_summary/totalQuoted", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 2 + }, + { + "op": "set", + "path": "/cost_summary/channels/0/quotedAmount", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/channels/0/chargedAmount", + "value": 0 + } + ] + }, + { + "id": "brand-snapshot-rejected-ambiguous-subject-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-rejected.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid" + }, + { + "id": "brand-snapshot-controlled-competitors-separated-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid" + }, + { + "id": "brand-snapshot-supplied-products-separated-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid" + }, + { + "id": "brand-snapshot-zero-and-missing-scores-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid" + }, + { + "id": "brand-snapshot-quality-note-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/2/qualityNotes", + "value": [ + "One generated phrase uses mixed languages and one source descriptor appears placeholder-like; the underlying evidence is preserved without translation or rewriting." + ] + } + ] + }, + { + "id": "brand-snapshot-zero-gap-comparison-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid" + }, + { + "id": "brand-snapshot-supported-comparison-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "append", + "path": "/comparison/supportedAgreements", + "value": { + "kind": "category", + "statement": "ChatGPT and Gemini generated the same awareness category.", + "evidenceIds": [ + "evidence-chatgpt", + "evidence-gemini" + ] + } + }, + { + "op": "append", + "path": "/comparison/supportedDifferences", + "value": { + "kind": "competitor", + "statement": "Only the ChatGPT report generated an additional competitor in this snapshot.", + "evidenceIds": [ + "evidence-chatgpt", + "evidence-perplexity" + ] + } + } + ] + }, + { + "id": "brand-snapshot-recognition-comparison-valid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "valid", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt", + "perplexity" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + }, + { + "op": "append", + "path": "/comparison/supportedDifferences", + "value": { + "kind": "recognition_status", + "statement": "ChatGPT returned a report while Gemini returned not recognized for this invocation.", + "evidenceIds": [ + "evidence-chatgpt", + "evidence-gemini" + ] + } + } + ] + }, + { + "id": "brand-snapshot-recognition-comparison-evidence-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must link evidence from at least two valid-outcome channel rows", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt", + "perplexity" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + }, + { + "op": "append", + "path": "/comparison/supportedDifferences", + "value": { + "kind": "recognition_status", + "statement": "A recognition comparison cites only the ChatGPT outcome.", + "evidenceIds": [ + "evidence-chatgpt" + ] + } + } + ] + }, + { + "id": "brand-snapshot-single-channel-comparison-evidence-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must link evidence from at least two report channel rows", + "mutations": [ + { + "op": "append", + "path": "/comparison/supportedAgreements", + "value": { + "kind": "category", + "statement": "A comparison claim cites only the ChatGPT observation.", + "evidenceIds": [ + "evidence-chatgpt" + ] + } + } + ] + }, + { + "id": "brand-snapshot-wrong-outcome-comparison-evidence-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must link evidence from at least two report channel rows", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt", + "perplexity" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + }, + { + "op": "append", + "path": "/comparison/supportedDifferences", + "value": { + "kind": "description", + "statement": "A report-content comparison cites one report and one not-recognized outcome.", + "evidenceIds": [ + "evidence-chatgpt", + "evidence-gemini" + ] + } + } + ] + }, + { + "id": "brand-snapshot-one-report-content-comparison-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "report-content comparison kind category requires at least two usable reports", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/1/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/1/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/1/report" + }, + { + "op": "set", + "path": "/channel_observations/2/outcome", + "value": "not_recognized" + }, + { + "op": "set", + "path": "/channel_observations/2/recognitionStatus", + "value": "not_recognized" + }, + { + "op": "delete", + "path": "/channel_observations/2/report" + }, + { + "op": "set", + "path": "/comparison/usableReportChannels", + "value": [ + "chatgpt" + ] + }, + { + "op": "set", + "path": "/comparison/completeness", + "value": "partial" + }, + { + "op": "set", + "path": "/findings", + "value": [] + }, + { + "op": "set", + "path": "/disposition", + "value": "conditional" + }, + { + "op": "append", + "path": "/comparison/supportedAgreements", + "value": { + "kind": "category", + "statement": "A category comparison was attempted without two reports.", + "evidenceIds": [ + "evidence-chatgpt", + "evidence-gemini" + ] + } + } + ] + }, + { + "id": "brand-snapshot-duplicate-channel-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must contain channel gemini exactly once", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/2/channel", + "value": "gemini" + } + ] + }, + { + "id": "brand-snapshot-missing-channel-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must NOT have fewer than 3 items", + "mutations": [ + { + "op": "delete", + "path": "/channel_observations/2" + } + ] + }, + { + "id": "brand-snapshot-universal-score-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "/comparison must NOT have additional properties", + "mutations": [ + { + "op": "set", + "path": "/comparison/universalVisibilityScore", + "value": 0.5 + } + ] + }, + { + "id": "brand-snapshot-averaged-score-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "/comparison must NOT have additional properties", + "mutations": [ + { + "op": "set", + "path": "/comparison/averageNativeScore", + "value": 0.5 + } + ] + }, + { + "id": "brand-snapshot-unresolved-evidence-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "references unknown evidence id evidence-missing", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/0/evidenceIds/0", + "value": "evidence-missing" + } + ] + }, + { + "id": "brand-snapshot-inconsistent-conditions-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must use conditions identical to every other channel", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/2/conditions/location", + "value": "Canada" + } + ] + }, + { + "id": "brand-snapshot-controlled-panel-reference-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "references controlled competitor panelIndex 3 outside the supplied panel", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/0/report/controlledCompetitorObservations/0/panelIndex", + "value": 3 + } + ] + }, + { + "id": "brand-snapshot-charged-total-mismatch-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "totalCharged must equal the sum", + "mutations": [ + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 2.5 + } + ] + }, + { + "id": "brand-snapshot-quoted-total-mismatch-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "totalQuoted must equal the sum", + "mutations": [ + { + "op": "set", + "path": "/cost_summary/totalQuoted", + "value": 2.5 + } + ] + }, + { + "id": "brand-snapshot-attempted-null-quote-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "may be null only when no quote was reached", + "mutations": [ + { + "op": "set", + "path": "/cost_summary/channels/0/quotedAmount", + "value": null + }, + { + "op": "set", + "path": "/cost_summary/totalQuoted", + "value": null + } + ] + }, + { + "id": "brand-snapshot-attempted-null-observed-at-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must be string", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/0/observedAt", + "value": null + } + ] + }, + { + "id": "brand-snapshot-not-attempted-observed-at-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-rejected.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must be null", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/0/observedAt", + "value": "2026-09-03T12:01:00Z" + } + ] + }, + { + "id": "brand-snapshot-not-attempted-charge-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-rejected.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must be zero for outcome not_attempted", + "mutations": [ + { + "op": "set", + "path": "/cost_summary/channels/0/chargedAmount", + "value": 0.5 + }, + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 0.5 + } + ] + }, + { + "id": "brand-snapshot-unavailable-charge-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-rejected.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "must be zero for outcome unavailable", + "mutations": [ + { + "op": "set", + "path": "/channel_observations/0/outcome", + "value": "unavailable" + }, + { + "op": "set", + "path": "/disposition", + "value": "defer" + }, + { + "op": "set", + "path": "/cost_summary/channels/0/chargedAmount", + "value": 0.5 + }, + { + "op": "set", + "path": "/cost_summary/totalCharged", + "value": 0.5 + } + ] + }, + { + "id": "brand-snapshot-disposition-mismatch-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "disposition must be proceed", + "mutations": [ + { + "op": "set", + "path": "/disposition", + "value": "conditional" + } + ] + }, + { + "id": "brand-snapshot-completion-mismatch-invalid", + "kind": "result", + "path": "tests/fixtures/contracts/brand-snapshot-three-reports.json", + "contract_id": "ai-brand-representation-snapshot/v1", + "expected": "invalid", + "expected_error": "completion.status must be complete", + "mutations": [ + { + "op": "set", + "path": "/completion/status", + "value": "incomplete" + }, + { + "op": "set", + "path": "/completion/stop_reason", + "value": "Synthetic invalid completion state." + } + ] + }, { "id": "content-question-review-valid", "kind": "result",