Skip to content

Fix #134: make xpathExpression values self-contained for namespace/ve… - #135

Open
humantypo wants to merge 1 commit into
mainfrom
issue-134-xpath-namespace-self-containment
Open

Fix #134: make xpathExpression values self-contained for namespace/ve…#135
humantypo wants to merge 1 commit into
mainfrom
issue-134-xpath-namespace-self-containment

Conversation

@humantypo

Copy link
Copy Markdown
Contributor

Notes

There are number of decision points associated with this issue so I wanted to try to capture the context of them in a way that will hopefully make it easier to track why it was done in this manner (and why not one of the listed alternatives).

1. Fix direction: self-containment, not translator-side rewriting

Chosen: adopt Steven's self-containment direction.

Rejected alternative: keep the single global Namespace table, and instead specify how a translator converting XML → JACAL/YACAL must detect prefix collisions and rewrite one side's prefixes to avoid them.

Why: Steven's own text calls conflict-detection-and-rewriting non-trivial, especially once the default namespace (empty prefix) is in play. Every downstream translator would have to reimplement that logic correctly. Self-containment pushes the same complexity onto the producer, once, at the point where the XML's actual in-scope namespaces are already known. It's cheaper and more reliable than asking every consumer to reconstruct scope information that XML's structure already had and JACAL/YACAL's flat structure throws away.

2. Scope: all three surfaces in one pass

Chosen: xpathExpression values, XPathAttributeSelectorType/XPathEntityAttributeSelectorType, and response Notice values all get the fix together.

Rejected alternative: fix request-side xpathExpression values now (what Steven's worked example shows concretely), defer selectors and response notices to follow-up issues.

Why: all three are the same underlying defect: a namespace-context resolution mechanism that doesn't travel with the expression it governs. Leaving any one out reintroduces the exact ambiguity being fixed for that surface, and the response case doesn't even have a broken fallback today —
there is no mechanism there at all.

3. Namespace shape: prefix→URI mapping, applied consistently everywhere (not just new sites)

Chosen: redefine Namespace as a JSON object / YAML mapping directly from prefix string to URI string ({"ns": "urn:example:resource"}), and apply that shape everywhere the concept appears, including the pre-existing XPathRequestDefaults/XPathPolicyDefaults usage, not only the new per-value sites.

Rejected alternatives:

  • Reuse the existing array-of-{Prefix,Name}-objects shape verbatim at the new sites. Rejected:
    fine for a table that appears once per document; repeated at every xpathExpression value, selector, and response notice it is materially more verbose than necessary in both JSON and YAML, and this is exactly what triggered the original "awkward for YAML" observation that started this review.
  • New mapping shape only at the new per-value sites, old array shape left alone in XPathRequestDefaults/XPathPolicyDefaults. Rejected: would leave two different encodings of the same concept live in the same profile at the same time, for no reason once the cutover (decision 6) is clean rather than phased.

Effect: prefix uniqueness becomes structural (an object/mapping cannot have two identical keys in the data model) rather than a bolted-on isUnique(Prefix) constraint the JSON schema could never actually enforce. Still needs the duplicate-serialized-key caveat (see decision 8).

4. Local context: full replacement, not merge with Defaults

Chosen: when a local Namespace is present, it is the entire effective namespace context. Nothing is inherited from XPathRequestDefaults/XPathPolicyDefaults.

Rejected alternative: merge semantics: local entries override same-prefix Defaults entries, everything else still inherited from Defaults. This does not actually reintroduce Steven's original cross-value ambiguity (each va lue's effective context would still be fully determined), and it
reduces duplication for documents where most namespaces are shared.

Why full replace won this: merge keeps a value's meaning dependent on its container. That is exactly the property self-containment exists to remove. It breaks the moment a value is copied into a response, cached, compared, or moved between policies, since a merged value's true meaning
requires re-resolving against whatever Defaults happened to be in scope at the copy site. Implementations would need to snapshot the merged context at every scope boundary a value might cross, which is real complexity for a problem (verbosity) that's smaller than the problem it's avoiding (context-dependent meaning). Namespace blocks are typically small; if authoring convenience matters, that belongs in tooling/templates, not wire semantics.

