Skip to content

bindings.pattern is typed as a glob and implemented as one only in Python; Rust and TypeScript use two different suffix matches that agree with it, and with each other, on the default alone #116

Description

@tercel

Summary

§9.1.1 types bindings.pattern as a glob, and §5.12.6 clause 1 makes matching against it a MUST. Only apcore-python implements a glob. apcore-rust and apcore-typescript implement two different suffix matches specialised for the default value, and neither is a subset of the other — so all three agree on "*.binding.yaml" and pairwise disagree on almost everything else.

Nothing is broken today, because the only value in use is the default. That is what makes it worth filing now rather than after a configuration declares something else.

This is the shape of #112 and of aiperceivable/apcore-toolkit#18: every implementation agrees with every other on the one value anyone has tried, and all three agree with a specification that does not describe what they do.

Where the spec says "glob"

Location Text
§9.1.1 (protocol-spec.md:5771) bindings.patternglob pattern … Binding file matching pattern
§9.2.1 (protocol-spec.md:5828) "It is a glob matched against filenames within bindings.dir"
schemas/defaults.schema.json:176 "Default glob binding files must match within dir"
§5.12.6 clause 1 (protocol-spec.md:2998) "it MUST match candidate files in that directory against bindings.pattern"

The three implementations

SDK Site Predicate
Python src/apcore/bindings.py:303 sorted(p.glob(resolved_pattern)) — a real glob
Rust src/bindings.rs:589 pattern.strip_prefix('*').unwrap_or(pattern), then name.ends_with(suffix)
TypeScript src/bindings.ts:225 actualPattern.replace('*', ''), then f.endsWith(suffix)

The TypeScript site carries the comment // Simple glob matching for *.binding.yaml, which states the scope accurately: it was written for the default, not for the type the spec declares.

Measured divergence

Directory contents: a.binding.yaml, api-v1.binding.yaml, data1.yaml, users.binding.yaml, zab.yaml.

Each column was produced by executing that SDK's own predicate in its own runtime — Python by calling pathlib.Path.glob (which is line 303), Rust and TypeScript by transcribing the filter expressions verbatim and running them under rustc and node, so strip_prefix vs replace semantics are exercised rather than assumed.

bindings.pattern Python Rust TypeScript Agree?
*.binding.yaml (default) 3 files 3 files 3 files
data*.yaml data1.yaml
api-*.binding.yaml api-v1.binding.yaml
?.binding.yaml a.binding.yaml
[ab].binding.yaml a.binding.yaml
a*b.yaml zab.yaml

Two rows are worth reading closely.

data*.yaml is the ordinary case — a glob with the star anywhere but the front. Python matches; the other two cannot, because a pattern that does not begin with * survives both transformations unchanged and is then compared with ends_with, which no filename satisfies.

a*b.yamlzab.yaml is the one that shows Rust and TypeScript also disagree with each other, and that TypeScript's result is wrong rather than merely narrow. strip_prefix('*') removes a star only at position 0, so Rust keeps a*b.yaml and matches nothing. replace('*', '') removes the first star wherever it sits, so TypeScript reduces the pattern to ab.yaml and matches zab.yaml — a file both other SDKs reject, and one a glob cannot match at all, since a*b.yaml is anchored at the start.

So the divergence is not "one strict implementation and two lenient ones". It is three implementations with no ordering between them.

Why the conformance corpus does not catch it

conformance/fixtures/bindings_dir_resolution.json is not silent on pattern — it has two cases, and one of them (config_file_pattern_is_honoured) deliberately uses a non-default value with a distinct-module-id decoy, precisely so that an implementation ignoring the configured pattern is visible.

The value it uses is *.bind.yaml. Running the fixture's own scenario through all three predicates:

files: greet.bind.yaml, decoy.binding.yaml     pattern: *.bind.yaml
Python : ['greet.bind.yaml']
Rust   : ["greet.bind.yaml"]
TypeScript: ["greet.bind.yaml"]

*.bind.yaml is leading-star-plus-literal-suffix — the one family on which a glob, a leading-star strip, and a first-star removal all coincide. The case is well constructed for the question it asks (is the configured pattern honoured at all?) and cannot answer the question this issue raises (does honouring it mean the same thing in three SDKs?).

That is the more useful finding here: the gap is not missing coverage, it is coverage whose only non-default value is drawn from the family where the implementations converge.

Relationship to #114

#114 closed the "key reaches no consumer" half and noted in passing that "bindings.pattern is in the same position — the pattern default lives in the loader signature rather than being read from config." Resolving that is what makes this reachable: before it, no configured pattern arrived at the predicate, so the predicate's semantics could not matter. Now load_binding_dir_with_config resolves the key in all three SDKs and hands it to three different matchers.

Proposed resolution

Two decisions, in order.

1. Decide what bindings.pattern actually is. The spec says "glob" in three places, but the only implementation that delivers one is the one whose standard library provides it free. Two options, and the second is the one I would take:

  • (a) Full glob everywhere. Matches what the spec already claims. Costs Rust and TypeScript a real matcher, including the parts languages disagree on most — character classes ([a-z], and the two incompatible negation spellings [!x] / [^x]), and whether ? and * cross a .. That is a lot of surface for a key whose realistic use is picking a filename suffix.
  • (b) Specify a named subset and stop calling it a glob. * and ? only; every other character literal, [ and { included. Roughly twenty lines per SDK, no dependency, and it removes the four rows above at once because there is nothing left to interpret differently.

2. Pin it with fixture cases that can fail. Whichever is chosen, bindings_dir_resolution.json needs at least one pattern from outside the leading-star family — data*.yaml alone separates all three implementations today. A case per divergent row above would be better.

Worth stating explicitly, since it is easy to add a case that looks like coverage and is not: a case is only useful here if it fails against at least one current implementation. *.bind.yaml passes against all three.

Related

apcore-toolkit hit the same gap one layer down (aiperceivable/apcore-toolkit#18: its BindingLoader.load had no pattern parameter at all). It resolved it by specifying option (b) — * and ? only, matched against the filename, with the matching algorithm written into the spec as pseudocode rather than only the syntax — and pinning it with a 43-case fixture. It deliberately did not inherit apcore's behaviour, because there was no single behaviour to inherit. If apcore takes (b), the two layers converge; if it takes (a), the toolkit's narrower subset stays a documented, deliberate difference rather than an accidental one.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions