Skip to content

feat(spec): a skill trigger condition's value must have the shape its operator reads (#7113) - #7212

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7113-skill-trigger-value
Aug 10, 2026
Merged

feat(spec): a skill trigger condition's value must have the shape its operator reads (#7113)#7212
os-zhuang merged 1 commit into
mainfrom
claude/issue-7113-skill-trigger-value

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7113.

SkillTriggerConditionSchema declared operator and value independently, so every operator accepted every shape. { field: 'userRole', operator: 'in', value: 'admin' } — a membership test whose list is not a list — was spec-valid. This is the dormant twin of #6227 on ViewFilterRuleSchema, and the fix mirrors PR #7114 key for key.

Phase 1 — the authorship census (the gate), in full

The PM's ruling was premise-gated: land option A only if no real (non-test) author writes the scalar-on-set-operator form; if any does, stop and report the migration size instead. Measured before writing any code.

This repo — git grep -n "triggerConditions" -- packages examples content docs

28 files, every hit classified:

class count detail
Schema / contract source 5 spec/src/ai/skill.zod.ts, skill.form.ts, stack.zod.ts, conversions/registry.ts, migrations/registry.ts
Generated baselines & ledgers 6 authorable-surface{,.base}.json, liveness/skill.json, liveness/README.md, undrilled-containers.baseline.json, spec-changes.json
Docs / ADR / audits / QA checklist 9 content/docs/ai/*, content/docs/references/ai/skill.mdx, docs/adr/0040, docs/audits/*, docs/qa/platform-checklist/areas/ai.json, docs/protocol-upgrade-guide.md
CHANGELOGs (historical prose) 3 spec, runtime, mcp
i18n form labels (generated) 4 platform-objects/.../translations/*.metadata-forms.generated.ts — label strings, not authored values
Tests 1 spec/src/ai/skill.test.ts

Real authored metadata carrying triggerConditions: zero. No examples/ app, no seed, no fixture declares one.

The cloud repo — cloned read-only at 485cbd3

Full git grep for triggerConditions returns 5 files; trigger_conditions (a persisted snake_case column) returns none.

site shape verdict
content/docs/ai/extending-with-skills.mdx:25 { field: 'objectName', operator: 'in', value: ['order','refund'] } docs example — array form, already compliant
packages/service-ai/src/__tests__/skill-registry.test.ts:94 operator: 'eq', value: 'lead' test — compliant
packages/service-ai/src/__tests__/skill-registry.test.ts:106 operator: 'in', value: ['lead','opportunity'] test — compliant
packages/service-ai/src/skill-registry.ts, agent-runtime.ts, service-ai-studio/.../solution-design-guardrail.test.ts consumer + prose not authorship

Every real skill definition in the cloud repo was inspected directly — service-ai/src/skills/schema-reader-skill.ts and service-ai-studio/src/skills/{actions-executor,builder-handoff,data-explorer,metadata-authoring,solution-design}-skill.ts. None of the six declares triggerConditions at all.

Gate verdict: CLEAR

No real (non-test) authorship of the scalar form exists in either repo. Migration size: zero source sites. One in-repo test handed a scalar to all five operators — it was asserting the decoupling itself, and is updated here to enumerate the shape each operator reads.

Not measurable from either repo, and flagged as such in the cloud follow-up: whether any already-persisted tenant skill metadata carries the scalar form.

Phase 2 — the change

file change
packages/spec/src/ai/skill.zod.ts Exported SKILL_TRIGGER_LIST_VALUE_OPERATORS / SKILL_TRIGGER_SCALAR_VALUE_OPERATORS; added checkSkillTriggerConditionValueShape + .superRefine(...) on SkillTriggerConditionSchema; TSDoc stating the constraint, citing #7113 and the #6227 precedent, and naming the cloud consumer's coercion as the thing that becomes a no-op
packages/spec/src/ai/skill-trigger-condition-value-shape.test.ts new — 20 pins (accept + reject), each rejection asserting issue code and path
packages/spec/src/ai/skill.test.ts should accept all operators now enumerates from SKILL_TRIGGER_LIST_VALUE_OPERATORS instead of handing a scalar to all five
packages/spec/api-surface/ai.json, packages/spec/export-origins/ai.json dual snapshots regenerated after a real build (+2 exports each)
.changeset/skill-trigger-condition-value-shaped-by-operator.md one changeset, minor

The constraint and its deliberate limit:

operator value must be why
in / not_in an array, any length the consumer answers them with list.includes(fieldValue) — the authored value is the list
eq / neq a string === / !== on an array is reference identity, so an array comparand is a dead predicate — eq never fires, neq always does
contains unchanged — either shape see the deviation below

No content/docs/releases/ edit, no docs/adr/** edit.

⚠️ Deviation from the dispatch, for review

The dispatch said eq / neq / containsz.string(). contains is left accepting both shapes. Measured in the consumer rather than assumed — SkillRegistry.evaluateCondition has two live contains branches, not one:

case 'contains': {
  if (typeof fieldValue === 'string' && typeof expected === 'string') return fieldValue.includes(expected);
  if (Array.isArray(fieldValue)) {
    return Array.isArray(expected) ? expected.every((v) => fieldValue.includes(v)) : fieldValue.includes(expected as string);
  }
  return false;
}

The array⊆array subset test is real code, and SkillContext is indexed [extraField: string]: unknown, so an array-valued context field is a shape the runtime is deliberately written for. Constraining contains to a string would make the schema stricter than its runtime — the #5685 error that PR #7114's own pins hold the line against ("it refuses NOTHING ELSE"), and un-declaring a working capability is an ADR-0049 retirement decision, not a rider on a shape fix. in / not_in / eq / neq have no such branch, which is why they are constrained and contains is not.

This is the item to overrule if the PM disagrees — it is a one-line change (add 'contains' to SKILL_TRIGGER_SCALAR_VALUE_OPERATORS) plus flipping the two contains accept pins.

Reverse verification — direction predicted first

Predictions were written down before the measurement. Method: detach only .superRefine(...) from the schema (keeping the exported vocabularies, so the test file still imports on the pre-change shape) and re-run — this isolates the behavior change rather than the module surface.

pin predicted measured
card example: in + scalar refused RED pre-change ✅ RED
refusal names the consumer coercion RED pre-change ✅ RED
in refuses a scalar RED pre-change ✅ RED
not_in refuses a scalar RED pre-change ✅ RED
eq refuses an array RED pre-change ✅ RED
neq refuses an array RED pre-change ✅ RED
nested carrier path triggerConditions.0.value RED pre-change ✅ RED
in / not_in accept an array GREEN both sides ✅ GREEN both
in / not_in accept [] (a real predicate) GREEN both sides ✅ GREEN both
eq / neq accept a string GREEN both sides ✅ GREEN both
contains accepts a string and an array GREEN both sides ✅ GREEN both
vocabulary membership + disjointness pins GREEN both sides ✅ GREEN both
missing value reports one non-custom issue GREEN both sides ✅ GREEN both

7 predicted RED, 7 measured RED — exactly the predicted set, no others. All accept pins green on both sides, which is the half that holds the contains decision and the empty-list allowance from regressing.

One prediction was wrong and the pin was rewritten rather than the code: a missing value on a list operator does not produce the refinement's issue — Zod 4 skips a superRefine when the object's own shape already failed, so only the required-check issue fires. The pin now records that measured behavior (expect(atValue[0].code).not.toBe('custom')) instead of the assumption.

Gates

Build ran before any dist-derived regen (the stale-dist trap, #7122), and the dual-snapshot rule was honored — both api-surface/ and export-origins/ regenerated after the real build, since the two vocabularies are new public exports.

gate result
pnpm --filter @objectstack/spec build ✅ clean (run first, twice — once more after the docblock fix below)
gen:api-surfacegen:export-originsgen:skill-refsgen:docs ✅ regenerated post-build
check:generated all 11 artifacts up to date
pnpm --filter @objectstack/spec test 9402 passed / 9402, 360 files
typecheck (check:scripts-typecheck + check:test-typecheck) ✅ passed; debt file unchanged — the new test file contributes 0 errors
check-adr-0087-registration ✅ no declared-breaking changeset

Special inspection items for the PM

  1. The contains deviation above — the one substantive call that differs from the dispatch.
  2. A docblock-position trap, found by the generator. build-docs.ts takes the first docblock in a file as the module blurb. Adding the vocabulary docs displaced the "Skill Trigger Condition Schema" summary and rewrote content/docs/references/ai/skill.mdx + skills/objectstack-ai/references/_index.md with a truncated sentence. Fixed by keeping the module summary first (it sits above the imports — pre-existing, and now load-bearing enough to carry an explicit warning comment). Both regenerated docs are byte-identical to main in the final tree; the fix is worth knowing about because it will bite the next person who adds a leading export to a .zod.ts.
  3. The changeset is minor, not major. It is a tightening of an authorable surface, which is breaking in principle — justified as non-major by the census finding zero real authored sites. If the release line disagrees, that is a changeset edit, not a code change.
  4. SkillSchema is imported at the top of the new test file rather than lazily, and one pin parses a whole Skill — that is deliberate, proving the refinement travels with the z.array(SkillTriggerConditionSchema) carrier and reports at triggerConditions.0.value.
  5. Cloud follow-up filed: objectstack-ai/cloud#1223 — unassigned, finding label only, no pm labels. It covers removing evaluateCondition's scalar coercion once this ships, and explicitly flags the one thing neither repo can measure: whether persisted tenant skill metadata carries the scalar form.

Auto-merge deliberately not enabled — the PM lands serially.


Generated by Claude Code

…ts operator reads (#7113)

`SkillTriggerConditionSchema.operator` and `.value` were declared independently,
so every operator accepted every shape: `{ operator: 'in', value: 'admin' }` — a
membership test whose list is not a list — was spec-valid. The dormant twin of
#6227 on `ViewFilterRuleSchema`; the fix mirrors that one (PR #7114) key for key.

Dormant is the point: the sole consumer (`SkillRegistry.evaluateCondition`,
cloud agent runtime) coerces the scalar itself, so nothing ever failed. What is
closed is a second dialect — a consumer-side lenient coercion standing in for a
contract the producer never declared. That coercion becomes a no-op here;
removing it is a producer-first follow-up in the cloud repo.

- `in` / `not_in` (SKILL_TRIGGER_LIST_VALUE_OPERATORS) require an array.
- `eq` / `neq` (SKILL_TRIGGER_SCALAR_VALUE_OPERATORS) require a string — `===`
  on an array is reference identity, so an array comparand is a dead predicate.
- `contains` is deliberately unchanged: it has two live branches (string
  substring, array subset), and #5685 rules against a schema stricter than its
  runtime.

Both vocabularies are exported so producers enumerate from the contract.
Authoring impact censused first across this repo and the cloud repo: no real
skill authors the scalar-on-set-operator form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCb6mPxnEjvhKnnka1RNxw
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 2:54am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ai tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 03:34
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit dc61def Aug 10, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7113-skill-trigger-value branch August 10, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ai size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding][spec] SkillTriggerConditionSchema.value is not operator-constrained either — the consumer coerces instead (dormant twin of #6227)

2 participants