Two JavaScript developers reviewing Temporal.PlainDate.from validation tests for accepted and rejected date inputs

Temporal Can Correct a Date You Meant to Reject

Thu, Oct 29, 2026

A submitted 2025-02-29 looks like an obvious rejection candidate: 2025 is not a leap year. Yet with an object input, Temporal.PlainDate.from({ year: 2025, month: 2, day: 29 }) uses the default overflow: "constrain" policy and can construct 2025-02-28 instead. That behavior is documented for the property-bag path by the TC39 PlainDate guide, which says object overflow defaults to constrain and clamps out-of-range fields.

For a date-admission boundary, successful construction is therefore not enough. The engineering task is narrower: admit only submitted year, month, and day values that satisfy an application-owned shape and number contract and represent exactly the ISO calendar date the caller submitted. Disallowed values must be rejected before anything is appended to the publication surrogate.

The reference laboratory is pinned to Node v24.22.1, V8 13.6.233.17-node.58, Linux x64, and imported @js-temporal/polyfill 0.5.1. It is not a browser-support survey, a Temporal migration article, or an arithmetic exercise. It compares object and string construction, isolates calendar overflow from numeric conversion, and then places a strict application gate in front of Temporal.PlainDate.from. The result is a bounded decision procedure for owned, syntactically valid, unique-key JSON fixtures. It is not a universal JSON parser, an arbitrary-object sanitizer, or a production publishing protocol.

Define the date fields this application may admit

The admission contract comes before the API call. After JSON.parse, the value must be an ordinary non-array object with exactly three keys: year, month, and day. Each value must have ECMAScript type number, pass Number.isSafeInteger, and the year must lie in the application range 1 through 9999. Calendar interpretation is ISO only. If every application guard passes, the adapter calls Temporal.PlainDate.from(input, { overflow: 'reject' }) and accepts only an exact calendar-valid result.

Those are authored application rules, not Temporal’s full input model. Temporal supports other property combinations and a much wider representable year range. The normative Temporal specification shows PlainDate.from delegating to ToTemporalDate, whose object path prepares calendar fields and then constructs a date; the article’s exact-three-key rule and 1–9999 range are deliberately stricter.

The safe-integer check is similarly an application gate. MDN’s Number.isSafeInteger reference describes a noncoercing predicate over already-created JavaScript values. It does not certify the original JSON numeric token. A sufficiently large JSON integer may already have been rounded by JSON.parse before this predicate sees it.

If a UI submits numeric strings, that can be a legitimate interface, but conversion then needs its own explicit parsing contract. This laboratory intentionally rejects numeric strings rather than letting Temporal convert them. For broader type migration and arithmetic concerns, use the wider Temporal migration playbook; that page already covers PlainDate.add() month-end overflow, while this page stays at the construction boundary.

Separate object construction from string parsing

A property bag and an ISO date string reach Temporal.PlainDate.from through different semantic paths. That distinction matters because “overflow” sounds like a universal switch, but the documented option is specifically relevant to object fields. The TC39 PlainDate documentation, accessed October 29, 2026, states that object overflow defaults to constrain, that reject throws on out-of-range object fields, and that overflow is ignored when the input is a string. Invalid date strings throw rather than being repaired through object clamping.

The normative Temporal specification makes the split visible. In ToTemporalDate, object inputs go through PrepareCalendarFields, obtain the overflow option, and call CalendarDateFromFields. String inputs instead go through ISO date-time parsing before a valid ISO date record is created; the overflow option is read but does not regulate a parsed invalid date into a valid one.

That is a separate problem from preserving date-only values at the HTML Date boundary. The HTML article is about an already-valid civil date crossing between a string and Date; this laboratory never introduces a DOM control, an instant, a time zone, or a UTC/local conversion.

Read the default object overflow policy

For an object such as { year: 2025, month: 2, day: 29 }, the default policy may return a different valid date. “Constrain” is useful when an application explicitly wants the nearest permitted field value, but that is not this contract. Here the submitted fields themselves are evidence, and altering one of them is a rejection condition. The important observation is not merely that the output is valid; it is that valid output can differ from admitted input.

Keep the invalid-string rejection rule intact

T03 sends the same invalid string, "2025-02-29", once with overflow: 'constrain' and once with overflow: 'reject'. The supplied preparation evidence records RangeError in both arms. That result matches the TC39 object/string distinction: changing the option does not authorize string normalization. Later, the dedicated evidence section addresses a conflicting MDN example directly instead of silently choosing whichever example is more convenient.

Pin the provider and declare independent expectations

The reference environment is specific: Node v24.22.1, V8 13.6.233.17-node.58, Linux x64, and @js-temporal/polyfill 0.5.1 imported with require. The supplied preparation run also recorded typeof globalThis.Temporal as undefined, so its observations belong to the imported polyfill, not to native Temporal. The official polyfill README at v0.5.1, accessed October 29, 2026, documents CommonJS import of { Temporal }; it does not require assigning Temporal to the global object. The npm registry record for version 0.5.1 identifies the pinned package version and its CommonJS entry point.

Reproduce the supplied experiment in a fresh writable project:

npm install --ignore-scripts --no-audit --no-fund @js-temporal/[email protected]
node temporal_input_fixture.cjs > temporal_input_results.json

Save the embedded source in the next section as temporal_input_fixture.cjs beside that project’s node_modules. Preserve the resulting JSON. A different package version, a native provider, or a changed fixture is a new experiment and needs its own metadata and digest.

Expected calendar answers must be defined independently of provider output. The simple oracles here are literal: 2024 is divisible by 4 and is a leap year under the ordinary Gregorian/ISO rule, so February 29 exists; 2025 is not a leap year, so February 29 does not exist; and month 13 is outside the ISO month set of 1 through 12. A provider cannot be allowed to define the answer that its own behavior is supposed to test. The supplied reference reports PASS_12_CASES and source SHA-256 55d7ee7081c5b38b39eb4259b810404bfa5f0cb1016076c8c37eba9071533630. These results come from the reference run. The source was recorded as 94 lines and 332 whitespace-delimited words; those counts help identify the source. If a copied fixture has different line endings, comments, indentation, or logic, its digest may differ even when the intended test ideas are similar. In that situation, keep the changed digest and treat the run as a distinct qualification instead of asserting byte-for-byte equivalence.

Run the complete twelve-case admission laboratory

The fixture below is the complete supplied source. It makes synchronous local calls, uses Node built-ins plus the pinned polyfill, and records an in-memory published array as an observable stand-in for a later side effect. It does not send a request, write a database row, handle retries, or model concurrency. Its twelve named groups are the unit of evidence; multiple assertions inside one group do not become extra cases.

'use strict';
// An owned PlainDate input-admission experiment; no browser or remote writes.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { Temporal } = require('@js-temporal/polyfill');
const entry = require.resolve('@js-temporal/polyfill');
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(entry), '..', 'package.json'), 'utf8'));
assert.equal(pkg.version, '0.5.1');
const started = new Date().toISOString();
const rows = [];
const invalid = Object.freeze({year:2025, month:2, day:29});

function inspect(input, options) {
  try {
    const value = Temporal.PlainDate.from(input, options);
    return {value:value.toString(), fields:[value.year,value.month,value.day], error:null};
  } catch (error) {
    if (!(error instanceof RangeError || error instanceof TypeError)) throw error;
    return {value:null, fields:null, error:error.name};
  }
}
function record(id, observed, expected, decision) {
  assert.deepEqual(observed, expected, id);
  rows.push({case:id, observed, expected, decision, status:'PASS'});
}
function gate(raw) {
  const evidence = {raw, calendar_calls:0, published:[]};
  const reject = reason => ({...evidence, decision:reason});
  // Fixture JSON has distinct keys; this is not a duplicate-key detector.
  const input = JSON.parse(raw);
  if (input === null || typeof input !== 'object' || Array.isArray(input)) return reject('REJECT_SHAPE');
  if (Object.keys(input).sort().join(',') !== 'day,month,year') return reject('REJECT_FIELDS');
  if (!['year','month','day'].every(k => typeof input[k] === 'number' && Number.isSafeInteger(input[k]))) return reject('REJECT_NUMBER_TYPE');
  if (input.year < 1 || input.year > 9999) return reject('REJECT_YEAR_POLICY');
  evidence.calendar_calls++;
  let value;
  try {
    value = Temporal.PlainDate.from(input, {overflow:'reject'});
  } catch (error) {
    if (!(error instanceof RangeError)) throw error;
    return reject('REJECT_CALENDAR');
  }
  assert.deepEqual([value.year,value.month,value.day], [input.year,input.month,input.day], 'Raw fields preserved');
  evidence.published.push(value.toString());
  return {...evidence, decision:'ACCEPT_EXACT_DATE'};
}
const date = (text, fields) => ({value:text,fields,error:null});
const range = {value:null,fields:null,error:'RangeError'};

record('T01', inspect(invalid), date('2025-02-28',[2025,2,28]), 'REJECT_SILENT_CONSTRAINT');
record('T02', inspect(invalid,{overflow:'reject'}), range, 'ACCEPT_CALENDAR_REJECTION');
record('T03', ['constrain','reject'].map(overflow => inspect('2025-02-29',{overflow})),
      [range,range], 'STRING_PATH_REJECTS_INVALID_DATE');
record('T04', [inspect({year:2025,month:13,day:31}), inspect({year:2025,month:13,day:31},{overflow:'reject'})],
      [date('2025-12-31',[2025,12,31]),range], 'REJECT_MONTH_CONSTRAINT');
record('T05', inspect({year:2025,month:2,day:28.9},{overflow:'reject'}),
      date('2025-02-28',[2025,2,28]), 'REJECT_TRUNCATED_INPUT');
record('T06', inspect({year:'2025',month:'2',day:'28'},{overflow:'reject'}),
      date('2025-02-28',[2025,2,28]), 'REJECT_COERCED_INPUT');

const validRaw = '{"year":2024,"month":2,"day":29}';
record('T07', gate(validRaw), {raw:validRaw,calendar_calls:1,published:['2024-02-29'],decision:'ACCEPT_EXACT_DATE'},
      'ACCEPT_EXACT_DATE');
const invalidRaw = '{"year":2025,"month":2,"day":29}';
record('T08', gate(invalidRaw), {raw:invalidRaw,calendar_calls:1,published:[],decision:'REJECT_CALENDAR'},
      'REJECT_BEFORE_PUBLICATION');
const badTypes = ['{"year":2025,"month":2,"day":28.9}', '{"year":"2025","month":2,"day":28}'];
record('T09', badTypes.map(gate), badTypes.map(raw => ({raw,calendar_calls:0,published:[],decision:'REJECT_NUMBER_TYPE'})),
      'REJECT_BEFORE_TEMPORAL');
const badPolicies = [
  ['{"year":2025,"month":2,"day":28,"calendar":"iso8601"}','REJECT_FIELDS'],
  ['{"year":0,"month":2,"day":28}','REJECT_YEAR_POLICY'],
  ['{"year":10000,"month":2,"day":28}','REJECT_YEAR_POLICY']];
record('T10', badPolicies.map(([raw]) => gate(raw)),
      badPolicies.map(([raw,decision]) => ({raw,calendar_calls:0,published:[],decision})),
      'REJECT_OUTSIDE_APP_CONTRACT');
const badShapes = [['null','REJECT_SHAPE'],['[]','REJECT_SHAPE'],['{"year":2025,"day":28}','REJECT_FIELDS']];
record('T11', badShapes.map(([raw]) => gate(raw)),
      badShapes.map(([raw,decision]) => ({raw,calendar_calls:0,published:[],decision})),
      'REJECT_INCOMPLETE_INPUT');
