suma 0.5.9 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54c10b8d56459f9d53cd1b5988cbcc2193675f4dffa43e115845400497faa8f9
4
- data.tar.gz: b46df0c3ef57f69e2e0e5e3b7dfdf3c464189a66f372ef2a5f7a5d3a8d47b34b
3
+ metadata.gz: 01e0b1dd212172912b654cac481e0ce1eea1efee1275f45d4332c0a6bbb28e69
4
+ data.tar.gz: 54399e492be26b0a09595debdc61c256f566da28ed13ca002cae2a6eb0925b57
5
5
  SHA512:
6
- metadata.gz: 51f6a6ed42abb76af7342d06e0b73c3132ddf7e65ffdb5d29c96892860fd3be534557581f6bd14b6dd15e022a8e2ac250cdb23f6ed4d68242457515b8951b415
7
- data.tar.gz: c562c0691baa4b3fb708b94dd5853014a18e4bad0cd8527cbf7922a7273ebd982a5239ea13914c0d60f279e2ceae9b534a43a58ff42b339c7dd5114f7e5be576
6
+ metadata.gz: 9fb0aebf85233af1df6162efb518605bd7ab01c7396e7af6012144fd05c74a135ba7d2833b7bb633fa45c755e067d6799569412df0b21458b33d2730a5081a3a
7
+ data.tar.gz: d9836497a01e7711aece5c76534ecfa4e7e0876bfd414aab783ada288b1906e07a3f3b6994d17085f294b79a827c16ecb61bab7cf305a823b9527933e5aa7f1c
data/.rubocop_todo.yml CHANGED
@@ -116,9 +116,11 @@ Lint/UnusedMethodArgument:
116
116
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
117
117
  Metrics/AbcSize:
118
118
  Exclude:
119
+ - 'lib/suma/gcr_package.rb'
119
120
  - 'bin/benchmark_corpus'
120
121
  - 'lib/suma/worker_pool.rb'
121
122
  - 'spec/suma/reproducibility_spec.rb'
123
+ - 'spec/suma/cli/build_integration_spec.rb'
122
124
  - 'lib/suma/cli/resolve.rb'
123
125
  - 'lib/suma/urn_resolver.rb'
124
126
  - 'lib/suma/cli/check_svg_quality.rb'
@@ -155,6 +157,8 @@ Metrics/BlockLength:
155
157
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
156
158
  Metrics/CyclomaticComplexity:
157
159
  Exclude:
160
+ - 'lib/suma/gcr_package.rb'
161
+ - 'lib/suma/corpus_graph.rb'
158
162
  - 'lib/suma/worker_pool.rb'
159
163
  - 'lib/suma/urn_resolver.rb'
160
164
  - 'lib/suma/cli/check_svg_quality.rb'
@@ -172,6 +176,8 @@ Metrics/CyclomaticComplexity:
172
176
  # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
173
177
  Metrics/MethodLength:
174
178
  Exclude:
179
+ - 'lib/suma/artifact_server.rb'
180
+ - 'lib/suma/gcr_package.rb'
175
181
  - 'bin/benchmark_corpus'
176
182
  - 'lib/suma/cli/build.rb'
177
183
  - 'lib/suma/worker_pool.rb'
@@ -204,6 +210,7 @@ Metrics/MethodLength:
204
210
  # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
205
211
  Metrics/ParameterLists:
206
212
  Exclude:
213
+ - 'lib/suma/gcr_package.rb'
207
214
  - 'lib/suma/processor.rb'
208
215
  - 'lib/suma/express_schema.rb'
209
216
  - 'lib/suma/term_extractor.rb'
@@ -216,6 +223,7 @@ Metrics/ParameterLists:
216
223
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
217
224
  Metrics/PerceivedComplexity:
218
225
  Exclude:
226
+ - 'lib/suma/gcr_package.rb'
219
227
  - 'lib/suma/worker_pool.rb'
220
228
  - 'lib/suma/eengine/wrapper.rb'
221
229
  - 'lib/suma/jsdai/figure_image.rb'
@@ -259,6 +267,16 @@ RSpec/ContextWording:
259
267
  # Configuration parameters: Max, CountAsOne.
260
268
  RSpec/ExampleLength:
261
269
  Exclude:
270
+ - 'spec/suma/site_build_spec.rb'
271
+ - 'spec/suma/output_canonicalizer_spec.rb'
272
+ - 'spec/suma/anchor_validator_spec.rb'
273
+ - 'spec/suma/translation_ingester_spec.rb'
274
+ - 'spec/suma/ifc_profile_pipeline_spec.rb'
275
+ - 'spec/suma/schema_manifest_generator_spec.rb'
276
+ - 'spec/suma/schema_naming_spec.rb'
277
+ - 'spec/suma/cli/build_integration_spec.rb'
278
+ - 'spec/suma/artifact_server_spec.rb'
279
+ - 'spec/suma/gcr_package_spec.rb'
262
280
  - 'spec/suma/schema_cache_spec.rb'
263
281
  - 'spec/suma/worker_pool_spec.rb'
264
282
  - 'spec/suma/schema_collection_rebuild_targets_spec.rb'
