@pubid/pubid 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/artifact/corpus.json.gz +0 -0
  2. package/dist/flavors/iec/index.js +12 -5
  3. package/dist/flavors/ieee/index.js +321 -113
  4. package/dist/flavors/ieee/model.d.ts +18 -0
  5. package/dist/flavors/ieee/model.js +62 -8
  6. package/dist/flavors/ieee/preprocessor.js +3 -0
  7. package/dist/flavors/ieee/update_codes.js +1 -1
  8. package/dist/flavors/iho/index.js +198 -35
  9. package/dist/flavors/jis/index.js +74 -2
  10. package/dist/flavors/nist/grammar.js +44 -16
  11. package/dist/flavors/nist/index.js +17 -2
  12. package/dist/urn/parsers.js +57 -20
  13. package/package.json +2 -2
  14. package/vendor/parg/adobe.json +1 -1
  15. package/vendor/parg/aiee.json +1 -1
  16. package/vendor/parg/amca.json +1 -1
  17. package/vendor/parg/ansi.json +1 -1
  18. package/vendor/parg/api.json +1 -1
  19. package/vendor/parg/ashrae.json +1 -1
  20. package/vendor/parg/asme.json +1 -1
  21. package/vendor/parg/astm.json +1 -1
  22. package/vendor/parg/bipm.json +1 -1
  23. package/vendor/parg/bsi.json +1 -1
  24. package/vendor/parg/calconnect.json +1 -1
  25. package/vendor/parg/ccsds.json +1 -1
  26. package/vendor/parg/cen_cenelec.json +1 -1
  27. package/vendor/parg/cie.json +1 -1
  28. package/vendor/parg/csa.json +1 -1
  29. package/vendor/parg/doi.json +1 -1
  30. package/vendor/parg/easc.json +1 -1
  31. package/vendor/parg/ecma.json +1 -1
  32. package/vendor/parg/etsi.json +1 -1
  33. package/vendor/parg/evs.json +1 -1
  34. package/vendor/parg/gb.json +1 -1
  35. package/vendor/parg/gost.json +1 -1
  36. package/vendor/parg/iala.json +1 -1
  37. package/vendor/parg/iana.json +1 -1
  38. package/vendor/parg/idf.json +1 -1
  39. package/vendor/parg/iec.json +1 -1
  40. package/vendor/parg/ieee.json +1 -29571
  41. package/vendor/parg/ietf.json +1 -1
  42. package/vendor/parg/iho.json +1 -0
  43. package/vendor/parg/ire.json +1 -1
  44. package/vendor/parg/isbn.json +1 -1
  45. package/vendor/parg/iso.json +1 -8510
  46. package/vendor/parg/itu.json +1 -1
  47. package/vendor/parg/jcgm.json +1 -1
  48. package/vendor/parg/jis.json +1 -1
  49. package/vendor/parg/nesc.json +1 -1
  50. package/vendor/parg/nist.json +1 -9588
  51. package/vendor/parg/oasis.json +1 -1
  52. package/vendor/parg/ogc.json +1 -1
  53. package/vendor/parg/oiml.json +1 -1
  54. package/vendor/parg/omg.json +1 -1
  55. package/vendor/parg/parg.json +1 -1
  56. package/vendor/parg/plateau.json +1 -1
  57. package/vendor/parg/sae.json +1 -1
  58. package/vendor/parg/tgpp.json +1 -1
  59. package/vendor/parg/un.json +1 -1
  60. package/vendor/parg/w3c.json +1 -1
  61. package/vendor/parg/xsf.json +1 -1