const normalizedBad = Temporal.PlainDate.from(invalid).toString();
const normalizedGood = Temporal.PlainDate.from({year:2025,month:2,day:28}).toString();
record('T12', {invalidResult:normalizedBad, validResult:normalizedGood,
           laterStrict:Temporal.PlainDate.from(normalizedBad,{overflow:'reject'}).toString()},
      {invalidResult:'2025-02-28',validResult:'2025-02-28',laterStrict:'2025-02-28'},
      'HOLD_WITHOUT_ORIGINAL_FIELDS');
assert.deepEqual(invalid,{year:2025,month:2,day:29});
assert.deepEqual(rows.map(r=>r.case), Array.from({length:12},(_,i)=>'T'+String(i+1).padStart(2,'0')));
console.log(JSON.stringify({status:'PASS_12_CASES',started_utc:started,finished_utc:new Date().toISOString(),
  node:process.version,v8:process.versions.v8,platform:process.platform,arch:process.arch,
  provider:'@js-temporal/polyfill',provider_version:pkg.version,native_temporal:typeof globalThis.Temporal,
  source_sha256:crypto.createHash('sha256').update(fs.readFileSync(__filename)).digest('hex'),cases:rows},null,2));

Run it with output redirected to a preserved ledger:

node temporal_input_fixture.cjs > temporal_input_results.json

The field legend is intentionally small. observed is what the provider or gate returned. expected is the independently declared literal result that assert.deepEqual requires. calendar_calls counts only calls made by gate; direct inspect calls in T01–T06 are not part of that counter. published is a local array appended only after strict construction and field equality. decision records the operational interpretation of that case, while status: 'PASS' means the observation matched the declared expectation.

That distinction makes PASS_12_CASES meaningful but bounded. T01 is expected to demonstrate a behavior the admission policy rejects: default object construction turns 2025-02-29 into 2025-02-28. T02 expects RangeError for the same object with overflow: 'reject'. T03 expects both invalid-string arms to throw. T04 expects month 13 to constrain to month 12 under default behavior and to throw under reject. T05 and T06 deliberately expect successful construction after conversion, proving that calendar strictness does not itself enforce the application’s number-type contract.

The gate cases then move the rejection boundary earlier. T07 expects one calendar call and one publication for numeric 2024-02-29. T08 expects one calendar call but no publication for numeric 2025-02-29. T09–T11 expect zero calendar calls because shape, key, type, safe-integer, or application-year rules fail first. T12 expects two different original objects to collapse to the same constrained string and expects that later strict parsing accepts that already-valid string.

A mismatch is not something to “fix” by weakening the expected row. A failed assertion, absent case, changed runtime, different provider version, or unexpected exception is new evidence requiring investigation. The harness intentionally rethrows exceptions outside the narrow RangeError/TypeError handling used by inspect and the calendar RangeError handled by gate, so an unfamiliar failure is not mislabeled as ordinary invalid-date rejection.

The supplied preparation run says all twelve groups passed in the pinned environment. The correct interpretation is therefore: this provider, version, source, and runtime matched these declared controls. It does not prove native Temporal, every object representation, every JSON payload, or any remote side effect. The literal expectations are what prevent a green process exit from becoming self-referential evidence. A harness that merely logged whatever the provider returned could always appear successful; these assertions instead require specific dates, specific exception classes, specific call counts, and specific publication counts. That is why the negative controls belong in the same executable ledger as the accepted control.

Observe the dates that default constraint changes

The first defect pattern is a successful constructor returning a valid date whose fields differ from the submitted fields. The frozen object in T01 remains { year: 2025, month: 2, day: 29 }; the returned PlainDate contains [2025, 2, 28]. That is construction of a different value, not mutation of the original object. The final assertion rechecks the frozen input unchanged.

This distinction matters operationally. If a reviewer inspects only the serialized 2025-02-28, the output looks harmless. If the reviewer compares input fields with returned fields before serialization, the policy violation is visible: the adapter admitted a transformed date.