@@ -338,6 +356,11 @@ RSpec/MessageSpies:
338
356
  # Configuration parameters: Max.
339
357
  RSpec/MultipleExpectations:
340
358
  Exclude:
359
+ - 'spec/suma/site_build_spec.rb'
360
+ - 'spec/suma/output_canonicalizer_spec.rb'
361
+ - 'spec/suma/cli/build_integration_spec.rb'
362
+ - 'spec/suma/artifact_server_spec.rb'
363
+ - 'spec/suma/gcr_package_spec.rb'
341
364
  - 'spec/suma/schema_cache_spec.rb'
342
365
  - 'spec/suma/worker_pool_spec.rb'
343
366
  - 'spec/suma/reproducibility_spec.rb'
@@ -400,11 +423,13 @@ RSpec/NestedGroups:
400
423
  RSpec/DescribeClass:
401
424
  Exclude:
402
425
  - 'spec/suma/reproducibility_spec.rb'
426
+ - 'spec/suma/ifc_profile_pipeline_spec.rb'
403
427
 
404
428
  RSpec/SpecFilePathFormat:
405
429
  Exclude:
406
430
  - '**/spec/routing/**/*'
407
431
  - 'spec/suma/cli/expressir_spec.rb'
432
+ - 'spec/suma/ifc_profile_pipeline_spec.rb'
408
433
 
409
434
  # Offense count: 1
410
435
  RSpec/StubbedMock:
@@ -415,6 +440,8 @@ RSpec/StubbedMock:
415
440
  # Configuration parameters: Max.
416
441
  RSpec/MultipleMemoizedHelpers:
417
442
  Exclude:
443
+ - 'spec/suma/ifc_profile_pipeline_spec.rb'
444
+ - 'spec/suma/gcr_package_spec.rb'
418
445
  - 'spec/suma/provenance_spec.rb'
419
446
  - 'spec/suma/worker_pool_spec.rb'
420
447
  - 'spec/suma/schema_cache_spec.rb'
data/CLAUDE.md CHANGED
@@ -37,11 +37,15 @@ exe/suma reformat PATH # Reformat EXP files (us
37
37
  exe/suma generate-schemas METANORMA_YAML SCHEMAS_YAML # Generate schema manifest from site manifest
38
38
  exe/suma extract-terms SCHEMA_YAML GLOSSARIST_DIR -u URN # Extract EXPRESS entity concepts to Glossarist v3 format
39
39
  exe/suma extract-terms ... --edition 3 --language-codes fra deu # Provenance + linked localizations
40
+ exe/suma ingest-translations CONCEPTS_DIR TRANSLATIONS_FILE [-o OUT] # Overlay translated localized-concept content onto extracted concepts
40
41
  exe/suma resolve URN --manifest SCHEMAS_YAML --concepts DIR --register register.yaml -o OUT # Emit canonical JSON + HTML
41
42
  exe/suma resolve --all --manifest SCHEMAS_YAML --concepts DIR --register register.yaml -o OUT # Emit every URN artifact
43
+ exe/suma package-gcr CONCEPTS_DIR --register register.yaml --shortname ID --version V --title T --description D -o OUT # Package concepts into a .gcr archive
44
+ exe/suma serve ARTIFACTS_DIR --port 2113 # Serve resolved URN artifacts with Accept negotiation
42
45
  exe/suma generate-register SCHEMA_YAML OUT_DIR -u URN --id ID --ref REF # Generate hierarchical register.yaml from schema manifest
43
46
  exe/suma validate links SCHEMAS_FILE DOCS_PATH # Validate EXPRESS cross-reference links
44
47
  exe/suma validate closure SCHEMAS_MANIFEST [DOCUMENTS_PATH] # Referential closure: every reference resolves (exit 1 on violations)
48
+ exe/suma validate anchors SCHEMA_DOCS_PATH # Rendered docs: every xref and in-page link resolves (exit 1 on dead links)
45
49
  ```
46
50
 
47
51
  ## Schema profiles
@@ -51,8 +55,10 @@ identifiers and paths map to types (`ExpressSchema::Type`), how
51
55
  identifiers become display names (`SchemaNaming`), and the ordered
52
56
  register categories (`SchemaCategory`). Registered profiles: `iso10303`
53
57
  (default; STEP conventions incl. `/applications/` → application
54
- protocol) and `generic` (no STEP conventions). The CLI takes
55
- `--profile` on `export`, `extract-terms`, and `generate-register`.
58
+ protocol), `generic` (no STEP conventions), and `ifc` (buildingSMART
59
+ IFC editions: standalone classification, `IFC4`-style edition names via
60
+ `/\Aifc\d+(x\d+)*\z/`). The CLI takes `--profile` on `export`,
61
+ `extract-terms`, and `generate-register`.
56
62
 
57
63
  ## ISO 10303 AP publications
58
64
 
@@ -81,10 +87,12 @@ per-publication parent section.
81
87
  - **`SchemaCategory`** — value object mapping `ExpressSchema::Type` to a register/export category (id, label, prefix, types, directory). Single source of truth for category identity across `SchemaExporter`, `SchemaNaming`, and `RegisterManifestGenerator`
82
88
  - **`Urn`** — value object encapsulating ISO URN semantics (wildcard stripping, base/alias split, leaf composition for `for_schema`/`for_term`/`for_entity`)
83
89
  - **`TermExtractor`** — extracts EXPRESS entity concepts from a schema manifest into Glossarist v3 YAML format. Generates definitions with URN cross-references, processes remarks into notes, and resolves EXPRESS xrefs to URN mentions. Assigns stable UUIDv5 identifiers
90
+ - **`TranslationIngester`** — T5 translation ingestion: overlays a YAML sequence of localized-concept entries (keyed by `concept_id` or localized UUID) onto extracted concepts through the Glossarist/lutaml-model framework. Entry fields win; omitted fields keep the source-language content; a new language is created with the same UUIDv5 convention as the linked localizations, and the persisted localization "id" is preserved (Glossarist parses it as `#id`, not `#uuid`)
84
91
  - **`RegisterManifestGenerator`** (file: `lib/suma/register_manifest_generator.rb`) — generates a Glossarist v3 `register.yaml` with hierarchical sections from a schema manifest. Classifies schemas via `ExpressSchema::Type` (DRY), delegates naming to `SchemaNaming` (OCP), orders resources before modules. Uses the Section model's `children` field for hierarchy
