@sdd-method/maintain-cli 0.13.1 → 0.15.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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/catalogue/aggregate.js +50 -13
  3. package/dist/lib/catalogue/build.js +72 -23
  4. package/dist/lib/catalogue/builders/adr-governs.js +1 -1
  5. package/dist/lib/catalogue/builders/adrs.js +2 -2
  6. package/dist/lib/catalogue/builders/c4-containers.js +6 -2
  7. package/dist/lib/catalogue/builders/contract-revisions.js +45 -7
  8. package/dist/lib/catalogue/builders/contracts.js +4 -11
  9. package/dist/lib/catalogue/builders/data-model.js +1 -1
  10. package/dist/lib/catalogue/builders/entities-manifest.js +10 -4
  11. package/dist/lib/catalogue/builders/entity-archetype.js +121 -0
  12. package/dist/lib/catalogue/builders/entity-merge.js +90 -0
  13. package/dist/lib/catalogue/builders/features.js +1 -1
  14. package/dist/lib/catalogue/builders/glossary-xref.js +11 -5
  15. package/dist/lib/catalogue/builders/journey-requirement.js +5 -5
  16. package/dist/lib/catalogue/builders/quality-scores.js +1 -1
  17. package/dist/lib/catalogue/builders/requirements.js +1 -1
  18. package/dist/lib/catalogue/builders/service-contract-consumes.js +2 -2
  19. package/dist/lib/catalogue/builders/service-entity-tables.js +1 -1
  20. package/dist/lib/catalogue/canonical-schema.yaml +50 -30
  21. package/dist/lib/catalogue/contract-id.js +49 -12
  22. package/dist/lib/catalogue/manifest-loader.js +1 -1
  23. package/dist/lib/catalogue/retired-catalogues.js +1 -1
  24. package/dist/lib/catalogue/rubric-coverage-map.yaml +7 -3
  25. package/dist/lib/catalogue/schema-compat.js +1 -1
  26. package/dist/lib/catalogue/staleness-verdicts.js +3 -3
  27. package/dist/lib/structure/catalogue.js +31 -10
  28. package/dist/lib/structure/compose.js +1 -1
  29. package/dist/lib/structure/effects.js +1 -1
  30. package/dist/lib/structure/intended.js +194 -0
  31. package/dist/lib/structure/model.js +1 -1
  32. package/dist/lib/structure/render.js +1 -1
  33. package/dist/lib/structure/view.js +8 -3
  34. package/dist/lib/validate/agreement-evidence.js +2 -2
  35. package/dist/lib/validate/behaviour-store.js +49 -12
  36. package/dist/lib/validate/dispatch.js +2 -2
  37. package/dist/lib/validate/journey-impact.js +7 -7
  38. package/dist/lib/validate/structural-model.js +48 -35
  39. package/dist/maintain-program.js +7 -1
  40. package/dist/maintain-version.json +2 -2
  41. package/dist/verbs/structure.js +95 -0
  42. package/dist/verify-version.json +2 -2
  43. package/package.json +2 -2