Follow the invalid leap day to February 28

T01 and T02 are a paired control. Under default object overflow, the invalid 2025 leap day succeeds as 2025-02-28. With { overflow: 'reject' }, the same object produces RangeError. The TC39 PlainDate guide documents both default constraint and strict rejection for object inputs.

For this application, the first outcome supports REJECT_SILENT_CONSTRAINT; the second shows that strict calendar rejection blocks this particular alteration. It does not yet prove the entire input contract, because conversion may occur before the calendar-range decision.

Test an out-of-range month as a separate control

T04 ensures the conclusion is not peculiar to leap days. { year: 2025, month: 13, day: 31 } becomes 2025-12-31 under default constraint, while the explicit reject arm throws RangeError. The changed month is just as material as the changed day: it means the published value would no longer be the submitted value. This is property-bag construction evidence only; it is not a recurrence rule or month-end arithmetic example.

Prove that overflow rejection does not own the input schema

overflow: 'reject' answers a calendar-regulation question after Temporal has prepared numeric calendar fields. It does not mean “accept only numbers that were already integral number primitives.” T05 demonstrates that boundary with { day: 28.9 }: the supplied reference observes 2025-02-28, not a rejection. T06 supplies numeric strings for all three fields and also observes 2025-02-28.

That behavior follows the normative conversion sequence. In the Temporal specification, PrepareCalendarFields converts year with ToIntegerWithTruncation, while month and day use ToPositiveIntegerWithTruncation. Those operations ultimately call ToNumber and truncate the fractional part before later calendar regulation. A string such as "2025" can therefore become a number, and 28.9 can become 28, before overflow: 'reject' checks whether the resulting date fields are in calendar range.

That is not a bug in reject. The API contract permits conversion. The application contract in this article is intentionally stricter because the adapter must preserve submitted numeric field identity. Other applications may legitimately choose a parsing step that accepts strings or decimals; they should own that policy explicitly instead of assuming the overflow option already enforces it. The important sequencing point is that numeric conversion happens before the calendar overflow decision. If the value 28.9 has already become integer day 28, then there is no out-of-range day left for overflow: 'reject' to reject. Likewise, after "2025" has converted to numeric 2025, calendar validation cannot tell whether the caller supplied a string or a number. Schema provenance has already been erased unless the application checked it first.

Admit parsed numeric fields before calling Temporal

The gate function orders checks so that calendar construction receives only values already admitted by the application. First it rejects null, arrays, and non-object values. Next it requires exactly day,month,year, preventing an extra calendar key or an incomplete record from entering this adapter. Then it requires all three values to be number primitives and safe integers. Finally it applies the year interval 1–9999. Only after those guards does it increment calendar_calls and invoke strict PlainDate.from.

Number.isSafeInteger is useful here precisely because it does not convert a string to a number. The MDN reference defines it as a test that the supplied value is a number and a safe integer. The preceding typeof check makes that intent explicit to reviewers rather than relying on one predicate to communicate the entire policy.

A frontend form may start from text. That does not require weakening this boundary; it requires a separate, owned translation step that defines acceptable syntax, range, error handling, and evidence. This is analogous to application-owned validation for script-populated fields: a platform primitive answers its own contract, while an application can impose a stricter admission rule. The linked article concerns DOM validity, not Temporal, and its event-origin mechanics are deliberately outside this laboratory.

The practical rule is simple: parsing policy and calendar validity are independent gates. If product policy says the API payload must contain numeric integers, then a numeric string should fail before Temporal even gets the chance to coerce it. That ordering also makes reviews easier. A failing calendar case should already be known to have the required keys and admitted numeric types, while a type failure should never be confused with a leap-year rule. The ledger can therefore answer not only whether an input was rejected, but which layer owned the rejection and whether Temporal was reached at all.

Keep application limits distinct from API limitations