85
92
  - **`SchemaNaming`** — pure module that converts EXPRESS schema IDs to human-readable display names. Strips type suffixes (`_schema`/`_arm`/`_mim`/`_bom`), title-cases with acronym preservation (AIC, AEC, BREP, 2D, 3D…), lowercases function words, appends type labels (ARM/MIM)
86
- - **`SchemaTemplate::Plain` / `SchemaTemplate::Document`** — pure renderers for the AsciiDoc body fed to Metanorma (no I/O, no schema knowledge). `Document` adds cross-reference anchors and produces XML-only output
87
- - **`SchemaCompiler`** — orchestrates one schema's compilation: writes adoc + config, invokes `Metanorma::Compile`. Templates are injected via the constructor (composition over inheritance)
93
+ - **`SchemaTemplate::Plain` / `SchemaTemplate::Document`** — pure renderers for the AsciiDoc body fed to Metanorma (no I/O, no schema knowledge). Both render the schema through `formatted_hyperlinked_adoc` (live cross-references) under the shared `Anchors` block, which emits one `[[id]]` per linkable element — schema-qualified ids plus bare `entity.attribute` pairs, matching Expressir's two xref forms. `Plain` produces HTML+XML for top-level schemas; `Document` produces XML-only for schemas-only entries. `validate anchors` verifies zero dead links in rendered docs
94
+ - **`AnchorValidator`** — T4 dead-link gate: for every rendered `doc_*.adoc` + `doc_*.html`, each `<<target,...>>` xref must have an anchor in the HTML (unresolved xrefs are silently dropped by the renderer) and every in-page `href="#..."` must hit an id; cross-document xrefs are out of scope. `validate anchors SCHEMA_DOCS_PATH` exits 1 on violations
95
+ - **`SchemaCompiler`** — orchestrates one schema's compilation: writes adoc + config, invokes `Metanorma::Compile`. Templates are injected via the constructor (composition over inheritance). A render key over adoc, config, schema source, and `Provenance.render_toolchain` is stored in `.suma-render-key` beside the outputs; an unchanged key with the requested outputs present reuses them without invoking Metanorma
88
96
  - **`SchemaCollection`** — orchestrates processing of all schemas from a config; selects `Plain` for top-level schemas and `Document` for schemas-only entries
89
97
  - **`SchemaExporter`** — standalone export of schemas from manifest or `.exp` files to a directory, with optional ZIP
90
98
  - **`CollectionManifest`** — pure data model (lutaml-model attributes + YAML mappings) for one node of a Metanorma collection manifest. No imperative methods
@@ -97,9 +105,13 @@ per-publication parent section.
97
105
  - **`OwlProjection`** — S2 formal semantics: projects a parsed corpus into OWL/Turtle. Entities → `owl:Class`; `SUBTYPE OF` → `rdfs:subClassOf`; SELECT → `owl:unionOf`; enums → `owl:oneOf` named individuals; simple type aliases → `rdfs:Datatype`; explicit/derived/inverse attributes → object/datatype properties attached through named `owl:Restriction` nodes (cardinality from optionality and aggregate bounds); WHERE rules and DERIVE/INVERSE clauses → annotated (`rdfs:comment`) constraints. IRIs come from `Urn` (`<base>:tech:<schema>.<element>`), per-schema ontologies declare `owl:imports` for their `USE FROM`/`REFERENCE` targets, and the corpus ontology imports every schema. `#validate` checks materialization, property declarations, orphan classes, and superclass acyclicity. Output contains no blank nodes, so projections are deterministic and golden-comparable
98
106
  - **`SchemaCache`** — content-addressed on-disk store for generated `.exp` output and parsed Expressir repositories. Keys compose source bytes with the Expressir version (plus Suma/Marshal versions for parsed entries), so a hit is equivalent to a fresh build. Enabled by `SUMA_SCHEMA_CACHE_DIR`; hit/miss counters expose parse reuse. **`NullCache`** is the disabled-mode null object
