@pubid/pubid 0.1.8 → 0.1.10

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 (76) hide show
  1. package/artifact/corpus.json.gz +0 -0
  2. package/dist/flavors/cen_cenelec/grammar.js +10 -4
  3. package/dist/flavors/cen_cenelec/index.js +20 -1
  4. package/dist/flavors/cen_cenelec/model.d.ts +1 -0
  5. package/dist/flavors/cen_cenelec/model.js +4 -1
  6. package/dist/flavors/evs/grammar.js +53 -7
  7. package/dist/flavors/evs/index.js +74 -7
  8. package/dist/flavors/evs/model.d.ts +37 -1
  9. package/dist/flavors/evs/model.js +173 -11
  10. package/dist/flavors/ieee/artifact_tree.d.ts +2 -0
  11. package/dist/flavors/ieee/artifact_tree.js +36 -0
  12. package/dist/flavors/ieee/grammar.js +99 -19
  13. package/dist/flavors/ieee/index.js +353 -18
  14. package/dist/flavors/ieee/model.d.ts +5 -0
  15. package/dist/flavors/ieee/model.js +132 -20
  16. package/dist/flavors/ieee/preprocessor.js +28 -6
  17. package/dist/flavors/nist/grammar.js +18 -9
  18. package/dist/flavors/nist/index.js +16 -7
  19. package/dist/flavors/nist/preprocessor.js +9 -1
  20. package/dist/parg/runtime.d.ts +25 -0
  21. package/dist/parg/runtime.js +76 -0
  22. package/dist/parg/suite.d.ts +4 -0
  23. package/dist/parg/suite.js +14 -0
  24. package/dist/urn/parsers.js +7 -0
  25. package/package.json +6 -4
  26. package/vendor/parg/adobe.json +1 -0
  27. package/vendor/parg/aiee.json +1 -0
  28. package/vendor/parg/amca.json +1 -0
  29. package/vendor/parg/ansi.json +1 -0
  30. package/vendor/parg/api.json +1 -0
  31. package/vendor/parg/ashrae.json +1 -0
  32. package/vendor/parg/asme.json +1 -0
  33. package/vendor/parg/astm.json +1 -0
  34. package/vendor/parg/bipm.json +1 -0
  35. package/vendor/parg/bsi.json +1 -0
  36. package/vendor/parg/calconnect.json +1 -0
  37. package/vendor/parg/ccsds.json +1 -0
  38. package/vendor/parg/cen_cenelec.json +1 -0
  39. package/vendor/parg/cie.json +1 -0
  40. package/vendor/parg/csa.json +1 -0
  41. package/vendor/parg/doi.json +1 -0
  42. package/vendor/parg/easc.json +1 -0
  43. package/vendor/parg/ecma.json +1 -0
  44. package/vendor/parg/etsi.json +1 -0
  45. package/vendor/parg/evs.json +1 -0
  46. package/vendor/parg/gb.json +1 -0
  47. package/vendor/parg/gost.json +1 -0
  48. package/vendor/parg/iala.json +1 -0
  49. package/vendor/parg/iana.json +1 -0
  50. package/vendor/parg/idf.json +1 -0
  51. package/vendor/parg/iec.json +1 -0
  52. package/vendor/parg/ieee.json +1 -0
  53. package/vendor/parg/ietf.json +1 -0
  54. package/vendor/parg/ire.json +1 -0
  55. package/vendor/parg/isbn.json +1 -0
  56. package/vendor/parg/iso.json +1 -0
  57. package/vendor/parg/itu.json +1 -0
  58. package/vendor/parg/jcgm.json +1 -0
  59. package/vendor/parg/jis.json +1 -0
  60. package/vendor/parg/nesc.json +1 -0
  61. package/vendor/parg/nist.json +1 -0
  62. package/vendor/parg/oasis.json +1 -0
  63. package/vendor/parg/ogc.json +1 -0
  64. package/vendor/parg/oiml.json +1 -0
  65. package/vendor/parg/omg.json +1 -0
  66. package/vendor/parg/parg.json +1 -0
  67. package/vendor/parg/plateau.json +1 -0
  68. package/vendor/parg/sae.json +1 -0
  69. package/vendor/parg/tgpp.json +1 -0
  70. package/vendor/parg/un.json +1 -0
  71. package/vendor/parg/w3c.json +1 -0
  72. package/vendor/parg/xsf.json +1 -0
  73. package/dist/pg/runtime.d.ts +0 -85
  74. package/dist/pg/runtime.js +0 -170
  75. package/dist/pg/suite.d.ts +0 -20
  76. package/dist/pg/suite.js +0 -90