T10 and T11 make the authored contract visible by rejecting values that Temporal might otherwise understand or that are incomplete for this adapter. An object with calendar: "iso8601" is rejected because the adapter permits exactly three keys, even though Temporal itself supports calendar-bearing objects. Year 0 and year 10000 are rejected because this application permits 1–9999, not because Temporal’s representable range ends there. The TC39 guide explicitly describes a far wider PlainDate range, so attributing the narrower interval to the API would be incorrect.

Similarly, null, an array, and an object missing month are rejected without a calendar call. The adapter declares the representation it owns rather than reverse-engineering policy from whatever the library happens to accept. That same separation appears in the separate HTML step-base contract: native acceptance answers a particular platform rule, not every application requirement layered on top of it.

The numeric scope is equally important. After JSON.parse, Number.isSafeInteger can judge only the resulting ECMAScript value. It cannot tell whether the original token used exponent notation, carried insignificant syntax, or had already rounded to the same Number. MDN’s Number reference notes that JSON parsing into JavaScript Numbers can lose integer precision beyond the safe-integer range. Retaining the raw JSON string preserves investigation material, but the helper is not a lossless numeric-token parser or duplicate-key detector.

For lexical preservation, a different parser contract is required. This fixture assumes syntactically valid JSON with unique keys and owned local objects, and it intentionally does not expand into arbitrary getters, proxies, or transport-layer validation. That limitation is useful rather than embarrassing: it makes the evidence auditable. A complete HTTP ingress validator would need observations about content type, body-size limits, parser configuration, duplicate-key handling, authentication context, and error mapping. None of those controls are exercised here, so none should be implied by the passing ledger.

Reconcile every calendar call with its publication evidence

T07 through T11 reveal where each decision occurs. The accepted control, T07, starts with {"year":2024,"month":2,"day":29}. It passes shape, exact-key, number, safe-integer, and year checks; calendar_calls becomes 1; strict PlainDate.from accepts the valid leap day; field equality passes; and published receives exactly "2024-02-29". The case therefore records ACCEPT_EXACT_DATE.

T08 uses the same admitted schema with 2025-02-29. The input is structurally and numerically acceptable, so the gate makes one calendar call. Strict construction throws RangeError, the gate records REJECT_CALENDAR, and published remains empty. This is an important control because it proves that “passed schema validation” does not mean “valid calendar date.”

T09–T11 stop earlier. Fractional day: 28.9 and string year: "2025" fail the number-type/safe-integer rule; the extra calendar field and out-of-policy years fail application policy; null, [], and the missing-month object fail shape or field rules. Every one of those rows has calendar_calls: 0 and an empty publication list. Flattening all of them into “invalid date” would erase the stage that rejected the input and make future debugging weaker. It would also obscure ownership. A REJECT_NUMBER_TYPE usually belongs with the payload or adapter contract; REJECT_YEAR_POLICY belongs with application policy; REJECT_CALENDAR belongs at the calendar-validity boundary. Keeping those reasons distinct allows a later product change (for example, permitting a wider year range) to update one rule without accidentally weakening the requirement for integer number types or strict calendar validity.

Case group

What passed first

Calendar calls

Published values

Decision evidence

T07

Schema, numeric contract, year policy, calendar validity

1

1

Exact accepted control

T08

Schema, numeric contract, year policy

1

0

Calendar rejection before publication

T09

Shape and keys only

0

0

Disallowed parsed type/value

T10

Shape may pass, application policy does not

0

0

Extra field or year policy rejection

T11

Shape/required-field contract fails

0

0

Incomplete or wrong container

Require one publication only after exact field agreement

The gate does not append immediately after successful construction. It first compares [value.year, value.month, value.day] with the admitted input fields. In the tested overflow: 'reject' path this is a defensive assertion that makes exactness visible in evidence. Only then does it append the normalized ISO string.

The counters must not be oversold. published.length === 1 in T07 means one local array append occurred. published.length === 0 on rejection means no append occurred in this fixture. It says nothing about a server transaction, a database trigger, analytics already emitted elsewhere, or concurrent work in a real application. Proving those broader claims would require instrumenting and testing those actual side effects under their deployment conditions.