99
107
  - **`BuildPlan`** — incremental-build decision: hashes every schema source, diffs against the persisted state, and expands changed schemas to their transitive dependents through `CorpusGraph`. State defaults to `plain_schemas/.suma-build-state.json`; `SchemaCollection#rebuild_targets` is the build-facing query
108
+ - **`SiteBuild`** — T3 site freshness: keys every source file under the site root (excluding generated trees, render copies, and the Relaton cache) plus the render toolchain, records the key and produced files in `_site/.suma-site-build.json`, and lets `Processor` skip the collection render when nothing changed. Generated per-document artifacts (`document*.xml*`, `*_index.html`, `*.err.html`, `*.log.txt`, temp files) are never treated as inputs
109
+ - **`OutputCanonicalizer`** — T3 determinism: Metanorma assigns random UUIDs to manifest nodes without identifiers and stamps rendered documents with a random 8-letter root type. This pass rewrites those to position-derived UUIDv5s and a fixed marker in the emitted manifest and root XML, while preserving UUIDs that occur in source YAML, so identical builds are byte-identical
100
110
  - **`WorkerPool`** — bounded worker pool for independent per-schema work. Schema export runs in the pool (`build --jobs N`); Metanorma document compilation stays sequential and composes with `StagedCollectionBuilder`'s process isolation
101
111
  - **`UrnResolver`** — S4 dereferenceable identity: resolves `tech:` schema/element URNs from a manifest, `term:` concept URNs from Glossarist documents, and the dataset URN from `register.yaml`; emits canonical JSON plus a static HTML document per URN at a path derived from the URN itself. Every URN the register emits resolves against the artifact set
102
- - **`Provenance`** — S5 build provenance: sha256 of every input and output (files or directory trees) plus Ruby/Expressir/Suma toolchain versions, written as canonical JSON by `build --provenance FILE`. `Provenance.verify` recomputes the digests independently; `build --sign minisign|sigstore` (with `--sign-key` where needed) optionally signs the manifest, and `bin/release_provenance` signs keylessly with cosign in the release workflow (`SUMA_PROVENANCE_SIGN`)
112
+ - **`ArtifactServer`** — T2 development server for the resolved artifact tree (`serve DIRECTORY`): dependency-free TCP responder negotiating JSON vs HTML by the Accept header at URN-derived paths; production hosting maps the same paths statically
113
+ - **`GcrPackage`** — T2 packaging (`package-gcr`): bundles extracted concepts into a GCR archive for the Glossarist Vocabulary Browser — `metadata.yaml` (identity, statistics, schema_version), `register.yaml`, and `concepts/` in the browser's canonical flat format (`termid`, `term`, `{lang}` blocks). Standard variant only; `created_at` is injectable for byte-reproducible archives
114
+ - **`Provenance`** — S5 build provenance: sha256 of every input and output (files or directory trees) plus Ruby/Expressir/Suma toolchain versions, written as canonical JSON by `build --provenance FILE`. `Provenance.verify` recomputes the digests independently and reports unrecorded files under the recorded output roots (`_site`, `schema_docs`, `plain_schemas`); explicit exclusions cover build metadata such as the site state file and staged store. `build --sign minisign|sigstore` (with `--sign-key` where needed) optionally signs the manifest, and `bin/release_provenance` signs keylessly with cosign in the release workflow (`SUMA_PROVENANCE_SIGN`). `Provenance.render_toolchain` keys rendered-document reuse
103
115
  - **`LinkValidator`** — validates EXPRESS cross-reference links against a `SchemaIndex`; returns `LinkValidationResult` structs for unresolved links
104
116
 
105
117
  ### CLI structure
data/README.adoc CHANGED
@@ -49,10 +49,13 @@ $ suma
49
49
  suma convert-jsdai XML_FILE IMAGE_FILE OUTPUT_DIR # Convert JSDAI XML and image files to SVG and EXP
50
50
  suma export *FILES -o OUTPUT # Export EXPRESS schemas or project them to OWL
51
51
  suma extract-terms SCHEMA_MANIFEST_FILE GLOSSARIST_OUTPUT_PATH # Extract entity concepts to Glossarist v3
52
+ suma ingest-translations CONCEPTS_DIR TRANSLATIONS_FILE [-o OUT] # Overlay translated content onto extracted concepts
52
53
  suma generate-register SCHEMA_MANIFEST_FILE OUTPUT_PATH # Generate hierarchical register.yaml
53
54
  suma generate-schemas METANORMA_MANIFEST_FILE SCHEMA_MANIFEST_FILE # Generate a schema manifest from a site manifest
54
55
  suma reformat EXPRESS_FILE_PATH # Reformat EXPRESS files
55
56
  suma resolve [URN] --manifest=MANIFEST -o OUTPUT # Emit canonical JSON + HTML for URNs
57
+ suma package-gcr CONCEPTS_DIR --register=REGISTER -o OUTPUT # Package concepts into a .gcr archive
58
+ suma serve DIRECTORY # Serve resolved artifacts (Accept negotiation)
56
59
  suma validate SUBCOMMAND ...ARGS # Validate express documents
