@primmel/primmel 1.8.1 → 1.9.0

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 (93) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +20 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/check-rules.d.ts.map +1 -1
  6. package/dist/src/check-rules.js +55 -0
  7. package/dist/src/check-rules.js.map +1 -1
  8. package/dist/src/check.d.ts +74 -2
  9. package/dist/src/check.d.ts.map +1 -1
  10. package/dist/src/check.js +514 -15
  11. package/dist/src/check.js.map +1 -1
  12. package/dist/src/export/impact.d.ts +45 -0
  13. package/dist/src/export/impact.d.ts.map +1 -0
  14. package/dist/src/export/impact.js +234 -0
  15. package/dist/src/export/impact.js.map +1 -0
  16. package/dist/src/export/rdf.d.ts.map +1 -1
  17. package/dist/src/export/rdf.js +4 -1
  18. package/dist/src/export/rdf.js.map +1 -1
  19. package/dist/src/export/retrieval.d.ts +408 -0
  20. package/dist/src/export/retrieval.d.ts.map +1 -0
  21. package/dist/src/export/retrieval.js +1156 -0
  22. package/dist/src/export/retrieval.js.map +1 -0
  23. package/dist/src/model-diff.d.ts +23 -0
  24. package/dist/src/model-diff.d.ts.map +1 -1
  25. package/dist/src/model-diff.js +132 -30
  26. package/dist/src/model-diff.js.map +1 -1
  27. package/dist/src/ser-des/config/calculation.d.ts.map +1 -1
  28. package/dist/src/ser-des/config/calculation.js +119 -4
  29. package/dist/src/ser-des/config/calculation.js.map +1 -1
  30. package/dist/src/ser-des/config/dimension.d.ts +12 -0
  31. package/dist/src/ser-des/config/dimension.d.ts.map +1 -0
  32. package/dist/src/ser-des/config/dimension.js +57 -0
  33. package/dist/src/ser-des/config/dimension.js.map +1 -0
  34. package/dist/src/ser-des/config/field-parser.d.ts.map +1 -1
  35. package/dist/src/ser-des/config/field-parser.js +6 -0
  36. package/dist/src/ser-des/config/field-parser.js.map +1 -1
  37. package/dist/src/ser-des/config/index.d.ts.map +1 -1
  38. package/dist/src/ser-des/config/index.js +5 -0
  39. package/dist/src/ser-des/config/index.js.map +1 -1
  40. package/dist/src/ser-des/config/packageManifest.d.ts.map +1 -1
  41. package/dist/src/ser-des/config/packageManifest.js +7 -1
  42. package/dist/src/ser-des/config/packageManifest.js.map +1 -1
  43. package/dist/src/ser-des/config/quantityRegister.d.ts.map +1 -1
  44. package/dist/src/ser-des/config/quantityRegister.js +38 -0
  45. package/dist/src/ser-des/config/quantityRegister.js.map +1 -1
  46. package/dist/src/ser-des/config/reference.d.ts.map +1 -1
  47. package/dist/src/ser-des/config/reference.js +18 -0
  48. package/dist/src/ser-des/config/reference.js.map +1 -1
  49. package/dist/src/ser-des/config/requirement.d.ts.map +1 -1
  50. package/dist/src/ser-des/config/requirement.js +43 -0
  51. package/dist/src/ser-des/config/requirement.js.map +1 -1
  52. package/dist/src/ser-des/config/subject.d.ts +10 -1
  53. package/dist/src/ser-des/config/subject.d.ts.map +1 -1
  54. package/dist/src/ser-des/config/subject.js +113 -63
  55. package/dist/src/ser-des/config/subject.js.map +1 -1
  56. package/dist/src/ser-des/config/symbol.d.ts.map +1 -1
  57. package/dist/src/ser-des/config/symbol.js +14 -2
  58. package/dist/src/ser-des/config/symbol.js.map +1 -1
  59. package/dist/src/ser-des/config/term.d.ts.map +1 -1
  60. package/dist/src/ser-des/config/term.js +82 -20
  61. package/dist/src/ser-des/config/term.js.map +1 -1
  62. package/dist/src/ser-des/index.d.ts +2 -0
  63. package/dist/src/ser-des/index.d.ts.map +1 -1
  64. package/dist/src/ser-des/index.js +13 -1
  65. package/dist/src/ser-des/index.js.map +1 -1
  66. package/dist/src/ser-des/parse-block.d.ts +1 -0
  67. package/dist/src/ser-des/parse-block.d.ts.map +1 -1
  68. package/dist/src/ser-des/parse-block.js +34 -0
  69. package/dist/src/ser-des/parse-block.js.map +1 -1
  70. package/dist/src/ser-des/parse.d.ts.map +1 -1
  71. package/dist/src/ser-des/parse.js +2 -0
  72. package/dist/src/ser-des/parse.js.map +1 -1
  73. package/dist/src/ser-des/types.d.ts +2 -1
  74. package/dist/src/ser-des/types.d.ts.map +1 -1
  75. package/dist/src/types/Calculation.d.ts +15 -0
  76. package/dist/src/types/Calculation.d.ts.map +1 -1
  77. package/dist/src/types/Package.d.ts +8 -0
  78. package/dist/src/types/Package.d.ts.map +1 -1
  79. package/dist/src/types/Quantity.d.ts +7 -0
  80. package/dist/src/types/Quantity.d.ts.map +1 -1
  81. package/dist/src/types/Reference.d.ts +11 -0
  82. package/dist/src/types/Reference.d.ts.map +1 -1
  83. package/dist/src/types/Requirement.d.ts +17 -0
  84. package/dist/src/types/Requirement.d.ts.map +1 -1
  85. package/dist/src/types/Standard.d.ts +2 -1
  86. package/dist/src/types/Standard.d.ts.map +1 -1
  87. package/dist/src/types/Subject.d.ts +10 -0
  88. package/dist/src/types/Subject.d.ts.map +1 -1
  89. package/dist/src/types/Term.d.ts +33 -1
  90. package/dist/src/types/Term.d.ts.map +1 -1
  91. package/dist-browser/index.mjs +1664 -1458
  92. package/dist-cli/primmel.mjs +2148 -183
  93. package/package.json +1 -1