Stop trying to validate lost original fields afterward

T12 is the key reason to enforce the contract before normalization. Start with two distinct object inputs: invalid { year: 2025, month: 2, day: 29 } and valid { year: 2025, month: 2, day: 28 }. Under default object constraint, both serialize to the same string, 2025-02-28. The mapping from original field sets to normalized output is therefore many-to-one.

Once only that string remains, later strict parsing cannot identify which original object produced it. T12 calls Temporal.PlainDate.from(normalizedBad, { overflow: 'reject' }) and gets 2025-02-28, because the string itself is now a valid date. The later check validates the normalized value; it does not reconstruct provenance or prove that the earlier submitted day was valid.

This is the same general evidence problem illustrated by why a lossy representation cannot recover the original input. That article concerns large numeric identifiers and precision loss, not dates, but the boundary lesson transfers: when distinct inputs have already collapsed to one representation, a validator applied afterward cannot infer the lost distinction.

Historical repair therefore needs original fields plus an authoritative policy. If only a normalized date survives, the correct state for this workflow is HOLD, not “subtract or add a day,” not “compare with today,” and not an invented leap-year correction. The stored date may have been valid from the beginning. To claim otherwise, an investigation would need independent provenance such as the original submitted fields, a trustworthy source record, or another audited system that establishes intended policy.

Resolve the documented example conflict transparently

On October 29, 2026, the MDN Temporal.PlainDate.from page contains an example in its overflow section where Temporal.PlainDate.from("2021-02-29") is shown producing "2021-02-28". That particular example conflicts with the stronger TC39 sources used for the contract here.

The TC39 PlainDate documentation, also accessed October 29, says overflow is ignored for string inputs and invalid date strings throw. The normative Temporal specification takes string input through ParseISODateTime and then creates an ISO date record whose validity precondition must hold; the object-only calendar regulation step is not used to clamp an invalid string.

The supplied measured provider result gives a third layer of evidence. T03 calls the pinned polyfill with "2025-02-29" under both constrain and reject, and both arms produce RangeError. Thus the article follows the normative rejection rule and the measured v0.5.1 behavior, while recording the MDN example discrepancy rather than copying it.

This does not justify dismissing the entire MDN page. Most of its input and option description aligns with the object overflow model; the issue is the specific invalid-string example. Nor does T03 prove native Temporal or a different polyfill. A claim about another provider would need a fresh run that records that provider’s identity, version or build, runtime metadata, exact fixture source, and actual result. That new evidence could then be compared with the normative rule and documented discrepancy. In practical review terms, the hierarchy is clear: use the normative algorithm for the contract, use implementation documentation as supporting explanation, and use the pinned run to establish what this provider actually did. When those layers disagree, record the disagreement instead of averaging them into an ambiguous statement.

Choose accept, reject, repair or hold from the evidence

The operational labels describe next actions inside this bounded workflow. ACCEPT means the parsed value passed the application contract, strict calendar construction succeeded, the returned fields matched, and only then was the normalized date appended. REJECT means a disallowed submitted value stopped before publication. REPAIR applies to an adapter known to constrain or coerce fields when policy forbids that behavior; it does not authorize rewriting historical business data. HOLD applies when a historical normalized date lacks the original fields or authoritative policy needed to determine whether it was ever wrong.

A passing normalized string is never enough by itself. Likewise, a changed provider or unexpected exception should not be relabeled until the test is green. The correct response is investigation: preserve the discrepant output, confirm runtime metadata and source digest, and decide whether the contract, provider, or expectation needs review. REPAIR also needs a precise target: the code path that admitted altered fields. Replacing default construction with a stricter gate is an adapter repair; rewriting previously stored 2025-02-28 values is a separate data-correction project and requires provenance that this laboratory deliberately does not have.

Build a decision table with an accountable next step

Situation

Evidence

Decision

Next action

Owner

Valid exact numeric date