57
60
  ----
58
61
 
@@ -85,6 +88,20 @@ Options:
85
88
  The generated `schemas-*.yaml` file name is derived from the input file name
86
89
  with the word `metanorma` replaced with `schemas`.
87
90
 
91
+ Repeated builds reuse work and are reproducible: a schema document is rendered
92
+ again only when its adoc, schema config, schema source, or the rendering
93
+ toolchain changed, and the collection render is skipped entirely when no site
94
+ input changed (state: `_site/.suma-site-build.json`). Metanorma's random
95
+ manifest identifiers and per-document type markers are canonicalized, so two
96
+ identical builds produce byte-identical output. A provenance manifest covers
97
+ `schema_docs`, `plain_schemas`, and `_site`, and verification reports files
98
+ that appeared under those roots without being recorded (build metadata such as
99
+ the site state file is excluded explicitly).
100
+
101
+ Top-level schema documents render with live cross-reference links: every
102
+ `schema.entity` and `entity.attribute` reference Expressir emits becomes an
103
+ in-page link, and `suma validate anchors` verifies that no link is dead.
104
+
88
105
  [example]
89
106
  ====
90
107
  .To compile SRL subset test collection
@@ -210,6 +227,19 @@ $ suma validate closure SCHEMAS_MANIFEST [DOCUMENTS_PATH] [--format terminal|jso
210
227
  When `DOCUMENTS_PATH` is given, documentation-layer link failures are merged
211
228
  into the same report.
212
229
 
230
+ ==== Anchors subcommand
231
+
232
+ The `anchors` subcommand checks rendered schema documents for dead links:
233
+ every Expressir cross-reference (`<<schema.entity,...>>`,
234
+ `<<entity.attribute,...>>`) emitted in a compiled `doc_*.adoc` must have a
235
+ matching anchor in its rendered `doc_*.html`, and every in-page `href` must
236
+ point at an existing id. It exits non-zero on violations.
237
+
238
+ [source,sh]
239
+ ----
240
+ $ suma validate anchors SCHEMA_DOCS_PATH
241
+ ----
242
+
213
243
 
214
244
  === Generate schemas command
215
245
 
@@ -337,6 +367,37 @@ $ bundle exec suma extract-terms schemas-activity-modules.yml terms_output
337
367
  ----
338
368
  ====
339
369
 
370
+ === Ingest translations command
371
+
372
+ The `suma ingest-translations` command overlays translated content onto
373
+ extracted concepts. A translations file is a YAML sequence of
374
+ localized-concept documents, each keyed by concept id (or localized
375
+ UUID) and carrying the fields to apply:
376
+
377
+ [source,yaml]
378
+ ----
379
+ - concept_id: example_schema.part
380
+ data:
381
+ language_code: fra
382
+ terms:
383
+ - designation: pièce
384
+ type: expression
385
+ normative_status: preferred
386
+ definition:
387
+ - content: "Composant d'un assemblage."
388
+ ----
389
+
390
+ Every field the entry provides replaces that localization's field; every
391
+ field it omits keeps the source-language content (a language absent from
392
+ the extracted concepts is created with the same stable UUID convention
393
+ the linked localizations use). Updated concepts are written in place, or
394
+ to `--output` when given.
395
+
396
+ [source,sh]
397
+ ----
398
+ $ suma ingest-translations CONCEPTS_DIR TRANSLATIONS_FILE [-o OUTPUT_DIR]
399
+ ----
400
+
340
401
 
341
402
  === Generate register command
342
403
 
@@ -695,6 +756,7 @@ names, and the ordered register categories. Registered profiles:
695
756
 
696
757
  `iso10303`:: Default; STEP conventions including `/applications/` classifying as an application protocol
697
758
  `generic`:: No STEP conventions (one category for every schema)
759
+ `ifc`:: buildingSMART IFC editions: one standalone schema per edition (`ifc4`, `ifc4x3`, ...), edition names preserved uppercase (`IFC4`, `IFC4X3`), Ifc-prefixed declarations, one `Other Schemas` register category
698
760
 
699
761
  An AP publication is a lead schema bundled with the module and resource
700
762
  schemas it USEs. `--publication SCHEMA_ID` computes the transitive
@@ -727,6 +789,45 @@ $ suma export --format owl --corpus --urn urn:iso:std:iso:10303:-2:ed-2:en \
727
789
  -o OUTPUT schemas.yaml
728
790
  ----
729
791
 
792
+ === Package concepts for the Vocabulary Browser
793
+
794
+ The `package-gcr` command bundles extracted concepts into a GCR
795
+ (Glossarist Concept Repository) archive — the sealed distribution format
796
+ consumed by the Glossarist Vocabulary Browser:
797
+
798
+ [source,sh]
799
+ ----
800
+ $ suma package-gcr CONCEPTS_DIR --register register.yaml \
801
+ --shortname my-dataset --version 1.0.0 \
802
+ --title "My Dataset" --description "Concepts from my corpus" \
803
+ -o OUT
804
+ ----
805
+
806
+ The archive contains `metadata.yaml` (identity, statistics), the original
807
+ `register.yaml`, and `concepts/` in the browser's canonical flat format
808
+ (`termid`, `term`, one block per language). Both a versioned
809
+ (`{shortname}-{version}.gcr`) and an unversioned (`{shortname}.gcr`)
810
+ archive are written. Pass `--created-at` to pin the package timestamp for
811
+ byte-reproducible archives. Point the browser's `datasets.yml` at the
812
+ published archive (or at the concepts directory in development via
813
+ `localPath`).
814
+
815
+ === Serve resolved artifacts
816
+
817
+ `suma serve` serves a `resolve --all` artifact tree at its URN-derived
818
+ paths, returning JSON or HTML according to the request's `Accept`
819
+ header:
820
+
821
+ [source,sh]
822
+ ----
823
+ $ suma serve OUT --port 2113
824
+ $ curl -H "Accept: application/json" \
825
+ http://localhost:2113/urn/iso/std/iso/10303/-2/ed-2/en/tech/action_schema.action
826
+ ----
827
+
828
+ Production hosting maps the same paths statically; no application server
829
+ is required.
830
+
730
831
  === Resolve URNs command
731
832
 
732
833
  The `resolve` command turns URNs into static, dereferenceable artifacts: a
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ # Validates that every cross-reference a rendered schema document emits
5
+ # resolves inside it. The Expressir hyperlinked source emits xref macros
6
+ # (`<<schema.entity,...>>`, `<<entity.attribute,...>>`); when a target is
7
+ # missing, the renderer silently drops the link, so the check is done on
8
+ # the source macros against the anchors present in the rendered HTML —
9
+ # plus the reverse check that every in-page href hits an existing id.
10
+ class AnchorValidator
11
+ XREF_PATTERN = /<<([^,>]+)(?:,[^>]*)?>>/
12
+ ID_PATTERN = /id="([^"]+)"/
13
+ FRAGMENT_PATTERN = /href="#([^"]+)"/
14
+
15
+ # @param directory [String] tree of compiled schema docs (doc_*.adoc
16
+ # with their rendered doc_*.html)
17
+ def initialize(directory)
18
+ @directory = directory
19
+ end
20
+
21
+ # @return [Array<LinkValidationResult>] dead links (empty = valid)
22
+ def validate
23
+ Dir.glob(File.join(@directory, "**", "doc_*.adoc"))
24
+ .flat_map { |adoc| validate_document(adoc) }
25
+ end
26
+
27
+ private
28
+
29
+ def validate_document(adoc)
30
+ html = adoc.sub(/\.adoc\z/, ".html")
31
+ return [missing_html(adoc, html)] unless File.file?(html)
32
+
33
+ content = File.read(adoc)
34
+ ids, fragments = rendered_digests(html)
35
+
36
+ xref_violations(adoc, content, ids, fragments) +
37
+ fragment_violations(html, ids)
38
+ end
39
+
40
+ def rendered_digests(html)
41
+ rendered = File.read(html)
42
+ [rendered.scan(ID_PATTERN).flatten.uniq,
43
+ rendered.scan(FRAGMENT_PATTERN).flatten.uniq]
44
+ end
45
+
46
+ def xref_violations(adoc, content, ids, fragments)
47
+ content.scan(XREF_PATTERN).flatten.uniq.filter_map do |target|
48
+ next if cross_document?(target) || resolved?(target, ids, fragments)
49
+
50
+ LinkValidationResult.new(
51
+ file: adoc, line: nil, link: target,
52
+ reason: "cross-reference has no anchor in the rendered document"
53
+ )
54
+ end
55
+ end
56
+
57
+ def fragment_violations(html, ids)
58
+ File.read(html).scan(FRAGMENT_PATTERN).flatten.uniq.filter_map do |frag|
59
+ next if ids.include?(frag)
60
+
61
+ LinkValidationResult.new(
62
+ file: html, line: nil, link: "##{frag}",
63
+ reason: "in-page link points at no anchor in the document"
64
+ )
65
+ end
66
+ end
67
+
68
+ # Xrefs that cross documents (Asciidoctor `path.adoc#anchor` syntax)
69
+ # resolve against other pages and are out of scope here.
70
+ def cross_document?(target)
71
+ target.include?("/") || target.include?("#")
72
+ end
73
+
74
+ def resolved?(target, ids, fragments)
75
+ ids.include?(target) || fragments.include?(target)
76
+ end
77
+
78
+ def missing_html(adoc, html)
79
+ LinkValidationResult.new(
80
+ file: adoc, line: nil, link: html,
81
+ reason: "no rendered HTML next to the adoc source"
82
+ )
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Suma
6
+ # Development server for the emitted URN artifact tree: serves the
7
+ # static files at their URN-derived paths, negotiating JSON vs HTML by
8
+ # the Accept header. Production hosting maps the same paths statically.
9
+ class ArtifactServer
10
+ DEFAULT_PORT = 2113
11
+
12
+ attr_reader :directory, :port
13
+
14
+ # @param directory [String] the resolve output directory
15
+ # @param port [Integer] TCP port; 0 picks an ephemeral port
16
+ def initialize(directory, port: DEFAULT_PORT)
17
+ @directory = File.expand_path(directory)
18
+ @port = port
19
+ end
20
+
21
+ # Resolve one HTTP request to a response.
22
+ #
23
+ # @param path [String] request path (with or without extension)
24
+ # @param accept [String, nil] Accept header value
25
+ # @return [Array(String, String, String)] status, content type, body
26
+ def call(path, accept = nil)
27
+ clean = path.to_s.split("?").first.to_s
28
+ return not_found if clean.include?("..")
29
+
30
+ clean = "/index.html" if clean.empty? || clean == "/"
31
+ file = artifact_file(clean, accept)
32
+ return not_found unless file
33
+
34
+ ["200", content_type(file), File.read(file, encoding: "utf-8")]
35
+ end
36
+
37
+ # Handle a single connection (one HTTP request).
38
+ def serve(socket)
39
+ request = socket.gets&.strip
40
+ return unless request
41
+
42
+ accept = read_headers(socket)["accept"]
43
+ status, type, body = call(request.split[1].to_s, accept)
44
+ socket.write(
45
+ "HTTP/1.1 #{status} #{status_name(status)}\r\n" \
46
+ "Content-Type: #{type}\r\nContent-Length: #{body.bytesize}\r\n" \
47
+ "Connection: close\r\n\r\n#{body}",
48
+ )
49
+ ensure
50
+ socket&.close
51
+ end
52
+
53
+ # Accept connections until interrupted.
54
+ def start
55
+ server = TCPServer.new("localhost", @port)
56
+ @port = server.addr[1]
57
+ Utils.log "Serving #{@directory} at http://localhost:#{@port}"
58
+ loop { serve(server.accept) }
59
+ ensure
60
+ server&.close
61
+ end
62
+
63
+ private
64
+
65
+ def read_headers(socket)
66
+ headers = {}
67
+ while (line = socket.gets) && line != "\r\n" && !line.strip.empty?
68
+ key, value = line.split(":", 2)
69
+ headers[key.strip.downcase] = value.strip if value
70
+ end
71
+ headers
72
+ end
73
+
74
+ def artifact_file(clean, accept)
75
+ base = clean.delete_prefix("/")
76
+ candidates = if base.end_with?(".json", ".html")
77
+ [base]
78
+ else
79
+ wants_json = accept.to_s.include?("application/json")
80
+ ["#{base}.#{wants_json ? 'json' : 'html'}"]
81
+ end
82
+ candidates.map { |candidate| File.join(@directory, candidate) }
83
+ .find { |file| File.file?(file) }
84
+ end
85
+
86
+ def content_type(file)
87
+ if file.end_with?(".json")
88
+ "application/json"
89
+ else
90
+ "text/html; charset=utf-8"
91
+ end
92
+ end
93
+
94
+ def not_found
95
+ ["404", "text/plain", "not found\n"]
96
+ end
97
+
98
+ def status_name(status)
99
+ status == "200" ? "OK" : "Not Found"
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Suma
6
+ module Cli
7
+ # Serve the emitted URN artifact tree with Accept negotiation.
8
+ class ArtifactServerCli < Thor
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc "serve DIRECTORY", "Serve resolved URN artifacts locally"
14
+ option :port, type: :numeric, default: ArtifactServer::DEFAULT_PORT,
15
+ desc: "TCP port to listen on"
16
+ def serve(directory)
17
+ Utils.log "Ctrl-C stops the server."
18
+ ArtifactServer.new(directory, port: options[:port]).start
19
+ end
20
+ end
21
+ end
22
+ end
@@ -33,6 +33,9 @@ module Suma
33
33
  option :sign_key, type: :string,
