Skip to content

Close JSON Schema gap-analysis findings - #169

Open
bergundy wants to merge 76 commits into
mainfrom
fix/all-gap-analysis
Open

Close JSON Schema gap-analysis findings#169
bergundy wants to merge 76 commits into
mainfrom
fix/all-gap-analysis

Conversation

@bergundy

@bergundy bergundy commented Aug 31, 2026

Copy link
Copy Markdown
Member

Summary

  • reconcile the JSON Schema specifications with the completed gap-analysis audit
  • close parser, planning, naming, reference, generated-layout, documentation-safety, and validation gaps
  • align Go, Java, Python, and TypeScript generation and runtime behavior, regenerate checked-in outputs, and add regression and conformance coverage

Breaking changes

  • limit materialized time and date-time offsets to the portable -18:00 through +18:00 range and use Java OffsetDateTime
  • reject colliding stable TypeScript DEFAULT_<FIELD> exports instead of renaming an existing constant when another model is added

Validation

  • cargo validate (Node 26, JDK 21, .NET 9)

@bergundy
bergundy marked this pull request as ready for review September 1, 2026 18:06
@bergundy
bergundy requested a review from a team as a code owner September 1, 2026 18:06
Comment thread src/generator/mod.rs Outdated
origins: &mut BTreeMap<PathBuf, PathBuf>,
path: PathBuf,
contents: String,
origin: PathBuf,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we should represent origin as a pathbuf if it often isn't a real path.

Comment thread src/generator/mod.rs Outdated
@@ -59,6 +59,31 @@ impl GeneratedFiles {
}
}

/// Inserts one generated file while retaining enough provenance for P7.2's

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't like referencing the json principles in the generic code. What are we actually trying to accomplish here?

Comment thread src/generator/go.rs Outdated
@@ -808,6 +826,38 @@ fn insert_generated_file(
Ok(())
}