Guards pass; one calendar call; fields agree; one local append

ACCEPT

Publish normalized ISO date through the separately owned downstream path

Adapter/application owner

Fractional or string parsed field

Type/safe-integer guard fails; zero calendar calls

REJECT

Return contract error; change parsing policy only if product explicitly permits it

API contract owner

Invalid calendar date

Schema passes; strict construction throws; one calendar call; no append

REJECT

Report calendar-invalid input without altering fields

Adapter owner

Default constraint observed in owned adapter

Input and output fields differ, as in T01/T04

REPAIR

Change adapter to prevalidate and use strict construction; add regression cases

Frontend platform owner

Historical normalized date only

Valid ISO string exists, originals/policy unavailable

HOLD

Preserve evidence and seek provenance; do not infer correction

Data/domain owner

Provider/runtime/source changed

Metadata differs, case missing, assertion fails, or unexpected exception appears

HOLD

Qualify the new environment and document discrepancy before release

Frontend platform owner

No row accepts merely because the final string parses. The evidence phase and the owned contract decide whether publication is authorized.

Keep the adapter contract stable across maintenance

A regression suite should preserve the structure of all twelve groups, not just a couple of happy-path dates. The default-clamping negatives T01 and T04 prove why a permissive constructor is unsafe for this admission policy. T02 proves explicit calendar rejection for the same invalid object. T03 protects the object/string distinction. T05 and T06 prove that strict overflow alone does not enforce type identity. T07–T11 prove the ordered gate and publication behavior. T12 protects against the tempting but invalid idea that later strict parsing can recover earlier field validity.

That means “we switched to overflow: 'reject'” is an incomplete regression claim. A future refactor could still accidentally remove the safe-integer/type gate and allow numeric strings or fractions to convert before calendar validation. The adapter contract owner should therefore own the shape, exact-key, parsed-number, year-range, and accepted-representation policy. The frontend platform owner should own provider/version qualification, runtime metadata, and the fixture’s ability to surface discrepancies.

When rerunning under a future provider, retain a separate result artifact containing provider identity, provider version or native build information, Node/V8 or equivalent runtime metadata, platform, architecture, source digest, and every named case result. If a discrepancy appears, keep the old and new evidence side by side rather than overwriting the previous run. Raw fixture material should be preserved according to the application’s ordinary handling rules; this article does not define a general retention policy.

There is no need to add a benchmark, browser matrix, Temporal migration program, or timezone suite to make this contract credible. Those would answer different questions. The maintenance obligation is narrower and stronger: keep the admission oracles independent, keep the conversion negatives, and keep proof that rejected inputs cannot reach this local publication surrogate. Reviewers should also resist deleting cases that appear redundant after a repair. T02 without T01 would prove only that reject can throw; T05 and T06 are what show that this strict API call still accepts values the application forbids; T12 explains why downstream revalidation cannot substitute for preserving original fields. Together, the controls define the failure model, not just the implementation.

Develop careful JavaScript boundaries with Refonte Learning

Engineers building this kind of adapter benefit from practicing JavaScript boundaries as explicit contracts rather than treating library success as business acceptance. Refonte Learning’s Frontend Development program currently lists JavaScript ES6+, APIs/AJAX, API integration, and practical project work among its curriculum and competencies. Those are site-published program facts, not independently audited outcomes.

The page also contains conflicting duration information: “4 months” in Program Specifics and “3 Months” on the related Frontend Development card. Confirm the duration before enrollment. Specialized coverage of Temporal, @js-temporal/polyfill, the pinned Node version, or this exact twelve-case date-admission laboratory is not confirmed by the published curriculum.

A useful next skills step is concrete: take one date-only API payload in your own frontend or full-stack codebase, write down the exact admitted keys and parsed value types, add negative controls for coercion and calendar invalidity, and record which phase can append to the publication surrogate. That exercise trains the same distinction this laboratory depends on: a platform API can validate its own semantics while the application still owns the contract for what may reach it.