Effect: Namespace: {} is a meaningful, distinct state — "this expression deliberately uses no prefixed names" — not the same as "inherit from Defaults." (This distinction becomes moot once Defaults-level Namespace is deleted outright per decision 6, but the requiredness question survives it (see decision 9).

Explicit exclusion from "complete context": bindings XPath/XML defines implicitly (via the reserved xml prefix) are not required in the local map. Everything else needed to preserve the original XML expression's static context must be local.

5. XPathVersion scope: intrinsic to the value type, centralized for selectors — in every representation, including XML

Chosen: XPathVersion becomes a required, local property of XPathExpressionValueType, in XML as well as JSON/YAML, a xpathExpression value's version was previously always resolved from XPathRequestDefaults/XPathPolicyDefaults in every representation, since XPathExpressionValueType's abstract UML never had a version property of its own at all before this change. It travels with the value everywhere the value can go, symmetrically with Namespace. Selector types (XPathAttributeSelectorType/XPathEntityAttributeSelectorType) keep a centralized, policy-wide version from XPathPolicyDefaults, in every representation. There is no local override.

Rejected alternatives:

  • Leave XPathVersion centralized for everything, add it locally only where nothing else exists to carry it (i.e. only on response Notice values). Rejected: two properties on the same value type (Namespace, XPathVersion) would follow different scoping rules with no principled reason for the asymmetry once one of them (a value crossing into a response) needs the local treatment anyway.
  • Give selectors the same local-override capability as values. Rejected: this would let one policy mix XPath dialects across different attribute selectors, which is a materially larger semantic change ACAL has never asked for, complicates cross-representation validation and XPath-engine configuration, and does nothing to address Steven's actual namespace problem.
  • Treat XML as exempt from this decision, since it has native namespace syntax. Rejected: the profile's own canonical XML example (examples/acal-xpath/Request.xml) declared xmlns:md on RequestEntity — an ancestor of the Value element, not the Value element itself — which is exactly Steven's scoping defect. XML's native xmlns:* syntax made the gap easy to miss, not exempt from it: a value can be copied into a response, cached, or moved into another policy in any representation, none of which carry the original ancestor/Defaults context.

Why the value/selector split holds, in every representation: XPathExpressionValueType is a first-class value. It can move from a request or policy into a response, notice, cache, or another processing stage, so both its version and its namespace context must travel with it or it stops being self-describing the moment it moves — in XML this means xmlns:* and a new XPathVersion attribute declared directly on the Value element itself, not inherited from RequestEntity or any other ancestor (xs:anyAttribute on Core's ValueType already permits this, so no XSD complex-type addition was needed, only prose). A selector's Path is policy syntax — it never escapes the policy it's written in, in any representation, so a single policy-wide XPath version continues to serve its actual use; XML selectors keep the pre-existing ancestor-based in-scope-namespace lookup unchanged, and only JSON/YAML selectors gained the new Namespace property, because only JSON/YAML lacked a native way to express it at all.

Effect: producers copying an xpathExpression value into a response must materialize its effective version and namespace context at copy time, in every representation — this is now a real, statable producer obligation rather than an unaddressed gap. XPathRequestDefaultsType's removal (decision 7) follows the same representation-independent logic: the XSD's XPathRequestDefaults substitution-group element was removed too, since nothing was left consuming it once request-side values became self-contained.

6. Compatibility strategy: clean cutover, not phased legacy fallback

Chosen: remove Namespace from XPathRequestDefaultsType/XPathPolicyDefaultsType outright, in this same change. Local Namespace/XPathVersion become required immediately. No SHOULD-now/MUST-later staging, no legacy-fallback-to-Defaults path.

Rejected alternative: a phased rollout — keep Defaults-level Namespace alive as a working legacy fallback, introduce local Namespace/XPathVersion as SHOULD in this stage, strengthen to MUST in a later one. This was the initial framing and was reconsidered once the document's actual
publication stage was checked.

Why: acal-xpath-v1.0.md is CSD02 — a public Committee Specification Draft, explicitly a revisable stage, not a stable release. A defect this fresh is cheap to correct now and gets more expensive to correct at every later stage. Carrying two coexisting namespace-resolution mechanisms through an indefinite transition period buys protection for implementers the TC has no evidence exist yet, at the cost of permanent implementation and specification complexity.

7. XPathRequestDefaultsType: deleted outright, not left vestigial

Chosen: delete XPathRequestDefaultsType, its Tree wrapper in the JSON/YAML schemas, and its extension point on RequestType, entirely.

Rejected alternative: keep an empty/reserved XPathRequestDefaultsType around for symmetry with XPathPolicyDefaultsType, which survives (as XPathVersion-only — selectors still need it).

Why: this is a direct structural consequence of decisions 5 and 6, not a separately-argued choice. XPathRequestDefaultsType currently has exactly two properties, XPathVersion and Namespace. Selectors (the only thing that still needs a centralized version) are policy-only, served by XPathPolicyDefaults. Once request-side xpathExpression values carry both properties locally, nothing in a request consumes XPathRequestDefaultsType for anything. An empty extension point kept only for symmetry has no function and no precedent in this repo's conventions.

Effect: this is a bigger removal than "deprecate" — an entire type and its RequestType extension point disappear from the XPath Profile's XML/JSON/YAML artifacts together. Worth stating explicitly in the PR description, since it's the most visible structural change in the diff and easy to read as accidental scope creep if not flagged as a deliberate consequence.

8. Duplicate-key safety: directive implementer prose, not a schema constraint

Chosen: state a normative SHALL in prose — producers SHALL NOT emit a Namespace object/mapping containing the same key twice; consumers SHALL reject one that does, as a requirement on the parsing/deserialization step, with an explicit note that JSON Schema cannot enforce it.

Rejected alternative: don't police it at all. Treat a document with a duplicate Namespace key as malformed input, out of scope for the spec to define behavior for, the same way XACML doesn't define behavior for malformed XML.

Why: JSON Schema validates the parsed instance; by the time a validator sees it, a document with duplicate keys has already been silently resolved (typically last-key-wins, but this is parser-dependent, not spec-mandated by RFC 8259) or already rejected, entirely outside the schema's visibility. Given the map shape's correctness now depends on key uniqueness (decision 3), silently allowing implementation-defined duplicate-key handling would leave the actual failure mode—a namespace silently resolving to the wrong URI—undocumented for implementers who would have no reason to suspect it. This follows the same pattern already established for Issue 120's DataType-uniqueness $comment: where JSON has no structural backstop, the profile states a procedural recipe instead (e.g. implementer-guidance-is-serialization-native-not-mirrored in
architectural_decisions.md).

9. Namespace requiredness: required, {} for "none"

Chosen: Namespace stays required alongside XPathVersion on XPathExpressionValueType and both selector types, using {} to mean "no namespaces needed."

Rejected alternative: make it optional, with absence equivalent to {}.

Why: with Defaults-level Namespace deleted outright (decision 6/7), "absent" and "present-empty" no longer point at two different fallback targets — there's nothing to fall back to either way, so the original reason for insisting on a distinguishable empty state is gone. But required-with-{} was kept anyway, for a different reason: it keeps Namespace and XPathVersion parallel (both
required, both fully local), and makes every value visibly self-describing — a reader or validator never has to wonder whether Namespace was simply forgotten versus deliberately empty.