34
34
  desc: "Signing key (minisign requires it; sigstore " \
35
35
  "signs keylessly when omitted)"
36
+ option :no_valid, type: :boolean, default: false,
37
+ desc: "Skip Metanorma document validation " \
38
+ "(equivalent of metanorma --no-valid)"
36
39
 
37
40
  def build(metanorma_site_manifest)
38
41
  unless File.exist?(metanorma_site_manifest)
@@ -62,6 +65,7 @@ module Suma
62
65
  provenance_file: options[:provenance],
63
66
  sign_method: options[:sign],
64
67
  sign_key: options[:sign_key],
68
+ no_valid: options[:no_valid],
65
69
  ).run
66
70
  end
67
71
 
data/lib/suma/cli/core.rb CHANGED
@@ -42,6 +42,8 @@ module Suma
42
42
  desc: "Sign the provenance manifest: minisign or sigstore"
43
43
  option :sign_key, type: :string,
44
44
  desc: "Signing key (sigstore signs keylessly when omitted)"
45
+ option :no_valid, type: :boolean, default: false,
46
+ desc: "Skip Metanorma document validation"
45
47
  def build(_site_manifest)
46
48
  Cli::Build.start
47
49
  end
@@ -70,6 +72,8 @@ module Suma
70
72
  desc: "Owning edition recorded on each concept"