@@ -1,9 +1,11 @@
1
1
  import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
2
+ import { PubidRuntime } from "../../parg/runtime.js";
3
+ import { artifactToBuilderTree } from "./artifact_tree.js";
2
4
  import { grammarImplementation } from "../index.js";
3
5
  import { ieeeGrammar } from "./grammar.js";
4
6
  import { preParse, preprocessIeee } from "./preprocessor.js";
5
7
  import { applyUpdateCodes } from "./update_codes.js";
6
- import { AdoptedStandardClass, AieeClass, AmendmentClass, ConformanceClass, CorrigendumClass, CsaDualPublishedClass, DualPublishedClass, IecIeeeCopublishedClass, IeeeCode, IeeeDraft, IeeeRelationship, IeeeTypedStage, InterpretationClass, IreClass, JointDevelopmentClass, MultiNumberedClass, NescDraftClass, NescEditionClass, NescHandbookClass, NescRedlineClass, NescStandardClass, ProjectDraftIdentifierClass, SiStandardClass, StandardClass, locateStage, } from "./model.js";
8
+ import { AdoptedStandardClass, AieeClass, AmendmentClass, ConformanceClass, CorrigendumClass, CsaDualPublishedClass, DualPublishedClass, IecIeeeCopublishedClass, IeeeCode, IeeeDraft, IeeeRelationship, IeeeTypedStage, InterpretationClass, IreClass, JointDevelopmentClass, MultiNumberedClass, NescDraftClass, NescEditionClass, NescHandbookClass, NescPreprintClass, NescRedlineClass, NescStandardClass, DRAFT_MONTH_NAMES, ProjectDraftIdentifierClass, SiStandardClass, StandardClass, locateStage, } from "./model.js";
7
9
  /**
8
10
  * Port of lib/pubid/ieee/{builder}.rb + Identifier.parse orchestration
9
11
  * (pre-parser dispatch, adopted/dual constructors, sub-builders).
@@ -34,6 +36,7 @@ class IeeeBuilder {
34
36
  originalInput = "";
35
37
  build(parsed) {
36
38
  const d = Array.isArray(parsed) ? mergeParsedArray(parsed) : parsed;
39
+ this.normalizeDraftDashYear(d);
37
40
  if (d["ieee_portion"] !== undefined && d["csa_portion"] !== undefined) {
38
41
  return this.buildCsaDualPublished(d);
39
42
  }
@@ -55,6 +58,117 @@ class IeeeBuilder {
55
58
  return this.buildSingleIdentifier(d, false);
56
59
  }
57
60
  // --- 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)
67
+ 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
+ }
79
+ }
80
+ }
81
+ }
82
+ for (const e of drafts) {
83
+ if (!isObj(e))
84
+ continue;
85
+ const dv = e["draft_version"];
86
+ if (!isObj(dv))
87
+ 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);
105
+ if (host !== undefined && isObj(host)) {
106
+ const hv = host["draft_version"];
107
+ const joined = Array.isArray(hv)
108
+ ? hv.map((v) => (isObj(v) ? (v["value"] ?? "") : String(v))).join("")
109
+ : 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"];
117
+ }
118
+ }
119
+ }
120
+ const dash = drafts.find((e) => isObj(e) && e["dash_year"] !== undefined) ??
121
+ (d["dash_year"] !== undefined ? d : undefined);
122
+ if (dash === undefined)
123
+ return;
124
+ const objs = drafts.filter(isObj);
125
+ const joined = objs
126
+ .map((e) => e["draft_version"])
127
+ .filter((v) => v !== undefined)
128
+ .map((v) => (isObj(v) ? (v["value"] ?? "") : String(v)))
129
+ .join("");
130
+ const bareJointDraft = d["joint_publishers"] !== undefined &&
131
+ d["part"] === undefined && d["iso_stage"] === undefined &&
132
+ objs.some((e) => e["draft_version"] !== undefined);
133
+ const dashLed = joined.startsWith("-");
134
+ const dashYear = extractValue(dash["dash_year"]);
135
+ const dashMonth = dash["dash_month"];
136
+ 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
+ }
158
+ }
159
+ 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
+ }
170
+ }
171
+ }
58
172
  buildSingleIdentifier(parsed, buildingSecondary) {
59
173
  const parsedHash = asHash(parsed);
60
174
  if (!buildingSecondary && parsedHash["primary_identifier"] !== undefined &&
@@ -98,6 +212,27 @@ class IeeeBuilder {
98
212
  if (parsedHash["joint_publishers"] !== undefined || parsedHash["iso_stage"] !== undefined) {
99
213
  return this.buildJointDevelopment(parsedHash);
100
214
  }
215
+ // The ORDINAL-LESS D= designator on a multi-publisher row is the
216
+ // joint compound form ("ISO/IEC/IEEE P16326:2017/D=WD.4a"); the
217
+ // ordinal compound ("D8=DIS.3") is a project draft whatever the
218
+ // publishers — the D-ordinal is the draft, the stage the target.
219
+ if (parsedHash["draft_iso_stage"] !== undefined) {
220
+ const draftHash = parsedHash["draft"] !== undefined
221
+ ? asHash(parsedHash["draft"])
222
+ : undefined;
223
+ const hasOrdinal = extractValue(parsedHash["draft_version"]) !== undefined ||
224
+ extractValue(draftHash?.["draft_version"]) !== undefined;
225
+ if (!hasOrdinal) {
226
+ const pubData = parsedHash["publishers"] !== undefined
227
+ ? asHash(parsedHash["publishers"])
228
+ : undefined;
229
+ const copubs = pubData?.["copublishers"] !== undefined
230
+ ? (Array.isArray(pubData["copublishers"]) ? pubData["copublishers"] : [pubData["copublishers"]])
231
+ : [];
232
+ if (copubs.length > 0)
233
+ return this.buildJointDevelopment(parsedHash);
234
+ }
235
+ }
101
236
  const attributes = this.extractAttributes(parsedHash);
102
237
  if (parsedHash["relationship_type"] !== undefined || parsedHash["relationship_clause"] !== undefined) {
103
238
  attributes["relationships"] = this.buildRelationships(parsedHash);
@@ -215,7 +350,12 @@ class IeeeBuilder {
215
350
  codeStr = codeStr.replace(/^P/, "");
216
351
  }
217
352
  if (codeStr !== undefined && codeParts.length > 0) {
218
- codeStr += `.${codeParts.join(".")}`;
353
+ // Captures carry their own separator when present (the artifact's
354
+ // part/subpart bind ".18" / "-10"); bare tokens fall back to the
355
+ // hand grammar's part_sep. Joins verbatim like the reference's
356
+ // extract_attributes — a dashed "62271-37-082" stays dashed.
357
+ const partSep = extractValue(parsed["part_sep"]) ?? ".";
358
+ codeStr += codeParts.map((cp) => /^[._-]/.test(cp) ? cp : `${partSep}${cp}`).join("");
219
359
  }
220
360
  if (codeStr !== undefined && parsed["year"] === undefined && codeParts.length === 0) {
221
361
  const m = /^(.+)-(\d{4})$/.exec(codeStr);
@@ -261,7 +401,15 @@ class IeeeBuilder {
261
401
  /^IEEE\s/.test(this.originalInput)) {
262
402
  typeValue = "Std";
263
403
  }
264
- if (typeValue !== undefined)
404
+ // The canonical type word is the short form (C2 ruling):
405
+ // "IEEE Standard 495-2007" and "IEEE Std 495-2007" are the same
406
+ // identifier. A DRAFT type word never reaches the wire (the
407
+ // draft-ness rides draft_status + the draft component; "Draft Std"
408
+ // is banned from canonical output) - it still feeds
409
+ // determineStageAbbr below (the version-less D1 fabrication).
410
+ if (typeValue === "Standard")
411
+ typeValue = "Std";
412
+ if (typeValue !== undefined && !/^Draft/.test(typeValue))
265
413
  attributes["type"] = typeValue;
266
414
  const typedStageAbbr = this.determineStageAbbr(typeValue, parsed);
267
415
  if (typedStageAbbr !== undefined) {
@@ -270,11 +418,13 @@ class IeeeBuilder {
270
418
  attributes["typed_stage"] = new IeeeTypedStage(record);
271
419
  }
272
420
  attributes["draft_status"] = draftStatusValue;
273
- for (const key of ["edition", "revision"]) {
421
+ for (const key of ["edition", "revision", "rev_sep"]) {
274
422
  const value = extractValue(parsed[key]);
275
423
  if (value !== undefined)
276
424
  attributes[key] = value;
277
425
  }
426
+ if (parsed["rev_bare"] !== undefined)
427
+ attributes["rev_bare"] = true;
278
428
  if (attributes["edition_month"] === undefined) {
279
429
  const month = extractValue(parsed["month"]);
280
430
  if (month !== undefined)
@@ -299,6 +449,13 @@ class IeeeBuilder {
299
449
  attributes["interpretation"] = true;
300
450
  if (parsed["redline"] !== undefined)
301
451
  attributes["redline"] = true;
452
+ // Glued identity year-month provenance (gem#520): the input tail is
453
+ // the glued "-YYYY-MM" on a draft-free, revision-free identity.
454
+ if (attributes["month"] !== undefined && attributes["year"] !== undefined &&
455
+ attributes["draft"] === undefined && attributes["revision"] === undefined &&
456
+ /-\d{4}-\d{2}$/.test(this.originalInput ?? "")) {
457
+ attributes["month_glued"] = true;
458
+ }
302
459
  return attributes;
303
460
  }
304
461
  determineStageAbbr(typeValue, parsed) {
@@ -377,6 +534,12 @@ class IeeeBuilder {
377
534
  isoIteration = extractValue(draftData["draft_iso_iteration"]);
378
535
  month = extractValue(draftData["month"]);
379
536
  year = extractValue(draftData["year"]);
537
+ // The apostrophe-year catalogue convention ("P1484.20.1/D7,
538
+ // May 07"): the 2-digit year expands with the <30 pivot so the
539
+ // hash carries an unambiguous year (the reference's builder).
540
+ if (year !== undefined && /^\d{2}$/.test(year)) {
541
+ year = `${Number(year) < 30 ? 20 : 19}${year}`;
542
+ }
380
543
  day = extractValue(draftData["day"]);
381
544
  if (this.originalInput !== "" && month !== undefined) {
382
545
  commaBeforeMonth = new RegExp(`,\\s+${month.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i")
@@ -390,6 +553,20 @@ class IeeeBuilder {
390
553
  else {
391
554
  version = extractValue(draftData);
392
555
  }
556
+ // A REV-carrying draft embeds its print date into the designator
557
+ // with the month canonicalized numeric (pubid ruling 2026-10-14):
558
+ // "P802.11-REVmc/D8.0, Aug 2016" renders ".../D8.0-2016-08".
559
+ // Drafts without a REV keep the comma face.
560
+ if ((attributes["revision"] !== undefined || attributes["rev_bare"] === true) &&
561
+ month !== undefined && year !== undefined && version !== undefined) {
562
+ const monthNumber = DRAFT_MONTH_NAMES[String(month).replace(/\.$/, "")];
563
+ if (monthNumber !== undefined) {
564
+ // The canonical face zero-pads ("D8.0-2016-08"), matching the
565
+ // reference's REV_MONTH_NUMBERS.
566
+ version = `${version}-${year}-${monthNumber.padStart(2, "0")}`;
567
+ month = year = day = undefined;
568
+ }
569
+ }
393
570
  if (version !== undefined || (isObj(draftData) && draftData["draft_iso_stage"] !== undefined)) {
394
571
  const draftObj = new IeeeDraft({
395
572
  version,
@@ -409,7 +586,11 @@ class IeeeBuilder {
409
586
  return;
410
587
  if (typeValue !== "Draft Std")
411
588
  return;
412
- attributes["draft"] = new IeeeDraft({ version: "1", year: attributes["year"] }).render();
589
+ // The reference stores the fabricated designator comma-joined with the
590
+ // identity year ("D1, 2012" — the gem's Draft#to_s), not space-joined.
591
+ attributes["draft"] = attributes["year"] === undefined
592
+ ? "D1"
593
+ : `D1, ${attributes["year"]}`;
413
594
  }
414
595
  handleKeyedHash(parsed, key, attributes, numberKey, yearKey) {
415
596
  if (isObj(parsed[key])) {
@@ -526,15 +707,17 @@ class IeeeBuilder {
526
707
  baseParts.push(extractValue(baseData["type"]) ?? "");
527
708
  }
528
709
  let numberStr = extractValue(baseData["number"]) ?? "";
710
+ const baseSep = extractValue(baseData["part_sep"]) ?? ".";
529
711
  if (baseData["part"] !== undefined) {
530
- numberStr += `.${extractValue(baseData["part"]) ?? ""}`;
712
+ const pv = extractValue(baseData["part"]) ?? "";
713
+ numberStr += /^[._-]/.test(pv) ? pv : `${baseSep}${pv}`;
531
714
  }
532
715
  if (baseData["subpart"] !== undefined) {
533
716
  const subparts = Array.isArray(baseData["subpart"]) ? baseData["subpart"] : [baseData["subpart"]];
534
717
  for (const sp of subparts) {
535
718
  const v = extractValue(sp);
536
719
  if (v !== undefined)
537
- numberStr += `.${v}`;
720
+ numberStr += /^[._-]/.test(v) ? v : `${baseSep}${v}`;
538
721
  }
539
722
  }
540
723
  if (baseData["year"] !== undefined) {
@@ -618,15 +801,33 @@ class IeeeBuilder {
618
801
  attributes["publisher"] = pubs[0];
619
802
  attributes["copublisher"] = pubs.slice(1);
620
803
  }
804
+ else if (parsed["publishers"] !== undefined) {
805
+ // Stage-bearing rows the artifact routes without a
806
+ // joint_publishers capture ("ISO/IEC/IEEE P26511/D8=DDIS.3"):
807
+ // the publishers hash carries the same information.
808
+ const pubData = asHash(parsed["publishers"]);
809
+ const lead = extractValue(pubData["publisher"]) ?? "";
810
+ const copubs = pubData["copublishers"] !== undefined
811
+ ? (Array.isArray(pubData["copublishers"])
812
+ ? pubData["copublishers"]
813
+ : [pubData["copublishers"]])
814
+ .map((cp) => extractValue(asHash(cp)["copublisher"]))
815
+ .filter((cp) => cp !== undefined)
816
+ : [];
817
+ attributes["publishers"] = [lead, ...copubs];
818
+ attributes["publisher"] = lead;
819
+ attributes["copublisher"] = copubs;
820
+ }
621
821
  const codeParts = [];
622
822
  if (parsed["part"] !== undefined)
623
823
  codeParts.push(extractValue(parsed["part"]) ?? "");
624
824
  let codeStr = extractValue(parsed["number"]);
625
825
  if (codeStr !== undefined && codeParts.length > 0) {
626
826
  // The ISO/IEC label's part may be dash-joined ("8802-9") - keep the
627
- // printed separator.
827
+ // printed separator. Separator-carrying captures (the artifact's
828
+ // part binds ".2") join verbatim.
628
829
  const sep = parsed["iec_label_dash"] !== undefined ? "-" : ".";
629
- codeStr += `${sep}${codeParts.join(sep)}`;
830
+ codeStr += codeParts.map((cp) => /^[._-]/.test(cp) ? cp : `${sep}${cp}`).join("");
630
831
  }
631
832
  // P = project (a draft): identity-bearing, preserved as spelled.
632
833
  if (parsed["project_marker"] !== undefined && codeStr !== undefined) {
@@ -665,8 +866,69 @@ class IeeeBuilder {
665
866
  }
666
867
  if (parsed["draft_version"] !== undefined) {
667
868
  const draftVer = (extractValue(parsed["draft_version"]) ?? "").replace(/^D/, "");
668
- if (draftVer !== "")
669
- attributes["ieee_draft"] = `D${draftVer}`;
869
+ if (draftVer !== "") {
870
+ // A date trailing the draft (", June 2010") belongs to the draft
871
+ // component (canonical month, comma form); the identity
872
+ // year/month the promotion filled from the same captures are
873
+ // vacated. Stage-first ISO rows are exempt: their canonical is
874
+ // the ISO face, which prints the date from the identity slots
875
+ // (", March 2017", pubid#216).
876
+ if (parsed["draft_month"] !== undefined && parsed["iso_stage"] === undefined) {
877
+ const dm = extractValue(parsed["draft_month"]) ?? "";
878
+ const dy = extractValue(parsed["draft_year"]) ?? "";
879
+ const monthNum = DRAFT_MONTH_NAMES[dm.replace(/\.$/, "")];
880
+ if (JSON.stringify(attributes["publishers"]) === JSON.stringify(["IEEE", "CSA"]) &&
881
+ monthNum !== undefined && dy !== "") {
882
+ // The joint IEEE/CSA drafts glue their date into the
883
+ // designator with the month numeric (pubid ruling
884
+ // 2026-10-14: no comma dates on these rows).
885
+ const glued = new IeeeDraft({
886
+ version: `${draftVer}-${dy}-${monthNum.padStart(2, "0")}`,
887
+ });
888
+ attributes["draft"] = glued.render();
889
+ }
890
+ else {
891
+ const draftObj = new IeeeDraft({
892
+ version: draftVer,
893
+ month: dm === "" ? undefined : dm,
894
+ year: dy === "" ? undefined : dy,
895
+ comma_before_month: true,
896
+ });
897
+ attributes["draft"] = draftObj.render();
898
+ }
899
+ if (parsed["year"] === undefined)
900
+ delete attributes["year"];
901
+ if (parsed["month"] === undefined)
902
+ delete attributes["month"];
903
+ }
904
+ else if (parsed["draft_year"] !== undefined && parsed["iso_stage"] === undefined) {
905
+ // The comma-year tail captures its separator (", 2013") —
906
+ // strip it (the #471 separator-in-capture rule). The stored
907
+ // form keeps the comma so it re-parses through the same tail.
908
+ const draftYear = (extractValue(parsed["draft_year"]) ?? "").replace(/^[, ]+/, "");
909
+ attributes["draft"] = `/D${draftVer}, ${draftYear}`;
910
+ if (parsed["year"] === undefined)
911
+ delete attributes["year"];
912
+ }
913
+ else if (JSON.stringify(attributes["publishers"]) === JSON.stringify(["IEEE", "CSA"]) &&
914
+ parsed["iso_stage"] === undefined &&
915
+ /^(19|20)\d\d$/.test(extractValue(parsed["year"]) ?? "") &&
916
+ /^\d{1,2}$/.test(extractValue(parsed["month"]) ?? "")) {
917
+ // The glued face re-parses with the date in the identity
918
+ // slots ("D2-2016-09" -> year/month + a bare D2): it
919
+ // normalizes back onto the draft-embedded date so the
920
+ // round trip closes on the CSA canonical.
921
+ const glued = new IeeeDraft({
922
+ version: `${draftVer}-${extractValue(parsed["year"])}-${extractValue(parsed["month"])}`,
923
+ });
924
+ attributes["draft"] = glued.render();
925
+ delete attributes["year"];
926
+ delete attributes["month"];
927
+ }
928
+ else {
929
+ attributes["ieee_draft"] = `D${draftVer}`;
930
+ }
931
+ }
670
932
  }
671
933
  // The "(E)" edition marker of an ISO/IEC label rides on the joint
672
934
  // reference as parenthetical_content - either from the trailing
@@ -681,21 +943,37 @@ class IeeeBuilder {
681
943
  if (attributes["parenthetical_content"] === undefined && parsed["edition_marker"] !== undefined) {
682
944
  attributes["parenthetical_content"] = extractValue(parsed["edition_marker"]);
683
945
  }
946
+ // A joint ISO-print redline is a distinct document of the same standard
947
+ // (pubid#215) - same flag semantics as the generic path.
948
+ if (parsed["redline"] !== undefined)
949
+ attributes["redline"] = true;
684
950
  // The joint stage-draft clause (docs/IEEE-DRAFT-STAGES.md §1.3):
685
951
  // variant 1's compound tail composes onto the IEEE ordinal
686
952
  // ("D5=DIS.3"); variant 1b is the ordinal-less stage draft whose
687
953
  // date rides inside the designator ("D=CDV:2020"). The doubled-D
688
954
  // alias spelling echoes as spelled.
689
955
  if (parsed["draft_iso_stage"] !== undefined) {
690
- const stageText = `${extractValue(parsed["draft_stage_d"]) ?? ""}${extractValue(parsed["draft_iso_stage"]) ?? ""}`;
691
- let jointDraft = parsed["draft_version"] !== undefined
692
- ? `D${(extractValue(parsed["draft_version"]) ?? "").replace(/^D/, "")}=${stageText}`
956
+ // The doubled-D alias ("=DDIS") is notation: the designator carries
957
+ // its own D-ordinal, the stage is the bare ISO/IEC token.
958
+ const rawStage = `${extractValue(parsed["draft_stage_d"]) ?? ""}${extractValue(parsed["draft_iso_stage"]) ?? ""}`;
959
+ const stageText = rawStage.startsWith("D") &&
960
+ ["PWI", "NP", "WD", "CD", "CDV", "DIS", "FDIS"].includes(rawStage.slice(1))
961
+ ? rawStage.slice(1)
962
+ : rawStage;
963
+ const jointVersion = extractValue(parsed["draft_version"]) ??
964
+ (parsed["draft"] !== undefined
965
+ ? extractValue(asHash(parsed["draft"])["draft_version"])
966
+ : undefined);
967
+ let jointDraft = jointVersion !== undefined
968
+ ? `D${jointVersion.replace(/^D/, "")}=${stageText}`
693
969
  : `D=${stageText}`;
694
970
  if (parsed["draft_iso_iteration"] !== undefined) {
695
971
  jointDraft += `.${extractValue(parsed["draft_iso_iteration"])}`;
696
972
  }
697
973
  if (parsed["draft_stage_year"] !== undefined) {
698
- jointDraft += `:${extractValue(parsed["draft_stage_year"])}`;
974
+ // The capture carries its separator (":2020") — strip it (the
975
+ // #471 separator-in-capture rule).
976
+ jointDraft += `:${(extractValue(parsed["draft_stage_year"]) ?? "").replace(/^[\-:]/, "")}`;
699
977
  }
700
978
  attributes["ieee_draft"] = jointDraft;
701
979
  }
@@ -791,6 +1069,40 @@ class IeeeBuilder {
791
1069
  if (record !== undefined)
792
1070
  attributes["typed_stage"] = new IeeeTypedStage(record);
793
1071
  }
1072
+ // A plain IEEE-designator draft on joint publishers ("/D8, June
1073
+ // 2010") carries no ISO stage machinery: it builds as the project
1074
+ // draft its bare-IEEE twin and the D= designator rows already pin
1075
+ // (pubid#430) rather than the joint-development model.
1076
+ if (attributes["draft"] !== undefined &&
1077
+ parsed["iso_stage"] === undefined &&
1078
+ parsed["iec_stage"] === undefined &&
1079
+ parsed["amd_number"] === undefined) {
1080
+ const pubs = (attributes["publishers"] ?? []);
1081
+ const projectAttrs = {
1082
+ draft: attributes["draft"],
1083
+ };
1084
+ // The code columns (number/parts/separator/prefix) are the
1085
+ // ProjectDraft render/hash face; the spelled P is the project
1086
+ // marker (identity), whatever the lead publisher.
1087
+ for (const k of ["number", "prefix", "parts", "separator"]) {
1088
+ // A "P" prefix is the project marker itself (the generic path
1089
+ // deletes it on the strip) - it must not ride the code columns.
1090
+ if (attributes[k] !== undefined && !(k === "prefix" && attributes[k] === "P")) {
1091
+ projectAttrs[k] = attributes[k];
1092
+ }
1093
+ }
1094
+ if (parsed["project_marker"] !== undefined)
1095
+ projectAttrs["project_marker"] = true;
1096
+ if (pubs.length > 0) {
1097
+ projectAttrs["publisher"] = pubs[0];
1098
+ if (pubs.length > 1)
1099
+ projectAttrs["copublisher"] = pubs.slice(1);
1100
+ }
1101
+ if (attributes["draft_status"] !== undefined) {
1102
+ projectAttrs["draft_status"] = attributes["draft_status"];
1103
+ }
1104
+ return new ProjectDraftIdentifierClass(projectAttrs);
1105
+ }
794
1106
  const joint = new JointDevelopmentClass(attributes);
795
1107
  if (parsed["amd_number"] === undefined)
796
1108
  return joint;
@@ -1046,7 +1358,12 @@ class IeeeBuilder {
1046
1358
  // nested name_first variant captures as a hash) and reaches Standard
1047
1359
  // via the code check; Edition is only the no-code fallback.
1048
1360
  let klass = NescEditionClass;
1049
- if (parsedHash["draft"] !== undefined) {
1361
+ if (parsedHash["preprint"] !== undefined) {
1362
+ // Preprint is a draft STAGE of the C2 code — checked before the
1363
+ // Standard route: the c2-preprint tree carries :code "C2" too.
1364
+ klass = NescPreprintClass;
1365
+ }
1366
+ else if (parsedHash["draft"] !== undefined) {
1050
1367
  klass = NescDraftClass;
1051
1368
  }
1052
1369
  else if (typeof parsedHash["variant"] === "string") {
@@ -1120,6 +1437,24 @@ function copublishedStructured(copublishedNumber) {
1120
1437
  return attrs;
1121
1438
  }
1122
1439
  // --- parse orchestration -------------------------------------------------------
1440
+ // The grammar runtime is the baked PARG artifact (pubid-grammar
1441
+ // artifacts/ieee.json via the parsanol engine) — the same artifact the
1442
+ // gem runs, so grammar changes land once and this port cannot drift.
1443
+ // Only the preprocessor, builder, model and the AIEE sub-grammar root
1444
+ // (parseAieeDirect) remain hand-written.
1445
+ let cachedArtifactRuntime;
1446
+ function parseSource(input) {
1447
+ cachedArtifactRuntime ??= PubidRuntime.load("ieee");
1448
+ try {
1449
+ return artifactToBuilderTree(cachedArtifactRuntime.parseShape(input));
1450
+ }
1451
+ catch (e) {
1452
+ // The engine throws its own error class; the reference's fallback
1453
+ // logic keys on Parslet::ParseFailed, so normalize (the messageless
1454
+ // wasm throws included).
1455
+ throw new ParseFailed(e instanceof Error && e.message !== "" ? e.message : String(e), 0);
1456
+ }
1457
+ }
1123
1458
  function ieeeParseSingle(input) {
1124
1459
  const normalized = applyUpdateCodes(input);
1125
1460
  // UpdateCodes may introduce the "; " double-label separator (the
@@ -1131,7 +1466,7 @@ function ieeeParseSingle(input) {
1131
1466
  return buildDual(re.parts);
1132
1467
  }
1133
1468
  const cleaned = preprocessIeee(normalized);
1134
- const tree = parseGrammar(ieeeGrammar, cleaned);
1469
+ const tree = parseSource(cleaned);
1135
1470
  const builder = new IeeeBuilder();
1136
1471
  builder.originalInput = input;
1137
1472
  return builder.build(tree);
@@ -1141,7 +1476,7 @@ function ieeeParseSingle(input) {
1141
1476
  // update-codes rewrite (e.g. "AIEE Nos 72 and 73 - 1932") must not make
1142
1477
  // a line parse that the reference rejects.
1143
1478
  function ieeeParseSingleNoCodes(input) {
1144
- const tree = parseGrammar(ieeeGrammar, preprocessIeee(input));
1479
+ const tree = parseSource(preprocessIeee(input));
1145
1480
  const builder = new IeeeBuilder();
1146
1481
  builder.originalInput = input;
1147
1482
  return builder.build(tree);
@@ -40,6 +40,7 @@ export declare class IeeeCode {
40
40
  static parse(codeStr: string): IeeeCode | undefined;
41
41
  render(): string;
42
42
  }
43
+ export declare const DRAFT_MONTH_NAMES: Record<string, string>;
43
44
  export declare class IeeeDraft {
44
45
  readonly version: string;
45
46
  readonly revision: string | undefined;
@@ -83,10 +84,13 @@ export declare abstract class IeeeIdentifier extends BaseIdentifier {
83
84
  readonly draft_status: string | undefined;
84
85
  readonly draft: string | undefined;
85
86
  readonly revision: string | undefined;
87
+ readonly rev_sep: string | undefined;
88
+ readonly rev_bare: boolean | undefined;
86
89
  readonly edition: string | undefined;
87
90
  readonly month: string | undefined;
88
91
  readonly day: string | undefined;
89
92
  readonly redline: boolean | undefined;
93
+ readonly month_glued: boolean | undefined;
90
94
  readonly edition_month: string | undefined;
91
95
  readonly space_before_draft: boolean | undefined;
92
96
  readonly typed_stage: IeeeTypedStage | undefined;
@@ -142,3 +146,4 @@ export declare const NescHandbookClass: IdentifierStatic;
142
146
  export declare const NescRedlineClass: IdentifierStatic;
143
147
  export declare const NescStandardClass: IdentifierStatic;
144
148
  export declare const NescDraftClass: IdentifierStatic;
149
+ export declare const NescPreprintClass: IdentifierStatic;