10. HRP/MDP example ripple: fixed in this same branch

Chosen: update HRP §7.1 and MDP §8.2's JACAL/YACAL examples (merged hours earlier as part of PR 133) to the new local-Namespace shape, in this branch.

Rejected alternative: land the XPath Profile fix alone; file a follow-up issue for HRP/MDP's examples once the new shape is settled and separately reviewed.

Why: those examples currently resolve prefixes via exactly the XPathRequestDefaults/Namespace mechanism this change deletes. Under the clean-cutover decision they become non-conformant the moment this change lands — not a latent gap discoverable later, but an immediate, self-inflicted contradiction between two merged/in-flight pieces of the same repo. Deferring the fix
would mean shipping a known-broken normative example on main.

11. Namespace key validation: XML NCName, not reused IdentifierType

Chosen: NamespaceMapType's propertyNames (JSON and YAML) validate a non-empty key against an NCName-lexical pattern (^[A-Za-z_][A-Za-z0-9_.-]*$) and explicitly exclude xmlns, instead of reusing Core's IdentifierType. acal-xpath-v1.0.md §5.2's Prefix property gained a matching normative sentence.

Rejected alternative: leave propertyNames as IdentifierType, inherited unchanged from the pre-#134 NamespaceDeclarationType.Prefix property, which used the same type. Core's IdentifierType $comment states its looseness is deliberate ("the pattern may be improved for stricter validation... if your ACAL implementation is already parsing and validating such values strictly according to the ACAL core specification, then your implementation-specific validation is probably more optimal") — so at first glance, extending that same type here looked like it was just carrying forward an already-decided, already-justified design stance rather than a new gap.

Why the rejected alternative was checked, not assumed: read closely, IdentifierType's $comment is about ACAL's own identifier grammar specifically — URN-shaped strings with {ShortId} macro-substitution, a concept the JACAL specification (§5.1.2.1) defines a complete grammar for, which implementations are expected to already validate against independently. A namespace prefix is not an ACAL identifier at all: it is an XML NCName [NAMESPACES], an unrelated external lexical space with no macro-substitution concept, that JACAL §5.1.2.1's grammar wouldn't even help validate. Reusing IdentifierType for Namespace's keys was inherited convenience from before this issue, not a considered application of that $comment's rationale — no verbiage anywhere actually addressed prefix validation for this case. This is a real, previously-unaddressed gap, not a documented "leave it to the implementer" stance for this specific case.