fn insert_generated_file_with_origin(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is this different from the one defined in mod.rs?

Comment thread src/generator/python.rs Outdated
@@ -85,11 +87,23 @@ fn generate_tree(
) -> Result<GeneratedFiles> {
let model_hoists = tree_model_hoists(branch)?;
let mut files = BTreeMap::new();
let mut origins = BTreeMap::new();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't really understand the whole origins thing here. We're adding a lot of context, best I can tell is so that we don't add the same file twice? But because of different sources or something?

Comment thread src/generator/mod.rs Outdated
/// input leaf/support fragment, or a descriptive virtual path for a fixed
/// generator-owned artifact.
pub(crate) fn insert_generated_file_with_origin(
files: &mut BTreeMap<PathBuf, String>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If I've come to grasp the point here correctly, I don't think a separate origin map makes sense. Why not just store the source of the file as part of the file itself?
files: &mut BTreeMap<PathBuf, String>, into files: &mut BTreeMap<PathBuf, (String, String)>, though probably with a struct instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Potentially there's enough complexity there that really it is a new object with some helpers for FileMap or whatever. Assuming the same thing is shared between langs, which it's unclear to me is the case, or why this is defined in the root mod.

@bergundy
bergundy requested a review from tconley1428 September 1, 2026 19:10
Comment thread src/generator/python.rs
path,
contents,
source,
"rename one input file or directory so the generated Python paths differ",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't see how it knows this remedy in this location, is this just sorta the default?

Comment thread src/generator/typescript.rs Outdated
/// strings/templates and regex literals, so this small lexer can distinguish a
/// real `Long` type occurrence without treating the exact const string
/// `"Long"` (or a pattern containing it) as a package dependency.
fn contains_identifier_outside_literals(source: &str, identifier: &str) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This whole thing seems pretty sketchy. Some kind of hand built parser for the typescript?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right. I missed this while skimming the generated code. I got used to not paying much attention to the code in this project.

Comment thread src/generator/typescript.rs Outdated
@@ -3470,6 +3619,7 @@ fn render_support_module(support_source: &str) -> String {
fn render_index_module(
services: &[RenderedService<'_>],
model_type_names: &BTreeSet<String>,
has_json_runtime_module: bool,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems like an abstraction leak, although one that partially already existed. Why does this generator know about Violation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It also knows about Long, which is proto / WIT specific FTR. I will clean this instance up though.

Comment thread src/generator/python.rs Outdated
&mut files,
path.clone(),
contents.clone(),
"<generated Python recursive-model hoist>",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How are these synthetic sources helpful? Won't they just create confusing error messages? Seems like this would need to know the original file it was hoisted from or something.

Comment thread src/generator/typescript.rs Outdated
&& tree_support_files.contains_key(&PathBuf::from("definitions.ts"));
insert_branch_index_file(&mut files, branch, has_json_runtime_module)?;
insert_files(&mut files, tree_support_files)?;
let additional_exports: &[&str] = if branch_has_json_models(branch) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Explicitly checking for json models is a leak here as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think we were very methodical about avoiding leaks so far. You'll notice the leak is pre-existing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yep. Pointing it out, if we decide not to fix it now, that's okay.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've been looking for them, but it is hard not to miss some. Filing an issue maybe, just as a reminder to come back and do some clean up.

Review of 18a5609 found three places where a spec had been relaxed so that a
known code defect became conformant, and a set of findings that were not
reached at all. The commit predates roughly half the adversarial review, so
some of what it encoded had since been retracted.

Reversals — the spec was amended in the wrong direction:

* format.md no longer lists Java among the nanosecond-resolution targets for
  `time`, and no longer prescribes canonicalizing through OffsetTime/LocalTime.
  Java holds `time` in a String; a String carrier has no resolution of its own,
  so the truncation is the artificial common-denominator floor P1 forbids. Only
  Python's microsecond limit is a licensed loss. Also drops the stale claim at
  the head of the section that `time` round-trips losslessly.
* allOf.md scopes the raw pre-merge gate to checks decidable on one branch that
  a merge could silently discard, and states that naming and satisfiability
  checks are not among them. The previous wording licensed running P15
  value-constant checks per authored branch, which over-rejects schemas whose
  merged form is clean, and contradicted the delegation this file states twice
  elsewhere.
* dependentRequired.md restores the rewrite fix-it for the draft-07
  `dependencies` array form, matching the exclusiveMaximum precedent. Reader-
  directed porting guidance dissolved the missing-diagnostic defect instead of
  recording it.
* generated-file-layout.md no longer asserts that reserved-name and P15
  diagnostics are actionable — two findings show they are not — and instead
  makes applicability the obligation.
* PRINCIPLES.md P1 no longer folds the binary64 exception under "provably
  cannot carry a wire distinction"; it is a domain restriction owned by type.

Decisions the specs did not previously make, each of which blocked a
well-defined code fix:

* oneOf.md: a non-annotation sibling of a `oneOf` rejects, with a fix-it
  naming the branch. Silent discard is what P7.1 exists to prevent, and the
  sibling additionally suppresses two unrelated type guards.
* nullability.md: constraints belong on the non-null branch, `default` on the
  wrapper; each rejects on the other node. Records that a wrapper default
  lowers against the branch's type.
* PRINCIPLES.md P11: names the boundaries the failure must survive, and states
  that Go does not meet the clause today because the SDK's payload converter
  severs the error chain, inverting non-retryability.
* PRINCIPLES.md 11a: defines the Violation.path grammar, including escaping for
  arbitrary map keys and the join for a bracket-leading segment, which the four
  targets currently disagree on.
* min/maxProperties.md: the count is over the wire object at each boundary,
  never over in-memory fields; states the asymmetry the optional+nullable
  collapse implies rather than leaving it to be discovered.
* min/maxProperties.md: the count keywords own satisfiability reconciliation
  against every keyword that bounds the key space, not only `required`.
* ref.md: bounds the input root by the invocation root, rejects absolute
  refs, and defines what "reproducible" promises — module and package names
  must not encode the checkout's location.
* PRINCIPLES.md P15: the namespace of an emitted file includes what it imports
  and what the emitter synthesizes into it, covering the service file's SDK
  imports and Go's synthesized <Service>Client.
* pattern.md / maxLength.md: a materialized node's assertion measures the
  canonical wire form at both boundaries, and a literal is canonicalized before
  it is checked.

Smaller corrections: const.md's Java block no longer uses the arrow switch that
does not compile under --release 8; multipleOf.md stops claiming UnmarshalJSON
calls the shared Validate; pattern.md's residual-risk sentence names the edges
actually open; description.md reconciles "does not reflow" with the wrapping
section and records that wrapping can move a doc tag into a position its tool
treats as meaningful; PRINCIPLES.md P12.2 states that sharing is a requirement
on the predicate rather than on the call graph, which the per-keyword rewrites
in the parent commit had left it contradicting.
A whole-diff audit of 18a5609 found five further places where the amendment
made a reproduced P1 conformant or unreachable, none of them caught by the
scoped reviews, plus a set of self-contradictions the edits introduced.

Reversals:

* PRINCIPLES.md no longer says Python's last-import-wins shadowing is "not
  reachable generated output". It is reachable: two service names differing
  only in case fold to one origin label, the loader accepts, and the emitted
  module defines the same class twice. The counterfactual framing holds only
  for the cross-module model-name case the loader rejects up front.
* pattern.md gate rule 7 restores the "outside a character class" scoping. Its
  absence asserted that `[]]`, `[]a]`, `[^]]` and `[]-a]` reject; they pass, and
  Node in `u` mode throws on import. The hole is now stated as a hole.
* pattern.md gate rule 8 no longer claims the D7 rule keeps evaluation linear.
  It fires only when the outer quantifier is unbounded, so `a{2}*` still panics
  at Go package init and `^(a|b)*$` still overflows Java's stack. Both are named
  as open. The neighbouring "complete gate" and "all handled" claims are
  softened to match.
* maxItems.md keeps the 2^53-1 ceiling but states the obligation the ceiling
  does not discharge: a bound in [2^31, 2^53-1] requires Java to emit a `long`
  literal, or the package does not compile. The ceiling read as though it made
  the range safe, which is the claim the finding disproves.
* deprecated.md scopes the "where the tooling supports it" hedge to TypeScript
  and Python, where the limit is genuine. It was covering Go's inert
  service/operation marker, which is a generator placement bug — one blank line
  fixes it — not a toolchain limit.

Self-contradictions introduced by the parent commit:

* PRINCIPLES.md TS §4 said converters join a per-module namespace and, two
  lines later, that they are validated run-wide. Replaced rather than appended.
* additionalProperties.md said key order is preserved and, in the same
  sentence, that member order is not guaranteed. "key order" dropped from the
  preserved list.
* type.md's `"null"` type-mapping row still read "only inside nullability
  pattern" after the loader bullet was widened to any `oneOf` branch.
* contentEncoding.md rejected the materializing-format combination one-sidedly;
  format.md had no counterpart bullet, leaving the question unadjudicated from
  the other side. Mirrored.
* The inert-`$ref`-sibling carve-out was written into allOf.md and comment.md
  but not ref.md, which still stated siblings merge unconditionally. Propagated,
  and all three now carry a Status: unimplemented marker — the fold gate admits
  only the four name keywords today, so the clause is the contract and the gate
  is the defect.

Overstatements narrowed: type.md's "only a free-form oneOf object branch"
(an un-hoisted array branch lands on the same fallback); enum.md stating the
union-variant leg as fact when its own report calls it unreproduced;
format.md's ecosystem row claiming such documents are never accepted, when the
dialect gate is conditional on a declared `$schema`.
@bergundy
bergundy force-pushed the fix/all-gap-analysis branch from cc2a5b7 to 78e64d2 Compare September 2, 2026 21:42
Comment thread src/generator/mod.rs
&mut self,
path: impl Into<PathBuf>,
contents: String,
source: impl Into<String>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think I would either make source optional, or add another insert method which does not take a source, like insert_system_file or something which has a default error message.

I'm like medium on the idea though. If you don't want to do that, throw a comment on this function describing that source can be a placeholder for a generated file, and that failing should be unexpected, indicating a validation failure upstream.

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.

2 participants