@@ -0,0 +1,121 @@
1
+ // Catalogue schema 7.1.0: backfill `entities.ppt_archetype` from the capability specs' `## Entity Classification`
2
+ // tables (M-ADR 0230 §4.3, at its acceptance).
3
+ //
4
+ // The archetype is a structure fact. Where the SDD has a structural model the model holds it, and the capability spec
5
+ // template drops its table, so this reads the tables only in an SDD without a model: that build is what `migrate
6
+ // structure` carries into a first model.
7
+ //
8
+ // Authored cells vary: "Party (Green)", "**Thing — audit record**", "Blue/Description", "Moment", "PartyRole". A cell's
9
+ // archetype is the first one it names, colour labels and qualifiers aside. Moment alone is moment-interval, and
10
+ // PartyRole is a role. A cell naming more than two is a list of options, not a classification, and names none.
11
+ // What cannot be classified is reported, once per build, never guessed.
12
+ import { readFile } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { collectTableLines, detectColumn, entitySlug, splitRow } from "./data-store-mapping.js";
15
+ export const PPT_ARCHETYPES = ["party", "place", "thing", "role", "moment-interval", "description"];
16
+ const RE_HEADING = /^## Entity Classification\b/;
17
+ const RE_TOKEN = /\b(?:moment[\s-]*interval|moment|party[\s-]*role|party|place|thing|role|description)/g;
18
+ const EXAMPLES = 5;
19
+ /** The archetypes a cell names, in order, each once. */
20
+ export function archetypesNamed(cell) {
21
+ const out = [];
22
+ for (const m of cell.toLowerCase().matchAll(RE_TOKEN)) {
23
+ const t = m[0].replace(/[\s-]+/g, "");
24
+ const a = t.startsWith("moment") ? "moment-interval" : t === "partyrole" ? "role" : t;
25
+ if (!out.includes(a))
26
+ out.push(a);
27
+ }
28
+ return out;
29
+ }
30
+ /** A cell's archetype: the first it names, or null when it names none or more than two. */
31
+ export function archetypeOf(cell) {
32
+ const named = archetypesNamed(cell);
33
+ return named.length === 0 || named.length > 2 ? null : named[0];
34
+ }
35
+ /** The entity a row's first cell names: markup and a trailing qualifier stripped. */
36
+ function entityName(cell) {
37
+ return cell
38
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
39
+ .replace(/[*`_]/g, "")
40
+ .split(/\s+[—–-]\s+/)[0]
41
+ .replace(/\s*\([^)]*\)\s*$/, "")
42
+ .trim();
43
+ }
44
+ function sample(xs) {
45
+ return xs.slice(0, EXAMPLES).join(", ") + (xs.length > EXAMPLES ? `, and ${xs.length - EXAMPLES} more` : "");
46
+ }
47
+ export async function enrichEntityArchetype(inputs) {
48
+ // A spec in the entity's own domain outranks one that reaches it by the cross-domain fallback; otherwise the first wins.
49
+ const chosen = new Map();
50
+ const several = [];
51
+ const none = [];
52
+ const unresolved = [];
53
+ const disagree = new Set();
54
+ const specs = [...new Map(inputs.capabilities.filter((c) => c.spec_path !== "").map((c) => [c.spec_path, c])).values()].sort((a, b) => a.spec_path.localeCompare(b.spec_path));
55
+ for (const cap of specs) {
56
+ let text;
57
+ try {
58
+ text = await readFile(join(inputs.sddPath, cap.spec_path), "utf8");
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ const lines = text.split(/\r?\n/);
64
+ const at = lines.findIndex((l) => RE_HEADING.test(l));
65
+ if (at === -1)
66
+ continue;
67
+ const table = collectTableLines(lines, at + 1);
68
+ if (table === null || table.length < 3)
69
+ continue;
70
+ const headers = splitRow(table[0]);
71
+ const col = detectColumn(headers, (h) => h.includes("archetype") || h.includes("ppt"));
72
+ if (col === -1)
73
+ continue;
74
+ const entityCol = Math.max(0, detectColumn(headers, (h) => h.includes("entity") || h.includes("aggregate")));
75
+ for (const line of table.slice(2)) {
76
+ const cells = splitRow(line);
77
+ const name = entityName(cells[entityCol] ?? "");
78
+ // a separator, the template's placeholder row, or a header repeated inside the table
79
+ if (name === "" || /^[-:]+$/.test(name) || name.startsWith("{") || name.toLowerCase() === (headers[entityCol] ?? "").toLowerCase())
80
+ continue;
81
+ const where = `${cap.spec_path}: ${name}`;
82
+ const cell = cells[col] ?? "";
83
+ const named = archetypesNamed(cell);
84
+ const archetype = archetypeOf(cell);
85
+ if (archetype === null) {
86
+ none.push(`${where} (${JSON.stringify(cell)})`);
87
+ continue;
88
+ }
89
+ if (named.length > 1)
90
+ several.push(`${where} (${JSON.stringify(cell)} read as ${archetype})`);
91
+ const slug = entitySlug(name);
92
+ const localId = inputs.entityIdByDomainSlug.get(`${cap.domain_id}|${slug}`);
93
+ const id = localId ?? inputs.entityIdBySlug.get(slug);
94
+ if (id === undefined) {
95
+ unresolved.push(where);
96
+ continue;
97
+ }
98
+ const mine = { archetype, spec: cap.spec_path, local: localId !== undefined };
99
+ const prior = chosen.get(id);
100
+ if (prior !== undefined && prior.archetype !== archetype) {
101
+ const [kept, other] = mine.local && !prior.local ? [mine, prior] : [prior, mine];
102
+ disagree.add(`${id} (${kept.archetype} in ${kept.spec}, catalogued; ${other.archetype} in ${other.spec})`);
103
+ }
104
+ if (prior === undefined || (mine.local && !prior.local))
105
+ chosen.set(id, mine);
106
+ }
107
+ }
108
+ const report = (n, what, items) => {
109
+ if (n > 0)
110
+ inputs.findings.add("coverage", `entities.ppt_archetype: ${n} ${what}: ${sample([...items].sort())}`);
111
+ };
112
+ report(none.length, "Entity Classification row(s) name no archetype, so none is catalogued", none);
113
+ report(several.length, "Entity Classification row(s) name two archetypes; the first is catalogued", several);
114
+ report(unresolved.length, "Entity Classification row(s) name no entity of this SDD", unresolved);
115
+ report(disagree.size, "entities classified differently by two specs; the entity's own domain's spec, else the first, is catalogued", disagree);
116
+ return inputs.entityRows.map((r) => {
117
+ const c = chosen.get(String(r["id"]));
118
+ return c === undefined || String(r["ppt_archetype"] ?? "") !== "" ? r : { ...r, ppt_archetype: c.archetype };
119
+ });
120
+ }
121
+ //# sourceMappingURL=entity-archetype.js.map
@@ -0,0 +1,90 @@
1
+ // entities: the data-model walker's rows merged with the glossary-alignment manifest's (M-ADR 0159 §4.4).
2
+ //
3
+ // Field by field: a manifest value wins where it is non-empty, and the data model's value stands where the
4
+ // manifest is silent, so source_domain_model_id and the columns= note survive a manifest entry naming the
5
+ // same entity. Notes from both are kept.
6
+ //
7
+ // A platform-wide manifest entry (`_platform-wide-<slug>`, M-ADR 0159 §4.2) whose slug names exactly one
8
+ // domain entity (`<domain>-<slug>`) IS that entity: it fills the domain entity's empty fields and emits no row
9
+ // of its own, so one entity is one element. Its glossary term moves with it, and the entity_glossary step
10
+ // derives the edge from the domain entity. An entry matching no domain entity, or several, stays platform-wide
11
+ // and is reported.
12
+ import { PLATFORM_WIDE_PK_PREFIX } from "./entities-manifest.js";
13
+ const MANIFEST_REL_PATH = "glossary-alignment-manifest.yaml";
14
+ function filled(v) {
15
+ return v !== undefined && v !== null && String(v) !== "";
16
+ }
17
+ function joinNotes(a, b) {
18
+ const parts = [];
19
+ for (const v of [a, b]) {
20
+ for (const p of String(v ?? "").split(";"))
21
+ if (p !== "" && !parts.includes(p))
22
+ parts.push(p);
23
+ }
24
+ return parts.join(";");
25
+ }
26
+ /** `over`'s non-empty values on top of `base`; notes from both. */
27
+ function overlay(base, over) {
28
+ const out = { ...base };
29
+ for (const [k, v] of Object.entries(over)) {
30
+ if (k === "notes")
31
+ out[k] = joinNotes(base[k], v);
32
+ else if (filled(v))
33
+ out[k] = v;
34
+ }
35
+ return out;
36
+ }
37
+ /** `extra`'s values only where `base` is empty; notes from both. */
38
+ function fillEmpty(base, extra) {
39
+ const out = { ...base };
40
+ for (const [k, v] of Object.entries(extra)) {
41
+ if (k === "id" || k === "name" || k === "domain_id")
42
+ continue;
43
+ if (k === "notes")
44
+ out[k] = joinNotes(base[k], v);
45
+ else if (!filled(base[k]) && filled(v))
46
+ out[k] = v;
47
+ }
48
+ return out;
49
+ }
50
+ export function mergeEntities(fromDataModels, fromManifest, findings,
51
+ /** The sentinel ids from a platform-wide domain hint (entities-manifest's out-set): the only ones that fold. */
52
+ platformWideIds) {
53
+ const byId = new Map();
54
+ for (const r of fromDataModels)
55
+ byId.set(String(r["id"]), r);
56
+ for (const r of fromManifest) {
57
+ const id = String(r["id"]);
58
+ const existing = byId.get(id);
59
+ byId.set(id, existing ? overlay(existing, r) : r);
60
+ }
61
+ const prefix = `${PLATFORM_WIDE_PK_PREFIX}-`;
62
+ for (const [id, row] of [...byId]) {
63
+ if (!id.startsWith(prefix) || !platformWideIds.has(id))
64
+ continue;
65
+ const slug = id.slice(prefix.length);
66
+ const matches = [...byId.values()].filter((e) => {
67
+ const dom = String(e["domain_id"] ?? "");
68
+ return dom !== "" && String(e["id"]) === `${dom}-${slug}`;
69
+ });
70
+ if (matches.length === 1) {
71
+ const target = matches[0];
72
+ const own = String(target["glossary_term"] ?? "");
73
+ const theirs = String(row["glossary_term"] ?? "");
74
+ if (own !== "" && theirs !== "" && own !== theirs) {
75
+ findings.add("divergence", `${MANIFEST_REL_PATH}: platform-wide entity ${row["name"]} names glossary term ${theirs}, but ${target["id"]} ` +
76
+ `already names ${own} — kept ${own}`);
77
+ }
78
+ byId.set(String(target["id"]), fillEmpty(target, row));
79
+ byId.delete(id);
80
+ }
81
+ else if (matches.length === 0) {
82
+ findings.add("coverage", `${MANIFEST_REL_PATH}: platform-wide entity ${row["name"]} names no domain entity — kept as ${id}`);
83
+ }
84
+ else {
85
+ findings.add("divergence", `${MANIFEST_REL_PATH}: platform-wide entity ${row["name"]} names ${matches.length} domain entities (${matches.map((m) => m["id"]).join(", ")}) — kept as ${id}; give it a domain`);
86
+ }
87
+ }
88
+ return [...byId.values()];
89
+ }
90
+ //# sourceMappingURL=entity-merge.js.map
@@ -158,7 +158,7 @@ export function featureStatus(text) {
158
158
  }
159
159
  /**
160
160
  * A feature spec's kind (schema 4.4.0): the value of its `**Kind:**` line, lower-cased, else `feature`. An
161
- * obligation (M-ADR 0226, Proposed) and a journey feature (M-ADR 0229 §4.2, Proposed) say so; any other value is written as given, so
161
+ * obligation (M-ADR 0226, Proposed) and a journey feature (M-ADR 0229 §4.2) say so; any other value is written as given, so
162
162
  * the catalogue's vocabulary gate names the spec rather than a default hiding the mistake.
163
163
  */
164
164
  export function featureKind(text) {
@@ -27,8 +27,9 @@
27
27
  // 4. Forward-method supersedes (new).
28
28
  // models[].entities[] with status: superseded + superseded_by
29
29
  // One row per supersedes mapping after resolving both endpoints'
30
- // glossary_term against glossary_items.csv. Orphan finding when an
31
- // endpoint doesn't resolve.
30
+ // glossary_term against glossary_items.csv: the successor (superseded_by)
31
+ // is the source and supersedes the superseded term, the target. Orphan
32
+ // finding when an endpoint doesn't resolve.
32
33
  //
33
34
  // 5. Forward-method entity-mapping (new, writes to entity_glossary).
34
35
  // models[].entities[] with status ∈ {aligned, matched_direct} and
@@ -40,6 +41,7 @@
40
41
  import { readFile } from "node:fs/promises";
41
42
  import { join } from "node:path";
42
43
  import { parse as parseYaml } from "yaml";
44
+ import { NON_CANONICAL_DOMAINS } from "./entities-manifest.js";
43
45
  const MANIFEST_REL_PATH = join("docs", "platform", "reference", "glossary-alignment-manifest.yaml");
44
46
  export async function buildGlossaryXref(inputs) {
45
47
  // Profile gate per M-ADR 0158 §amendment: the manifest is platform-owned;
@@ -259,14 +261,18 @@ export async function buildGlossaryXref(inputs) {
259
261
  inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: supersedes edge can't emit — target "${supersededBy}" (→ glossary "${targetEntity?.glossaryTerm ?? "?"}") doesn't resolve to a glossary item`);
260
262
  continue;
261
263
  }
262
- emitXref(sourceId, targetId, "supersedes");
264
+ // The successor supersedes the superseded term: `source supersedes target`, as adr_supersedes reads.
265
+ emitXref(targetId, sourceId, "supersedes");
263
266
  continue;
264
267
  }