@@ -36,7 +36,6 @@ class IeeeBuilder {
36
36
  originalInput = "";
37
37
  build(parsed) {
38
38
  const d = Array.isArray(parsed) ? mergeParsedArray(parsed) : parsed;
39
- this.normalizeDraftDashYear(d);
40
39
  if (d["ieee_portion"] !== undefined && d["csa_portion"] !== undefined) {
41
40
  return this.buildCsaDualPublished(d);
42
41
  }
@@ -58,119 +57,236 @@ class IeeeBuilder {
58
57
  return this.buildSingleIdentifier(d, false);
59
58
  }
60
59
  // --- single identifier -----------------------------------------------------
61
- // The artifact binds a dash-year draft tail as a second draft element
62
- // ({draft_version: {dash_year: ...}}); the reference's builder hoists
63
- // the dash fields and rejoins the version ("D9-2006", "/DPub2-2012").
64
- normalizeDraftDashYear(d) {
65
- const raw = d["draft"];
66
- if (raw === undefined)
60
+ // Deep-first collection of every :subpart node under a raw
61
+ // part_subpart_year wrapper (hash or array, any nesting).
62
+ collectSubparts(node, out = []) {
63
+ if (isObj(node)) {
64
+ for (const [key, value] of Object.entries(node)) {
65
+ if (key === "subpart")
66
+ out.push(value);
67
+ else
68
+ this.collectSubparts(value, out);
69
+ }
70
+ }
71
+ else if (Array.isArray(node)) {
72
+ for (const item of node)
73
+ this.collectSubparts(item, out);
74
+ }
75
+ return out;
76
+ }
77
+ // The part/subpart/year captures ride the raw part_subpart_year wrapper
78
+ // (parsanol-ruby#399: the repeated-subpart flat spread clobbers to the
79
+ // last value, so the run does not bind top-level). Hoist the wrapper's
80
+ // part/year and deep-collect its interior subparts, so every consumer —
81
+ // and every raw subtree built through here (the CSA dual's
82
+ // ieee_portion, the multi-numbered primary, the supplement bases) —
83
+ // sees the flat keys it has always read (the reference's
84
+ // unwrap_part_subpart_year).
85
+ unwrapPartSubpartYear(node) {
86
+ const run = node["part_subpart_year"];
87
+ if (run === undefined)
67
88
  return;
68
- const drafts = Array.isArray(raw) ? raw : [raw];
69
- // The compound-stage captures nest inside the draft elements (the
70
- // artifact's binding); the hand mirror bound them top-level, so the
71
- // joint-designator block reads them from `parsed` directly.
72
- for (const key of ["draft_iso_stage", "draft_iso_iteration", "draft_stage_d", "draft_stage_year"]) {
73
- if (d[key] === undefined) {
74
- for (const e of drafts) {
75
- if (isObj(e) && e[key] !== undefined) {
76
- d[key] = e[key];
77
- break;
78
- }
89
+ const nodes = Array.isArray(run) ? run : [run];
90
+ const flat = {};
91
+ for (const item of nodes) {
92
+ if (isObj(item))
93
+ Object.assign(flat, item);
94
+ }
95
+ if (flat["part"] !== undefined)
96
+ node["part"] = flat["part"];
97
+ if (flat["year"] !== undefined)
98
+ node["year"] = flat["year"];
99
+ if (node["subpart"] === undefined) {
100
+ const collected = this.collectSubparts(run);
101
+ if (collected.length > 0)
102
+ node["subpart"] = collected;
103
+ }
104
+ }
105
+ // The edition-2 wasm tree → the native engine's flat shape — the
106
+ // normalization prelude the reference's build_single_identifier runs
107
+ // before every dispatch (builder.rb: the scalar surfacing, the
108
+ // composite rename, the wrapper unwrap, the draft-array repairs and
109
+ // the dash-date hoist).
110
+ normalizeParsedHash(parsedHash) {
111
+ // The wasm bound map captures the optional rev_sep for every printed
112
+ // boundary; the reference's native engine binds it on the bare-REV
113
+ // ("-REV/D1.7" keeps "-") and lettered-revision branches
114
+ // ("-REVmc" keeps "-") and declines it on every numeric revision
115
+ // spelling (".Rev 3", "/rev43", "-rev1", "_Rev2", " Rev 8" — the
116
+ // rev_numbered route, parsanol-ruby#394 family), so the render glues
117
+ // the numeric lettered revisions on those rows.
118
+ if (parsedHash["rev_sep"] !== undefined) {
119
+ const revision = extractValue(parsedHash["revision"]) ?? "";
120
+ if (/^[0-9]+$/.test(revision)) {
121
+ delete parsedHash["rev_sep"];
122
+ }
123
+ }
124
+ // The grammar binds the bare top-level draft clause as
125
+ // draft_version_scalar (string) — surface it under the key the rest
126
+ // of the builder has always read.
127
+ if (parsedHash["draft_version"] === undefined && parsedHash["draft_version_scalar"] !== undefined) {
128
+ parsedHash["draft_version"] = parsedHash["draft_version_scalar"];
129
+ delete parsedHash["draft_version_scalar"];
130
+ }
131
+ // The composite draft rule binds draft_version_composite (raw)
132
+ // inside the draft elements; rename it under the interior key the
133
+ // draft-element walks read.
134
+ const draftElems0 = Array.isArray(parsedHash["draft"])
135
+ ? parsedHash["draft"]
136
+ : [parsedHash["draft"]];
137
+ for (const e of draftElems0) {
138
+ if (isObj(e) && e["draft_version"] === undefined && e["draft_version_composite"] !== undefined) {
139
+ e["draft_version"] = e["draft_version_composite"];
140
+ delete e["draft_version_composite"];
141
+ }
142
+ }
143
+ this.unwrapPartSubpartYear(parsedHash);
144
+ // A revision element INSIDE the draft array is the draft rule's own
145
+ // split artifact (the version's trailing ".1" segment) — the spine
146
+ // captured the same value; drop it.
147
+ if (Array.isArray(parsedHash["draft"]) && parsedHash["revision"] !== undefined) {
148
+ const artifact = parsedHash["draft"].some((e) => isObj(e) && e["revision"] !== undefined &&
149
+ String(extractValue(e["revision"]) ?? "") === String(extractValue(parsedHash["revision"]) ?? ""));
150
+ if (artifact)
151
+ delete parsedHash["revision"];
152
+ }
153
+ // The edition-2 tree materializes a multi-segment draft version
154
+ // ("D2.1.1") as per-token nodes carrying no draft_version key (the
155
+ // v1 tree degraded such a clause to a whole-clause LEAF string when
156
+ // no draft date follows, and to a date-only hash when it does).
157
+ // Normalize to the v1 shape before the stage derivation and draft
158
+ // handling read it.
159
+ if (Array.isArray(parsedHash["draft"])) {
160
+ const elems = parsedHash["draft"].filter(isObj);
161
+ if (elems.length > 0 && elems.every((e) => e["draft_version"] === undefined)) {
162
+ const hasDate = elems.some((e) => e["draft_date_month"] !== undefined || e["draft_date_year"] !== undefined);
163
+ if (hasDate) {
164
+ const dateElems = elems.filter((e) => e["draft_date_month"] !== undefined || e["draft_date_year"] !== undefined || e["draft_date_day"] !== undefined);
165
+ parsedHash["draft"] = Object.assign({}, ...dateElems);
166
+ }
167
+ else {
168
+ parsedHash["draft"] = elems
169
+ .flatMap((e) => Object.values(e))
170
+ .map((v) => String(extractValue(v) ?? ""))
171
+ .join("");
79
172
  }
80
173
  }
81
174
  }
82
- for (const e of drafts) {
83
- if (!isObj(e))
84
- continue;
85
- const dv = e["draft_version"];
86
- if (!isObj(dv))
175
+ const drafts = Array.isArray(parsedHash["draft"])
176
+ ? parsedHash["draft"]
177
+ : [parsedHash["draft"]].filter((v) => v !== undefined);
178
+ // The edition-2 grammar renames the nested draft-version dash
179
+ // captures (draft_version_dash_year/month); hoist them to the
180
+ // element level the dash walk reads, and drop the wrapper key.
181
+ for (const d of drafts) {
182
+ if (!isObj(d) || !isObj(d["draft_version"]))
87
183
  continue;
88
- if (dv["dash_year"] !== undefined)
89
- e["dash_year"] = dv["dash_year"];
90
- if (dv["dash_month"] !== undefined)
91
- e["dash_month"] = dv["dash_month"];
92
- delete e["draft_version"];
93
- }
94
- // A numeric month trailing a draft is the draft's own date: it embeds
95
- // in the designator ("D09, 04 2011" -> "D09-2011-04") rather than
96
- // printing the long-format comma face (a month name keeps the comma,
97
- // "D7.2, Nov 2006") - pubid ruling 2026-10-13.
98
- if (d["dash_year"] === undefined && d["draft"] !== undefined) {
99
- const m = d["month"] ?? d["trailing_month"];
100
- const y = d["year"] ?? d["trailing_year"];
101
- const mVal = extractValue(m);
102
- const yVal = extractValue(y);
103
- if (mVal !== undefined && /^\d{1,2}$/.test(String(mVal)) && yVal !== undefined) {
104
- const host = drafts.find((e) => isObj(e) && e["draft_version"] !== undefined);
184
+ const dv = d["draft_version"];
185
+ if (dv["draft_version_dash_year"] !== undefined || dv["dash_year"] !== undefined) {
186
+ d["dash_year"] = dv["draft_version_dash_year"] ?? dv["dash_year"];
187
+ }
188
+ if (dv["draft_version_dash_month"] !== undefined || dv["dash_month"] !== undefined) {
189
+ d["dash_month"] = dv["draft_version_dash_month"] ?? dv["dash_month"];
190
+ }
191
+ delete d["draft_version"];
192
+ }
193
+ // The dash host: the top-level spine, else a draft element carrying
194
+ // its own dash date (draft_date_dash_year/month on the edition-2
195
+ // grammar, dash_year/month on v1 — read both spellings).
196
+ let dash = parsedHash["dash_year"] !== undefined ? parsedHash : undefined;
197
+ if (dash === undefined) {
198
+ for (const d of drafts) {
199
+ if (isObj(d) && (d["draft_date_dash_year"] !== undefined || d["dash_year"] !== undefined)) {
200
+ dash = d;
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ // A numeric month trailing a draft is the draft's own date: it
206
+ // embeds in the designator ("D09, 04 2011" -> "D09-2011-04") rather
207
+ // than printing the long-format comma face — pubid ruling
208
+ // 2026-10-13.
209
+ if (dash === undefined && parsedHash["draft"] !== undefined) {
210
+ const mVal = extractValue((parsedHash["month"] ?? parsedHash["trailing_month"]));
211
+ const yVal = extractValue((parsedHash["year"] ?? parsedHash["trailing_year"]));
212
+ if (mVal !== undefined && /^\d{1,2}$/.test(mVal) && yVal !== undefined) {
213
+ const host = drafts.find((d) => isObj(d) && d["draft_version"] !== undefined);
105
214
  if (host !== undefined && isObj(host)) {
106
215
  const hv = host["draft_version"];
107
216
  const joined = Array.isArray(hv)
108
- ? hv.map((v) => (isObj(v) ? (v["value"] ?? "") : String(v))).join("")
217
+ ? hv.map((v) => extractValue(v) ?? "").join("")
109
218
  : String(extractValue(hv) ?? "");
110
- host["draft_version"] = [joined, String(yVal), String(mVal)]
111
- .filter((s) => s !== "")
112
- .join("-");
113
- delete d["month"];
114
- delete d["year"];
115
- delete d["trailing_month"];
116
- delete d["trailing_year"];
219
+ host["draft_version"] = [joined, yVal, mVal].filter((s) => s !== "").join("-");
220
+ delete parsedHash["month"];
221
+ delete parsedHash["year"];
222
+ delete parsedHash["trailing_month"];
223
+ delete parsedHash["trailing_year"];
117
224
  }
118
225
  }
119
226
  }
120
- const dash = drafts.find((e) => isObj(e) && e["dash_year"] !== undefined) ??
121
- (d["dash_year"] !== undefined ? d : undefined);
122
227
  if (dash === undefined)
123
228
  return;
229
+ // The draft's own dash date embeds onto the draft even when a base
230
+ // identity year is present ("…-2011/D4-2015-07"); only the root-year
231
+ // assignment waits for an unoccupied year slot.
124
232
  const objs = drafts.filter(isObj);
125
233
  const joined = objs
126
234
  .map((e) => e["draft_version"])
127
235
  .filter((v) => v !== undefined)
128
- .map((v) => (isObj(v) ? (v["value"] ?? "") : String(v)))
236
+ .map((v) => (Array.isArray(v) ? v.map((x) => extractValue(x) ?? "").join("") : String(extractValue(v) ?? "")))
129
237
  .join("");
130
- const bareJointDraft = d["joint_publishers"] !== undefined &&
131
- d["part"] === undefined && d["iso_stage"] === undefined &&
238
+ const dashY = extractValue((dash["dash_year"] ?? dash["draft_date_dash_year"]));
239
+ const dashM = extractValue((dash["dash_month"] ?? dash["draft_date_dash_month"]));
240
+ const clearDash = () => {
241
+ delete dash["dash_year"];
242
+ delete dash["dash_month"];
243
+ delete dash["draft_date_dash_year"];
244
+ delete dash["draft_date_dash_month"];
245
+ };
246
+ const embed = () => {
247
+ const host = drafts.find((d) => isObj(d) && d["draft_version"] !== undefined);
248
+ if (host === undefined || !isObj(host))
249
+ return undefined;
250
+ const hv = host["draft_version"];
251
+ const vJoined = Array.isArray(hv)
252
+ ? hv.map((v) => extractValue(v) ?? "").join("")
253
+ : String(extractValue(hv) ?? "");
254
+ host["draft_version"] = [vJoined, dashY, dashM]
255
+ .filter((s) => s !== undefined)
256
+ .join("-");
257
+ return host;
258
+ };
259
+ const bareJointDraft = parsedHash["joint_publishers"] !== undefined &&
260
+ parsedHash["part"] === undefined && parsedHash["iso_stage"] === undefined &&
132
261
  objs.some((e) => e["draft_version"] !== undefined);
133
262
  const dashLed = joined.startsWith("-");
134
- const dashYear = extractValue(dash["dash_year"]);
135
- const dashMonth = dash["dash_month"];
136
263
  if (bareJointDraft && dashLed) {
137
- d["part"] = dashYear;
138
- }
139
- else if (dashMonth !== undefined || dashLed) {
140
- // A dash_month beside a draft host is the draft's own date
141
- // ("/R-i-2011-04" -> "D09-2011-04"); with no host it is the
142
- // identifier's year/month.
143
- const host = objs.find((e) => e["draft_version"] !== undefined);
144
- const dm = extractValue(dashMonth);
145
- if (dm !== undefined && host !== undefined) {
146
- const hv = host["draft_version"];
147
- const joined = Array.isArray(hv)
148
- ? hv.map((v) => (isObj(v) ? (v["value"] ?? "") : String(v))).join("")
149
- : String(extractValue(hv) ?? "");
150
- host["draft_version"] = [joined, dashYear, String(dm)].join("-");
151
- delete dash["dash_year"];
152
- delete dash["dash_month"];
153
- }
154
- else if (d["year"] === undefined) {
155
- d["year"] = dashYear;
156
- d["month"] = dm;
157
- }
264
+ parsedHash["part"] = { value: dashY };
265
+ }
266
+ else if (dashM !== undefined && embed() !== undefined) {
267
+ clearDash();
268
+ }
269
+ else if ((dashM !== undefined || dashLed) && parsedHash["year"] === undefined) {
270
+ parsedHash["year"] = dashY;
271
+ parsedHash["month"] = dashM;
272
+ }
273
+ else if (embed() !== undefined) {
274
+ clearDash();
275
+ // the hyphenated "/D-2-2007" spelling's year the spine captured at
276
+ // top level is the draft's own — vacate it
277
+ if (extractValue(parsedHash["year"]) === dashY)
278
+ delete parsedHash["year"];
158
279
  }
159
280
  else {
160
- const host = objs.find((e) => e["draft_version"] !== undefined);
161
- if (host !== undefined) {
162
- host["draft_version"] = `${joined}-${dashYear}`;
163
- delete dash["dash_year"];
164
- delete dash["dash_month"];
165
- }
166
- else if (d["year"] === undefined) {
167
- d["year"] = dashYear;
168
- d["month"] = extractValue(dashMonth);
169
- }
281
+ parsedHash["year"] = dashY;
282
+ parsedHash["month"] = dashM;
170
283
  }
171
284
  }
172
285
  buildSingleIdentifier(parsed, buildingSecondary) {
173
286
  const parsedHash = asHash(parsed);
287
+ // The edition-2 wasm tree → the native engine's flat shape, before
288
+ // every dispatch reads it (the reference's prelude).
289
+ this.normalizeParsedHash(parsedHash);
174
290
  if (!buildingSecondary && parsedHash["primary_identifier"] !== undefined &&
175
291
  (parsedHash["secondary_crossref"] !== undefined || parsedHash["secondary_joint"] !== undefined)) {
176
292
  return this.buildMultiNumbered(parsedHash);
@@ -256,6 +372,8 @@ class IeeeBuilder {
256
372
  attributes["parts"] = codeObj.parts;
257
373
  if (codeObj?.originalSeparator !== undefined)
258
374
  attributes["separator"] = codeObj.originalSeparator;
375
+ if (codeObj !== undefined)
376
+ attributes["__code_obj"] = codeObj;
259
377
  // P = project (the document is a draft): the marker is
260
378
  // identity-bearing, so stripping the letter must not strip the
261
379
  // fact (docs/IEEE-DRAFT-STAGES.md §1.1).
@@ -264,7 +382,15 @@ class IeeeBuilder {
264
382
  }
265
383
  delete attributes["__code"];
266
384
  this.renameSupplementKeys(attributes);
267
- return new klass(attributes);
385
+ // The parse path carries the freshly parsed Code (per-part
386
+ // separators — the reference's `code:` string → Components::Code.parse
387
+ // in base #initialize); the column-only rebuild stays the from_hash
388
+ // face.
389
+ const built = new klass(attributes);
390
+ if (attributes["__code_obj"] instanceof IeeeCode) {
391
+ built["__code_obj"] = attributes["__code_obj"];
392
+ }
393
+ return built;
268
394
  }
269
395
  renameSupplementKeys(attributes) {
270
396
  const ordinal = attributes["cor_number"] ?? attributes["amd_number"] ?? attributes["conf_number"];
@@ -368,11 +494,10 @@ class IeeeBuilder {
368
494
  }
369
495
  if (codeStr !== undefined && codeParts.length > 0) {
370
496
  // Captures carry their own separator when present (the artifact's
371
- // part/subpart bind ".18" / "-10"); bare tokens fall back to the
372
- // hand grammar's part_sep. Joins verbatim like the reference's
373
- // extract_attributes — a dashed "62271-37-082" stays dashed.
374
- const partSep = extractValue(parsed["part_sep"]) ?? ".";
375
- codeStr += codeParts.map((cp) => /^[._-]/.test(cp) ? cp : `${partSep}${cp}`).join("");
497
+ // part/subpart bind ".18" / "-10"); join verbatim like the
498
+ // reference's extract_attributes — a dashed "62271-37-082" stays
499
+ // dashed.
500
+ codeStr += codeParts.join("");
376
501
  }
377
502
  if (codeStr !== undefined && parsed["year"] === undefined && codeParts.length === 0) {
378
503
  const m = /^(.+)-(\d{4})$/.exec(codeStr);
@@ -388,6 +513,7 @@ class IeeeBuilder {
388
513
  if (codeStr !== undefined)
389
514
  attributes["__code"] = codeStr;
390
515
  if (codeObj !== undefined) {
516
+ attributes["__code_obj"] = codeObj;
391
517
  attributes["number"] = codeObj.number;
392
518
  if (codeObj.prefix !== undefined)
393
519
  attributes["prefix"] = codeObj.prefix;
@@ -460,8 +586,11 @@ class IeeeBuilder {
460
586
  if (fdraft !== undefined && typeof fdraft === "object" && !Array.isArray(fdraft)) {
461
587
  const fdStage = extractValue(fdraft["fdraft_stage"]);
462
588
  if (fdStage !== undefined) {
463
- const fdYear = extractValue(fdraft["year"]);
464
- const fdMonth = extractValue(fdraft["month"]);
589
+ // fdraft's month/year rename with the fdraft_ prefix on the
590
+ // edition-2 grammar (nested-only); the v1 spellings stay as the
591
+ // fallback.
592
+ const fdYear = extractValue((fdraft["fdraft_year"] ?? fdraft["year"]));
593
+ const fdMonth = extractValue((fdraft["fdraft_month"] ?? fdraft["month"]));
465
594
  let date = "";
466
595
  if (fdYear !== undefined) {
467
596
  date = `-${fdYear}`;
@@ -597,10 +726,16 @@ class IeeeBuilder {
597
726
  if (version !== undefined)
598
727
  version = version.replace(/^-/, "").replace(/^0(?=\d+\.\d)/, "");
599
728
  }
600
- // The revision capture carries its own leading dot (".1" — the
601
- // #471 separator-in-capture rule); Draft#to_s prints the dot
602
- // itself, so keep only the tail or "D6..1" renders doubled.
603
- revision = extractValue(draftData["revision"])?.replace(/^\./, "");
729
+ // The dotted ordinal stays part of the designator (the #601
730
+ // convergence): a split field would make the parse and from_hash
731
+ // paths build different Draft objects on wire-identical rows.
732
+ // #to_s renders version + revision identically, so the whole tail
733
+ // rides the version on every route.
734
+ if (draftData["revision"] !== undefined && version !== undefined) {
735
+ const tail = extractValue(draftData["revision"])?.replace(/^\./, "");
736
+ if (tail !== undefined && tail !== "")
737
+ version = `${version}.${tail}`;
738
+ }
604
739
  isoStage = extractValue(draftData["draft_iso_stage"]);
605
740
  isoIteration = extractValue(draftData["draft_iso_iteration"]);
606
741
  // FDIS/CDV/… are ISO/IEC stage tokens, not draft ordinals (C1
@@ -610,8 +745,15 @@ class IeeeBuilder {
610
745
  // Draft#to_s renders as the D= designator face ("…/D=DIS-2020").
611
746
  // The glued month-name date ("-June2010", the 2026-10-19 FCD
612
747
  // ruling) rides the designator's date slot with the month numeric.
613
- month = extractValue(draftData["month"]);
614
- year = extractValue(draftData["year"]);
748
+ // The edition-2 grammar renames draft_date's nested month/year/day
749
+ // captures with the draft_date_ prefix (kept off the identity
750
+ // keys); the v1 spellings stay as the fallback.
751
+ const monthSlice = draftData["draft_date_month"] ?? draftData["month"];
752
+ month = extractValue(monthSlice);
753
+ const yearSlice = draftData["draft_date_year"] ?? draftData["year"];
754
+ year = yearSlice !== undefined ? extractValue(yearSlice) : undefined;
755
+ const daySlice = draftData["draft_date_day"] ?? draftData["day"];
756
+ day = daySlice !== undefined ? extractValue(daySlice) : undefined;
615
757
  if (version !== undefined) {
616
758
  // The stage iteration rides glued onto the word ("DIS2" — the
617
759
  // lossless face, pubid#477; the 2026-10-19 DIS2 ruling).
@@ -631,7 +773,6 @@ class IeeeBuilder {
631
773
  if (year !== undefined && /^\d{2}$/.test(year)) {
632
774
  year = `${Number(year) < 30 ? 20 : 19}${year}`;
633
775
  }
634
- day = extractValue(draftData["day"]);
635
776
  if (this.originalInput !== "" && month !== undefined) {
636
777
  commaBeforeMonth = new RegExp(`,\\s+${month.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i")
637
778
  .test(this.originalInput);
@@ -656,7 +797,8 @@ class IeeeBuilder {
656
797
  // with the month canonicalized numeric (pubid ruling 2026-10-14):
657
798
  // "P802.11-REVmc/D8.0, Aug 2016" renders ".../D8.0-2016-08".
658
799
  // Drafts without a REV keep the comma face.
659
- if ((attributes["revision"] !== undefined || attributes["rev_bare"] === true) &&
800
+ if ((attributes["revision"] !== undefined || attributes["rev_bare"] === true ||
801
+ attributes["cor_number"] !== undefined || attributes["cor_year"] !== undefined) &&
660
802
  month !== undefined && year !== undefined && version !== undefined) {
661
803
  const monthNumber = DRAFT_MONTH_NAMES[String(month).replace(/\.$/, "")];
662
804
  if (monthNumber !== undefined) {
@@ -753,7 +895,13 @@ class IeeeBuilder {
753
895
  }
754
896
  handleReaffirmed(parsed, attributes) {
755
897
  if (isObj(parsed["reaffirmed"])) {
756
- attributes["reaffirmed"] = extractValue(parsed["reaffirmed"]["year"]);
898
+ // The edition-2 grammar renames the nested year capture
899
+ // (reaffirmed_year) so the reaffirmation year cannot leak onto the
900
+ // identity year key; the v1 spelling stays as the fallback.
901
+ const reaf = parsed["reaffirmed"];
902
+ const value = reaf["reaffirmed_year"] ?? reaf["year"];
903
+ if (value !== undefined)
904
+ attributes["reaffirmed"] = extractValue(value);
757
905
  }
758
906
  else if (parsed["reaffirmed"] !== undefined) {
759
907
  attributes["reaffirmed"] = extractValue(parsed["reaffirmed"]);
@@ -823,6 +971,9 @@ class IeeeBuilder {
823
971
  }
824
972
  // --- supplements and wrappers ----------------------------------------------
825
973
  reconstructBaseString(baseData) {
974
+ // The part/subpart/year captures sit one level down, inside the raw
975
+ // part_subpart_year wrapper — hoist them before the flat reads.
976
+ this.unwrapPartSubpartYear(baseData);
826
977
  const baseParts = [];
827
978
  if (baseData["publishers"] !== undefined) {
828
979
  const pubData = asHash(baseData["publishers"]);
@@ -981,6 +1132,7 @@ class IeeeBuilder {
981
1132
  if (codeStr !== undefined) {
982
1133
  const codeObj = IeeeCode.parse(codeStr);
983
1134
  if (codeObj !== undefined) {
1135
+ attributes["__code_obj"] = codeObj;
984
1136
  attributes["number"] = codeObj.number;
985
1137
  if (codeObj.prefix !== undefined)
986
1138
  attributes["prefix"] = codeObj.prefix;
@@ -1208,7 +1360,11 @@ class IeeeBuilder {
1208
1360
  attributes["project_marker"] = true;
1209
1361
  parsed["project_marker"] = "P";
1210
1362
  }
1211
- if (parsed["iso_published"] !== undefined && !pWithDraft &&
1363
+ if (
1364
+ // The bound tree spreads the raw iso_published interior flat, so a
1365
+ // stage-first joint row carries a top-level iso_stage — the stage
1366
+ // word keeps the joint ISO face (the reference's iso_stage.nil? gate).
1367
+ parsed["iso_published"] !== undefined && parsed["iso_stage"] === undefined && !pWithDraft &&
1212
1368
  (parsed["printed_dash_year"] !== undefined || parsed["printed_month_year"] !== undefined ||
1213
1369
  (numericDraft && parsed["draft_month"] !== undefined) ||
1214
1370
  (parsed["year"] === undefined && parsed["parameters"] !== undefined &&
@@ -1260,6 +1416,7 @@ class IeeeBuilder {
1260
1416
  }
1261
1417
  const codeObj = IeeeCode.parse(finalCode);
1262
1418
  if (codeObj !== undefined) {
1419
+ printedAttrs["__code_obj"] = codeObj;
1263
1420
  printedAttrs["number"] = codeObj.number;
1264
1421
  if (codeObj.prefix !== undefined)
1265
1422
  printedAttrs["prefix"] = codeObj.prefix;
@@ -1283,7 +1440,11 @@ class IeeeBuilder {
1283
1440
  if (printedAttrs[k] === undefined)
1284
1441
  delete printedAttrs[k];
1285
1442
  }
1286
- return new StandardClass(printedAttrs);
1443
+ const printed = new StandardClass(printedAttrs);
1444
+ if (printedAttrs["__code_obj"] instanceof IeeeCode) {
1445
+ printed["__code_obj"] = printedAttrs["__code_obj"];
1446
+ }
1447
+ return printed;
1287
1448
  }
1288
1449
  // A stage word with a glued year tail ("/D=CD2-201802") rides the
1289
1450
  // D= designator with the IEEE lead (the reference's builder, the
@@ -1434,7 +1595,21 @@ class IeeeBuilder {
1434
1595
  if (attributes["draft_status"] !== undefined) {
1435
1596
  projectAttrs["draft_status"] = attributes["draft_status"];
1436
1597
  }
1437
- return new ProjectDraftIdentifierClass(projectAttrs);
1598
+ // Ruby's draft_attrs[:code] = "P" + code re-strips in
1599
+ // ProjectDraftIdentifier#initialize and parses the stripped string —
1600
+ // rebuild the runtime code obj from the P-less columns the same way.
1601
+ const strippedCode = new IeeeCode({
1602
+ number: projectAttrs["number"],
1603
+ prefix: projectAttrs["prefix"],
1604
+ parts: projectAttrs["parts"],
1605
+ original_separator: projectAttrs["separator"],
1606
+ }).render();
1607
+ const strippedObj = IeeeCode.parse(strippedCode);
1608
+ if (strippedObj !== undefined)
1609
+ projectAttrs["__code_obj"] = strippedObj;
1610
+ const projectBuilt = new ProjectDraftIdentifierClass(projectAttrs);
1611
+ projectBuilt["__code_obj"] = strippedObj;
1612
+ return projectBuilt;
1438
1613
  }
1439
1614
  // The iso_published joint rows normalize the publisher order to
1440
1615
  // ISO-first (the reference: "IEEE/ISO/IEC 8802-3:2021/Amd 4-2021"
@@ -1458,11 +1633,15 @@ class IeeeBuilder {
1458
1633
  attributes["copublisher"] = attributes["publishers"].slice(1);
1459
1634
  }
1460
1635
  const joint = new JointDevelopmentClass(attributes);
1636
+ if (attributes["__code_obj"] instanceof IeeeCode) {
1637
+ joint["__code_obj"] = attributes["__code_obj"];
1638
+ }
1639
+ const jointIdentifier = joint;
1461
1640
  if (parsed["amd_number"] === undefined)
1462
- return joint;
1641
+ return jointIdentifier;
1463
1642
  const amdYear = parsed["amd_year"] !== undefined ? extractValue(parsed["amd_year"]) : undefined;
1464
1643
  return new AmendmentClass({
1465
- base: joint,
1644
+ base: jointIdentifier,
1466
1645
  number: extractValue(parsed["amd_number"]),
1467
1646
  year: amdYear,
1468
1647
  });
@@ -1470,8 +1649,19 @@ class IeeeBuilder {
1470
1649
  buildSiPsi(parsed) {
1471
1650
  const attributes = {};
1472
1651
  const siType = extractValue(parsed["si_type"]);
1473
- if (parsed["publishers"] !== undefined) {
1474
- const publishersStr = extractValue(parsed["publishers"]) ?? "";
1652
+ // The bare top-level draft clause binds as draft_version_scalar
1653
+ // (string) — surface it under the key this builder reads (the
1654
+ // reference's build_si_psi_identifier).
1655
+ if (parsed["draft_version"] === undefined && parsed["draft_version_scalar"] !== undefined) {
1656
+ parsed["draft_version"] = parsed["draft_version_scalar"];
1657
+ delete parsed["draft_version_scalar"];
1658
+ }
1659
+ // Extract publishers (should be "IEEE/ASTM") — the SI rule's bare
1660
+ // publishers clause binds as publishers_scalar (string); the
1661
+ // composite joint rows deliver publishers (raw).
1662
+ const pubSrc = parsed["publishers"] ?? parsed["publishers_scalar"];
1663
+ if (pubSrc !== undefined) {
1664
+ const publishersStr = extractValue(pubSrc) ?? "";
1475
1665
  const pubs = publishersStr.split("/");
1476
1666
  attributes["publisher"] = pubs[0];
1477
1667
  if (pubs.length > 1)
@@ -1481,6 +1671,7 @@ class IeeeBuilder {
1481
1671
  if (codeStr !== undefined) {
1482
1672
  const codeObj = IeeeCode.parse(codeStr);
1483
1673
  if (codeObj !== undefined) {
1674
+ attributes["__code_obj"] = codeObj;
1484
1675
  attributes["number"] = codeObj.number;
1485
1676
  if (codeObj.prefix !== undefined)
1486
1677
  attributes["prefix"] = codeObj.prefix;
@@ -1504,7 +1695,11 @@ class IeeeBuilder {
1504
1695
  attributes["relationships"] = this.buildRelationships(parsed);
1505
1696
  }
1506
1697
  this.handleParameters(parsed, attributes);
1507
- return new SiStandardClass(attributes);
1698
+ const siBuilt = new SiStandardClass(attributes);
1699
+ if (attributes["__code_obj"] instanceof IeeeCode) {
1700
+ siBuilt["__code_obj"] = attributes["__code_obj"];
1701
+ }
1702
+ return siBuilt;
1508
1703
  }
1509
1704
  buildMultiNumbered(parsed) {
1510
1705
  const primaryId = this.buildSingleIdentifier(asHash(parsed["primary_identifier"]), false);
@@ -1661,6 +1856,7 @@ class IeeeBuilder {
1661
1856
  if (codeStr !== undefined) {
1662
1857
  const codeObj = IeeeCode.parse(codeStr);
1663
1858
  if (codeObj !== undefined) {
1859
+ attributes["__code_obj"] = codeObj;
1664
1860
  attributes["number"] = codeObj.number;
1665
1861
  if (codeObj.prefix !== undefined)
1666
1862
  attributes["prefix"] = codeObj.prefix;
@@ -1678,7 +1874,11 @@ class IeeeBuilder {
1678
1874
  if (separatorStr !== undefined)
1679
1875
  attributes["date_separator"] = separatorStr;
1680
1876
  attributes["original_format"] = separatorStr !== undefined || monthStr !== undefined ? "long" : "short";
1681
- return new AieeClass(attributes);
1877
+ const aieeBuilt = new AieeClass(attributes);
1878
+ if (attributes["__code_obj"] instanceof IeeeCode) {
1879
+ aieeBuilt["__code_obj"] = attributes["__code_obj"];
1880
+ }
1881
+ return aieeBuilt;
1682
1882
  }
1683
1883
  buildIre(parsed) {
1684
1884
  const attributes = {};
@@ -1800,7 +2000,15 @@ let cachedArtifactRuntime;
1800
2000
  function parseSource(input) {
1801
2001
  cachedArtifactRuntime ??= PubidRuntime.load("ieee");
1802
2002
  try {
1803
- return artifactToBuilderTree(cachedArtifactRuntime.parseShape(input));
2003
+ // The edition-2 artifact's bindings block produces the builder-ready
2004
+ // hash at the engine (typed scalars + raw wrappers) — the same bound
2005
+ // tree the reference's Pubid::Parg::Model.bound hands its builder.
2006
+ const bound = cachedArtifactRuntime.parseAndBind(input);
2007
+ // A rejected input binds to null (the parseShape contract threw) —
2008
+ // keep the throw so the reference's ParseFailed-keyed fallbacks fire.
2009
+ if (bound === null || bound === undefined)
2010
+ throw new ParseFailed("no match", 0);
2011
+ return artifactToBuilderTree(bound);
1804
2012
  }
1805
2013
  catch (e) {
1806
2014
  // The engine throws its own error class; the reference's fallback