71
73
  option :language_codes, type: :array,
72
74
  desc: "Additional linked localization languages"
75
+ option :profile, type: :string, default: "iso10303",
76
+ desc: "Corpus profile (iso10303, generic, or ifc)"
73
77
  def extract_terms(schema_manifest_file, glossarist_output_path)
74
78
  TermExtractor.new(
75
79
  schema_manifest_file,
@@ -78,6 +82,7 @@ module Suma
78
82
  language_code: options[:language_code],
79
83
  edition: options[:edition],
80
84
  language_codes: options[:language_codes],
85
+ profile: options[:profile],
81
86
  ).call
82
87
  end
83
88
 
@@ -94,7 +99,7 @@ module Suma
94
99
  option :owner, type: :string, default: Suma::RegisterManifestGenerator::DEFAULT_OWNER,
95
100
  desc: "Owner of the dataset (e.g. 'ISO/TC 184/SC 4')"
96
101
  option :profile, type: :string, default: "iso10303",
97
- desc: "Corpus profile (iso10303 or generic)"
102
+ desc: "Corpus profile (iso10303, generic, or ifc)"
98
103
  option :year, type: :numeric,
99
104
  desc: "Edition year for the register"
100
105
  option :supersedes, type: :string,
@@ -141,7 +146,7 @@ module Suma
141
146
  "USE-FROM/REFERENCE closure (manifest " \
