@pubid/pubid 0.1.11 → 0.1.13

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.
@@ -284,6 +284,14 @@ class IeeeBuilder {
284
284
  determineIdentifierClass(attributes) {
285
285
  if (attributes["publisher"] === "AIEE")
286
286
  return AieeClass;
287
+ // A stage-bearing row is a joint development whatever its type
288
+ // code: the stage is the printed identity ("IEEE P15026.2/FDIS",
289
+ // pubid#203) and only the joint model carries iso_stage and the D=
290
+ // designator (the reference's chooser).
291
+ if (attributes["iso_stage"] !== undefined ||
292
+ (attributes["ieee_draft"] ?? "").startsWith("D=")) {
293
+ return JointDevelopmentClass;
294
+ }
287
295
  const typeCode = attributes["type"];
288
296
  if (typeCode === "P")
289
297
  return ProjectDraftIdentifierClass;
@@ -438,6 +446,33 @@ class IeeeBuilder {
438
446
  if (day !== undefined)
439
447
  attributes["day"] = day;
440
448
  this.handleDraft(parsed, attributes);
449
+ this.foldDraftDateTail(attributes);
450
+ // The bare-IEEE fdraft clause (pubid#203): a slash-led stage word
451
+ // after the code ("IEEE P15026.2/FDIS, August 2010") rides the D=
452
+ // designator with the IEEE date convention ("D=FDIS-201008"); the
453
+ // ISO face is not elected (the reference's fdraft rule).
454
+ const fdraft = parsed["fdraft"];
455
+ if (fdraft !== undefined && typeof fdraft === "object" && !Array.isArray(fdraft)) {
456
+ const fdStage = extractValue(fdraft["fdraft_stage"]);
457
+ if (fdStage !== undefined) {
458
+ const fdYear = extractValue(fdraft["year"]);
459
+ const fdMonth = extractValue(fdraft["month"]);
460
+ let date = "";
461
+ if (fdYear !== undefined) {
462
+ date = `-${fdYear}`;
463
+ const monthNum = fdMonth !== undefined
464
+ ? DRAFT_MONTH_NAMES[String(fdMonth).replace(/\.$/, "")]
465
+ : undefined;
466
+ if (monthNum !== undefined)
467
+ date += monthNum.padStart(2, "0");
468
+ }
469
+ attributes["publishers"] ??= [attributes["publisher"] ?? "IEEE"];
470
+ attributes["ieee_draft"] = `D=${fdStage}${date}`;
471
+ const fdRecord = locateStage(fdStage);
472
+ if (fdRecord !== undefined)
473
+ attributes["typed_stage"] = new IeeeTypedStage(fdRecord);
474
+ }
475
+ }
441
476
  this.applyDefaultDraftVersion(attributes, typeValue);
442
477
  this.handleKeyedHash(parsed, "corrigendum", attributes, "cor_number", "cor_year");
443
478
  this.handleKeyedHash(parsed, "amendment", attributes, "amd_number", "amd_year");
@@ -533,23 +568,39 @@ class IeeeBuilder {
533
568
  if (draftData["draft_version"] !== undefined) {
534
569
  const dv = draftData["draft_version"];
535
570
  version = Array.isArray(dv) ? dv.map((v) => extractValue(v) ?? "").join("") : extractValue(dv);
571
+ // The reference's designator normalization (gem#552): a leading
572
+ // hyphen drops, and a three-digit-plus zero-padded designator
573
+ // sheds its zero ("/013.0" -> "13.0" — the 2026-10-19 ruling;
574
+ // the strip is scoped to the dotted designator class — "D043"/
575
+ // "D09"/"D0.16" keep their conventional widths).
536
576
  if (version !== undefined)
537
- version = version.replace(/^-/, "");
577
+ version = version.replace(/^-/, "").replace(/^0(?=\d+\.\d)/, "");
538
578
  }
539
579
  revision = extractValue(draftData["revision"]);
540
580
  isoStage = extractValue(draftData["draft_iso_stage"]);
541
581
  isoIteration = extractValue(draftData["draft_iso_iteration"]);
542
582
  // FDIS/CDV/… are ISO/IEC stage tokens, not draft ordinals (C1
543
- // ruling, testsuite#2). "/DDIS" and "/DFDIS" are hallucinated
583
+ // ruling, testsuite#2). "/DDIS" and "/DFCD" are hallucinated
544
584
  // concatenations — the designator D does not merge onto a stage
545
585
  // word. The draft of a stage rides the iso_stage slot, which
546
586
  // Draft#to_s renders as the D= designator face ("…/D=DIS-2020").
547
- if (version !== undefined && /^(FDIS|FCD|CDV|DIS|CD|WD|PWI|NP)$/.test(version)) {
548
- isoStage = version;
549
- version = undefined;
550
- }
587
+ // The glued month-name date ("-June2010", the 2026-10-19 FCD
588
+ // ruling) rides the designator's date slot with the month numeric.
551
589
  month = extractValue(draftData["month"]);
552
590
  year = extractValue(draftData["year"]);
591
+ if (version !== undefined) {
592
+ // The stage iteration rides glued onto the word ("DIS2" — the
593
+ // lossless face, pubid#477; the 2026-10-19 DIS2 ruling).
594
+ const stageDate = /^(FDIS|FCD|CDV|DIS|CD|WD|PWI|NP)(\d+)?(?:-([A-Za-z]+)(\d{4}))?$/.exec(version);
595
+ if (stageDate !== null) {
596
+ isoStage = stageDate[1] + (stageDate[2] ?? "");
597
+ if (stageDate[3] !== undefined) {
598
+ month = DRAFT_MONTH_NAMES[stageDate[3]] !== undefined ? stageDate[3] : month;
599
+ year = stageDate[4];
600
+ }
601
+ version = undefined;
602
+ }
603
+ }
553
604
  // The apostrophe-year catalogue convention ("P1484.20.1/D7,
554
605
  // May 07"): the 2-digit year expands with the <30 pivot so the
555
606
  // hash carries an unambiguous year (the reference's builder).
@@ -598,6 +649,42 @@ class IeeeBuilder {
598
649
  attributes["draft"] = draftObj.render();
599
650
  }
600
651
  }
652
+ // A draft-date that routed to the identity slots (the grammar's
653
+ // month/year captures beside a bare draft ordinal) folds onto the
654
+ // draft designator: a REV-carrier embeds it numerically
655
+ // ("P1310Rev3/D2, February 2011" renders "D2-2011-02" — the
656
+ // reference's builder), a plain draft keeps the printed face routed
657
+ // through the draft's own parse so the month canonicalizes to the
658
+ // crawl abbreviation ("D08, September 2018" renders "D08, Sept,
659
+ // 2018"; a space-faced input stays space-faced).
660
+ foldDraftDateTail(attributes) {
661
+ const month = attributes["month"];
662
+ const year = attributes["year"];
663
+ const draft = attributes["draft"];
664
+ if (typeof month !== "string" || typeof year !== "string" ||
665
+ typeof draft !== "string" || !/^\/D[^ =,]+$/.test(draft)) {
666
+ return;
667
+ }
668
+ const draftNum = draft.slice(2);
669
+ if (attributes["revision"] !== undefined || attributes["rev_bare"] === true) {
670
+ const monthNum = DRAFT_MONTH_NAMES[String(month).replace(/\.$/, "")];
671
+ if (monthNum === undefined)
672
+ return;
673
+ attributes["draft"] =
674
+ new IeeeDraft({ version: `${draftNum}-${year}-${monthNum.padStart(2, "0")}` }).render();
675
+ }
676
+ else {
677
+ const escaped = month.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
678
+ const commaFace = this.originalInput !== "" &&
679
+ new RegExp(`,\\s+${escaped}`, "i").test(this.originalInput);
680
+ const reparsed = IeeeDraft.parse(`${draftNum}${commaFace ? ", " : " "}${month} ${year}`);
681
+ if (reparsed === undefined)
682
+ return;
683
+ attributes["draft"] = reparsed.render();
684
+ }
685
+ delete attributes["month"];
686
+ delete attributes["year"];
687
+ }
601
688
  applyDefaultDraftVersion(attributes, typeValue) {
602
689
  if (attributes["draft"] !== undefined)
603
690
  return;
@@ -885,6 +972,18 @@ class IeeeBuilder {
885
972
  if (attributes["month"] === undefined)
886
973
  attributes["month"] = extractValue(parsed["draft_month"]);
887
974
  }
975
+ // A status word ("Unapproved") marks the draft (pubid#318).
976
+ const jointDraftStatus = extractValue(parsed["draft_status"]);
977
+ if (jointDraftStatus !== undefined)
978
+ attributes["draft_status"] = jointDraftStatus;
979
+ // A dash_year tail ("/D-3-2007") is the identity year when the
980
+ // slots are otherwise empty — the ISO face prints it colon-joined
981
+ // ("ISO/IEC/IEEE FDIS P15288:2007", the reference's 9258 flow).
982
+ if (attributes["year"] === undefined && parsed["dash_year"] !== undefined) {
983
+ const dashYear = (extractValue(parsed["dash_year"]) ?? "").replace(/^[-:]/, "");
984
+ if (/^\d{4}$/.test(dashYear))
985
+ attributes["year"] = dashYear;
986
+ }
888
987
  // The IEEE-led tail word-stage ("IEEE/ISO/IEC P15026-3/CD,
889
988
  // December 2021") composes the D= designator with the glued
890
989
  // 6-digit date — the identity its D= twin spelling parses to (the
@@ -925,7 +1024,22 @@ class IeeeBuilder {
925
1024
  }
926
1025
  if (parsed["draft_version"] !== undefined) {
927
1026
  const draftVer = (extractValue(parsed["draft_version"]) ?? "").replace(/^D/, "");
928
- if (draftVer !== "") {
1027
+ // FCD is an ISO stage (the 2026-10-19 ruling): a glued month-name
1028
+ // date rides the designator's date slot with the month numeric —
1029
+ // the D= designator face, never the "DFCD" glue.
1030
+ const stageDate = /^(FDIS|FCD|CDV|DIS|CD|WD|PWI|NP)(\d+)?(?:-([A-Za-z]+)(\d{4}))?$/.exec(draftVer);
1031
+ if (stageDate !== null) {
1032
+ const monthNum = stageDate[3] !== undefined ? DRAFT_MONTH_NAMES[stageDate[3]] : undefined;
1033
+ const stageWord = stageDate[1] + (stageDate[2] ?? "");
1034
+ attributes["ieee_draft"] =
1035
+ stageDate[3] !== undefined && monthNum !== undefined
1036
+ ? `D=${stageWord}-${stageDate[4]}${monthNum.padStart(2, "0")}`
1037
+ : `D=${stageWord}`;
1038
+ delete attributes["draft"];
1039
+ delete attributes["year"];
1040
+ delete attributes["month"];
1041
+ }
1042
+ else if (draftVer !== "") {
929
1043
  // A date trailing the draft (", June 2010") belongs to the draft
930
1044
  // component (canonical month, comma form); the identity
931
1045
  // year/month the promotion filled from the same captures are
@@ -1052,8 +1166,14 @@ class IeeeBuilder {
1052
1166
  typeof parsed["parameters"] === "object" && !Array.isArray(parsed["parameters"]) &&
1053
1167
  parsed["parameters"]["parenthetical_content"] !== undefined))) {
1054
1168
  const sep = parsed["part_dash"] !== undefined ? "-" : ".";
1055
- let printedCode = [extractValue(parsed["number"]), extractValue(parsed["part"])]
1056
- .filter((x) => x !== undefined).join(sep);
1169
+ // The part capture carries its own separator (C1
1170
+ // separator-in-capture); join verbatim, synthesizing one only
1171
+ // for a bare capture — the reference's printed_joint rule.
1172
+ const partVal = extractValue(parsed["part"]);
1173
+ let printedCode = extractValue(parsed["number"]) ?? "";
1174
+ if (partVal !== undefined) {
1175
+ printedCode += /^[._-]/.test(partVal) ? partVal : `${sep}${partVal}`;
1176
+ }
1057
1177
  // P = project: the marker is identity, preserved as spelled.
1058
1178
  if (parsed["project_marker"] !== undefined)
1059
1179
  printedCode = `P${printedCode}`;
@@ -1109,11 +1229,45 @@ class IeeeBuilder {
1109
1229
  }
1110
1230
  return new StandardClass(printedAttrs);
1111
1231
  }
1232
+ // A stage word with a glued year tail ("/D=CD2-201802") rides the
1233
+ // D= designator with the IEEE lead (the reference's builder, the
1234
+ // pubid#203 class) — the tail's capture carries its separator.
1235
+ if (parsed["iso_stage"] !== undefined && parsed["draft_stage_year"] !== undefined) {
1236
+ const stageWord = extractValue(parsed["iso_stage"]) ?? "";
1237
+ const pubs = (attributes["publishers"] ?? []);
1238
+ if (pubs.length === 0) {
1239
+ attributes["publishers"] = ["IEEE"];
1240
+ attributes["publisher"] = "IEEE";
1241
+ attributes["copublisher"] = [];
1242
+ }
1243
+ attributes["ieee_draft"] = `D=${stageWord}${extractValue(parsed["draft_stage_year"]) ?? ""}`;
1244
+ const stageRecord = locateStage(stageWord);
1245
+ if (stageRecord !== undefined) {
1246
+ attributes["typed_stage"] = new IeeeTypedStage(stageRecord);
1247
+ }
1248
+ attributes["lead_party"] = "IEEE";
1249
+ delete attributes["year"];
1250
+ delete attributes["month"];
1251
+ }
1112
1252
  if (parsed["iso_stage"] !== undefined) {
1113
- attributes["lead_party"] = "ISO";
1114
- const stageAbbr = extractValue(parsed["iso_stage"]);
1115
- attributes["iso_stage"] = stageAbbr;
1116
- if (stageAbbr !== undefined) {
1253
+ // A bare-IEEE project carrying a stage never elects the ISO lead
1254
+ // (the reference's bare_ieee_project gate): the lead stays IEEE.
1255
+ const bareIeeeProject = parsed["project_marker"] !== undefined &&
1256
+ attributes["publisher"] === "IEEE" &&
1257
+ (attributes["copublisher"] ?? []).length === 0;
1258
+ if (!bareIeeeProject)
1259
+ attributes["lead_party"] = "ISO";
1260
+ let stageAbbr = extractValue(parsed["iso_stage"]);
1261
+ // The IEC "/V<version>" iteration rides glued onto the stage word
1262
+ // ("_CDV/V3" -> "CDV3") — the family's lossless spelling (the
1263
+ // reference's builder, pubid#477).
1264
+ const stageIteration = extractValue(parsed["iso_stage_iteration"]);
1265
+ if (stageAbbr !== undefined && stageIteration !== undefined) {
1266
+ stageAbbr += stageIteration;
1267
+ }
1268
+ if (!bareIeeeProject)
1269
+ attributes["iso_stage"] = stageAbbr;
1270
+ if (stageAbbr !== undefined && !bareIeeeProject) {
1117
1271
  const record = locateStage(stageAbbr);
1118
1272
  if (record !== undefined)
1119
1273
  attributes["typed_stage"] = new IeeeTypedStage(record);
@@ -1179,6 +1333,18 @@ class IeeeBuilder {
1179
1333
  }
1180
1334
  return new ProjectDraftIdentifierClass(projectAttrs);
1181
1335
  }
1336
+ // The iso_published joint rows normalize the publisher order to
1337
+ // ISO-first (the reference: "IEEE/ISO/IEC 8802-3:2021/Amd 4-2021"
1338
+ // builds the base [ISO, IEC, IEEE], lead ISO); the CD-stage rows and
1339
+ // the IEEE-led project rows preserve the printed order (P90003 stays
1340
+ // IEEE/ISO/IEC).
1341
+ if (parsed["iso_published"] !== undefined && parsed["iso_stage"] === undefined &&
1342
+ Array.isArray(attributes["publishers"])) {
1343
+ const pubs = attributes["publishers"];
1344
+ attributes["publishers"] = [...pubs.filter((p) => p !== "IEEE"), ...pubs.filter((p) => p === "IEEE")];
1345
+ attributes["publisher"] = attributes["publishers"][0];
1346
+ attributes["copublisher"] = attributes["publishers"].slice(1);
1347
+ }
1182
1348
  const joint = new JointDevelopmentClass(attributes);
1183
1349
  if (parsed["amd_number"] === undefined)
1184
1350
  return joint;
@@ -1114,8 +1114,9 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1114
1114
  iso_stage: { type: "string" },
1115
1115
  ieee_draft: { type: "string" },
1116
1116
  parenthetical_content: { type: "string" },
1117
+ draft_status: { type: "string" },
1117
1118
  },
1118
- extraMappings: keyValue({ wire: "publishers", to: "publishers" }, { wire: "lead_party", to: "lead_party" }, { wire: "iso_stage", to: "iso_stage" }, { wire: "ieee_draft", to: "ieee_draft" }, { wire: "parenthetical_content", to: "parenthetical_content" }),
1119
+ extraMappings: keyValue({ wire: "publishers", to: "publishers" }, { wire: "lead_party", to: "lead_party" }, { wire: "iso_stage", to: "iso_stage" }, { wire: "ieee_draft", to: "ieee_draft" }, { wire: "parenthetical_content", to: "parenthetical_content" }, { wire: "draft_status", to: "draft_status" }),
1119
1120
  wireDelete: { keys: ["publisher", "copublisher"] },
1120
1121
  render: (id) => {
1121
1122
  const publishers = id["publishers"];
@@ -1177,8 +1178,11 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1177
1178
  result += `, ${id.month} ${dateYear}`;
1178
1179
  }
1179
1180
  }
1181
+ // Lossless: parse(to_s) == self (gem c148d267, the 2026-10-19
1182
+ // ruling) — the parenthetical rides unconditionally on both
1183
+ // joint faces.
1180
1184
  const marker = id["parenthetical_content"];
1181
- if (marker !== undefined && /^[A-Z](?:\s*[/&]\s*[A-Z])*$/.test(marker)) {
1185
+ if (marker !== undefined) {
1182
1186
  result += ` (${marker})`;
1183
1187
  }
1184
1188
  // The joint ISO-print redline is a distinct document (pubid#215):
@@ -1191,6 +1195,11 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1191
1195
  const parts = [];
1192
1196
  if (publishers !== undefined && publishers.length > 0)
1193
1197
  parts.push(publishers.join("/"));
1198
+ // A status word ("Unapproved") marks the draft (pubid#318) — it
1199
+ // prints after the publishers on the joint IEEE face.
1200
+ const draftStatus = id["draft_status"];
1201
+ if (draftStatus !== undefined)
1202
+ parts.push(draftStatus);
1194
1203
  // The P-state prints as spelled (P = project draft; no P =
1195
1204
  // standard). Never added or stripped.
1196
1205
  let codeStr = codeObj?.render() ?? "";
@@ -1224,6 +1233,9 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1224
1233
  !(ieeeDraft ?? "").endsWith(`-${id.year}`)) {
1225
1234
  result += `-${id.year}`;
1226
1235
  }
1236
+ const marker = id["parenthetical_content"];
1237
+ if (marker !== undefined)
1238
+ result += ` (${marker})`;
1227
1239
  if (id.redline === true)
1228
1240
  result += " - Redline";
1229
1241
  return result;
@@ -226,14 +226,11 @@ function normalizeRelatonSuffixes(input) {
226
226
  cleaned = `${m[1]}${date}${draftPart}/R-${m[3]}`;
227
227
  }
228
228
  }
229
- // /D-N drafts with a trailing numeric date: reposition onto the number.
230
- {
231
- const re = /^(.*)\/D-([0-9A-Za-z][0-9A-Za-z.+]*?)-((?:19|20)\d\d)(?:-(0[1-9]|1[0-2]))?$/;
232
- const m = re.exec(cleaned);
233
- if (m !== null) {
234
- cleaned = `${m[1]}-${m[3]}${m[4] !== undefined ? `-${m[4]}` : ""}/D${m[2]}`;
235
- }
236
- }
229
+ // The "/D-N-YYYY" draft-dash-date spelling is NOT repositioned: the
230
+ // artifact grammar reads it natively (draft + printed_dash_year) and
231
+ // the joint route renders the reference's dash-year face. The former
232
+ // reposition onto the number predated the artifact and broke the
233
+ // joint classification (the year left the designator's slot).
237
234
  // /E-N editions: "/E-2-2023-02" → "Edition 2.0 2023-02".
238
235
  {
239
236
  const re = /^(.*?)\/E-(\d+)(?:-((?:19|20)\d\d)(?:-(0[1-9]|1[0-2]))?)?$/;
@@ -255,10 +252,14 @@ function normalizeRelatonSuffixes(input) {
255
252
  }
256
253
  function normalizeRevisionNotation(input) {
257
254
  let cleaned = input;
258
- // Numbered revisions preserved, repositioned to "/R-<n>".
259
- cleaned = cleaned.replace(/(\/D-?[0-9A-Za-z.]+)\s+[Rr][Ee][Vv]\s*(\d+)/, "$1/R-$2");
260
- cleaned = cleaned.replace(/[-/_.]?\s?[Rr][Ee][Vv][-\s]?(\d+)(\/D-?[0-9A-Za-z.]+)/, "$2/R-$1");
261
- cleaned = cleaned.replace(/(\d)[-._]?\s?[Rr][Ee][Vv]\s*(\d+)\s*$/, "$1/R-$2");
255
+ // The reference's two glue-normalizing gsubs (parser.rb): a "REV.x"/
256
+ // "REV-x" after-word separator and a digit-glued "2rev1" normalize to
257
+ // a spaced boundary; the render keeps the glued face. The former
258
+ // /R-<n> reposition rules predate the artifact grammar and diverged
259
+ // from the reference (they ate "-rev1" spellings the artifact
260
+ // captures natively, which cost the glued-date rows their parse).
261
+ cleaned = cleaned.replace(/(REV|Rev|rev)[.\-](?=[A-Za-z])/g, "$1");
262
+ cleaned = cleaned.replace(/(\d)(REV|Rev|rev)/g, "$1 $2");
262
263
  // The joint IEEE/CSA catalogue spells the type word before the
263
264
  // project number ("IEEE/CSA Draft P844.1/D2, Sept, 2016"): the P
264
265
  // already carries the project semantics (docs/IEEE-DRAFT-STAGES.md
@@ -322,6 +323,12 @@ function normalizeJointStageSpellings(input) {
322
323
  // dash-date, and rewriting it onto the number breaks the parse.
323
324
  cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
324
325
  cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
326
+ // The dash-stage-with-part spelling ("ISO/IEC/IEEE P15288-DIS-1403",
327
+ // "… P12207-FDIS-1707, June 2017"): the stage glues between the number
328
+ // and the part. The reference renders the stage-first dot face
329
+ // ("DIS P15288.1403") — re-spell so the stage-first route parses it
330
+ // (a stage WITHOUT a trailing part, "P29119-2-DIS", routes elsewhere).
331
+ cleaned = cleaned.replace(new RegExp(`^${pubs}P?(\\d+)-(FDIS|DIS|CDV|CD|WD)(\\d*)-(\\d+(?:\\.\\d+)*)((?:, [A-Za-z]+ \\d{4})?)$`), "$1$3$4 P$2.$5$6");
325
332
  return cleaned;
326
333
  }
327
334
  // The Parser.parse cleaning pipeline, in Ruby order.
@@ -344,8 +351,10 @@ export function preprocessIeee(input) {
344
351
  cleaned = cleaned.replace(/\s+(P&V)\s*$/, " ($1)");
345
352
  cleaned = cleaned.replace(/(\d+\.\d+)\s+(\d+\.)/g, "$1$2");
346
353
  cleaned = cleaned.replace(/\b(1|2)\s+(\d{3})\b/g, "$1$2");
347
- cleaned = cleaned.replace(/\b(January|February|March|April|May|June|July|August|September|October|November|December)(\d{4})\b/g, "$1 $2");
348
- cleaned = cleaned.replace(/\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)(\d{4})\b/g, "$1 $2");
354
+ // The month-year glue split ("June2010" -> "June 2010") is NOT the
355
+ // reference's behavior — the reference keeps the glue (the FCD ruling:
356
+ // "FCD-June2010" is the stage + the glued month-name date), and the
357
+ // split cost the FCD rows their classification.
349
358
  cleaned = normalizeJointStageSpellings(cleaned);
350
359
  cleaned = cleaned.replace(/(IEC\s+\d+(?:-\d+)?(?::\d{4})?(?:\s+(?:First\s+)?[Ee]dition\s+\d+(?:\.\d+)?\s+\d{4}-\d{2})?)\s+(IEEE\s+Std\s+\S+|IEEE\s+\S+)/g, "$1; $2");
351
360
  cleaned = cleaned.replace(/^(IEC\s+\d+(?:-\d+)?):\d{4}(\s+(?:First\s+)?[Ee]dition\s+\d+(?:\.\d+)?\s+\d{4}-\d{2})/, "$1$2");
@@ -417,12 +426,9 @@ export function preprocessIeee(input) {
417
426
  cleaned = cleaned.replace(/^(ISO\/IEC \d+[-.]\d*)\s*:\s*(\d{4})/, "$1:$2");
418
427
  cleaned = cleaned.replace(/^(ISO\/IEC \d+)\s*:\s*(\d{4})/, "$1:$2");
419
428
  cleaned = cleaned.replace(/^(IEC\/IEEE P[\w.-]+)_D/, "$1/D");
420
- // Scoped: the D= stage-draft rows keep the IEEE-led list (the ruby
421
- // ieee_format routes them with the input publisher order and lead
422
- // IEEE - "IEEE/ISO/IEC P29119-2/D=DIS-201112"); the other IEEE/ISO/IEC
423
- // spellings print the ISO-led face.
424
- cleaned = cleaned.replace(/^IEEE\/ISO\/IEC\s+(?!P[\w.-]+\/D=)(P[\w.-]+)/, "ISO/IEC/IEEE $1");
425
- cleaned = cleaned.replace(/^IEEE\/IEC\/ISO\s+(P[\w.-]+)/, "IEC/ISO/IEEE $1");
429
+ // The printed publisher order is identity (pubid#469, the
430
+ // dual-perspective ruling): no IEEE/ISO/IEC or IEEE/IEC/ISO rewrites —
431
+ // the reference parses as printed and renders the printed order back.
426
432
  cleaned = cleaned.replace(/^(IEEE\/IEC P[\w.-]+)\s+D(\d)/, "$1/D$2");
427
433
  cleaned = cleaned.replace(/^(IEEE\/IEC P[\w.-]+)\s+(CDV|FDIS|CD|DIS|ED\d)/, "$1/$2");
428
434
  cleaned = cleaned.replace(/^ISO\s+\/IEC\/IEEE/, "ISO/IEC/IEEE");
@@ -443,7 +449,6 @@ export function preprocessIeee(input) {
443
449
  cleaned = cleaned.replaceAll("Edn.", "Edition");
444
450
  cleaned = cleaned.replaceAll("ASHARE", "ASHRAE");
445
451
  cleaned = cleaned.replace(/^PC(\d)/, "P$1");
446
- cleaned = cleaned.replace(/^IEEE\/ISO\/IEC\s+(8802[\w.-]+)/, "ISO/IEC/IEEE $1");
447
452
  cleaned = cleaned.replace(/^(IEEE\s+C?\d[\d.]*\/D\d+)([A-Z][a-z]+\s+\d{4})/, "$1, $2");
448
453
  cleaned = cleaned.replace(/^(ANSI\/IEEE Std):\s+.*$/, "$1");
449
454
  cleaned = cleaned.replace(/^(IEEE\s+[\d.]+)\s+(IEC\s+\d+[-\d]*\s+.*edition\s+\d{4}-\d{2})$/i, "$1; $2");
@@ -99,14 +99,12 @@ export const UPDATE_CODES = [
99
99
  { plain: "ISO/IEC/IEEE CD P26513/D1, August 2015", replacement: "ISO/IEC/IEEE P26513/D1, August 2015" },
100
100
  { plain: "IEEE Std P1073.1.3.4/D3.0", replacement: "IEEE Std P11073.00101" },
101
101
  { plain: "IEEE P802.1Qbu/03.0, July 2015", replacement: "IEEE P802.1Qbu/D-3.0-2015" },
102
- { plain: "IEEE P802.11aqTM/013.0 October 2017", replacement: "IEEE P802.11aqTM/D-13.0-2017" },
103
102
  { plain: "IEC/IEEE P62271-37-013:2015 D13.4", replacement: "IEC/IEEE P62271.37-013/D-13.4-2015" },
104
103
  { plain: "ISO/IEC/IEEE DIS P42020:201x(E), June 2017", replacement: "ISO/IEC/IEEE DIS P42020-2017-06" },
105
104
  { plain: "ISO/IEC/IEEE DIS P16326/201x, December 2018", replacement: "ISO/IEC/IEEE DIS P16326-2018-12" },
106
105
  { plain: "ISO/IEC/IEEE/P21839, 2019(E)", replacement: "ISO/IEC/IEEE P21839-2019" },
107
106
  { plain: "ISO/IEC/IEEE CD2 P12207-2: 201x(E), February 2019", replacement: "ISO/IEC/IEEE CD2 P12207.2-2019-02" },
108
107
  { plain: "ISO/IEC/IEEE P42010.WD4:2019(E)", replacement: "ISO/IEC/IEEE P42010/D-4-2019" },
109
- { plain: "IEC/IEEE P63195_CDV/V3, February 2020", replacement: "IEC/IEEE CDV P63195-2020-02" },
110
108
  { plain: "ISO/IEC/IEEE/P16085_DIS, March 2020", replacement: "ISO/IEC/IEEE DIS P16085-2020-03" },
111
109
  { plain: "ANSI/IEEE Std: Outdoor Apparatus Bushings", replacement: "ANSI/IEEE Std 21-1976-11" },
112
110
  { plain: "Draft National Electrical Safety Code, January 2016", replacement: "IEEE PC2-2016-01" },
@@ -373,9 +373,10 @@ function cast(type, value, h, policy, series = "") {
373
373
  return undefined;
374
374
  const stripped = v.trim();
375
375
  const code = TRANSLATION_MAP[stripped.toLowerCase()] ?? stripped.toLowerCase();
376
- // The gem's translation attr is the component; its to_s prefixes
377
- // the space (" .uppl"), and the wire serializes that form.
378
- return { translation_component: new NistTranslation({ code }), translation: ` ${code}` };
376
+ // The gem's Translation#to_s (short) renders the parenthesised
377
+ // suffix — the 1.x corpus shape ("(spa)", pubid#497); the wire
378
+ // serializes that form.
379
+ return { translation_component: new NistTranslation({ code }), translation: `(${code})` };
379
380
  }
380
381
  case "version": {
381
382
  const v = s(value);
@@ -157,8 +157,10 @@ export class NistTranslation {
157
157
  toWire() {
158
158
  return { code: this.code };
159
159
  }
160
+ // The 1.x corpus shape: the trailing parenthesised suffix "(spa)"
161
+ // (pubid#497); the mr face keeps the dotted form (".spa").
160
162
  render(format = "short") {
161
- return format === "mr" ? `.${this.code}` : ` ${this.code}`;
163
+ return format === "mr" ? `.${this.code}` : `(${this.code})`;
162
164
  }
163
165
  }
164
166
  export class NistStage {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "TypeScript PubID identifiers: resolution, normalization and canonical hashes for every identifier in the pubid-testsuite corpus (41 standards bodies), with a conformance runner mirroring the Ruby reference",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"bindings":null,"version":"0.1.0","grammar":"PubidOmg","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"OMG"}},{"Entity":{"atom":2}},{"Str":{"pattern":" "}},{"Entity":{"atom":24}},{"Entity":{"atom":7}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Alternative":{"atoms":[5,6]}},{"Entity":{"atom":13}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[+]"}},{"Alternative":{"atoms":[9,10,11,12]}},{"Repetition":{"atom":8,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":18}},{"Re":{"pattern":"[\\-]"}},{"Re":{"pattern":"[/]"}},{"Alternative":{"atoms":[16,17]}},{"Entity":{"atom":13}},{"Repetition":{"atom":19,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[15,20]}},{"Repetition":{"atom":21,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[4,14,22]}},{"Named":{"name":"acronym","atom":23}},{"Entity":{"atom":2}},{"Entity":{"atom":54}},{"Entity":{"atom":28}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":27,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":31}},{"Str":{"pattern":"."}},{"Entity":{"atom":28}},{"Repetition":{"atom":32,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[30,33]}},{"Repetition":{"atom":34,"min":0,"max":null,"tag":"Repetition"}},{"Entity":{"atom":51}},{"Str":{"pattern":" beta"}},{"Entity":{"atom":42}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Alternative":{"atoms":[39,40,41]}},{"Lookahead":{"atom":38,"positive":false}},{"Entity":{"atom":2}},{"Entity":{"atom":28}},{"Repetition":{"atom":45,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":42}},{"Lookahead":{"atom":47,"positive":false}},{"Sequence":{"atoms":[44,46,48]}},{"Repetition":{"atom":49,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[37,43,50]}},{"Repetition":{"atom":36,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[29,35,52]}},{"Named":{"name":"version","atom":53}},{"Entity":{"atom":59}},{"Entity":{"atom":2}},{"Entity":{"atom":58}},{"Str":{"pattern":"/"}},{"Alternative":{"atoms":[56,57]}},{"Entity":{"atom":63}},{"Entity":{"atom":42}},{"Repetition":{"atom":61,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"part","atom":62}},{"Sequence":{"atoms":[55,60]}},{"Repetition":{"atom":64,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[25,26,65]}},{"Entity":{"atom":2}},{"Entity":{"atom":63}},{"Sequence":{"atoms":[67,68]}},{"Alternative":{"atoms":[66,69]}},{"Repetition":{"atom":70,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[0,1,3,71]}}],"root":72},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":[]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"OMG IDL 4.2","expect":{}},{"entry":"identifier","kind":"accept","input":"OMG UML 2.5.1","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}}],"docs":{},"source":"# Port of lib/pubid/omg/parser.rb: OMG <acronym>[ <version>[<sep><part>]]\n# with the beta-label word-boundary guards (a bare \"beta\" must not be\n# swallowed by the part rule).\n grammar PubidOmg version \"0.1.0\" {\n digit = %x30-39\n acronym_char = %x41-5A / %x61-7A / %x30-39 / %x2B\n acronym_join = %x2D / %x2F\n acronym = (letter 1*acronym_char *(acronym_join 1*acronym_char)) as acronym\n word_boundary = !alnum_char\n beta = \" beta\" !alnum_char ([space 1*digit !alnum_char])\n version = (1*digit *(dot 1*digit) [beta]) as version\n part_separator = space / slash\n part = (1*alnum_char) as part\n identifier = \"OMG\" space acronym [((space version [part_separator part]) / (space part))]\n alnum_char = %x41-5A / %x61-7A / %x30-39\n letter = %x41-5A / %x61-7A\n dot = \".\"\n slash = \"/\"\n space = \" \"\n entry identifier: identifier\n\n test {\n accept \"OMG IDL 4.2\"\n accept \"OMG UML 2.5.1\"\n reject \"nonsense\"\n }\n}\n","source_hash":"2dac987a51409c0af26e59449f71fab80a2249a14f119a9a244633ce7149faa6","compiler_version":"1.3.93","checksum":"sha256:229f295dc796802ddf19b56803bb420107218420ba5dc7acab3d58d1a9a7bb9e"}
1
+ {"bindings":null,"version":"0.1.0","grammar":"PubidOmg","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"OMG"}},{"Entity":{"atom":2}},{"Str":{"pattern":" "}},{"Entity":{"atom":24}},{"Entity":{"atom":7}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Alternative":{"atoms":[5,6]}},{"Entity":{"atom":13}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[+]"}},{"Alternative":{"atoms":[9,10,11,12]}},{"Repetition":{"atom":8,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":18}},{"Re":{"pattern":"[\\-]"}},{"Re":{"pattern":"[/]"}},{"Alternative":{"atoms":[16,17]}},{"Entity":{"atom":13}},{"Repetition":{"atom":19,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[15,20]}},{"Repetition":{"atom":21,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[4,14,22]}},{"Named":{"name":"acronym","atom":23}},{"Entity":{"atom":2}},{"Entity":{"atom":63}},{"Entity":{"atom":28}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":27,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":31}},{"Str":{"pattern":"."}},{"Entity":{"atom":28}},{"Repetition":{"atom":32,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[30,33]}},{"Repetition":{"atom":34,"min":0,"max":null,"tag":"Repetition"}},{"Entity":{"atom":60}},{"Str":{"pattern":" beta"}},{"Entity":{"atom":42}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Alternative":{"atoms":[39,40,41]}},{"Lookahead":{"atom":38,"positive":false}},{"Entity":{"atom":2}},{"Entity":{"atom":28}},{"Repetition":{"atom":45,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":42}},{"Lookahead":{"atom":47,"positive":false}},{"Sequence":{"atoms":[44,46,48]}},{"Repetition":{"atom":49,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[37,43,50]}},{"Entity":{"atom":2}},{"Str":{"pattern":"Beta"}},{"Entity":{"atom":28}},{"Repetition":{"atom":54,"min":1,"max":null,"tag":"Repetition"}},{"Repetition":{"atom":55,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":42}},{"Lookahead":{"atom":57,"positive":false}},{"Sequence":{"atoms":[52,53,56,58]}},{"Alternative":{"atoms":[51,59]}},{"Repetition":{"atom":36,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[29,35,61]}},{"Named":{"name":"version","atom":62}},{"Entity":{"atom":68}},{"Entity":{"atom":2}},{"Entity":{"atom":67}},{"Str":{"pattern":"/"}},{"Alternative":{"atoms":[65,66]}},{"Entity":{"atom":72}},{"Entity":{"atom":42}},{"Repetition":{"atom":70,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"part","atom":71}},{"Sequence":{"atoms":[64,69]}},{"Repetition":{"atom":73,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[25,26,74]}},{"Entity":{"atom":2}},{"Entity":{"atom":72}},{"Sequence":{"atoms":[76,77]}},{"Alternative":{"atoms":[75,78]}},{"Repetition":{"atom":79,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[0,1,3,80]}}],"root":81},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule beta: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)"]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"OMG IDL 4.2","expect":{}},{"entry":"identifier","kind":"accept","input":"OMG UML 2.5.1","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}}],"docs":{},"source":"# Port of lib/pubid/omg/parser.rb: OMG <acronym>[ <version>[<sep><part>]]\n# with the beta-label word-boundary guards (a bare \"beta\" must not be\n# swallowed by the part rule). The GLUED CATALOGUE label (\"Beta1\" --\n# capitalized, the /spec/ALMAS/1.5/Beta1 URL segment) classifies into\n# the version (the 2026-10-19 pubid#548 ruling); the LOWERCASE glued\n# form (\"beta2\") stays a document part (the DDS ruling, spec-pinned). A\n# /spec/ALMAS/1.5/Beta1 URL segment) classifies into the version (the\n# 2026-10-19 pubid#548 ruling); a Beta-prefixed part without digits\n# (\"BetaWave\") still lexes as the part — the boundary guard keeps it.\n grammar PubidOmg version \"0.1.0\" {\n digit = %x30-39\n acronym_char = %x41-5A / %x61-7A / %x30-39 / %x2B\n acronym_join = %x2D / %x2F\n acronym = (letter 1*acronym_char *(acronym_join 1*acronym_char)) as acronym\n word_boundary = !alnum_char\n beta = (\" beta\" !alnum_char ([space 1*digit !alnum_char]) / (space \"Beta\" [1*digit] !alnum_char))\n version = (1*digit *(dot 1*digit) [beta]) as version\n part_separator = space / slash\n part = (1*alnum_char) as part\n identifier = \"OMG\" space acronym [((space version [part_separator part]) / (space part))]\n alnum_char = %x41-5A / %x61-7A / %x30-39\n letter = %x41-5A / %x61-7A\n dot = \".\"\n slash = \"/\"\n space = \" \"\n entry identifier: identifier\n\n test {\n accept \"OMG IDL 4.2\"\n accept \"OMG UML 2.5.1\"\n reject \"nonsense\"\n }\n}\n","source_hash":"9ed6dcd5dff95df514aa465c3cbd923780c6275761db3415b8eba5e05213cd20","compiler_version":"1.3.93","checksum":"sha256:d33c557faf6c8ac5ea071abee1576c15c08e7a93f988423f77aff7c4f5831c54"}