@@ -0,0 +1,1156 @@
1
+ "use strict";
2
+ // ─────────────────────────────────────────────────────────────────────
3
+ // Retrieval export (primmel/primmel-ts#65 — the AI-native retrieval
4
+ // projection): the canonical, versioned serialization of a package's
5
+ // typed units for RAG and agent consumers.
6
+ //
7
+ // A Primmel package carries knowledge no document format holds — the
8
+ // machine limits, the applicability filters, the acceptance chains, the
9
+ // subject-intrinsic constraints. Retrieval consumers (the OIML SMART
10
+ // estate's model plane first among them) project every package into
11
+ // typed units and index them next to the prose corpus; today each
12
+ // consumer re-derives that projection and the re-derivation is where
13
+ // the mapping bugs live. This module is the UPSTREAM canonical form of
14
+ // that projection: one export, one contract, versioned as
15
+ // `primmel-retrieval/1`.
16
+ //
17
+ // The contract (the guarantees the issue asks for):
18
+ //
19
+ // 1. CLAUSE URNS FIRST-CLASS, ALWAYS. Every unit's provenance is the
20
+ // DOCUMENT's own clause numbering plus the document identifier —
21
+ // `clause: { doc, clause, urn }` with urn = <doc>#clause-<clause>
22
+ // (e.g. urn:oiml:pub:r:60-1:2021#clause-3.6). A producer-internal
23
+ // anchor (a metanorma UUID, `_eb46a3a3-…` style) is NOT a clause:
24
+ // it rides as the optional `anchor` extra, never as the clause,
25
+ // and units whose only provenance is an anchor are counted in the
26
+ // stats (anchorOnlyProvenance) — visible debt, never silently
27
+ // presented as citable provenance.
28
+ // 2. CANONICAL EDITION SEMANTICS. The document block exposes two
29
+ // distinct, stable fields: `edition` (the PUBLICATION edition the
30
+ // model corresponds to — the manifest's newest `editions` entry,
31
+ // the baseUrn's year segment as fallback) and `model_version`
32
+ // (the package's own `version`). Edition steering reads the first;
33
+ // freshness gating reads the second; neither ever borrows the
34
+ // other's value.
35
+ // 3. THE FLAT RETRIEVAL FACET. Every unit carries `facet` — one flat
36
+ // scalar map (string values only), versioned as
37
+ // `retrieval-facet/1` on the document's `facet_version`: the
38
+ // document fields (identifier, doc number, doctype, edition,
39
+ // language), the clause anchor, the unit kind, the applicability
40
+ // dimensions (`app_<dim>` keys), and the currency keys (unit_id,
41
+ // unit_hash, model_version). Retrieval indexes accept only scalar
42
+ // metadata per vector; the facet is the canonical pre-flattened
43
+ // form so a consumer's ingestion is a MAPPING, never a
44
+ // re-derivation. The facet is a derived projection of the unit's
45
+ // authored content, so it is EXCLUDED from the content_hash input
46
+ // (identity and currency ride the authored fields; the facet
47
+ // re-derives from them deterministically).
48
+ // 4. STABLE UNIT IDS + CONTENT DIGESTS. Unit ids are the package's
49
+ // OWN authored identifiers (requirement `/req/class-a/mpe`, term
50
+ // `/term/durability`, …) — the stability tier is STABLE PUBLIC
51
+ // IDENTIFIER: an id moves only when the package re-authors the
52
+ // identifier, and a rename of display text (name/label/statement)
53
+ // never moves it. Beside the id rides `content_hash` — sha256 over
54
+ // the unit's canonical JSON content (sorted keys, compact
55
+ // separators, UTF-8 — the form every JSON stack reproduces:
56
+ // `json.dumps(c, sort_keys=True, separators=(",", ":"),
57
+ // ensure_ascii=False)`). Identity = id; currency = digest. A
58
+ // rename moves the digest, never the id.
59
+ // 6. THE MACHINE PASSPORT. Every unit carries `passport` — the
60
+ // compact digest an agent (an MCP server) can carry and verify
61
+ // without loading the package: kind, id, the headline text
62
+ // (statement/definition), the machine expression (limit / check /
63
+ // derivation), the declared units, the applicability summary, the
64
+ // acceptance summary, the provenance URNs, and the content hash.
65
+ // `passportCanonical` renders it as the canonical string form.
66
+ // 7. LANGUAGE-TAGGED VARIANTS. Every unit carries `language` — the
67
+ // package's default spelling, the ISO 24229 tag of the INLINE
68
+ // prose values — and, when the package ships `text` blocks,
69
+ // `variants`: the alternate spellings resolved onto the unit by
70
+ // the C89 address rule (the longest registered dot-boundary
71
+ // prefix; the kernel element id, not the namespaced unit id),
72
+ // keyed by the addressed field path, each entry
73
+ // `{ spelling, via?, value }`. Both are authored content and
74
+ // participate in the content_hash; a text block addressed at an
75
+ // element the projection does not ship is counted
76
+ // (droppedTextBlocks), never silently lost.
77
+ //
78
+ // Congruence with the deployed consumer (oimlsmart/smart
79
+ // derive-model-plane.ts → oimlsmart/rag model_plane.py): the unit ids,
80
+ // the clause { doc, clause, urn } shape, the sha256 currency signal,
81
+ // and the bundle-level `source_hash` (the SAME algorithm — sha256 over
82
+ // every byte of the package directory, sorted walk, path + NUL +
83
+ // file-digest + LF) are deliberately identical so the consumer's pins
84
+ // (`standard → { source_hash, node_count }`) key without translation.
85
+ // The deliberate divergences: the document block nests under `package`
86
+ // (the bundle carries label/base_urn top-level), the digest input is
87
+ // the compact-canonical JSON documented above (the consumer's D1
88
+ // content_hash uses Python's default separators — the input shapes
89
+ // differ regardless), and the kind vocabulary is the kernel's honest
90
+ // one (a calculation with a rule type is `formula`, matching the
91
+ // consumer's calculations/formulas split; a verdict is `characteristic`,
92
+ // matching the deployed plane's characteristics.yaml projection).
93
+ //
94
+ // ONE-WAY PROJECTION, NEVER THE KERNEL'S TRUTH — the same doctrine as
95
+ // the ReqIF/RDF surfaces: the package stays the single source of truth;
96
+ // the export is generated, never authored, never re-imported.
97
+ //
98
+ // The diff-as-data API (the issue's ask 5) is the model diff
99
+ // (src/model-diff.ts), whose id keying this projection shares.
100
+ // ─────────────────────────────────────────────────────────────────────
101
+ Object.defineProperty(exports, "__esModule", { value: true });
102
+ exports.RETRIEVAL_FACET_VERSION = exports.RETRIEVAL_PROJECTION = void 0;
103
+ exports.canonicalJson = canonicalJson;
104
+ exports.retrievalDigest = retrievalDigest;
105
+ exports.passportCanonical = passportCanonical;
106
+ exports.packageSourceHash = packageSourceHash;
107
+ exports.normalizeClause = normalizeClause;
108
+ exports.collectClauses = collectClauses;
109
+ exports.termClauses = termClauses;
110
+ exports.applicabilitySummary = applicabilitySummary;
111
+ exports.retrievalDocParts = retrievalDocParts;
112
+ exports.retrievalFacet = retrievalFacet;
113
+ exports.resolveTextAddress = resolveTextAddress;
114
+ exports.packageEdition = packageEdition;
115
+ exports.exportStandardRetrieval = exportStandardRetrieval;
116
+ exports.exportPackageRetrieval = exportPackageRetrieval;
117
+ const node_crypto_1 = require("node:crypto");
118
+ const node_fs_1 = require("node:fs");
119
+ const node_path_1 = require("node:path");
120
+ const package_1 = require("../ser-des/package");
121
+ /**
122
+ * The projection shape version (semver'd by the document, not the
123
+ * package): a shape change — a field renamed, removed, or re-typed —
124
+ * bumps the version and is a re-index signal for every consumer;
125
+ * additive fields within a version are legal (consumers ignore what
126
+ * they do not read).
127
+ */
128
+ exports.RETRIEVAL_PROJECTION = 'primmel-retrieval/1';
129
+ /**
130
+ * The facet shape version (ask 3 — the pre-flattened retrieval facet):
131
+ * semver'd independently of the projection — a facet key renamed,
132
+ * removed, or re-typed bumps it (a re-index signal); additive keys
133
+ * within a version are legal (indexes ignore what they do not read).
134
+ */
135
+ exports.RETRIEVAL_FACET_VERSION = 'retrieval-facet/1';
136
+ // ── canonical JSON + the content digest ──────────────────────────────
137
+ /** Recursive key sort — the digest and serialization canonicalizer. */
138
+ function sortDeep(v) {
139
+ if (Array.isArray(v)) {
140
+ return v.map(sortDeep);
141
+ }
142
+ if (v && typeof v === 'object') {
143
+ const out = {};
144
+ for (const k of Object.keys(v).sort()) {
145
+ const val = sortDeep(v[k]);
146
+ if (val !== undefined) {
147
+ out[k] = val;
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ return v;
153
+ }
154
+ /**
155
+ * The canonical JSON form: sorted keys, compact separators, UTF-8 — the
156
+ * form every JSON stack reproduces (`json.dumps(c, sort_keys=True,
157
+ * separators=(",", ":"), ensure_ascii=False)`). The digest input.
158
+ */
159
+ function canonicalJson(value) {
160
+ return JSON.stringify(sortDeep(value));
161
+ }
162
+ /** sha256 hex over the canonical JSON of a value — the currency digest. */
163
+ function retrievalDigest(value) {
164
+ return (0, node_crypto_1.createHash)('sha256')
165
+ .update(canonicalJson(value), 'utf8')
166
+ .digest('hex');
167
+ }
168
+ /**
169
+ * The canonical serialized form of a unit's passport (ask 6): the
170
+ * string an agent carries and re-hashes to verify the unit.
171
+ */
172
+ function passportCanonical(passport) {
173
+ return canonicalJson(passport);
174
+ }
175
+ // ── the package source hash (the bundle freshness signal) ────────────
176
+ function* walkPackage(dir) {
177
+ for (const entry of (0, node_fs_1.readdirSync)(dir).sort()) {
178
+ if (entry === '.DS_Store') {
179
+ continue;
180
+ }
181
+ const p = (0, node_path_1.join)(dir, entry);
182
+ if ((0, node_fs_1.statSync)(p).isDirectory()) {
183
+ yield* walkPackage(p);
184
+ }
185
+ else {
186
+ yield p;
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * sha256 over every file of the package (path + bytes), deterministic —
192
+ * the SAME algorithm the deployed consumer's derive-model-plane.ts runs
193
+ * (sorted walk, `.DS_Store` skipped, relative path + NUL + the file's
194
+ * own sha256 + LF), so a source_hash here and a source_hash there are
195
+ * the same signal and the consumer's pins key without translation. ANY
196
+ * package change — a constraint re-authored, a clause re-cited — moves
197
+ * the hash; that byte-sensitivity is deliberate (identity rides the
198
+ * unit ids, currency rides the digests).
199
+ */
200
+ function packageSourceHash(dir) {
201
+ const h = (0, node_crypto_1.createHash)('sha256');
202
+ for (const file of walkPackage(dir)) {
203
+ h.update((0, node_path_1.relative)(dir, file));
204
+ h.update('\0');
205
+ h.update((0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(file)).digest());
206
+ h.update('\n');
207
+ }
208
+ return h.digest('hex');
209
+ }
210
+ // ── provenance: the clause-URN normalization (ask 1) ─────────────────
211
+ /**
212
+ * A producer-internal anchor is never a clause: the metanorma UUID form
213
+ * (`_eb46a3a3-…` — the shape the consumer strips at ingestion) is
214
+ * detected exactly like the consumer detects it.
215
+ */
216
+ const UUID_ANCHOR = /^_?[0-9a-f]{8}-[0-9a-f]{4}-/i;
217
+ const DOC_FRAGMENT = /#([^#]*)$/;
218
+ /**
219
+ * Normalize one raw provenance edge onto the document's own numbering.
220
+ * The `doc` slot of a loaded ref may carry an embedded `#fragment` (the
221
+ * derives-from fold's shape for non-clause anchors — ref.ts); a
222
+ * `#clause-<n>` fragment restores the clause, any other fragment rides
223
+ * as the `anchor` extra. A UUID-shaped clause or fragment is an anchor,
224
+ * never a clause.
225
+ */
226
+ function normalizeClause(doc, clause, fragment = '') {
227
+ let rest = doc ?? '';
228
+ let clauseOut = clause ?? '';
229
+ let frag = fragment ?? '';
230
+ let anchor;
231
+ const hash = DOC_FRAGMENT.exec(rest);
232
+ if (hash) {
233
+ rest = rest.slice(0, hash.index);
234
+ const embedded = hash[1] ?? '';
235
+ if (!clauseOut && embedded.startsWith('clause-')) {
236
+ const tail = embedded.slice('clause-'.length);
237
+ const slash = tail.indexOf('/');
238
+ if (slash >= 0) {
239
+ clauseOut = tail.slice(0, slash);
240
+ if (!frag) {
241
+ frag = tail.slice(slash + 1);
242
+ }
243
+ }
244
+ else {
245
+ clauseOut = tail;
246
+ }
247
+ }
248
+ else if (embedded && !frag) {
249
+ // A non-clause fragment (a table anchor, a UUID) is an anchor,
250
+ // never a clause number.
251
+ anchor = embedded;
252
+ }
253
+ else if (embedded) {
254
+ anchor = embedded;
255
+ }
256
+ }
257
+ if (clauseOut && UUID_ANCHOR.test(clauseOut)) {
258
+ // A UUID in the clause slot is a producer-internal anchor that lost
259
+ // its way — demote it, never present it as the document's numbering.
260
+ anchor = anchor ?? clauseOut;
261
+ clauseOut = '';
262
+ }
263
+ const urn = rest +
264
+ (clauseOut
265
+ ? `#clause-${clauseOut}${frag ? `/${frag}` : ''}`
266
+ : anchor && !UUID_ANCHOR.test(anchor)
267
+ ? `#${anchor}`
268
+ : '');
269
+ const out = { doc: rest, clause: clauseOut, urn };
270
+ if (anchor !== undefined) {
271
+ out.anchor = anchor;
272
+ }
273
+ if (frag) {
274
+ out.fragment = frag;
275
+ }
276
+ return out;
277
+ }
278
+ /** A ref target that is a document URN folds into provenance (the §18.4
279
+ * citation fold's defensive mirror — after a package load the fold has
280
+ * already run; a hand-built Standard may carry them unfolded). */
281
+ function refTargetClause(target) {
282
+ const m = /^(urn:[^#]+)(?:#(.+))?$/.exec(target);
283
+ if (!m) {
284
+ return null;
285
+ }
286
+ return { doc: m[1], clause: '', fragment: m[2] ?? '' };
287
+ }
288
+ /**
289
+ * Collect every provenance edge of an element, normalized and
290
+ * de-duplicated by URN, in authored order: the structured channels
291
+ * (source / sourceRef / sourceRefs), the scalar reference URN (the
292
+ * conformance test's v2 channel), and urn-targeted refs not already
293
+ * folded. The first edge is the unit's primary `clause`.
294
+ */
295
+ function collectClauses(el) {
296
+ const out = [];
297
+ const seen = new Set();
298
+ const push = (r) => {
299
+ if (!r || (!r.doc && !r.clause)) {
300
+ return;
301
+ }
302
+ const c = normalizeClause(r.doc ?? '', r.clause ?? '', r.fragment);
303
+ if (!seen.has(c.urn)) {
304
+ seen.add(c.urn);
305
+ out.push(c);
306
+ }
307
+ };
308
+ push(el.source);
309
+ push(el.sourceRef);
310
+ for (const r of el.sourceRefs ?? []) {
311
+ push(r);
312
+ }
313
+ // The conformance test's scalar reference (v2) arrives quote-wrapped
314
+ // from the codec's legacy path — strip before the URN test.
315
+ const reference = (el.reference ?? '').replace(/^"|"$/g, '');
316
+ if (reference.startsWith('urn:')) {
317
+ push(refTargetClause(reference));
318
+ }
319
+ for (const r of el.refs ?? []) {
320
+ if (r && typeof r.target === 'string' && r.target.startsWith('urn:')) {
321
+ push(refTargetClause(r.target));
322
+ }
323
+ }
324
+ return out;
325
+ }
326
+ /**
327
+ * A term's provenance (ask 1's sharpest case): the `source` facet is a
328
+ * plain string carrying whitespace-separated URNs (e.g. the VIML URN
329
+ * plus the Recommendation's clause URN); each parses onto a normalized
330
+ * edge. When no parsed edge carries a clause number and the term
331
+ * declares a `section` (the document's own terminology clause), the
332
+ * section becomes an edge on the package's base URN — the document's
333
+ * own numbering, exactly as authored.
334
+ */
335
+ function termClauses(term, baseUrn) {
336
+ const out = [];
337
+ const seen = new Set();
338
+ const push = (c) => {
339
+ if (c && !seen.has(c.urn)) {
340
+ seen.add(c.urn);
341
+ out.push(c);
342
+ }
343
+ };
344
+ for (const token of (term.source ?? '').split(/\s+/).filter(t => t)) {
345
+ push(token.startsWith('urn:') ? normalizeClause(token, '') : null);
346
+ }
347
+ if (term.section && !out.some(c => c.clause)) {
348
+ push(normalizeClause(baseUrn, term.section));
349
+ }
350
+ return out;
351
+ }
352
+ // ── the passport (ask 6) ─────────────────────────────────────────────
353
+ /** The canonical-compact applicability summary: `dim=v1|v2; …`, sorted. */
354
+ function applicabilitySummary(entries) {
355
+ return (entries ?? [])
356
+ .map(e => {
357
+ const mode = e.match && e.match !== 'any' ? `(${e.match})` : '';
358
+ return `${e.dimension}${mode}=${[...e.values].sort().join('|')}`;
359
+ })
360
+ .sort()
361
+ .join(';');
362
+ }
363
+ function passportOf(unit) {
364
+ return {
365
+ v: 1,
366
+ kind: unit.kind,
367
+ id: unit.id,
368
+ text: unit.statement ?? unit.definition ?? unit.name ?? '',
369
+ expression: unit.expression ?? '',
370
+ units: unit.units ?? [],
371
+ applicability: applicabilitySummary(unit.applicability),
372
+ acceptance: unit.acceptance ?? '',
373
+ provenance: (unit.clauses ?? []).map(c => c.urn),
374
+ content_hash: unit.content_hash,
375
+ };
376
+ }
377
+ const BASE_URN_PARTS = /^urn:oiml:pub:([rdbge]):(\d[\d-]*)(?::(\d{4}))?$/i;
378
+ function retrievalDocParts(baseUrn) {
379
+ const m = BASE_URN_PARTS.exec(baseUrn);
380
+ if (!m) {
381
+ return { doctype: '', doc_number: '', year: '', label: '' };
382
+ }
383
+ const doctype = (m[1] ?? '').toLowerCase();
384
+ const docNumber = m[2] ?? '';
385
+ const year = m[3] ?? '';
386
+ return {
387
+ doctype,
388
+ doc_number: docNumber,
389
+ year,
390
+ label: `OIML ${doctype.toUpperCase()} ${docNumber}${year ? `:${year}` : ''}`,
391
+ };
392
+ }
393
+ /**
394
+ * Build the unit's flat facet (ask 3): the document fields every vector
395
+ * of this package carries, the unit's kind + currency keys, its primary
396
+ * clause anchor, and its applicability dimensions flattened to
397
+ * `app_<dim>` keys (values sorted, `|`-joined; a non-`any` match mode
398
+ * rides beside as `app_<dim>_match`).
399
+ *
400
+ * Key congruence with the deployed consumer's chunk wire schema
401
+ * (ChunkMetaModel): `doc_id`, `docidentifier`, `doctype`, `doc_number`,
402
+ * `edition`, `language`, `clause_anchor`, `clause_title`, `status`,
403
+ * `unit_id`, `block`, `unit_hash` carry the same names and semantics.
404
+ * The lane constants the schema also carries (`tier`, `corpus`,
405
+ * `producer`, `text_ref`, `superseded_by`) are DEPLOYMENT stamps, not
406
+ * package content — the consumer's adapter sets them per lane; the
407
+ * facet never invents them. Three deliberate value choices:
408
+ *
409
+ * - `language` is the package's authored default spelling (the
410
+ * ISO 24229 code, e.g. `eng-Latn`) — the consumer lanes stamp a
411
+ * constant two-letter code today; the facet carries the authored
412
+ * truth and the adapter maps.
413
+ * - `clause_anchor` is the primary edge's document clause number, ''
414
+ * when the unit names no clause (never a producer UUID — ask 1
415
+ * already demoted those to `anchor`). The consumer's "model"
416
+ * fallback for clause-less model units is its lane marker, applied
417
+ * at its adapter.
418
+ * - `edition` is the package block's canonical edition (ask 2), not
419
+ * a re-parse of the base URN — the two coincide on URN-carrying
420
+ * packages, and the package block is authoritative when they drift.
421
+ */
422
+ function retrievalFacet(unit, pkg) {
423
+ const parts = retrievalDocParts(pkg.base_urn);
424
+ const facet = {
425
+ unit_id: unit.id,
426
+ unit_hash: unit.content_hash,
427
+ block: unit.kind,
428
+ doc_id: pkg.id,
429
+ docidentifier: parts.label || pkg.title,
430
+ doctype: parts.doctype,
431
+ doc_number: parts.doc_number,
432
+ edition: pkg.edition,
433
+ model_version: pkg.model_version,
434
+ language: pkg.default_spelling,
435
+ clause_anchor: unit.clause?.clause ?? '',
436
+ clause_title: unit.name ?? unit.id,
437
+ ...(present(pkg.status) ? { status: pkg.status } : {}),
438
+ };
439
+ const valuesByDim = new Map();
440
+ const matchByDim = new Map();
441
+ for (const entry of unit.applicability ?? []) {
442
+ const values = entry.values.length > 0 ? entry.values : Object.keys(entry.mapping ?? {});
443
+ const set = valuesByDim.get(entry.dimension) ?? new Set();
444
+ for (const v of values) {
445
+ set.add(v);
446
+ }
447
+ valuesByDim.set(entry.dimension, set);
448
+ if (entry.match &&
449
+ entry.match !== 'any' &&
450
+ !matchByDim.has(entry.dimension)) {
451
+ matchByDim.set(entry.dimension, entry.match);
452
+ }
453
+ }
454
+ for (const dim of [...valuesByDim.keys()].sort()) {
455
+ facet[`app_${dim}`] = [...valuesByDim.get(dim)].sort().join('|');
456
+ const match = matchByDim.get(dim);
457
+ if (match) {
458
+ facet[`app_${dim}_match`] = match;
459
+ }
460
+ }
461
+ return facet;
462
+ }
463
+ // ── language-tagged variants (ask 7) ─────────────────────────────────
464
+ /**
465
+ * Resolve a `text` block's address onto its element: the LONGEST
466
+ * dot-boundary prefix registered in the package (element ids may
467
+ * themselves carry dots — `r144-3/sec-3.4`); the rest is the path into
468
+ * the element's structure, the terminal segment the prose field. The
469
+ * same rule the C89 linter resolves with (check.ts) — the projection
470
+ * and the linter never disagree about which element a text block
471
+ * addresses.
472
+ */
473
+ function resolveTextAddress(address, elementIds) {
474
+ const segments = address.split('.');
475
+ if (segments.length < 2 || segments.some(s => s === '')) {
476
+ return null;
477
+ }
478
+ for (let i = segments.length - 1; i >= 1; i--) {
479
+ const candidate = segments.slice(0, i).join('.');
480
+ if (elementIds.has(candidate)) {
481
+ return { elementId: candidate, path: segments.slice(i).join('.') };
482
+ }
483
+ }
484
+ return null;
485
+ }
486
+ /**
487
+ * The id-keyed collections a text block may address — the mirror of the
488
+ * C89 linter's element registry (check.ts). Resolution runs against the
489
+ * FULL registry (not only the projected kinds): an address whose
490
+ * longest registered prefix is an unprojected element (a form, an
491
+ * instrument) must NOT fall through to a shorter projected prefix —
492
+ * that misattribution is worse than a drop, and the drop is counted.
493
+ */
494
+ const TEXT_ADDRESS_COLLECTIONS = [
495
+ 'requirements',
496
+ 'requirementClasses',
497
+ 'conformanceTests',
498
+ 'conformanceClasses',
499
+ 'terms',
500
+ 'forms',
501
+ 'subforms',
502
+ 'symbols',
503
+ 'tables',
504
+ 'calculations',
505
+ 'notes',
506
+ 'provisions',
507
+ 'processes',
508
+ 'instruments',
509
+ 'attributeDefinitions',
510
+ 'capabilities',
511
+ 'behaviors',
512
+ 'conditionSets',
513
+ 'subjects',
514
+ 'referenceMaterials',
515
+ 'artifactDefinitions',
516
+ 'monitors',
517
+ 'passports',
518
+ 'invariants',
519
+ 'testSequences',
520
+ 'formulasUsed',
521
+ 'stateMachines',
522
+ 'figures',
523
+ 'links',
524
+ ];
525
+ /**
526
+ * Attach the language tag and the variants to the built unit contents
527
+ * (pre-digest — both are authored content): `language` is the package's
528
+ * default spelling on every unit; `variants` resolves each `text` block
529
+ * onto its unit (by the KERNEL element id — a term's text block
530
+ * addresses `frobnicator`, not `/term/frobnicator`) and keys the
531
+ * entries by the addressed field path. Returns the count of text blocks
532
+ * whose variants reached no unit (the droppedTextBlocks honesty tally).
533
+ */
534
+ function attachVariants(built, standard, pkg) {
535
+ if (present(pkg.default_spelling)) {
536
+ for (const b of built) {
537
+ b.content.language = pkg.default_spelling;
538
+ }
539
+ }
540
+ const texts = standard.texts ?? [];
541
+ if (texts.length === 0) {
542
+ return 0;
543
+ }
544
+ const registry = new Set();
545
+ for (const field of TEXT_ADDRESS_COLLECTIONS) {
546
+ for (const item of standard[field] ?? []) {
547
+ if (item?.id) {
548
+ registry.add(item.id);
549
+ }
550
+ }
551
+ }
552
+ const unitByElement = new Map(built.map(b => [b.elementId, b.content]));
553
+ let dropped = 0;
554
+ for (const t of texts) {
555
+ const resolved = resolveTextAddress(t.id, registry);
556
+ const content = resolved
557
+ ? unitByElement.get(resolved.elementId)
558
+ : undefined;
559
+ if (!resolved || !content) {
560
+ dropped++;
561
+ continue;
562
+ }
563
+ const variants = (content.variants ??= {});
564
+ variants[resolved.path] = [
565
+ ...(variants[resolved.path] ?? []),
566
+ ...t.entries,
567
+ ];
568
+ }
569
+ return dropped;
570
+ }
571
+ /**
572
+ * Attach the provenance edges to a built unit content. The digest,
573
+ * passport and facet arrive later (`completeUnit`) — the language tag
574
+ * and variants (ask 7) attach between assembly and digestion, so they
575
+ * participate in the content_hash like any authored field.
576
+ */
577
+ function assemble(content, clauses) {
578
+ if (clauses.length > 0) {
579
+ content.clause = clauses[0];
580
+ content.clauses = clauses;
581
+ }
582
+ return content;
583
+ }
584
+ /**
585
+ * Complete an assembled unit content into the shipped unit: the content
586
+ * digest (over the authored fields — language and variants included),
587
+ * then the derived projections (passport, facet) that read it.
588
+ */
589
+ function completeUnit(content, pkg) {
590
+ const hash = retrievalDigest(content);
591
+ const unit = {
592
+ ...content,
593
+ content_hash: hash,
594
+ };
595
+ unit.passport = passportOf(unit);
596
+ // The facet attaches last (a derived projection of the digested
597
+ // content — the unit_hash key reads the computed content_hash).
598
+ unit.facet = retrievalFacet(unit, pkg);
599
+ return unit;
600
+ }
601
+ /** The parent class/scope id of a scoped id (`/req/a/b` → `/req/a`). */
602
+ function scopeOf(id) {
603
+ const slash = id.lastIndexOf('/');
604
+ return slash > 0 ? id.slice(0, slash) : undefined;
605
+ }
606
+ const present = (s) => typeof s === 'string' && s.length > 0;
607
+ const presentList = (xs) => xs && xs.length > 0 ? xs : undefined;
608
+ function requirementUnit(r) {
609
+ return assemble({
610
+ id: r.id,
611
+ kind: 'requirement',
612
+ ...(scopeOf(r.id) ? { class: scopeOf(r.id) } : {}),
613
+ ...(present(r.name) ? { name: r.name } : {}),
614
+ ...(present(r.statement) ? { statement: r.statement } : {}),
615
+ ...(present(r.guidance) ? { guidance: r.guidance } : {}),
616
+ ...(present(r.obligation) ? { obligation: r.obligation } : {}),
617
+ modality: r.limit?.modality || 'shall',
618
+ ...(r.limit?.expression
619
+ ? {
620
+ expression: r.limit.expression,
621
+ ...(presentList(r.limit.uses)
622
+ ? { expression_inputs: r.limit.uses }
623
+ : {}),
624
+ }
625
+ : {}),
626
+ ...(presentList(r.applicability)
627
+ ? { applicability: r.applicability }
628
+ : {}),
629
+ ...(presentList(r.bindsTo) ? { binds_to: r.bindsTo } : {}),
630
+ ...(r.limit?.accepts
631
+ ? {
632
+ acceptance: `${r.limit.accepts.verdict} ${r.limit.accepts.op} ${r.limit.accepts.limit}`,
633
+ }
634
+ : {}),
635
+ ...(present(r.acceptanceCriteria)
636
+ ? { acceptance_criteria: r.acceptanceCriteria }
637
+ : {}),
638
+ ...(present(r.verificationMethod)
639
+ ? {
640
+ verification: {
641
+ method: r.verificationMethod,
642
+ description: r.verificationDescription ?? '',
643
+ },
644
+ }
645
+ : {}),
646
+ ...(present(r.channel) ? { channel: r.channel } : {}),
647
+ ...(presentList(r.dependencies) ? { dependencies: r.dependencies } : {}),
648
+ }, collectClauses(r));
649
+ }
650
+ function conformanceTestUnit(t) {
651
+ const payload = {
652
+ ...(present(t.kind) ? { test_kind: t.kind } : {}),
653
+ ...(present(t.methodRef) ? { method_ref: t.methodRef } : {}),
654
+ ...(presentList(t.preconditions)
655
+ ? {
656
+ preconditions: t.preconditions.map(p => ({
657
+ id: p.id,
658
+ check: p.check,
659
+ description: p.description,
660
+ on_violation: p.onViolation,
661
+ })),
662
+ }
663
+ : {}),
664
+ };
665
+ return assemble({
666
+ id: t.id,
667
+ kind: 'conformance_test',
668
+ ...(scopeOf(t.id) ? { class: scopeOf(t.id) } : {}),
669
+ ...(present(t.name) ? { name: t.name } : {}),
670
+ ...(present(t.purpose) ? { statement: t.purpose } : {}),
671
+ ...(present(t.method) ? { definition: t.method } : {}),
672
+ ...(present(t.guidance) ? { guidance: t.guidance } : {}),
673
+ ...(present(t.obligation) ? { obligation: t.obligation } : {}),
674
+ ...(presentList(t.applicability)
675
+ ? { applicability: t.applicability }
676
+ : {}),
677
+ ...(presentList(t.targets) ? { targets: t.targets } : {}),
678
+ ...(presentList(t.bindsTo) ? { binds_to: t.bindsTo } : {}),
679
+ ...(present(t.acceptancePassIf)
680
+ ? { acceptance: t.acceptancePassIf }
681
+ : present(t.acceptanceCriteriaDescription)
682
+ ? { acceptance: t.acceptanceCriteriaDescription }
683
+ : {}),
684
+ ...(presentList(t.dependencies) ? { dependencies: t.dependencies } : {}),
685
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
686
+ }, collectClauses(t));
687
+ }
688
+ function termUnit(t, baseUrn) {
689
+ const payload = {
690
+ ...(present(t.section) ? { section: t.section } : {}),
691
+ ...(present(t.language) ? { language: t.language } : {}),
692
+ ...(t.vocabRef ? { vocab_ref: t.vocabRef } : {}),
693
+ // The alias family (MN 114 v3.2, clause 13.10.1): the parser folds
694
+ // the v2 `alt` spelling into the canonical aliases channel; the
695
+ // payload keeps the deployed `alt` key (contract stability) and adds
696
+ // the v3.2 facets.
697
+ ...(presentList(t.aliases ?? t.alt) ? { alt: (t.aliases ?? t.alt) } : {}),
698
+ ...(presentList(t.colloquial) ? { colloquial: t.colloquial } : {}),
699
+ ...(t.aliasSpellings && Object.keys(t.aliasSpellings).length > 0
700
+ ? { alias_spellings: t.aliasSpellings }
701
+ : {}),
702
+ ...(presentList(t.abbreviations) ? { abbreviations: t.abbreviations } : {}),
703
+ ...(presentList(t.seeAlso) ? { see_also: t.seeAlso } : {}),
704
+ };
705
+ return assemble({
706
+ id: `/term/${t.id}`,
707
+ kind: 'term',
708
+ ...(present(t.label) ? { name: t.label } : {}),
709
+ ...(present(t.definition) ? { definition: t.definition } : {}),
710
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
711
+ }, termClauses(t, baseUrn));
712
+ }
713
+ function attributeUnit(a) {
714
+ const payload = {
715
+ ...(present(a.symbol) ? { symbol: a.symbol } : {}),
716
+ ...(present(a.quantityKind) ? { quantity_kind: a.quantityKind } : {}),
717
+ ...(present(a.valueType) ? { value_type: a.valueType } : {}),
718
+ ...(present(a.origin) ? { origin: a.origin } : {}),
719
+ ...(present(a.scope) ? { scope: a.scope } : {}),
720
+ ...(present(a.category) ? { category: a.category } : {}),
721
+ ...(present(a.irdi) ? { irdi: a.irdi } : {}),
722
+ ...(presentList(a.enumValues) ? { enum_values: a.enumValues } : {}),
723
+ };
724
+ return assemble({
725
+ id: `/attribute/${a.id}`,
726
+ kind: 'attribute',
727
+ ...(present(a.name) ? { name: a.name } : {}),
728
+ ...(present(a.definition) ? { definition: a.definition } : {}),
729
+ ...(present(a.unit) && a.unit !== '1' ? { units: [a.unit] } : {}),
730
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
731
+ }, collectClauses(a));
732
+ }
733
+ function behaviorUnit(b) {
734
+ const payload = {
735
+ ...(present(b.kind) ? { behavior_kind: b.kind } : {}),
736
+ ...(present(b.stimulus) ? { stimulus: b.stimulus } : {}),
737
+ };
738
+ return assemble({
739
+ id: `/behavior/${b.id}`,
740
+ kind: 'behavior',
741
+ name: b.id,
742
+ ...(present(b.response) ? { statement: b.response } : {}),
743
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
744
+ }, collectClauses(b));
745
+ }
746
+ function calculationUnits(c) {
747
+ const out = new Set();
748
+ for (const i of c.inputs ?? []) {
749
+ if (present(i.unit) && i.unit !== '1') {
750
+ out.add(i.unit);
751
+ }
752
+ }
753
+ if (c.output && present(c.output.unit) && c.output.unit !== '1') {
754
+ out.add(c.output.unit);
755
+ }
756
+ return [...out].sort();
757
+ }
758
+ function calculationUnit(c) {
759
+ // The consumer's calculations/formulas split: a calculation carrying
760
+ // an engine rule type projects as a `formula` unit (the operator
761
+ // signature), the rest as `calculation` (typed IO).
762
+ const kind = present(c.ruleType)
763
+ ? 'formula'
764
+ : 'calculation';
765
+ const units = calculationUnits(c);
766
+ const payload = {
767
+ name: c.name,
768
+ ...(present(c.ruleType) ? { rule_type: c.ruleType } : {}),
769
+ ...(present(c.category) ? { category: c.category } : {}),
770
+ ...(presentList(c.inputs)
771
+ ? {
772
+ inputs: c.inputs.map(i => ({
773
+ name: i.name,
774
+ type: i.type,
775
+ ...(present(i.unit) ? { unit: i.unit } : {}),
776
+ })),
777
+ }
778
+ : {}),
779
+ ...(c.output && present(c.output.type) ? { output: c.output } : {}),
780
+ ...(c.lookup ? { lookup: c.lookup } : {}),
781
+ ...(present(c.profile) ? { profile: c.profile } : {}),
782
+ };
783
+ return assemble({
784
+ id: present(c.identifier) ? c.identifier : `/calculation/${c.id}`,
785
+ kind,
786
+ name: c.label || c.name || c.id,
787
+ ...(present(c.description) ? { definition: c.description } : {}),
788
+ ...(present(c.expression) ? { expression: c.expression } : {}),
789
+ ...(presentList(c.params) ? { expression_inputs: c.params } : {}),
790
+ ...(units.length > 0 ? { units } : {}),
791
+ payload,
792
+ }, collectClauses({
793
+ sourceRef: c.sourceRef,
794
+ sourceRefs: c.sourceRefs,
795
+ refs: c.refs,
796
+ }));
797
+ }
798
+ function symbolUnit(s) {
799
+ const units = present(s.unit) && s.unit !== '1' ? [s.unit] : undefined;
800
+ const payload = {
801
+ ...(present(s.type) ? { symbol_type: s.type } : {}),
802
+ ...(present(s.kind) ? { symbol_kind: s.kind } : {}),
803
+ ...(present(s.quantityKind) ? { quantity_kind: s.quantityKind } : {}),
804
+ ...(present(s.origin) ? { origin: s.origin } : {}),
805
+ ...(present(s.latex) ? { latex: s.latex } : {}),
806
+ ...(s.formula && present(s.formula.display)
807
+ ? { display: s.formula.display }
808
+ : {}),
809
+ ...(present(s.calculation) ? { calculation: s.calculation } : {}),
810
+ };
811
+ return assemble({
812
+ id: `/symbol/${s.id}`,
813
+ kind: 'symbol',
814
+ ...(present(s.name) ? { name: s.name } : {}),
815
+ ...(present(s.definition) ? { definition: s.definition } : {}),
816
+ ...(s.formula && present(s.formula.expression)
817
+ ? {
818
+ expression: s.formula.expression,
819
+ ...(presentList(s.formula.inputs)
820
+ ? { expression_inputs: s.formula.inputs }
821
+ : {}),
822
+ }
823
+ : {}),
824
+ ...(units ? { units } : {}),
825
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
826
+ }, collectClauses({
827
+ sourceRef: s.sourceRef,
828
+ sourceRefs: s.sourceRefs,
829
+ refs: s.refs,
830
+ }));
831
+ }
832
+ function constraintUnit(c) {
833
+ return assemble({
834
+ id: `/constraint/${c.id}`,
835
+ kind: 'constraint',
836
+ ...(present(c.name) ? { name: c.name } : {}),
837
+ ...(present(c.violationMeaning) ? { statement: c.violationMeaning } : {}),
838
+ ...(present(c.check) ? { expression: c.check } : {}),
839
+ payload: {
840
+ ...(present(c.stereotype) ? { stereotype: c.stereotype } : {}),
841
+ ...(present(c.onViolation) ? { on_violation: c.onViolation } : {}),
842
+ },
843
+ }, collectClauses(c));
844
+ }
845
+ function characteristicUnit(v) {
846
+ // The deployed plane's `characteristic` kind: the canonical verdict
847
+ // quantity (derive once, reference everywhere).
848
+ const units = present(v.unit) && v.unit !== '1' ? [v.unit] : undefined;
849
+ const payload = {
850
+ ...(present(v.behavior) ? { behavior: v.behavior } : {}),
851
+ ...(present(v.quantityKind) ? { quantity_kind: v.quantityKind } : {}),
852
+ ...(v.seriesReduction ? { series_reduction: v.seriesReduction } : {}),
853
+ };
854
+ return assemble({
855
+ id: `/characteristic/${v.id}`,
856
+ kind: 'characteristic',
857
+ ...(present(v.symbol) ? { name: v.symbol } : {}),
858
+ ...(present(v.derive)
859
+ ? {
860
+ expression: v.derive,
861
+ ...(presentList(v.inputs) ? { expression_inputs: v.inputs } : {}),
862
+ }
863
+ : {}),
864
+ ...(units ? { units } : {}),
865
+ ...(v.acceptance && present(v.acceptance.rule)
866
+ ? { acceptance: v.acceptance.rule }
867
+ : {}),
868
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
869
+ }, collectClauses(v));
870
+ }
871
+ function tableUnit(t) {
872
+ const units = (t.columnDefs ?? [])
873
+ .map(c => c.unit)
874
+ .filter(u => present(u) && u !== '1')
875
+ .sort();
876
+ const payload = {
877
+ ...(presentList(t.columnDefs)
878
+ ? {
879
+ columns: t.columnDefs.map(c => ({
880
+ name: c.name,
881
+ type: c.type,
882
+ ...(present(c.unit) ? { unit: c.unit } : {}),
883
+ })),
884
+ }
885
+ : present(t.columns)
886
+ ? { columns: t.columns }
887
+ : {}),
888
+ ...(presentList(t.data) ? { rows: t.data } : {}),
889
+ ...(t.profiles ? { profiles: t.profiles } : {}),
890
+ };
891
+ return assemble({
892
+ id: `/table/${t.id}`,
893
+ kind: 'table',
894
+ ...(present(t.title) ? { name: t.title } : {}),
895
+ ...(present(t.description) ? { definition: t.description } : {}),
896
+ ...(units.length > 0 ? { units: [...new Set(units)] } : {}),
897
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
898
+ }, collectClauses({
899
+ sourceRef: t.sourceRef,
900
+ sourceRefs: t.sourceRefs,
901
+ refs: t.refs,
902
+ }));
903
+ }
904
+ function sequenceUnit(s) {
905
+ const payload = {
906
+ steps: s.steps.map(st => ({
907
+ order: st.order,
908
+ ...(present(st.test) ? { test: st.test } : {}),
909
+ ...(present(st.phase) ? { phase: st.phase } : {}),
910
+ ...(present(st.role) ? { role: st.role } : {}),
911
+ ...(st.dependsOn !== null ? { depends_on: st.dependsOn } : {}),
912
+ })),
913
+ ...(present(s.sampleApplicability)
914
+ ? { sample_applicability: s.sampleApplicability }
915
+ : {}),
916
+ };
917
+ return assemble({
918
+ id: `/sequence/${s.id}`,
919
+ kind: 'sequence',
920
+ ...(present(s.name) ? { name: s.name } : {}),
921
+ ...(present(s.description) ? { statement: s.description } : {}),
922
+ payload,
923
+ }, collectClauses({ sourceRefs: s.sourceRefs, refs: s.refs }));
924
+ }
925
+ function noteUnit(n) {
926
+ return assemble({
927
+ id: `/note/${n.id}`,
928
+ kind: 'note',
929
+ name: n.type,
930
+ ...(present(n.message) ? { statement: n.message } : {}),
931
+ }, []);
932
+ }
933
+ function stateMachineUnit(m) {
934
+ // The state machine keys by the bound entity's name (the construct
935
+ // declares no id — the model diff's E12 keying).
936
+ const id = m.entityName;
937
+ const payload = {
938
+ ...(present(m.kind) ? { machine_kind: m.kind } : {}),
939
+ ...(present(m.initialState) ? { initial: m.initialState } : {}),
940
+ ...(presentList(m.states) ? { states: m.states.map(s => s.name) } : {}),
941
+ ...(presentList(m.transitions)
942
+ ? {
943
+ transitions: m.transitions.map(t => ({
944
+ from: t.from,
945
+ to: t.to,
946
+ ...(present(t.actionName) ? { action: t.actionName } : {}),
947
+ ...(present(t.guard) ? { guard: t.guard } : {}),
948
+ })),
949
+ }
950
+ : {}),
951
+ };
952
+ return assemble({
953
+ id: `/state-machine/${id}`,
954
+ kind: 'state_machine',
955
+ name: id,
956
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
957
+ }, []);
958
+ }
959
+ function dimensionUnit(d) {
960
+ const payload = {
961
+ ...(present(d.scope) ? { scope: d.scope } : {}),
962
+ ...(present(d.cardinality) ? { cardinality: d.cardinality } : {}),
963
+ values: d.values.map(v => ({
964
+ id: v.id,
965
+ ...(present(v.label) ? { label: v.label } : {}),
966
+ ...(presentList(v.implies) ? { implies: v.implies } : {}),
967
+ ...(present(v.termRef) ? { term_ref: v.termRef } : {}),
968
+ })),
969
+ };
970
+ return assemble({
971
+ id: `/dimension/${d.id}`,
972
+ kind: 'dimension',
973
+ ...(present(d.label) ? { name: d.label } : {}),
974
+ ...(present(d.description) ? { definition: d.description } : {}),
975
+ payload,
976
+ }, collectClauses(d));
977
+ }
978
+ // ── the document block (ask 2) ───────────────────────────────────────
979
+ /**
980
+ * The publication edition the model corresponds to: the manifest's
981
+ * newest `editions` entry (the register is newest-first), the base
982
+ * URN's trailing year segment as fallback. NEVER the package version —
983
+ * the two fields answer different questions (edition steering vs
984
+ * freshness gating) and never borrow each other's value.
985
+ */
986
+ function packageEdition(manifest) {
987
+ const first = manifest?.editions?.[0];
988
+ if (present(first)) {
989
+ return first;
990
+ }
991
+ const tail = /:(\d{4})$/.exec(manifest?.baseUrn ?? '');
992
+ return tail?.[1] ?? '';
993
+ }
994
+ function packageBlock(standard) {
995
+ const m = standard.packageManifest;
996
+ if (m) {
997
+ return {
998
+ id: m.id,
999
+ title: m.title,
1000
+ kind: m.kind ?? 'rec',
1001
+ edition: packageEdition(m),
1002
+ model_version: m.version,
1003
+ editions: [...(m.editions ?? [])],
1004
+ base_urn: m.baseUrn,
1005
+ ...(present(m.status) ? { status: m.status } : {}),
1006
+ default_spelling: m.defaultSpelling ?? '',
1007
+ spellings: [
1008
+ ...(m.spellings ?? (m.defaultSpelling ? [m.defaultSpelling] : [])),
1009
+ ],
1010
+ ...(presentList(m.supersedes) ? { supersedes: [...m.supersedes] } : {}),
1011
+ };
1012
+ }
1013
+ // A manifestless Standard (a single-file load): the metadata header
1014
+ // carries what the manifest would.
1015
+ const meta = standard.meta;
1016
+ return {
1017
+ id: meta?.namespace ?? '',
1018
+ title: meta?.title ?? '',
1019
+ kind: '',
1020
+ edition: meta?.edition ?? '',
1021
+ model_version: '',
1022
+ editions: present(meta?.edition) ? [meta.edition] : [],
1023
+ base_urn: '',
1024
+ default_spelling: '',
1025
+ spellings: [],
1026
+ };
1027
+ }
1028
+ /**
1029
+ * Project a loaded Standard into the retrieval document (pure — no I/O;
1030
+ * the id projection, the clause normalization, the digests).
1031
+ */
1032
+ function exportStandardRetrieval(standard, options = {}) {
1033
+ const pkg = packageBlock(standard);
1034
+ // Build the unit contents paired with the KERNEL element id — the
1035
+ // address a `text` block targets (a term's variants address
1036
+ // `frobnicator`, not the namespaced unit id `/term/frobnicator`).
1037
+ const built = [];
1038
+ const push = (content, elementId) => {
1039
+ built.push({ content, elementId });
1040
+ };
1041
+ for (const r of standard.requirements ?? []) {
1042
+ push(requirementUnit(r), r.id);
1043
+ }
1044
+ for (const t of standard.conformanceTests ?? []) {
1045
+ push(conformanceTestUnit(t), t.id);
1046
+ }
1047
+ for (const t of standard.terms ?? []) {
1048
+ push(termUnit(t, pkg.base_urn), t.id);
1049
+ }
1050
+ for (const a of standard.attributeDefinitions ?? []) {
1051
+ push(attributeUnit(a), a.id);
1052
+ }
1053
+ for (const b of standard.behaviors ?? []) {
1054
+ push(behaviorUnit(b), b.id);
1055
+ }
1056
+ for (const c of standard.calculations ?? []) {
1057
+ // The kernel element id is the construct id (`calculation frobIndex`),
1058
+ // never the declared `identifier` path the unit id prefers.
1059
+ push(calculationUnit(c), c.id);
1060
+ }
1061
+ for (const s of standard.symbols ?? []) {
1062
+ push(symbolUnit(s), s.id);
1063
+ }
1064
+ for (const c of standard.constraints ?? []) {
1065
+ push(constraintUnit(c), c.id);
1066
+ }
1067
+ for (const v of standard.verdicts ?? []) {
1068
+ push(characteristicUnit(v), v.id);
1069
+ }
1070
+ for (const t of standard.tables ?? []) {
1071
+ push(tableUnit(t), t.id);
1072
+ }
1073
+ for (const s of standard.testSequences ?? []) {
1074
+ push(sequenceUnit(s), s.id);
1075
+ }
1076
+ for (const n of standard.notes ?? []) {
1077
+ push(noteUnit(n), n.id);
1078
+ }
1079
+ for (const m of standard.stateMachines ?? []) {
1080
+ // The construct declares no id (the unit keys by the bound entity's
1081
+ // name) — C89 registers nothing, so no text block can address it.
1082
+ push(stateMachineUnit(m), m.entityName);
1083
+ }
1084
+ for (const i of standard.instruments ?? []) {
1085
+ for (const d of i.dimensions ?? []) {
1086
+ // Dimensions nest under the instrument in the C89 registry — a
1087
+ // text block addresses them by path through the instrument, which
1088
+ // the projection does not ship as a unit: those blocks count as
1089
+ // dropped, never misattached.
1090
+ push(dimensionUnit(d), d.id);
1091
+ }
1092
+ }
1093
+ // The language tag + variants attach pre-digest (authored content —
1094
+ // they move the content_hash like any authored field).
1095
+ const droppedTextBlocks = attachVariants(built, standard, pkg);
1096
+ const units = built.map(b => completeUnit(b.content, pkg));
1097
+ const byKind = {};
1098
+ let withClause = 0;
1099
+ let anchorOnlyProvenance = 0;
1100
+ let nonUrnDocRefs = 0;
1101
+ let withoutProvenance = 0;
1102
+ let withVariants = 0;
1103
+ for (const u of units) {
1104
+ byKind[u.kind] = (byKind[u.kind] ?? 0) + 1;
1105
+ if (u.variants && Object.keys(u.variants).length > 0) {
1106
+ withVariants++;
1107
+ }
1108
+ const clauses = u.clauses ?? [];
1109
+ if (clauses.some(c => c.clause)) {
1110
+ withClause++;
1111
+ }
1112
+ else if (clauses.length > 0) {
1113
+ anchorOnlyProvenance++;
1114
+ }
1115
+ else {
1116
+ withoutProvenance++;
1117
+ }
1118
+ if (clauses.some(c => c.doc && !c.doc.startsWith('urn:'))) {
1119
+ nonUrnDocRefs++;
1120
+ }
1121
+ }
1122
+ const document = {
1123
+ projection: exports.RETRIEVAL_PROJECTION,
1124
+ facet_version: exports.RETRIEVAL_FACET_VERSION,
1125
+ package: pkg,
1126
+ ...(options.sourceHash !== undefined
1127
+ ? { source_hash: options.sourceHash }
1128
+ : {}),
1129
+ units,
1130
+ };
1131
+ return {
1132
+ document,
1133
+ json: JSON.stringify(sortDeep(document), null, 2) + '\n',
1134
+ stats: {
1135
+ units: units.length,
1136
+ byKind,
1137
+ withClause,
1138
+ anchorOnlyProvenance,
1139
+ nonUrnDocRefs,
1140
+ withoutProvenance,
1141
+ withVariants,
1142
+ droppedTextBlocks,
1143
+ },
1144
+ };
1145
+ }
1146
+ /**
1147
+ * Export a package directory: load, hash the package bytes (the bundle
1148
+ * freshness signal — the deployed consumer's exact algorithm), project.
1149
+ */
1150
+ function exportPackageRetrieval(dir) {
1151
+ const { standard } = (0, package_1.loadPackageWithIssues)(dir);
1152
+ return exportStandardRetrieval(standard, {
1153
+ sourceHash: packageSourceHash(dir),
1154
+ });
1155
+ }
1156
+ //# sourceMappingURL=retrieval.js.map