142
147
  "input only)"
143
148
  option :profile, type: :string, default: "iso10303",
144
- desc: "Corpus profile (iso10303 or generic)"
149
+ desc: "Corpus profile (iso10303, generic, or ifc)"
145
150
  option :format, type: :string, default: "express", banner: "FORMAT",
146
151
  desc: "Output format: express (default) or owl"
147
152
  option :corpus, type: :boolean, default: false,
@@ -164,8 +169,7 @@ module Suma
164
169
  desc "validate SUBCOMMAND ...ARGS", "Validate express documents"
165
170
  subcommand "validate", Cli::Validate
166
171
 
167
- desc "resolve [URN]", "Resolve URNs to canonical JSON + HTML artifacts"
168
- # Mirrors so Core accepts the switches Cli::Resolve reparses; the
172
+ desc "resolve [URN]", "Resolve URNs to canonical JSON + HTML artifacts" # Mirrors so Core accepts the switches Cli::Resolve reparses; the
169
173
  # inner class owns their declarations.
170
174
  option :manifest, type: :string, required: true,
171
175
  desc: "Schema manifest used for schema/entity URNs"
@@ -183,6 +187,48 @@ module Suma
183
187
  Cli::Resolve.start
184
188
  end
185
189
 
190
+ desc "package-gcr CONCEPTS_DIR",
191
+ "Package extracted concepts into a .gcr archive"
192
+ # Mirrors so Core accepts the switches Cli::PackageGcr reparses; the
193
+ # inner class owns their declarations.
194
+ option :register, type: :string, required: true,
195
+ desc: "register.yaml (dataset identity)"
196
+ option :shortname, type: :string, required: true,
197
+ desc: "Dataset id"
198
+ option :version, type: :string, required: true,
199
+ desc: "Semver of this dataset release"
200
+ option :title, type: :string, required: true,
201
+ desc: "Human-readable dataset title"
202
+ option :description, type: :string, required: true,
203
+ desc: "Dataset description"
204
+ option :created_at, type: :string,
205
+ desc: "Package timestamp (pin for reproducibility)"
206
+ option :output, type: :string, aliases: "-o", required: true,
207
+ desc: "Output directory for the .gcr archives"
208
+ def package_gcr(_concepts_dir)
209
+ Cli::PackageGcr.start
210
+ end
211
+
212
+ desc "ingest-translations CONCEPTS_DIR TRANSLATIONS_FILE",
213
+ "Overlay translated localized-concept content onto extracted " \
214
+ "Glossarist concepts"
215
+ # Mirrors so Core accepts the switches Cli::IngestTranslations
216
+ # reparses; the inner class owns their declarations.
217
+ option :output, type: :string, aliases: "-o",
218
+ desc: "Write updated concepts here (default: " \
219
+ "update CONCEPTS_DIR in place)"
220
+ def ingest_translations(_concepts_dir, _translations_file)
221
+ Cli::IngestTranslations.start
222
+ end
223
+
224
+ desc "serve DIRECTORY", "Serve resolved URN artifacts locally"
225
+ option :port, type: :numeric,
226
+ default: Suma::ArtifactServer::DEFAULT_PORT,
227
+ desc: "TCP port to listen on"
228
+ def serve(_directory)
229
+ Cli::ArtifactServerCli.start
230
+ end
231
+
186
232
  desc "check_svg_quality [PATH]",
187
233
  "Check SVG quality and sort by severity (critical files first)"
188
234
  option :pattern, type: :string, default: Cli::CheckSvgQuality::DEFAULT_PATTERN,
@@ -35,7 +35,7 @@ module Suma
35
35
  "USE-FROM/REFERENCE closure (manifest " \
36
36
  "input only)"
37
37
  option :profile, type: :string, default: "iso10303",
38
- desc: "Corpus profile (iso10303 or generic)"
38
+ desc: "Corpus profile (iso10303, generic, or ifc)"
39
39
  option :format, type: :string, default: "express", banner: "FORMAT",
40
40
  desc: "Output format: express (default) or owl"
41
41
  option :corpus, type: :boolean, default: false,