265
268
  if ((status === "aligned" || status === "matched_direct") && glossaryTerm !== "") {
266
269
  // Entity-mapping walker writes to entity_glossary.csv.
267
270
  if (domain === "" || !inputs.knownDomainIds.has(domain)) {
268
- // Domain not in this SDD's domains.csv — likely "platform-wide"
269
- // or another scope identifier. Skip with a coverage finding.
271
+ // A platform-wide entry is the domain entity its slug names (entity-merge.ts), and the entity_glossary
272
+ // edge is derived from that entity after the merge; the merge reports one it cannot place. Any other
273
+ // domain not in this SDD's domains.csv is skipped with a coverage finding.
274
+ if (NON_CANONICAL_DOMAINS.has(domain))
275
+ continue;
270
276
  inputs.findings.add("coverage", `${MANIFEST_REL_PATH}: entity ${name} domain "${domain}" not in known domains — entity_glossary row skipped`);
271
277
  continue;
272
278
  }
@@ -1,4 +1,4 @@
1
- // journey_requirement: the requirements a user journey cites for its steps (M-ADR 0229 §4.4, Proposed; schema 4.4.0).
1
+ // journey_requirement: the requirements a user journey cites for its steps (M-ADR 0229 §4.4; schema 4.4.0).
2
2
  //
3
3
  // Derived, never authored, from either master (§4.2):
4
4
  // tag option A: the end-to-end `.feature` files under the manifest's `journeys.test_paths`. A file tagged
@@ -64,13 +64,13 @@ export async function buildJourneyRequirement(inputs) {
64
64
  for (const m of text.matchAll(REQ_TAG)) {
65
65
  const quiet = atAggregate && inputs.reportedFeatureIds.has(m[1]);
66
66
  if (!knownFeatures.has(m[1])) {
67
- findings.add("external", `${where}: @req:${m[1]}/${m[2]} names a feature this catalogue does not hold; resolves at aggregation (M-ADR 0229 §4.4, Proposed)`);
67
+ findings.add("external", `${where}: @req:${m[1]}/${m[2]} names a feature this catalogue does not hold; resolves at aggregation (M-ADR 0229 §4.4)`);
68
68
  continue;
69
69
  }
70
70
  const ids = bySlug.get(`${m[1]}/${m[2]}`) ?? [];
71
71
  if (ids.length !== 1) {
72
72
  if (!quiet)
73
- findings.add("orphan", `${where}: @req:${m[1]}/${m[2]} ${ids.length === 0 ? "names no requirement of that feature" : `matches ${ids.length} requirements of that feature`}; no journey_requirement row (M-ADR 0229 §4.4, Proposed)`);
73
+ findings.add("orphan", `${where}: @req:${m[1]}/${m[2]} ${ids.length === 0 ? "names no requirement of that feature" : `matches ${ids.length} requirements of that feature`}; no journey_requirement row (M-ADR 0229 §4.4)`);
74
74
  continue;
75
75
  }
76
76
  const id = `${journeyId}__${ids[0]}`;
@@ -95,7 +95,7 @@ export async function buildJourneyRequirement(inputs) {
95
95
  continue;
96
96
  if (journeys.length > 1) {
97
97
  if (!atAggregate)
98
- findings.add("divergence", `${where}: tagged with ${journeys.length} journeys (${journeys.join(", ")}); one file is one journey's master (M-ADR 0229 §4.2, Proposed)`);
98
+ findings.add("divergence", `${where}: tagged with ${journeys.length} journeys (${journeys.join(", ")}); one file is one journey's master (M-ADR 0229 §4.2)`);
99
99
  continue;
100
100
  }
101
101
  const journeyId = journeys[0];
@@ -129,7 +129,7 @@ export async function buildJourneyRequirement(inputs) {
129
129
  const journeys = [...new Set([...spec.matchAll(JOURNEY_TAG)].map((m) => m[1]))];
130
130
  if (journeys.length !== 1) {
131
131
  if (!atAggregate)
132
- findings.add(journeys.length === 0 ? "orphan" : "divergence", `${where}: a journey feature's living spec ${journeys.length === 0 ? "names no journey (@journey:<journey-id>)" : `names ${journeys.length} journeys`}; one journey feature masters one journey (M-ADR 0229 §4.2, Proposed)`);
132
+ findings.add(journeys.length === 0 ? "orphan" : "divergence", `${where}: a journey feature's living spec ${journeys.length === 0 ? "names no journey (@journey:<journey-id>)" : `names ${journeys.length} journeys`}; one journey feature masters one journey (M-ADR 0229 §4.2)`);
133
133
  continue;
134
134
  }
135
135
  const journeyId = journeys[0];
@@ -119,7 +119,7 @@ const num = (v) => {
119
119
  return null;
120
120
  };
121
121
  /**
122
- * The review's other assessed inputs (M-ADR 0230 §4.9, Proposed): `inputs:` as a list of paths, or of `{path}`
122
+ * The review's other assessed inputs (M-ADR 0230 §4.9): `inputs:` as a list of paths, or of `{path}`
123
123
  * entries, each read at the review's corpus_ref. ';'-joined; null when the review names none.
124
124
  */
125
125
  function inputPaths(v) {
@@ -18,7 +18,7 @@
18
18
  // change_work_order per (change, repository): the record's work order, agreed or completed.
19
19
  // change_work_item per (change, work item): the ids its proposal's `## Work Items` section lists, keyed by
20
20
  // the undated name so the row survives archive.
21
- // requirement_revisions one row per distinct text of a requirement (schema 6.0.0, M-ADR 0230 §4.7, Proposed): the
21
+ // requirement_revisions one row per distinct text of a requirement (schema 6.0.0, M-ADR 0230 §4.7): the
22
22
  // living spec's is current, an agreed open change's ADDED or MODIFIED block intended, an
23
23
  // archived change's block the living spec no longer holds history. Identified by its digest.
24
24
  // requirement_assertions per (revision, repository): observed from a baseline's `**Observed in:**` line;
@@ -59,13 +59,13 @@ export async function buildServiceContractConsumes(inputs) {
59
59
  const mapped = inputs.legacyContractIdMap?.get(contractId);
60
60
  if (mapped !== undefined && mapped.length === 1) {
61
61
  inputs.findings.add("inferred", `service_contract: alias target "${contractId}" (${serviceId} ← "${iface.interfaceText}") re-keyed to ` +
62
- `"${mapped[0]}" — 2.x kindless contract id migrated per the 3.0.0 map (regenerable, not drift); ` +
62
+ `"${mapped[0]}" — an earlier-form contract id (2.x kindless, or a pre-7.0.0 unsplit stem) migrated per the contract-id map (regenerable, not drift); ` +
63
63
  `update .sdd/service-contract-aliases.yaml to retire this finding`);
64
64
  contractId = mapped[0];
65
65
  }
66
66
  else if (mapped !== undefined && mapped.length > 1) {
67
67
  inputs.findings.add("convention", `service_contract: alias target "${contractId}" (${serviceId} ← "${iface.interfaceText}") maps to ` +
68
- `${mapped.length} contracts at 3.0.0 [${mapped.join(", ")}] — a 2.x cross-family collision; ` +
68
+ `${mapped.length} current contracts [${mapped.join(", ")}] — a cross-family collision under the earlier form; ` +
69
69
  `qualify the alias with the kind-suffixed id; edge dropped`);
70
70
  continue;
71
71
  }
@@ -128,7 +128,7 @@ export async function buildServiceEntityFromTables(inputs) {
128
128
  }
129
129
  return [...rowsByPk.values()];
130
130
  }
131
- async function loadAliasMap(sddPath, findings) {
131
+ export async function loadAliasMap(sddPath, findings) {
132
132
  const empty = { byDisplayLower: new Map() };
133
133
  const path = join(sddPath, ALIAS_REL_PATH);
134
134
  let text;