@sigloch/contracts 0.8.0 → 2.0.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.
- package/dist/se/element-uid.d.ts +59 -0
- package/dist/se/element-uid.js +74 -0
- package/dist/se/evaluate-all.js +3 -1
- package/dist/se/format-e-parser.d.ts +37 -5
- package/dist/se/format-e-parser.js +147 -74
- package/dist/se/index.d.ts +3 -3
- package/dist/se/index.js +3 -3
- package/dist/se/metric-rules.d.ts +33 -11
- package/dist/se/metric-rules.js +99 -39
- package/dist/se/readiness.js +7 -3
- package/dist/se/rules.js +53 -0
- package/dist/se/schema-quality-rules.d.ts +12 -4
- package/dist/se/schema-quality-rules.js +0 -43
- package/package.json +1 -1
- package/dist/se/semantic-id.d.ts +0 -30
- package/dist/se/semantic-id.js +0 -90
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ElementUid — the family's canonical element id form (CR-SM-217).
|
|
3
|
+
*
|
|
4
|
+
* <TYPE>-<slug> ACTOR-claude-code · REQ-safety · MOD-harness
|
|
5
|
+
*
|
|
6
|
+
* Decided on 2026-07-27 over the previous `Name.TypeAbbr.Counter` schema, in this order:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Token-neutral.** Measured on the graphcode SSOT graph (1871 uid occurrences,
|
|
9
|
+
* cl100k_base): `ACTOR-claude-code.ACTOR` 5.28 tok/uid vs `claudecode.ACTOR.001`
|
|
10
|
+
* 5.30 — 0.3 % apart. The spelling costs nothing, so it is decided on correctness.
|
|
11
|
+
* 2. **It is what the real graphs use.** 626 of 1145 family elements are already
|
|
12
|
+
* `TYPE-slug`; the schema's declared canon had 89.
|
|
13
|
+
* 3. **It keeps a free type cross-check.** CR-SM-216 moved the type into the `### <TYPE>`
|
|
14
|
+
* section, which removes the "type appears twice" redundancy that used to catch a
|
|
15
|
+
* mis-sectioned node. A `TYPE-slug` uid carries the type in its prefix without paying
|
|
16
|
+
* for it (`REQ-safety` = 3 tokens, `REQ-safety.REQ` = 5), so the check survives.
|
|
17
|
+
*
|
|
18
|
+
* `parseElementUid` exists **only** for that cross-check. It is never a type source —
|
|
19
|
+
* the type comes from the graph or from the Format-E section (CR-SM-216 contract).
|
|
20
|
+
*
|
|
21
|
+
* **Not every id is an ElementUid, and that is allowed.** Ids that mirror an external
|
|
22
|
+
* identifier live in their own namespace and are simply unchecked, never rejected:
|
|
23
|
+
* CR elements carry their change-request number (`CR-GC-100`, 88 in graphcode, 123 in
|
|
24
|
+
* graph-view-edit) and graphify's pre-gate candidates are `cand_<hex>`. The canon binds
|
|
25
|
+
* *new* ids minted through `toElementUid`; existing ids stay until their repo touches
|
|
26
|
+
* them.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod/v4';
|
|
29
|
+
import { ElementType } from './ontology.js';
|
|
30
|
+
/** Longest slug we accept; keeps ids readable and index-friendly. */
|
|
31
|
+
export declare const MAX_SLUG_LENGTH = 60;
|
|
32
|
+
export declare const ElementUid: z.ZodString;
|
|
33
|
+
export type ElementUid = z.infer<typeof ElementUid>;
|
|
34
|
+
/**
|
|
35
|
+
* Build a canonical uid. Pure and deterministic — the same (type, name) always yields
|
|
36
|
+
* the same uid.
|
|
37
|
+
*
|
|
38
|
+
* Collisions are the **caller's** problem: only the caller knows the namespace, so
|
|
39
|
+
* disambiguation (`-2`, `-3`) belongs there. Documented rather than hidden behind a
|
|
40
|
+
* counter that would make this function stateful.
|
|
41
|
+
*/
|
|
42
|
+
export declare function toElementUid(type: ElementType, name: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Split a uid into its parts. Throws when the prefix is not an ElementType.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ For the prefix↔section cross-check only — **never** as a type source. Deriving a
|
|
47
|
+
* type from an id's spelling is what made aimpro CR-230 drop an entire graph silently.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseElementUid(uid: string): {
|
|
50
|
+
type: ElementType;
|
|
51
|
+
slug: string;
|
|
52
|
+
};
|
|
53
|
+
/** Non-throwing variant: `undefined` for ids of any other convention (legacy, graphify candidates). */
|
|
54
|
+
export declare function tryParseElementUid(uid: string): {
|
|
55
|
+
type: ElementType;
|
|
56
|
+
slug: string;
|
|
57
|
+
} | undefined;
|
|
58
|
+
/** True when `uid` is a canonical ElementUid. */
|
|
59
|
+
export declare function isElementUid(uid: string): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ElementUid — the family's canonical element id form (CR-SM-217).
|
|
3
|
+
*
|
|
4
|
+
* <TYPE>-<slug> ACTOR-claude-code · REQ-safety · MOD-harness
|
|
5
|
+
*
|
|
6
|
+
* Decided on 2026-07-27 over the previous `Name.TypeAbbr.Counter` schema, in this order:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Token-neutral.** Measured on the graphcode SSOT graph (1871 uid occurrences,
|
|
9
|
+
* cl100k_base): `ACTOR-claude-code.ACTOR` 5.28 tok/uid vs `claudecode.ACTOR.001`
|
|
10
|
+
* 5.30 — 0.3 % apart. The spelling costs nothing, so it is decided on correctness.
|
|
11
|
+
* 2. **It is what the real graphs use.** 626 of 1145 family elements are already
|
|
12
|
+
* `TYPE-slug`; the schema's declared canon had 89.
|
|
13
|
+
* 3. **It keeps a free type cross-check.** CR-SM-216 moved the type into the `### <TYPE>`
|
|
14
|
+
* section, which removes the "type appears twice" redundancy that used to catch a
|
|
15
|
+
* mis-sectioned node. A `TYPE-slug` uid carries the type in its prefix without paying
|
|
16
|
+
* for it (`REQ-safety` = 3 tokens, `REQ-safety.REQ` = 5), so the check survives.
|
|
17
|
+
*
|
|
18
|
+
* `parseElementUid` exists **only** for that cross-check. It is never a type source —
|
|
19
|
+
* the type comes from the graph or from the Format-E section (CR-SM-216 contract).
|
|
20
|
+
*
|
|
21
|
+
* **Not every id is an ElementUid, and that is allowed.** Ids that mirror an external
|
|
22
|
+
* identifier live in their own namespace and are simply unchecked, never rejected:
|
|
23
|
+
* CR elements carry their change-request number (`CR-GC-100`, 88 in graphcode, 123 in
|
|
24
|
+
* graph-view-edit) and graphify's pre-gate candidates are `cand_<hex>`. The canon binds
|
|
25
|
+
* *new* ids minted through `toElementUid`; existing ids stay until their repo touches
|
|
26
|
+
* them.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod/v4';
|
|
29
|
+
import { ElementType } from './ontology.js';
|
|
30
|
+
/** Longest slug we accept; keeps ids readable and index-friendly. */
|
|
31
|
+
export const MAX_SLUG_LENGTH = 60;
|
|
32
|
+
/** Built from the live ElementType enum — there is no second type catalogue. */
|
|
33
|
+
const UID_RE = new RegExp(`^(${ElementType.options.join('|')})-([a-z0-9]+(?:-[a-z0-9]+)*)$`);
|
|
34
|
+
export const ElementUid = z.string()
|
|
35
|
+
.regex(UID_RE, 'ElementUid must match <TYPE>-<kebab-slug>')
|
|
36
|
+
.refine(uid => (uid.split('-').slice(1).join('-')).length <= MAX_SLUG_LENGTH, `slug must be at most ${MAX_SLUG_LENGTH} characters`);
|
|
37
|
+
/**
|
|
38
|
+
* Build a canonical uid. Pure and deterministic — the same (type, name) always yields
|
|
39
|
+
* the same uid.
|
|
40
|
+
*
|
|
41
|
+
* Collisions are the **caller's** problem: only the caller knows the namespace, so
|
|
42
|
+
* disambiguation (`-2`, `-3`) belongs there. Documented rather than hidden behind a
|
|
43
|
+
* counter that would make this function stateful.
|
|
44
|
+
*/
|
|
45
|
+
export function toElementUid(type, name) {
|
|
46
|
+
const slug = name
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
49
|
+
.replace(/^-+|-+$/g, '')
|
|
50
|
+
.slice(0, MAX_SLUG_LENGTH)
|
|
51
|
+
.replace(/-+$/, '');
|
|
52
|
+
return `${type}-${slug}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Split a uid into its parts. Throws when the prefix is not an ElementType.
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ For the prefix↔section cross-check only — **never** as a type source. Deriving a
|
|
58
|
+
* type from an id's spelling is what made aimpro CR-230 drop an entire graph silently.
|
|
59
|
+
*/
|
|
60
|
+
export function parseElementUid(uid) {
|
|
61
|
+
const m = UID_RE.exec(uid);
|
|
62
|
+
if (!m)
|
|
63
|
+
throw new Error(`Not an ElementUid: "${uid}" — expected <TYPE>-<kebab-slug>`);
|
|
64
|
+
return { type: m[1], slug: m[2] };
|
|
65
|
+
}
|
|
66
|
+
/** Non-throwing variant: `undefined` for ids of any other convention (legacy, graphify candidates). */
|
|
67
|
+
export function tryParseElementUid(uid) {
|
|
68
|
+
const m = UID_RE.exec(uid);
|
|
69
|
+
return m ? { type: m[1], slug: m[2] } : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** True when `uid` is a canonical ElementUid. */
|
|
72
|
+
export function isElementUid(uid) {
|
|
73
|
+
return ElementUid.safeParse(uid).success;
|
|
74
|
+
}
|
package/dist/se/evaluate-all.js
CHANGED
|
@@ -23,7 +23,9 @@ export const ALL_RULE_DEFS = [
|
|
|
23
23
|
...FM_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
24
24
|
...VIEW_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
25
25
|
];
|
|
26
|
-
|
|
26
|
+
// CR-SM-221: 'RD-' was missing — 'RD-01'.startsWith('R-') is false, so the
|
|
27
|
+
// decomposition rules ran in `default` only and never in the `se` profile.
|
|
28
|
+
const SE_PREFIXES = ['R-', 'RD-', 'UC-', 'FC-', 'SC-', 'MT-', 'CR-', 'AO-', 'FM-', 'NFR-', 'RT-', 'PH-', 'CA-', 'IO-', 'VR-', 'CL-'];
|
|
27
29
|
const CODING_PREFIXES = ['BQ-', 'ND-'];
|
|
28
30
|
export function getRuleDefsForProfile(profile) {
|
|
29
31
|
if (profile === 'se')
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Format E Parser — Graph mutations from compact text format.
|
|
3
3
|
* SE-relevant subset: nodes (+/-/~) and edges (+/-), no chat-canvas or views.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* CR-SM-216 (Format-E v2): the element type comes from the `### <TYPE>` section a node
|
|
6
|
+
* is declared under, never from the spelling of its id. The old id-derived typing made
|
|
7
|
+
* every consumer with a different id convention fail *silently* — aimpro CR-230 lost a
|
|
8
|
+
* whole graph that way (`TYPE-slug` ids rejected, result empty, no error).
|
|
5
9
|
*
|
|
6
10
|
* @sigloch/contracts/se
|
|
7
11
|
*/
|
|
12
|
+
import { ElementType } from './ontology.js';
|
|
8
13
|
import type { TraceType, OntologyGraph } from './ontology.js';
|
|
9
14
|
export interface FormatEOperation {
|
|
10
15
|
type: 'add_node' | 'remove_node' | 'update_node' | 'add_edge' | 'remove_edge' | 'strict_add_node' | 'strict_add_edge';
|
|
11
16
|
semanticId: string;
|
|
17
|
+
/**
|
|
18
|
+
* CR-SM-216: the element type, taken from the node's `### <TYPE>` section. Set on
|
|
19
|
+
* every node-creating operation — consumers must read it instead of re-deriving a
|
|
20
|
+
* type from the id.
|
|
21
|
+
*/
|
|
22
|
+
elementType?: ElementType;
|
|
12
23
|
description?: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
24
|
+
/**
|
|
25
|
+
* CR-147: Parsed @key value attributes from lines below the node entry.
|
|
26
|
+
* Values are strings, EXCEPT JSON object/array literals which are hydrated
|
|
27
|
+
* (BOK-CR-026) — object-valued bindings like `realRef`/`testRef` must reach
|
|
28
|
+
* `attributes` as objects or R-26/R-19 reject them as invalid.
|
|
29
|
+
*/
|
|
30
|
+
attributes?: Record<string, unknown>;
|
|
15
31
|
sourceId?: string;
|
|
16
32
|
targetId?: string;
|
|
17
33
|
traceType?: TraceType;
|
|
@@ -22,7 +38,23 @@ export interface FormatEDiff {
|
|
|
22
38
|
}
|
|
23
39
|
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
24
40
|
export declare function extractFormatE(llmOutput: string): string | null;
|
|
41
|
+
export interface ParseFormatEOptions {
|
|
42
|
+
/**
|
|
43
|
+
* CR-SM-216: resolve the type of a uid that this text does not declare. A mutation
|
|
44
|
+
* diff adding edges between existing nodes carries no `## Nodes` block, so the
|
|
45
|
+
* caller binds this to its store. Without it such a diff is an error, never a
|
|
46
|
+
* silent skip.
|
|
47
|
+
*/
|
|
48
|
+
resolveType?: (uid: string) => ElementType | undefined;
|
|
49
|
+
}
|
|
25
50
|
/** Parse a Format E text block into validated operations. */
|
|
26
|
-
export declare function parseFormatE(input: string): FormatEDiff;
|
|
27
|
-
/**
|
|
51
|
+
export declare function parseFormatE(input: string, options?: ParseFormatEOptions): FormatEDiff;
|
|
52
|
+
/**
|
|
53
|
+
* Serialize an OntologyGraph to compact Format E text.
|
|
54
|
+
*
|
|
55
|
+
* CR-SM-216: nodes are written under `### <TYPE>` sections — the type is declared once
|
|
56
|
+
* per section instead of once per id. Measured on the graphcode SSOT graph (369
|
|
57
|
+
* elements), 12 section headers cost ~48 tokens where a per-node type attribute would
|
|
58
|
+
* have cost ~1476.
|
|
59
|
+
*/
|
|
28
60
|
export declare function serializeToFormatE(graph: OntologyGraph): string;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format E Parser — Graph mutations from compact text format.
|
|
3
|
+
* SE-relevant subset: nodes (+/-/~) and edges (+/-), no chat-canvas or views.
|
|
4
|
+
*
|
|
5
|
+
* CR-SM-216 (Format-E v2): the element type comes from the `### <TYPE>` section a node
|
|
6
|
+
* is declared under, never from the spelling of its id. The old id-derived typing made
|
|
7
|
+
* every consumer with a different id convention fail *silently* — aimpro CR-230 lost a
|
|
8
|
+
* whole graph that way (`TYPE-slug` ids rejected, result empty, no error).
|
|
9
|
+
*
|
|
10
|
+
* @sigloch/contracts/se
|
|
11
|
+
*/
|
|
12
|
+
import { ElementType } from './ontology.js';
|
|
1
13
|
import { isValidTrace } from './meta-model.js';
|
|
2
|
-
import { isSemanticId, extractFromSemanticId } from './semantic-id.js';
|
|
3
14
|
// ---------------------------------------------------------------------------
|
|
4
15
|
// Extraction
|
|
5
16
|
// ---------------------------------------------------------------------------
|
|
@@ -21,19 +32,48 @@ const OP_PREFIX = {
|
|
|
21
32
|
'~': 'update',
|
|
22
33
|
'!': 'strict_add',
|
|
23
34
|
};
|
|
24
|
-
|
|
35
|
+
/**
|
|
36
|
+
* CR-SM-215: the target group is `(.+)` — Format-E allows fan-out
|
|
37
|
+
* `A -x-> B, C, D`, one edge per target. `graph-api-core`'s codec has always parsed
|
|
38
|
+
* it; this parser rejected it as `Invalid edge syntax`, so the same text produced
|
|
39
|
+
* different operations depending on which parser saw it.
|
|
40
|
+
*/
|
|
41
|
+
const EDGE_RE = /^([+\-~!])?\s*(\S+)\s+-(\w+)->\s+(.+?)\s*$/;
|
|
25
42
|
const NODE_RE = /^([+\-~!])?\s*(\S+?)(?:\|(.*))?$/;
|
|
26
43
|
/** CR-147: @key value attribute line (indented, below a node entry). */
|
|
27
44
|
const ATTR_RE = /^\s*@(\w+)\s+(.+)$/;
|
|
45
|
+
/**
|
|
46
|
+
* BOK-CR-026: hydrate a JSON object/array attribute value. The ontology's bindings
|
|
47
|
+
* (`realRef {file,symbol?,lang?}`, `testRef {file,tool,…}`) are objects; kept as raw
|
|
48
|
+
* strings they fail RealRefSchema/TestRefSchema and the element reads as unbound.
|
|
49
|
+
* Only `{…}`/`[…]` are attempted — every other value stays the string it is, and a
|
|
50
|
+
* malformed literal falls back to the string rather than failing the whole parse.
|
|
51
|
+
*/
|
|
52
|
+
function hydrateAttrValue(raw) {
|
|
53
|
+
if (!/^[{[]/.test(raw))
|
|
54
|
+
return raw;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
28
62
|
/** CR-148: Trace-type normalization aliases (source→target→from→to). */
|
|
29
63
|
const TRACE_NORMALIZE = {
|
|
30
64
|
FLOW: { SCHEMA: 'relation' }, // FLOW→SCHEMA io → relation
|
|
31
65
|
};
|
|
66
|
+
/** `### <TYPE>` — the node type section (CR-SM-216). */
|
|
67
|
+
const TYPE_SECTION_RE = /^###\s+([A-Za-z_]+)\s*$/;
|
|
32
68
|
/** Parse a Format E text block into validated operations. */
|
|
33
|
-
export function parseFormatE(input) {
|
|
69
|
+
export function parseFormatE(input, options = {}) {
|
|
34
70
|
const operations = [];
|
|
35
71
|
const errors = [];
|
|
36
72
|
let section = null;
|
|
73
|
+
let currentType = null;
|
|
74
|
+
/** uid → type, from this text's node sections. */
|
|
75
|
+
const declared = new Map();
|
|
76
|
+
const typeOf = (uid) => declared.get(uid) ?? options.resolveType?.(uid);
|
|
37
77
|
for (const rawLine of input.split('\n')) {
|
|
38
78
|
const line = rawLine.trim();
|
|
39
79
|
if (!line || line.startsWith('//') || line.startsWith('#!'))
|
|
@@ -45,7 +85,7 @@ export function parseFormatE(input) {
|
|
|
45
85
|
if (lastOp && (lastOp.type === 'add_node' || lastOp.type === 'update_node' || lastOp.type === 'strict_add_node')) {
|
|
46
86
|
if (!lastOp.attributes)
|
|
47
87
|
lastOp.attributes = {};
|
|
48
|
-
lastOp.attributes[attrMatch[1]] = attrMatch[2].trim();
|
|
88
|
+
lastOp.attributes[attrMatch[1]] = hydrateAttrValue(attrMatch[2].trim());
|
|
49
89
|
}
|
|
50
90
|
else {
|
|
51
91
|
errors.push(`@attribute line without preceding node: "${line}"`);
|
|
@@ -55,49 +95,64 @@ export function parseFormatE(input) {
|
|
|
55
95
|
// Section headers
|
|
56
96
|
if (/^##\s*nodes?\s*$/i.test(line)) {
|
|
57
97
|
section = 'nodes';
|
|
98
|
+
currentType = null;
|
|
58
99
|
continue;
|
|
59
100
|
}
|
|
60
101
|
if (/^##\s*edges?\s*$/i.test(line)) {
|
|
61
102
|
section = 'edges';
|
|
103
|
+
currentType = null;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// CR-SM-216: `### <TYPE>` carries the element type for the nodes below it.
|
|
107
|
+
const typeSection = TYPE_SECTION_RE.exec(line);
|
|
108
|
+
if (typeSection && section === 'nodes') {
|
|
109
|
+
const parsed = ElementType.safeParse(typeSection[1]);
|
|
110
|
+
if (parsed.success) {
|
|
111
|
+
currentType = parsed.data;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
currentType = null;
|
|
115
|
+
errors.push(`Unknown element type section: "### ${typeSection[1]}"`);
|
|
116
|
+
}
|
|
62
117
|
continue;
|
|
63
118
|
}
|
|
64
119
|
// Skip other markdown headers
|
|
65
120
|
if (line.startsWith('#'))
|
|
66
121
|
continue;
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
// Edge: detect by arrow in the structural part only — before the description
|
|
123
|
+
// pipe. Node descriptions may legitimately contain '->' ("FUNC->FUNC compose");
|
|
124
|
+
// edges carry no pipe, so splitting on '|' disambiguates (CR-GC-247). Since
|
|
125
|
+
// CR-SM-215 widened the target group to `(.+?)`, this guard is what keeps a
|
|
126
|
+
// multi-word node description from being read as an edge.
|
|
127
|
+
const isEdgeLine = line.split('|', 1)[0].includes('->');
|
|
128
|
+
if (isEdgeLine || section === 'edges') {
|
|
129
|
+
const edgeMatch = isEdgeLine ? EDGE_RE.exec(line) : null;
|
|
70
130
|
if (edgeMatch) {
|
|
71
|
-
parseEdge(edgeMatch, operations, errors);
|
|
131
|
+
parseEdge(edgeMatch, typeOf, operations, errors);
|
|
72
132
|
}
|
|
73
133
|
else {
|
|
74
134
|
errors.push(`Invalid edge line: "${line}"`);
|
|
75
135
|
}
|
|
76
136
|
continue;
|
|
77
137
|
}
|
|
78
|
-
//
|
|
138
|
+
// Node
|
|
79
139
|
if (section === 'nodes') {
|
|
80
140
|
const nodeMatch = NODE_RE.exec(line);
|
|
81
|
-
if (nodeMatch) {
|
|
82
|
-
|
|
141
|
+
if (!nodeMatch) {
|
|
142
|
+
errors.push(`Invalid node line: "${line}"`);
|
|
143
|
+
}
|
|
144
|
+
else if (!currentType) {
|
|
145
|
+
// CR-SM-216: no type section, no type. Guessing one from the id is what
|
|
146
|
+
// CR-230 punished; an error is the point.
|
|
147
|
+
errors.push(`Node "${nodeMatch[2]}" is not under a "### <TYPE>" section`);
|
|
83
148
|
}
|
|
84
149
|
else {
|
|
85
|
-
|
|
150
|
+
declared.set(nodeMatch[2], currentType);
|
|
151
|
+
parseNode(nodeMatch, currentType, operations, errors);
|
|
86
152
|
}
|
|
87
153
|
continue;
|
|
88
154
|
}
|
|
89
|
-
//
|
|
90
|
-
const autoEdge = EDGE_RE.exec(line);
|
|
91
|
-
if (autoEdge) {
|
|
92
|
-
parseEdge(autoEdge, operations, errors);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const autoNode = NODE_RE.exec(line);
|
|
96
|
-
if (autoNode && isSemanticId(autoNode[2])) {
|
|
97
|
-
parseNode(autoNode, operations, errors);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
// Unknown line
|
|
155
|
+
// Unknown line (edges are handled above, nodes need their section)
|
|
101
156
|
if (line.length > 0)
|
|
102
157
|
errors.push(`Unrecognized line: "${line}"`);
|
|
103
158
|
}
|
|
@@ -111,60 +166,40 @@ function normalizeTraceType(srcType, tgtType, traceType) {
|
|
|
111
166
|
}
|
|
112
167
|
return traceType;
|
|
113
168
|
}
|
|
114
|
-
function parseNode(m, ops, errors) {
|
|
169
|
+
function parseNode(m, elementType, ops, errors) {
|
|
115
170
|
const opChar = m[1] || '+';
|
|
116
171
|
const id = m[2];
|
|
117
172
|
const descr = m[3]?.trim();
|
|
118
173
|
const action = OP_PREFIX[opChar] ?? 'add';
|
|
119
|
-
if (!
|
|
120
|
-
errors.push(`
|
|
174
|
+
if (!id) {
|
|
175
|
+
errors.push(`Node line without an id: "${m[0]}"`);
|
|
121
176
|
return;
|
|
122
177
|
}
|
|
123
178
|
if (action === 'remove') {
|
|
124
179
|
ops.push({ type: 'remove_node', semanticId: id });
|
|
125
180
|
}
|
|
126
181
|
else if (action === 'update') {
|
|
127
|
-
ops.push({ type: 'update_node', semanticId: id, description: descr });
|
|
182
|
+
ops.push({ type: 'update_node', semanticId: id, elementType, description: descr });
|
|
128
183
|
}
|
|
129
184
|
else if (action === 'strict_add') {
|
|
130
|
-
ops.push({ type: 'strict_add_node', semanticId: id, description: descr });
|
|
185
|
+
ops.push({ type: 'strict_add_node', semanticId: id, elementType, description: descr });
|
|
131
186
|
}
|
|
132
187
|
else {
|
|
133
|
-
ops.push({ type: 'add_node', semanticId: id, description: descr });
|
|
188
|
+
ops.push({ type: 'add_node', semanticId: id, elementType, description: descr });
|
|
134
189
|
}
|
|
135
190
|
}
|
|
136
|
-
function parseEdge(m, ops, errors) {
|
|
191
|
+
function parseEdge(m, typeOf, ops, errors) {
|
|
137
192
|
const opChar = m[1] || '+';
|
|
138
193
|
const sourceId = m[2];
|
|
139
194
|
const traceType = m[3];
|
|
140
|
-
const targetId = m[4];
|
|
141
195
|
const action = OP_PREFIX[opChar] ?? 'add';
|
|
142
|
-
if (!isSemanticId(sourceId)) {
|
|
143
|
-
errors.push(`Invalid source SemanticId: "${sourceId}"`);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (!isSemanticId(targetId)) {
|
|
147
|
-
errors.push(`Invalid target SemanticId: "${targetId}"`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
196
|
if (!VALID_TRACE_TYPES.has(traceType)) {
|
|
151
197
|
errors.push(`Invalid trace type: "${traceType}"`);
|
|
152
198
|
return;
|
|
153
199
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const src = extractFromSemanticId(sourceId);
|
|
158
|
-
const tgt = extractFromSemanticId(targetId);
|
|
159
|
-
// CR-148: Normalize before meta-model check (e.g. FLOW→SCHEMA io → relation)
|
|
160
|
-
resolvedTraceType = normalizeTraceType(src.type, tgt.type, traceType);
|
|
161
|
-
if (!isValidTrace({ source: src.type, target: tgt.type, type: resolvedTraceType })) {
|
|
162
|
-
errors.push(`Meta-model violation: ${src.type} -${traceType}-> ${tgt.type} is not valid`);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
errors.push(`Cannot extract types from edge: ${sourceId} -${traceType}-> ${targetId}`);
|
|
200
|
+
const srcType = typeOf(sourceId);
|
|
201
|
+
if (!srcType) {
|
|
202
|
+
errors.push(`Cannot resolve type of "${sourceId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
168
203
|
return;
|
|
169
204
|
}
|
|
170
205
|
const edgeType = action === 'remove'
|
|
@@ -172,33 +207,71 @@ function parseEdge(m, ops, errors) {
|
|
|
172
207
|
: action === 'strict_add'
|
|
173
208
|
? 'strict_add_edge'
|
|
174
209
|
: 'add_edge';
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
// CR-SM-215: 1:n fan-out — `A -x-> B, C` is n independent edges. Validation runs
|
|
211
|
+
// per target (like `graph-api-core`'s codec), so one bad target does not discard
|
|
212
|
+
// its siblings.
|
|
213
|
+
const targets = m[4].split(',').map(t => t.trim()).filter(Boolean);
|
|
214
|
+
for (const targetId of targets) {
|
|
215
|
+
const tgtType = typeOf(targetId);
|
|
216
|
+
if (!tgtType) {
|
|
217
|
+
errors.push(`Cannot resolve type of "${targetId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// CR-148: Normalize before the meta-model check (e.g. FLOW→SCHEMA io → relation)
|
|
221
|
+
const resolvedTraceType = normalizeTraceType(srcType, tgtType, traceType);
|
|
222
|
+
if (!isValidTrace({ source: srcType, target: tgtType, type: resolvedTraceType })) {
|
|
223
|
+
errors.push(`Meta-model violation: ${srcType} -${traceType}-> ${tgtType} is not valid`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
ops.push({
|
|
227
|
+
type: edgeType,
|
|
228
|
+
semanticId: `${sourceId}->${targetId}`,
|
|
229
|
+
sourceId,
|
|
230
|
+
targetId,
|
|
231
|
+
traceType: resolvedTraceType,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
182
234
|
}
|
|
183
235
|
// ---------------------------------------------------------------------------
|
|
184
236
|
// Serializer
|
|
185
237
|
// ---------------------------------------------------------------------------
|
|
186
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Serialize an OntologyGraph to compact Format E text.
|
|
240
|
+
*
|
|
241
|
+
* CR-SM-216: nodes are written under `### <TYPE>` sections — the type is declared once
|
|
242
|
+
* per section instead of once per id. Measured on the graphcode SSOT graph (369
|
|
243
|
+
* elements), 12 section headers cost ~48 tokens where a per-node type attribute would
|
|
244
|
+
* have cost ~1476.
|
|
245
|
+
*/
|
|
187
246
|
export function serializeToFormatE(graph) {
|
|
188
247
|
const lines = [];
|
|
189
|
-
// Nodes
|
|
190
|
-
|
|
248
|
+
// Nodes, grouped by type
|
|
249
|
+
const modelingElements = graph.elements.filter(el => el.type !== 'SESSION'); // skip audit sessions
|
|
250
|
+
if (modelingElements.length > 0) {
|
|
191
251
|
lines.push('## Nodes');
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
252
|
+
const byType = new Map();
|
|
253
|
+
for (const el of modelingElements) {
|
|
254
|
+
const group = byType.get(el.type);
|
|
255
|
+
if (group)
|
|
256
|
+
group.push(el);
|
|
257
|
+
else
|
|
258
|
+
byType.set(el.type, [el]);
|
|
259
|
+
}
|
|
260
|
+
for (const type of [...byType.keys()].sort()) {
|
|
261
|
+
lines.push(`### ${type}`);
|
|
262
|
+
for (const el of byType.get(type) ?? []) {
|
|
263
|
+
const descr = el.description ? `|${el.description}` : '';
|
|
264
|
+
lines.push(`+ ${el.id}${descr}`);
|
|
265
|
+
// CR-147: Serialize known attributes
|
|
266
|
+
if (el.attributes) {
|
|
267
|
+
for (const [k, v] of Object.entries(el.attributes)) {
|
|
268
|
+
if (v == null)
|
|
269
|
+
continue;
|
|
270
|
+
// BOK-CR-026: objects/arrays as JSON — String({}) collapses a realRef/testRef
|
|
271
|
+
// binding to "[object Object]" and loses it on the next parse.
|
|
272
|
+
const text = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
273
|
+
if (text.length > 0)
|
|
274
|
+
lines.push(` @${k} ${text}`);
|
|
202
275
|
}
|
|
203
276
|
}
|
|
204
277
|
}
|
package/dist/se/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Single source of truth for SE ontology schemas across all projects.
|
|
4
4
|
*/
|
|
5
5
|
/** Ontology schema version (element types + trace types). */
|
|
6
|
-
export declare const ONTOLOGY_VERSION = "
|
|
6
|
+
export declare const ONTOLOGY_VERSION = "4.0.0";
|
|
7
7
|
/** Rules engine version (validation rules incl. RC conformance). */
|
|
8
|
-
export declare const RULES_VERSION = "2.
|
|
8
|
+
export declare const RULES_VERSION = "2.21.0";
|
|
9
9
|
/** Meta-model version (trace pattern constraints + format-e parser). */
|
|
10
10
|
export declare const META_MODEL_VERSION = "1.4.0";
|
|
11
11
|
export * from './ontology.js';
|
|
@@ -24,5 +24,5 @@ export * from './quality-rules.js';
|
|
|
24
24
|
export * from './evaluate-all.js';
|
|
25
25
|
export * from './readiness.js';
|
|
26
26
|
export * from './meta-model.js';
|
|
27
|
-
export * from './
|
|
27
|
+
export * from './element-uid.js';
|
|
28
28
|
export * from './format-e-parser.js';
|
package/dist/se/index.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Single source of truth for SE ontology schemas across all projects.
|
|
4
4
|
*/
|
|
5
5
|
/** Ontology schema version (element types + trace types). */
|
|
6
|
-
export const ONTOLOGY_VERSION = '
|
|
6
|
+
export const ONTOLOGY_VERSION = '4.0.0'; // BREAKING: `SemanticId` (`Name.TypeAbbr.Counter`) deleted, `ElementUid` (`<TYPE>-<slug>`) is the family canon — the old canon was used by no production graph of the family while 626 of 1145 elements already carried TYPE-slug (CR-SM-217); realRef unifies codeRef+schemaRef (+physical-MOD CAD ref), symbol optional; testRef stays separate (CR-228 C); +RepoRelativePathSchema on testRef/realRef .file — no absolute/`..` paths (CR-GC-255)
|
|
7
7
|
/** Rules engine version (validation rules incl. RC conformance). */
|
|
8
|
-
export const RULES_VERSION = '2.
|
|
8
|
+
export const RULES_VERSION = '2.21.0'; // -MT-03 retired as a rule and reborn as the `allocationCohesion` measurement (CR-SM-223): the 80 % threshold fired on 6/7 graphcode, 4/4 gve and 10/11 family modules — in a flow-routed architecture cross-boundary interaction is the design, not a defect; the metric now reports internal/external per MOD, worst-first, and stays out of the violation stream so it cannot depress the readiness score; +RD-04 decomposition breadth (>11 children per level → warning) and MT-03 recalibrated to FLOW-transitive connection pairs — counting raw io traces reported internal=0 on every real SE graph (15/15 false positives), because the meta-model routes FUNC↔FUNC through FLOW; 'RD-' added to the se profile prefixes, where RD-01..04 were silently missing (CR-SM-221); -SC-01/-SC-03 deleted (BOK-CR-026): `realRef` is the single SCHEMA binding truth (R-26 presence, RC-03/RC-04 resolution); the legacy `zodDefinition`/`sourceFile`/`sourceExport` attributes are gone from every producer, SC_RULES = [SC-02]; R-20/R-26/RC-01/RC-03/RC-04 read realRef (unified codeRef+schemaRef); +R-27 physical-MOD realRef presence (CR-228 C); -R-24/R-25 REQ→MOD allocation rules deleted (CR-228 A: REQ→MOD allocate no longer a valid pattern, R-18 flags residual edges); NFR-01 budget target split physical→MOD / behavioral→FCHAIN; RULE_TO_DIMENSION completeness (R-18..R-23/R-26/MS-03/CR-R04 mapped, no advisory fall-through) (CR-228 B/D); +BQ-01/02/04/06/07 base-quality rules promoted from aimpro (K2-b, completes rule consolidation); +ND-01/02 near-duplicate + AO-D01/D03/CR-01/RT-01/PH-01/CA-01/IO-01 architecture rules promoted from aimpro (K2-b); +CR-R01..04/MS-03 change-request rules promoted from aimpro (K2-b); +FM-01..03/NFR-01 FMEA + VR-01/CL-01 view rules promoted from aimpro (K2-b); +MT-01..03 architecture metrics promoted from aimpro (K2-b); +SC-01..03/UC-01..06/FC-01..03 quality rules promoted from aimpro (K2-a); +RC-05 cross-module import drift (CR-212); +R-26/RC-03/RC-04 schemaRef (CR-211); +R-22..R-25/R-10/R-20 (CR-201/202/208/209/210)
|
|
9
9
|
/** Meta-model version (trace pattern constraints + format-e parser). */
|
|
10
10
|
export const META_MODEL_VERSION = '1.4.0'; // -REQ→MOD allocate pattern removed (CR-228 A); +FUNC→FUNC compose (blackbox function decomposition)
|
|
11
11
|
export * from './ontology.js';
|
|
@@ -24,5 +24,5 @@ export * from './quality-rules.js';
|
|
|
24
24
|
export * from './evaluate-all.js';
|
|
25
25
|
export * from './readiness.js';
|
|
26
26
|
export * from './meta-model.js';
|
|
27
|
-
export * from './
|
|
27
|
+
export * from './element-uid.js';
|
|
28
28
|
export * from './format-e-parser.js';
|
|
@@ -18,14 +18,41 @@ export declare function mt01Instability(graph: OntologyGraph): RuleViolation[];
|
|
|
18
18
|
* Components > 1 → info.
|
|
19
19
|
*/
|
|
20
20
|
export declare function mt02Lcom4(graph: OntologyGraph): RuleViolation[];
|
|
21
|
+
/** One module's allocation-cohesion measurement (CR-SM-223). */
|
|
22
|
+
export interface AllocationCohesion {
|
|
23
|
+
moduleId: string;
|
|
24
|
+
moduleName: string;
|
|
25
|
+
/** Connection pairs with both endpoints allocated to this module. */
|
|
26
|
+
internal: number;
|
|
27
|
+
/** Connection pairs with exactly one endpoint allocated to this module. */
|
|
28
|
+
external: number;
|
|
29
|
+
/** internal / (internal + external), in [0, 1]. */
|
|
30
|
+
cohesion: number;
|
|
31
|
+
}
|
|
21
32
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
33
|
+
* Allocation cohesion — a **measurement, not a rule** (CR-SM-223, decision 2026-07-29).
|
|
34
|
+
*
|
|
35
|
+
* It used to be MT-03 with an 80 % threshold, and it fired on nearly every module of
|
|
36
|
+
* every real graph: 6 of 7 on graphcode, 4 of 4 on graph-view-edit, 10 of 11 on the
|
|
37
|
+
* family graph. `CR-SM-221` first suspected the edge definition and made it
|
|
38
|
+
* FLOW-transitive — the hit rate did not move. The threshold was the miscalibration:
|
|
39
|
+
* in a flow-routed layered architecture, module interaction crosses boundaries by
|
|
40
|
+
* design, so "80 % of interaction is internal" describes a monolith, not a healthy
|
|
41
|
+
* module.
|
|
42
|
+
*
|
|
43
|
+
* Rather than fit a cut-off to 11 data points, this reports the number and lets the
|
|
44
|
+
* architect judge. Returned worst-first, so the head of the list is where to look.
|
|
45
|
+
* Modules with fewer than two allocated FUNCs, or with no external connection at all,
|
|
46
|
+
* carry no signal and are omitted.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately NOT a `RuleDefinition`: `computeReadiness` counts every violation into
|
|
49
|
+
* its dimension score regardless of severity, so a per-module advisory would depress
|
|
50
|
+
* the `alloc` score permanently. A measurement must not masquerade as a defect.
|
|
51
|
+
*
|
|
52
|
+
* Validation of this metric — and of MT-01/MT-02, which are thresholded the same way —
|
|
53
|
+
* is deferred (CR-SM-223).
|
|
27
54
|
*/
|
|
28
|
-
export declare function
|
|
55
|
+
export declare function allocationCohesion(graph: OntologyGraph): AllocationCohesion[];
|
|
29
56
|
export declare const MT_RULES: readonly [{
|
|
30
57
|
readonly id: "MT-01";
|
|
31
58
|
readonly name: "Module instability";
|
|
@@ -36,10 +63,5 @@ export declare const MT_RULES: readonly [{
|
|
|
36
63
|
readonly name: "Module cohesion (LCOM4)";
|
|
37
64
|
readonly severity: "info";
|
|
38
65
|
readonly evaluate: typeof mt02Lcom4;
|
|
39
|
-
}, {
|
|
40
|
-
readonly id: "MT-03";
|
|
41
|
-
readonly name: "Allocation cohesion";
|
|
42
|
-
readonly severity: "info";
|
|
43
|
-
readonly evaluate: typeof mt03AllocationCohesion;
|
|
44
66
|
}];
|
|
45
67
|
export declare function evaluateMTRules(graph: OntologyGraph): RuleViolation[];
|
package/dist/se/metric-rules.js
CHANGED
|
@@ -151,57 +151,117 @@ export function mt02Lcom4(graph) {
|
|
|
151
151
|
return violations;
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
154
|
+
* CR-SM-221: connection pairs between non-FLOW elements, FLOW-transitive.
|
|
155
|
+
*
|
|
156
|
+
* The SE meta-model routes FUNC↔FUNC communication through FLOW nodes
|
|
157
|
+
* (`FUNC —io→ FLOW —io→ FUNC`); a direct FUNC→FUNC io trace is the exception, not
|
|
158
|
+
* the rule. Counting raw io traces therefore found *zero* internal edges on every
|
|
159
|
+
* real graph — measured on graphcode (369 elements), graph-view-edit (257) and the
|
|
160
|
+
* family graph (638): 15 of 15 MODs reported internal=0, i.e. 15 false positives and
|
|
161
|
+
* no discriminating power at all.
|
|
162
|
+
*
|
|
163
|
+
* A pair counts once, no matter how many flows connect it: the metric asks "do these
|
|
164
|
+
* two talk to each other", not "how often".
|
|
159
165
|
*/
|
|
160
|
-
|
|
161
|
-
const
|
|
166
|
+
function connectionPairs(graph) {
|
|
167
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
168
|
+
const pairs = new Set();
|
|
169
|
+
const add = (a, b) => {
|
|
170
|
+
if (a !== b)
|
|
171
|
+
pairs.add(a < b ? `${a}|${b}` : `${b}|${a}`);
|
|
172
|
+
};
|
|
173
|
+
const intoFlow = new Map(); // flow → producers
|
|
174
|
+
const outOfFlow = new Map(); // flow → consumers
|
|
175
|
+
const push = (m, k, v) => {
|
|
176
|
+
const list = m.get(k);
|
|
177
|
+
if (list)
|
|
178
|
+
list.push(v);
|
|
179
|
+
else
|
|
180
|
+
m.set(k, [v]);
|
|
181
|
+
};
|
|
182
|
+
for (const t of graph.traces) {
|
|
183
|
+
if (t.type !== 'io')
|
|
184
|
+
continue;
|
|
185
|
+
const srcIsFlow = typeOf.get(t.source) === 'FLOW';
|
|
186
|
+
const tgtIsFlow = typeOf.get(t.target) === 'FLOW';
|
|
187
|
+
if (srcIsFlow && tgtIsFlow)
|
|
188
|
+
continue; // FLOW→FLOW carries no endpoint
|
|
189
|
+
if (!srcIsFlow && !tgtIsFlow) {
|
|
190
|
+
add(t.source, t.target);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (tgtIsFlow)
|
|
194
|
+
push(intoFlow, t.target, t.source);
|
|
195
|
+
else
|
|
196
|
+
push(outOfFlow, t.source, t.target);
|
|
197
|
+
}
|
|
198
|
+
for (const [flow, producers] of intoFlow) {
|
|
199
|
+
for (const consumer of outOfFlow.get(flow) ?? []) {
|
|
200
|
+
for (const producer of producers)
|
|
201
|
+
add(producer, consumer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return pairs;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Allocation cohesion — a **measurement, not a rule** (CR-SM-223, decision 2026-07-29).
|
|
208
|
+
*
|
|
209
|
+
* It used to be MT-03 with an 80 % threshold, and it fired on nearly every module of
|
|
210
|
+
* every real graph: 6 of 7 on graphcode, 4 of 4 on graph-view-edit, 10 of 11 on the
|
|
211
|
+
* family graph. `CR-SM-221` first suspected the edge definition and made it
|
|
212
|
+
* FLOW-transitive — the hit rate did not move. The threshold was the miscalibration:
|
|
213
|
+
* in a flow-routed layered architecture, module interaction crosses boundaries by
|
|
214
|
+
* design, so "80 % of interaction is internal" describes a monolith, not a healthy
|
|
215
|
+
* module.
|
|
216
|
+
*
|
|
217
|
+
* Rather than fit a cut-off to 11 data points, this reports the number and lets the
|
|
218
|
+
* architect judge. Returned worst-first, so the head of the list is where to look.
|
|
219
|
+
* Modules with fewer than two allocated FUNCs, or with no external connection at all,
|
|
220
|
+
* carry no signal and are omitted.
|
|
221
|
+
*
|
|
222
|
+
* Deliberately NOT a `RuleDefinition`: `computeReadiness` counts every violation into
|
|
223
|
+
* its dimension score regardless of severity, so a per-module advisory would depress
|
|
224
|
+
* the `alloc` score permanently. A measurement must not masquerade as a defect.
|
|
225
|
+
*
|
|
226
|
+
* Validation of this metric — and of MT-01/MT-02, which are thresholded the same way —
|
|
227
|
+
* is deferred (CR-SM-223).
|
|
228
|
+
*/
|
|
229
|
+
export function allocationCohesion(graph) {
|
|
230
|
+
const measurements = [];
|
|
162
231
|
const mods = graph.elements.filter(e => e.type === 'MOD');
|
|
163
|
-
const
|
|
232
|
+
const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
|
|
164
233
|
for (const mod of mods) {
|
|
165
|
-
const
|
|
166
|
-
const funcIds = new Set(allocTraces.map(t => t.source));
|
|
234
|
+
const funcIds = new Set(graph.traces.filter(t => t.type === 'allocate' && t.target === mod.id).map(t => t.source));
|
|
167
235
|
if (funcIds.size < 2)
|
|
168
236
|
continue;
|
|
169
|
-
let
|
|
170
|
-
let
|
|
171
|
-
for (const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
else if (srcIn || tgtIn) {
|
|
180
|
-
externalFlows++;
|
|
181
|
-
}
|
|
237
|
+
let internal = 0;
|
|
238
|
+
let external = 0;
|
|
239
|
+
for (const [a, b] of pairs) {
|
|
240
|
+
const aIn = funcIds.has(a);
|
|
241
|
+
const bIn = funcIds.has(b);
|
|
242
|
+
if (aIn && bIn)
|
|
243
|
+
internal++;
|
|
244
|
+
else if (aIn || bIn)
|
|
245
|
+
external++;
|
|
182
246
|
}
|
|
183
|
-
// No external
|
|
184
|
-
if (
|
|
185
|
-
continue;
|
|
186
|
-
const total = internalFlows + externalFlows;
|
|
187
|
-
if (total === 0)
|
|
247
|
+
// No external connections → nothing to compare against, no signal.
|
|
248
|
+
if (external === 0)
|
|
188
249
|
continue;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
197
|
-
}
|
|
250
|
+
measurements.push({
|
|
251
|
+
moduleId: mod.id,
|
|
252
|
+
moduleName: mod.name,
|
|
253
|
+
internal,
|
|
254
|
+
external,
|
|
255
|
+
cohesion: internal / (internal + external),
|
|
256
|
+
});
|
|
198
257
|
}
|
|
199
|
-
|
|
258
|
+
// Worst first; stable by id so the ranking is deterministic.
|
|
259
|
+
return measurements.sort((a, b) => a.cohesion - b.cohesion || (a.moduleId < b.moduleId ? -1 : a.moduleId > b.moduleId ? 1 : 0));
|
|
200
260
|
}
|
|
201
261
|
export const MT_RULES = [
|
|
202
262
|
{ id: 'MT-01', name: 'Module instability', severity: 'warning', evaluate: mt01Instability },
|
|
203
263
|
{ id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'info', evaluate: mt02Lcom4 },
|
|
204
|
-
|
|
264
|
+
// MT-03 retired as a rule (CR-SM-223) — see `allocationCohesion` above.
|
|
205
265
|
];
|
|
206
266
|
export function evaluateMTRules(graph) {
|
|
207
267
|
return MT_RULES.flatMap(r => r.evaluate(graph));
|
package/dist/se/readiness.js
CHANGED
|
@@ -9,7 +9,7 @@ export const ReadinessDimension = z.enum([
|
|
|
9
9
|
'arch', // Functional architecture (R-02, R-03, R-10, R-12)
|
|
10
10
|
'alloc', // Module allocation (R-04)
|
|
11
11
|
'ver', // Test coverage (R-01, R-05)
|
|
12
|
-
'schema', // Interface completeness (SC-
|
|
12
|
+
'schema', // Interface completeness (R-26 binding, SC-02 usage)
|
|
13
13
|
'cr', // CR traceability (CR-R01..R03)
|
|
14
14
|
'ms', // Milestone planning (MS-01..02)
|
|
15
15
|
]);
|
|
@@ -37,6 +37,8 @@ export const RULE_TO_DIMENSION = {
|
|
|
37
37
|
'BQ-01': 'req', 'BQ-02': 'req', 'BQ-04': 'req',
|
|
38
38
|
'BQ-06': 'req', 'BQ-07': 'req',
|
|
39
39
|
'RD-01': 'req', 'RD-02': 'req', 'RD-03': 'req',
|
|
40
|
+
// RD-04 is decomposition *breadth* — an architecture concern, not a requirement one
|
|
41
|
+
'RD-04': 'arch',
|
|
40
42
|
// trace/realization/allocation completeness rules (CR-228 D: previously unmapped → advisory fall-through)
|
|
41
43
|
'R-18': 'arch', 'R-19': 'ver', 'R-20': 'arch', 'R-21': 'ver',
|
|
42
44
|
'R-22': 'alloc', 'R-23': 'alloc', 'R-26': 'schema', 'R-27': 'arch',
|
|
@@ -52,13 +54,15 @@ export const RULE_TO_DIMENSION = {
|
|
|
52
54
|
// ver
|
|
53
55
|
'R-01': 'ver', 'R-05': 'ver',
|
|
54
56
|
// schema
|
|
55
|
-
'SC-
|
|
57
|
+
'SC-02': 'schema', // SC-01/SC-03 deleted (BOK-CR-026) — R-26 is the binding rule
|
|
56
58
|
// structural rules without primary dimension → assigned by closest concern
|
|
57
59
|
'R-08': 'arch',
|
|
58
60
|
// near-duplicate detection
|
|
59
61
|
'ND-01': 'arch', 'ND-02': 'schema',
|
|
60
62
|
// architecture metrics
|
|
61
|
-
|
|
63
|
+
// MT-03 is no longer here: it became a measurement (`allocationCohesion`), not a
|
|
64
|
+
// rule (CR-SM-223) — a per-module advisory would depress this score permanently.
|
|
65
|
+
'MT-01': 'alloc', 'MT-02': 'alloc',
|
|
62
66
|
// CR traceability
|
|
63
67
|
'CR-R01': 'cr', 'CR-R02': 'cr', 'CR-R03': 'cr', 'CR-R04': 'cr',
|
|
64
68
|
// architecture optimization
|
package/dist/se/rules.js
CHANGED
|
@@ -411,6 +411,58 @@ function noPrematureDecomposition(graph) {
|
|
|
411
411
|
}));
|
|
412
412
|
}
|
|
413
413
|
// ---------------------------------------------------------------------------
|
|
414
|
+
// RD-04: Decomposition breadth (CR-SM-221)
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
/**
|
|
417
|
+
* Max children on one decomposition level. The 7–11 convention existed only as
|
|
418
|
+
* prose ("darüber func-of-func"); below 7 is a guideline, not a violation, so only
|
|
419
|
+
* the upper bound is a rule.
|
|
420
|
+
*
|
|
421
|
+
* Counted per (parent, kind) — a MOD that both holds 12 FUNCs and composes 12
|
|
422
|
+
* sub-MODs has two breadth problems, not one. (The spike reference merged both into
|
|
423
|
+
* a single counter on the MOD id and would have reported 24 under one kind.)
|
|
424
|
+
*/
|
|
425
|
+
const DECOMPOSITION_BREADTH_MAX = 11;
|
|
426
|
+
function decompositionBreadth(graph) {
|
|
427
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
428
|
+
const byId = new Map(graph.elements.map(e => [e.id, e]));
|
|
429
|
+
const counts = new Map();
|
|
430
|
+
const bump = (parentId, kind) => {
|
|
431
|
+
const key = `${parentId}${kind}`;
|
|
432
|
+
const entry = counts.get(key);
|
|
433
|
+
if (entry)
|
|
434
|
+
entry.n++;
|
|
435
|
+
else
|
|
436
|
+
counts.set(key, { parentId, kind, n: 1 });
|
|
437
|
+
};
|
|
438
|
+
for (const t of graph.traces) {
|
|
439
|
+
const src = typeOf.get(t.source);
|
|
440
|
+
const tgt = typeOf.get(t.target);
|
|
441
|
+
if (t.type === 'compose' && src === 'FUNC' && tgt === 'FUNC') {
|
|
442
|
+
bump(t.source, 'sub-FUNC');
|
|
443
|
+
}
|
|
444
|
+
else if (t.type === 'allocate' && tgt === 'MOD' && src === 'FUNC') {
|
|
445
|
+
bump(t.target, 'allocated FUNC');
|
|
446
|
+
}
|
|
447
|
+
else if (t.type === 'compose' && (src === 'SYS' || src === 'MOD') && tgt === 'MOD') {
|
|
448
|
+
bump(t.source, 'sub-MOD');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return [...counts.values()]
|
|
452
|
+
.filter(c => c.n > DECOMPOSITION_BREADTH_MAX)
|
|
453
|
+
.map(c => {
|
|
454
|
+
const parent = byId.get(c.parentId);
|
|
455
|
+
return {
|
|
456
|
+
rule_id: 'RD-04',
|
|
457
|
+
severity: 'warning',
|
|
458
|
+
element_id: c.parentId,
|
|
459
|
+
message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${DECOMPOSITION_BREADTH_MAX})`,
|
|
460
|
+
fix_hint: 'Introduce an intermediate level (func-of-func / sub-MOD)',
|
|
461
|
+
context: { element_type: parent?.type, element_name: parent?.name },
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
414
466
|
// R-14: UC must have at least 1 compose trace (→ FCHAIN or REQ) (CR-117)
|
|
415
467
|
// ---------------------------------------------------------------------------
|
|
416
468
|
function ucMustHaveCompose(graph) {
|
|
@@ -870,6 +922,7 @@ export const V3_RULES = [
|
|
|
870
922
|
{ id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement },
|
|
871
923
|
{ id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency },
|
|
872
924
|
{ id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition },
|
|
925
|
+
{ id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth },
|
|
873
926
|
{ id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope },
|
|
874
927
|
{ id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency },
|
|
875
928
|
];
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SC-
|
|
3
|
-
*
|
|
2
|
+
* SC-02 — Schema Quality rule (CR-122, reduced by BOK-CR-026).
|
|
3
|
+
*
|
|
4
|
+
* SC-01 (`zodDefinition` present) and SC-03 (`sourceFile`/`sourceExport` present) were
|
|
5
|
+
* DELETED: both re-asked R-26's question ("is this SCHEMA bound to code?") on legacy
|
|
6
|
+
* attributes and fired on exactly the same elements (Jaccard 1.0 vs R-26). SC-03 had
|
|
7
|
+
* additionally become wrong — a SCHEMA correctly bound via `realRef` still fired it.
|
|
8
|
+
* `realRef` is the single SCHEMA binding truth (R-26 presence, RC-03/RC-04 resolution);
|
|
9
|
+
* a spec-only SCHEMA is `concept:true`, it does NOT carry a Zod body copy in the graph
|
|
10
|
+
* (same rule as TEST: bindings point at code, they never mirror it).
|
|
11
|
+
*
|
|
12
|
+
* What remains here is the one SCHEMA check that is NOT about binding: is the interface
|
|
13
|
+
* used by any FLOW.
|
|
4
14
|
*/
|
|
5
15
|
import type { OntologyGraph } from './ontology.js';
|
|
6
16
|
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
7
|
-
export declare function sc01HasDefinition(graph: OntologyGraph): RuleViolation[];
|
|
8
17
|
export declare function sc02IsReferenced(graph: OntologyGraph): RuleViolation[];
|
|
9
|
-
export declare function sc03IsImplemented(graph: OntologyGraph): RuleViolation[];
|
|
10
18
|
export declare const SC_RULES: RuleDefinition[];
|
|
11
19
|
export declare function evaluateSCRules(graph: OntologyGraph): RuleViolation[];
|
|
@@ -1,26 +1,4 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// SC-01: SCHEMA must have zodDefinition attribute
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
export function sc01HasDefinition(graph) {
|
|
5
|
-
return graph.elements
|
|
6
|
-
.filter(e => e.type === 'SCHEMA')
|
|
7
|
-
.filter(s => {
|
|
8
|
-
const def = s.attributes?.zodDefinition;
|
|
9
|
-
return !def || (typeof def === 'string' && def.trim().length === 0);
|
|
10
|
-
})
|
|
11
|
-
.map(s => ({
|
|
12
|
-
rule_id: 'SC-01',
|
|
13
|
-
severity: 'warning',
|
|
14
|
-
element_id: s.id,
|
|
15
|
-
message: `${s.id} has no zodDefinition attribute`,
|
|
16
|
-
fix_hint: 'Add zodDefinition attribute with Zod schema string',
|
|
17
|
-
context: {
|
|
18
|
-
element_type: s.type,
|
|
19
|
-
element_name: s.name,
|
|
20
|
-
},
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
2
|
// SC-02: SCHEMA must be referenced by at least one FLOW via relation trace
|
|
25
3
|
// ---------------------------------------------------------------------------
|
|
26
4
|
export function sc02IsReferenced(graph) {
|
|
@@ -42,31 +20,10 @@ export function sc02IsReferenced(graph) {
|
|
|
42
20
|
}));
|
|
43
21
|
}
|
|
44
22
|
// ---------------------------------------------------------------------------
|
|
45
|
-
// SC-03: SCHEMA should have sourceFile (implementation marker)
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
export function sc03IsImplemented(graph) {
|
|
48
|
-
return graph.elements
|
|
49
|
-
.filter(e => e.type === 'SCHEMA')
|
|
50
|
-
.filter(s => !s.attributes?.sourceFile && !s.attributes?.sourceExport)
|
|
51
|
-
.map(s => ({
|
|
52
|
-
rule_id: 'SC-03',
|
|
53
|
-
severity: 'info',
|
|
54
|
-
element_id: s.id,
|
|
55
|
-
message: `${s.id} has no sourceFile/sourceExport (not implemented yet)`,
|
|
56
|
-
fix_hint: 'Implement the Zod schema in code and set sourceFile/sourceExport',
|
|
57
|
-
context: {
|
|
58
|
-
element_type: s.type,
|
|
59
|
-
element_name: s.name,
|
|
60
|
-
},
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
23
|
// Aggregated array & convenience runner
|
|
65
24
|
// ---------------------------------------------------------------------------
|
|
66
25
|
export const SC_RULES = [
|
|
67
|
-
{ id: 'SC-01', name: 'Schema has definition', severity: 'warning', evaluate: sc01HasDefinition },
|
|
68
26
|
{ id: 'SC-02', name: 'Schema referenced by FLOW', severity: 'warning', evaluate: sc02IsReferenced },
|
|
69
|
-
{ id: 'SC-03', name: 'Schema is implemented', severity: 'info', evaluate: sc03IsImplemented },
|
|
70
27
|
];
|
|
71
28
|
export function evaluateSCRules(graph) {
|
|
72
29
|
return SC_RULES.flatMap(rule => rule.evaluate(graph));
|
package/package.json
CHANGED
package/dist/se/semantic-id.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SemanticId — Format-E canonical element ID schema.
|
|
3
|
-
* Aligned with graphengine CR-052/CR-053.
|
|
4
|
-
*
|
|
5
|
-
* Format: {Name}.{TypeAbbr}.{Counter}
|
|
6
|
-
* Example: ObserveSessions.UC.001
|
|
7
|
-
*
|
|
8
|
-
* @sigloch/contracts/se
|
|
9
|
-
*/
|
|
10
|
-
import { z } from 'zod/v4';
|
|
11
|
-
import type { ElementType } from './ontology.js';
|
|
12
|
-
/** 2-char abbreviation → full ElementType. */
|
|
13
|
-
export declare const ABBREV_TO_TYPE: Record<string, ElementType>;
|
|
14
|
-
/** Full ElementType → 2-char abbreviation. */
|
|
15
|
-
export declare const TYPE_TO_ABBREV: Record<string, string>;
|
|
16
|
-
/** SemanticId: `Name.TypeAbbr.Counter` where Counter is 3-digit or 6-char alphanumeric. */
|
|
17
|
-
export declare const SemanticId: z.ZodString;
|
|
18
|
-
export type SemanticId = z.infer<typeof SemanticId>;
|
|
19
|
-
/** Check if a string is a valid SemanticId. */
|
|
20
|
-
export declare function isSemanticId(s: string): boolean;
|
|
21
|
-
/** Extract name, type, and counter from a SemanticId. */
|
|
22
|
-
export declare function extractFromSemanticId(semanticId: string): {
|
|
23
|
-
name: string;
|
|
24
|
-
type: ElementType;
|
|
25
|
-
counter: string;
|
|
26
|
-
};
|
|
27
|
-
/** Build a SemanticId from parts. */
|
|
28
|
-
export declare function toSemanticId(name: string, type: ElementType, counter: string): string;
|
|
29
|
-
/** Get the 2-char abbreviation for an ElementType. */
|
|
30
|
-
export declare function getTypeAbbreviation(type: ElementType): string;
|
package/dist/se/semantic-id.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SemanticId — Format-E canonical element ID schema.
|
|
3
|
-
* Aligned with graphengine CR-052/CR-053.
|
|
4
|
-
*
|
|
5
|
-
* Format: {Name}.{TypeAbbr}.{Counter}
|
|
6
|
-
* Example: ObserveSessions.UC.001
|
|
7
|
-
*
|
|
8
|
-
* @sigloch/contracts/se
|
|
9
|
-
*/
|
|
10
|
-
import { z } from 'zod/v4';
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Type abbreviation maps (aligned with graphengine semantic-id.ts)
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
/** 2-char abbreviation → full ElementType. */
|
|
15
|
-
export const ABBREV_TO_TYPE = {
|
|
16
|
-
SY: 'SYS',
|
|
17
|
-
UC: 'UC',
|
|
18
|
-
RQ: 'REQ',
|
|
19
|
-
FN: 'FUNC',
|
|
20
|
-
FC: 'FCHAIN',
|
|
21
|
-
FL: 'FLOW',
|
|
22
|
-
AC: 'ACTOR',
|
|
23
|
-
MD: 'MOD',
|
|
24
|
-
TC: 'TEST',
|
|
25
|
-
TS: 'TEST', // legacy alias
|
|
26
|
-
SC: 'SCHEMA',
|
|
27
|
-
CR: 'CR',
|
|
28
|
-
MS: 'MS',
|
|
29
|
-
};
|
|
30
|
-
/** Full ElementType → 2-char abbreviation. */
|
|
31
|
-
export const TYPE_TO_ABBREV = {
|
|
32
|
-
SYS: 'SY',
|
|
33
|
-
UC: 'UC',
|
|
34
|
-
REQ: 'RQ',
|
|
35
|
-
FUNC: 'FN',
|
|
36
|
-
FCHAIN: 'FC',
|
|
37
|
-
FLOW: 'FL',
|
|
38
|
-
ACTOR: 'AC',
|
|
39
|
-
MOD: 'MD',
|
|
40
|
-
TEST: 'TC',
|
|
41
|
-
SCHEMA: 'SC',
|
|
42
|
-
SESSION: 'SE',
|
|
43
|
-
CR: 'CR',
|
|
44
|
-
MS: 'MS',
|
|
45
|
-
};
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Zod schema
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
/** SemanticId: `Name.TypeAbbr.Counter` where Counter is 3-digit or 6-char alphanumeric. */
|
|
50
|
-
export const SemanticId = z.string().regex(/^[A-Za-z][A-Za-z0-9_+]{0,49}\.[A-Z]{2}\.[a-z0-9]{3,6}$/, 'SemanticId must match {Name}.{TypeAbbr}.{Counter}');
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Pure functions
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
/** Check if a string is a valid SemanticId. */
|
|
55
|
-
export function isSemanticId(s) {
|
|
56
|
-
return SemanticId.safeParse(s).success;
|
|
57
|
-
}
|
|
58
|
-
/** Extract name, type, and counter from a SemanticId. */
|
|
59
|
-
export function extractFromSemanticId(semanticId) {
|
|
60
|
-
const parts = semanticId.split('.');
|
|
61
|
-
if (parts.length < 3) {
|
|
62
|
-
throw new Error(`Invalid semanticId: "${semanticId}" — expected Name.TypeAbbr.Counter`);
|
|
63
|
-
}
|
|
64
|
-
const name = parts[0];
|
|
65
|
-
const typeAbbr = parts[1];
|
|
66
|
-
const type = ABBREV_TO_TYPE[typeAbbr];
|
|
67
|
-
if (!type) {
|
|
68
|
-
throw new Error(`Unknown type abbreviation "${typeAbbr}" in: "${semanticId}"`);
|
|
69
|
-
}
|
|
70
|
-
return { name, type, counter: parts[2] };
|
|
71
|
-
}
|
|
72
|
-
/** Build a SemanticId from parts. */
|
|
73
|
-
export function toSemanticId(name, type, counter) {
|
|
74
|
-
const abbr = TYPE_TO_ABBREV[type];
|
|
75
|
-
if (!abbr)
|
|
76
|
-
throw new Error(`No abbreviation for type: ${type}`);
|
|
77
|
-
const sanitized = sanitizeName(name);
|
|
78
|
-
return `${sanitized}.${abbr}.${counter}`;
|
|
79
|
-
}
|
|
80
|
-
/** Get the 2-char abbreviation for an ElementType. */
|
|
81
|
-
export function getTypeAbbreviation(type) {
|
|
82
|
-
return TYPE_TO_ABBREV[type] ?? type.slice(0, 2).toUpperCase();
|
|
83
|
-
}
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Helpers
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
/** Sanitize a name for SemanticId: PascalCase, only alnum + underscore + plus, max 50 chars. */
|
|
88
|
-
function sanitizeName(name) {
|
|
89
|
-
return name.replace(/[^a-zA-Z0-9_+]/g, '').slice(0, 50);
|
|
90
|
-
}
|