Effect: xmlns — never a valid namespace prefix under [NAMESPACES] — is now rejected structurally in both JSON and YAML. xml remains schema-permitted: [NAMESPACES] allows redundantly redeclaring it to its own correct URI, and rejecting only a wrong-URI redeclaration is a cross-field constraint propertyNames can't express; §5.2 already states the xml binding is implicit and MUST be treated as available without appearing in Namespace, but doesn't forbid correctly restating it. The ASCII-practical NCName pattern is deliberately not maximally strict, in the same "improve later if needed" spirit as IdentifierType's own $comment — just applied to the correct grammar this time, with its own $comment saying so explicitly rather than silently reusing the neighboring one.


Two items identified during design, not decision forks — carry into drafting

"Visibly utilized" (exc-c14n) does not transfer as-is. It's defined over XML infoset structures (element/attribute names, xsi:type values, etc.), not over arbitrary substrings of an XPath expression string — reusing the term directly for "prefixes used within the XPath expression" is imprecise bordering on incorrect. The safe normative rule: require the complete effective in-scope namespace context to be copied into the local map; treat minimization (leaving out prefixes that aren't actually referenced) as implementer-optional, not as a MUST governed by an exclusive-C14N term that was never defined for this context.

Steven's issue example has two transcription slips, not design points, but worth not reproducing verbatim in the PR/spec text: both xmlns:ns attribute values in his XML snippet are missing their closing quote, and the second JSON Value object's "Name" field reads "ns:user/ns:username" (the XPath expression text) where it should read "urn:example:user" (the namespace URI).

Target-state shape (consolidated)

XPathExpressionValueType:
  XPathVersion   [required]  — local, travels with the value
  Namespace      [required]  — {} for none; prefix → URI mapping; complete local context
  XPathCategory  [required]  — unchanged
  XPath          [required]  — unchanged

XPathAttributeSelectorType / XPathEntityAttributeSelectorType:
  Namespace      [required]  — {} for none; same mapping shape; complete local context
  (XPathVersion  — NOT local; inherited from XPathPolicyDefaults, policy-wide)

XPathPolicyDefaultsType:
  XPathVersion   [required]  — retained, policy-wide, governs selectors only
  Namespace      — REMOVED

XPathRequestDefaultsType:
  — REMOVED ENTIRELY (type, Tree wrapper, RequestType extension point)

Applies in all three representations, not just JSON/YAML (see decision 5): XML expresses Namespace/XPathVersion natively — xmlns:* and a new XPathVersion attribute declared directly on the Value element carrying the expression, not inherited from RequestEntity or any ancestor — while JSON/YAML use the mapping shape above, identical as a concrete representation in both JACAL and YACAL. Selectors are unaffected in XML (ancestor-based lookup unchanged); XPathRequestDefaultsType's removal likewise reaches the XSD, not just JSON/YAML.

…rsion

XPath expressions previously resolved namespace prefixes and XPath version
from a single Request- or Policy-wide XPathRequestDefaults/XPathPolicyDefaults
table, in every representation including XML. XML permits the same prefix to
be bound to different URIs in different parts of one document, so two
individually-legal XML request entities using the same prefix for different
URIs could not both be represented in JACAL/YACAL - and response Notice values
had no defaults-carrying mechanism at all.

xpathExpression values now carry their own required XPathVersion and Namespace
(a prefix-to-URI mapping in JSON/YAML, xmlns:*/XPathVersion attributes on the
Value element in XML) unconditionally, in every representation. Selectors keep
centralized, policy-wide XPathVersion and ancestor-based XML namespace lookup
unchanged, since a selector's Path never leaves its policy.

XPathRequestDefaultsType is removed entirely (XML, JSON, YAML) as the direct
consequence: nothing consumes it once request-side values are self-contained.
Namespace's old array-of-Prefix/Name-object-pairs shape is replaced by a
prefix-to-URI mapping everywhere it appears, ending the verbosity a repeated
array shape produced once every value/selector needs its own copy. Namespace
map keys are now validated as XML NCNames rather than reusing Core's looser
IdentifierType, which was never intended for this lexical space.

Also fixes: the identical namespace-scoping defect in this profile's own
canonical XML example, HRP/MDP's just-merged JACAL/YACAL examples (which used
the now-removed mechanism), and several unrelated pre-existing bugs found
while verifying touched examples end-to-end (Rule3.xml's wrong-version
attribute identifier, Request.json's Content.Body missing an xmlns:xsi
declaration needed for standalone well-formedness).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Translation of XML namespace declarations is onerous and incomplete

1 participant