@sigloch/contracts 10.4.0 → 10.6.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/conformance-rules.d.ts +4 -0
- package/dist/se/conformance-rules.js +63 -0
- package/dist/se/cr-quality-rules.d.ts +5 -0
- package/dist/se/cr-quality-rules.js +5 -2
- package/dist/se/format-e-parser.d.ts +9 -78
- package/dist/se/format-e-parser.js +34 -299
- package/dist/se/function-criticality.d.ts +16 -0
- package/dist/se/function-criticality.js +27 -6
- package/dist/se/grammar-snapshot.d.ts +5 -5
- package/dist/se/grammar-snapshot.js +13 -3
- package/dist/se/index.d.ts +1 -1
- package/dist/se/index.js +1 -1
- package/dist/se/metric-rules.d.ts +47 -4
- package/dist/se/metric-rules.js +140 -85
- package/dist/se/module-crossings.d.ts +17 -0
- package/dist/se/module-crossings.js +58 -4
- package/dist/se/readiness.d.ts +62 -0
- package/dist/se/readiness.js +64 -1
- package/dist/se/rule-help.js +9 -1
- package/dist/se/rules.js +8 -11
- package/package.json +1 -1
|
@@ -50,6 +50,10 @@ export declare const CodeFactsSchema: z.ZodObject<{
|
|
|
50
50
|
to: z.ZodString;
|
|
51
51
|
}, z.core.$strip>>>;
|
|
52
52
|
declaredDependencies: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
53
|
+
crFiles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
54
|
+
open: "open";
|
|
55
|
+
done: "done";
|
|
56
|
+
}>>>;
|
|
53
57
|
}, z.core.$strip>;
|
|
54
58
|
export type CodeFacts = z.infer<typeof CodeFactsSchema>;
|
|
55
59
|
/** A conformance rule: pure over (graph, facts) — never touches I/O itself. */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { z } from 'zod/v4';
|
|
18
18
|
import { RealRefSchema, TestRefsSchema } from './ontology.js';
|
|
19
|
+
import { CLOSED_STATUS } from './cr-quality-rules.js';
|
|
19
20
|
/** Parser facts about one source file (extracted by the executor). */
|
|
20
21
|
export const FileFactsSchema = z.object({
|
|
21
22
|
/** File exists on disk (repo-relative path). */
|
|
@@ -59,6 +60,13 @@ export const CodeFactsSchema = z.object({
|
|
|
59
60
|
* every external binding in the graph at once, which is noise, not a finding.
|
|
60
61
|
*/
|
|
61
62
|
declaredDependencies: z.array(z.string()).optional(),
|
|
63
|
+
/**
|
|
64
|
+
* CR id → the directory its file lives in (`docs/cr/open/` or `docs/cr/done/`), for RC-07
|
|
65
|
+
* (CR-SM-329). The directory is where a CR's state is decided; the node only mirrors it.
|
|
66
|
+
* Optional, ABSENT = silence, same reasoning as `declaredDependencies`: a repo without
|
|
67
|
+
* `docs/cr/` was never looked at, it does not "have no CRs".
|
|
68
|
+
*/
|
|
69
|
+
crFiles: z.record(z.string(), z.enum(['open', 'done'])).optional(),
|
|
62
70
|
});
|
|
63
71
|
const missingFile = (facts, file) => facts.files[file]?.exists !== true;
|
|
64
72
|
// RC-01: every valid FUNC realRef must resolve — file on disk, symbol declared in
|
|
@@ -451,6 +459,60 @@ function externalRefMustNameDependency(graph, facts) {
|
|
|
451
459
|
}
|
|
452
460
|
return violations;
|
|
453
461
|
}
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// RC-07: a CR node agrees with its file in docs/cr (CR-SM-329).
|
|
464
|
+
//
|
|
465
|
+
// The CR text lives in `docs/cr/{open,done}/`, the graph carries a lean CR node for scope and
|
|
466
|
+
// planning. The DIRECTORY decides a CR's state — moving the file is the close; the node's
|
|
467
|
+
// `status` mirrors it, because CR-R01/02/03 and the milestone readiness read it. Two findings:
|
|
468
|
+
// 1. a node whose closed-ness (CLOSED_STATUS, the same set CR-R01 uses) contradicts the
|
|
469
|
+
// directory;
|
|
470
|
+
// 2. an OPEN file with no node — an open CR the graph cannot see. The missing node has no
|
|
471
|
+
// element to carry the finding, so it anchors at the first SYS (sorted, Gate 5); without
|
|
472
|
+
// a SYS the branch is silent — R-17 already reports that graph.
|
|
473
|
+
// A node WITHOUT a file is not reported: that is history (archived CRs, pre-docs/cr numbering).
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
function crNodeMatchesFile(graph, facts) {
|
|
476
|
+
const crFiles = facts.crFiles;
|
|
477
|
+
if (crFiles === undefined)
|
|
478
|
+
return []; // extractor never looked — silence, s. CodeFactsSchema
|
|
479
|
+
const crs = graph.elements.filter((e) => e.type === 'CR').sort((a, b) => a.id.localeCompare(b.id));
|
|
480
|
+
const violations = [];
|
|
481
|
+
for (const cr of crs) {
|
|
482
|
+
const dir = crFiles[cr.id];
|
|
483
|
+
if (dir === undefined)
|
|
484
|
+
continue;
|
|
485
|
+
const status = String(cr.attributes?.status ?? '');
|
|
486
|
+
if (CLOSED_STATUS.has(status) === (dir === 'done'))
|
|
487
|
+
continue;
|
|
488
|
+
violations.push({
|
|
489
|
+
rule_id: 'RC-07',
|
|
490
|
+
severity: 'warning',
|
|
491
|
+
element_id: cr.id,
|
|
492
|
+
message: `${cr.id} status '${status || 'none'}' contradicts docs/cr/${dir}/`,
|
|
493
|
+
fix_hint: dir === 'done'
|
|
494
|
+
? 'The file is in done/ — set status done through graph_mutate'
|
|
495
|
+
: 'The file is in open/ — set status open through graph_mutate, or close the CR by moving its file to done/',
|
|
496
|
+
context: { element_type: cr.type, element_name: cr.name },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const sys = graph.elements.filter((e) => e.type === 'SYS').sort((a, b) => a.id.localeCompare(b.id))[0];
|
|
500
|
+
if (sys === undefined)
|
|
501
|
+
return violations;
|
|
502
|
+
const nodeIds = new Set(crs.map((c) => c.id));
|
|
503
|
+
const orphans = Object.keys(crFiles).filter((id) => crFiles[id] === 'open' && !nodeIds.has(id)).sort();
|
|
504
|
+
for (const id of orphans) {
|
|
505
|
+
violations.push({
|
|
506
|
+
rule_id: 'RC-07',
|
|
507
|
+
severity: 'warning',
|
|
508
|
+
element_id: sys.id,
|
|
509
|
+
message: `open CR ${id} (docs/cr/open/) has no CR node`,
|
|
510
|
+
fix_hint: `Add a lean CR node ${id} (id, title, no text) with relation edges to its scope through graph_mutate`,
|
|
511
|
+
context: { element_type: sys.type, element_name: sys.name },
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return violations;
|
|
515
|
+
}
|
|
454
516
|
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
455
517
|
export const CODE_CONFORMANCE_RULES = [
|
|
456
518
|
{ id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'error', domain: ['FUNC'], evaluate: codeRefMustResolve },
|
|
@@ -459,6 +521,7 @@ export const CODE_CONFORMANCE_RULES = [
|
|
|
459
521
|
{ id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeUsed },
|
|
460
522
|
{ id: 'RC-05', name: 'cross-module import drift', severity: 'warning', domain: ['MOD'], evaluate: importDriftConformance },
|
|
461
523
|
{ id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', domain: ['FUNC', 'MOD', 'SCHEMA'], evaluate: externalRefMustNameDependency },
|
|
524
|
+
{ id: 'RC-07', name: 'CR node agrees with docs/cr', severity: 'warning', domain: ['CR', 'SYS'], evaluate: crNodeMatchesFile },
|
|
462
525
|
];
|
|
463
526
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
464
527
|
export function evaluateConformanceRules(graph, facts) {
|
|
@@ -5,5 +5,10 @@
|
|
|
5
5
|
import type { OntologyGraph } from './ontology.js';
|
|
6
6
|
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
7
7
|
import type { MetricPolicy } from './policy.js';
|
|
8
|
+
/**
|
|
9
|
+
* Abgeschlossen heisst: nicht mehr steuerbar. Alles andere ist Grundgesamtheit.
|
|
10
|
+
* Exportiert fuer RC-07 (CR-SM-329), das dieselbe Frage gegen `docs/cr/done/` stellt.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CLOSED_STATUS: ReadonlySet<string>;
|
|
8
13
|
export declare const CR_RULES: RuleDefinition[];
|
|
9
14
|
export declare function evaluateCRRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
|
@@ -33,8 +33,11 @@
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
/** Elementtypen, die einen Aenderungsumfang darstellen — MS gehoert bewusst nicht dazu. */
|
|
35
35
|
const SCOPE_TYPES = new Set(['FUNC', 'MOD', 'SCHEMA', 'REQ', 'UC']);
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Abgeschlossen heisst: nicht mehr steuerbar. Alles andere ist Grundgesamtheit.
|
|
38
|
+
* Exportiert fuer RC-07 (CR-SM-329), das dieselbe Frage gegen `docs/cr/done/` stellt.
|
|
39
|
+
*/
|
|
40
|
+
export const CLOSED_STATUS = new Set(['done', 'dropped', 'rejected']);
|
|
38
41
|
function crMustTrack(graph) {
|
|
39
42
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
40
43
|
const crs = graph.elements.filter(e => {
|
|
@@ -1,43 +1,15 @@
|
|
|
1
|
+
import type { AttributeSpec } from './ontology.js';
|
|
2
|
+
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
3
|
+
export declare function extractFormatE(llmOutput: string): string | null;
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
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).
|
|
5
|
+
* CR-SM-251: der deklarierte Typ des Attributs entscheidet, nicht die Schreibweise des Wertes.
|
|
9
6
|
*
|
|
10
|
-
*
|
|
7
|
+
* `ELEMENT_ATTRIBUTES` sagt fuer 14 Attribute (9 boolean, 5 number), welchen Typ sie tragen.
|
|
8
|
+
* Wer stattdessen raet (`raw === 'true'`, `/^\d+$/`), verschiebt den Defekt nur: ein
|
|
9
|
+
* Freitext-Attribut mit dem Wert "true" oder "8" kippt dann still den Typ. Unbekannte Keys —
|
|
10
|
+
* jeder Graph darf eigene tragen — fallen sauber auf String zurueck.
|
|
11
11
|
*/
|
|
12
|
-
|
|
13
|
-
import type { OntologyGraph, AttributeSpec, ReqKind } from './ontology.js';
|
|
14
|
-
export interface FormatEOperation {
|
|
15
|
-
type: 'add_node' | 'remove_node' | 'update_node' | 'add_edge' | 'remove_edge' | 'strict_add_node' | 'strict_add_edge';
|
|
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;
|
|
23
|
-
description?: string;
|
|
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- and array-valued bindings like `realRef`/`testRefs` must reach
|
|
28
|
-
* `attributes` as objects or R-26/R-19 reject them as invalid.
|
|
29
|
-
*/
|
|
30
|
-
attributes?: Record<string, unknown>;
|
|
31
|
-
sourceId?: string;
|
|
32
|
-
targetId?: string;
|
|
33
|
-
traceType?: TraceType;
|
|
34
|
-
}
|
|
35
|
-
export interface FormatEDiff {
|
|
36
|
-
operations: FormatEOperation[];
|
|
37
|
-
errors: string[];
|
|
38
|
-
}
|
|
39
|
-
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
40
|
-
export declare function extractFormatE(llmOutput: string): string | null;
|
|
12
|
+
export declare function attributeTypeOf(elementType: string | undefined, key: string): AttributeSpec['type'] | undefined;
|
|
41
13
|
/**
|
|
42
14
|
* BOK-CR-026: hydrate a JSON object/array attribute value. The ontology's bindings
|
|
43
15
|
* (`realRef {file,symbol?,lang?}`, `testRefs [{file,tool,…}]`) are objects/arrays; kept as raw
|
|
@@ -50,45 +22,4 @@ export declare function extractFormatE(llmOutput: string): string | null;
|
|
|
50
22
|
* package over. Two hydration rules would drift; there is one, and it lives here with the
|
|
51
23
|
* schemas it feeds.
|
|
52
24
|
*/
|
|
53
|
-
/**
|
|
54
|
-
* CR-SM-251: der deklarierte Typ des Attributs entscheidet, nicht die Schreibweise des Wertes.
|
|
55
|
-
*
|
|
56
|
-
* `ELEMENT_ATTRIBUTES` sagt fuer 14 Attribute (9 boolean, 5 number), welchen Typ sie tragen.
|
|
57
|
-
* Wer stattdessen raet (`raw === 'true'`, `/^\d+$/`), verschiebt den Defekt nur: ein
|
|
58
|
-
* Freitext-Attribut mit dem Wert "true" oder "8" kippt dann still den Typ. Unbekannte Keys —
|
|
59
|
-
* jeder Graph darf eigene tragen — fallen sauber auf String zurueck.
|
|
60
|
-
*/
|
|
61
|
-
export declare function attributeTypeOf(elementType: string | undefined, key: string): AttributeSpec['type'] | undefined;
|
|
62
25
|
export declare function hydrateAttrValue(raw: string, declaredType?: AttributeSpec['type']): unknown;
|
|
63
|
-
export interface ParseFormatEOptions {
|
|
64
|
-
/**
|
|
65
|
-
* CR-SM-216: resolve the type of a uid that this text does not declare. A mutation
|
|
66
|
-
* diff adding edges between existing nodes carries no `## Nodes` block, so the
|
|
67
|
-
* caller binds this to its store. Without it such a diff is an error, never a
|
|
68
|
-
* silent skip.
|
|
69
|
-
*/
|
|
70
|
-
resolveType?: (uid: string) => ElementType | undefined;
|
|
71
|
-
/**
|
|
72
|
-
* CR-SM-266 B: die Kinds eines uid, den dieser Text nicht deklariert. Seit die
|
|
73
|
-
* satisfy-Patterns ein `where` tragen, entscheidet nicht mehr das Typ-Paar allein — ein
|
|
74
|
-
* `MOD -satisfy-> REQ` ist nur gueltig, wenn das REQ strukturelle Kinds traegt.
|
|
75
|
-
*
|
|
76
|
-
* Ohne diesen Resolver sieht der Parser die Kinds eines BESTEHENDEN Knotens nicht und lehnt
|
|
77
|
-
* solche Kanten ab. Das ist Absicht: fehlschlagen und es sagen, nicht raten und durchlassen
|
|
78
|
-
* — dieselbe Entscheidung wie bei `resolveType` ("Without it such a diff is an error, never
|
|
79
|
-
* a silent skip"). Knoten, die DIESER Text anlegt, brauchen ihn nicht; ihre Kinds stehen als
|
|
80
|
-
* `@kinds` in denselben Zeilen.
|
|
81
|
-
*/
|
|
82
|
-
resolveKinds?: (uid: string) => readonly ReqKind[] | undefined;
|
|
83
|
-
}
|
|
84
|
-
/** Parse a Format E text block into validated operations. */
|
|
85
|
-
export declare function parseFormatE(input: string, options?: ParseFormatEOptions): FormatEDiff;
|
|
86
|
-
/**
|
|
87
|
-
* Serialize an OntologyGraph to compact Format E text.
|
|
88
|
-
*
|
|
89
|
-
* CR-SM-216: nodes are written under `### <TYPE>` sections — the type is declared once
|
|
90
|
-
* per section instead of once per id. Measured on the graphcode SSOT graph (369
|
|
91
|
-
* elements), 12 section headers cost ~48 tokens where a per-node type attribute would
|
|
92
|
-
* have cost ~1476.
|
|
93
|
-
*/
|
|
94
|
-
export declare function serializeToFormatE(graph: OntologyGraph): string;
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Format E
|
|
3
|
-
* SE-relevant subset: nodes (+/-/~) and edges (+/-), no chat-canvas or views.
|
|
2
|
+
* Format E — der GETEILTE Teil: Attribut-Hydration und das Herausloesen des Blocks.
|
|
4
3
|
*
|
|
5
|
-
* CR-SM-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* CR-SM-331: Parser UND Serializer standen bis hierher ebenfalls in dieser Datei — ein
|
|
5
|
+
* zweites Paar neben `FormatECodec` in @sigloch/graph-api-core. Zwei Implementierungen
|
|
6
|
+
* desselben Dialekts urteilten ueber denselben Text verschieden: eine Beschreibung mit
|
|
7
|
+
* Zeilenumbruch lehnte DIESER Parser ab ("Invalid node line", `NODE_RE` verlangte eine uid
|
|
8
|
+
* ohne Leerzeichen), waehrend der lebende Codec die uebergelaufene Zeile als KNOTENZEILE
|
|
9
|
+
* annahm und daraus einen Phantom-Knoten baute (ITEM-2026-183, am Gate reproduziert). Der
|
|
10
|
+
* strengere war der, den keine Produktionsstelle der Familie je aufrief.
|
|
11
|
+
*
|
|
12
|
+
* Ein Dialekt, eine Implementierung — und sie lebt in graph-api-core, nicht hier, weil sie
|
|
13
|
+
* DESKRIPTOR-PARAMETRISIERT sein muss: `loadFixture` (test-fixtures.ts) parst Format E mit
|
|
14
|
+
* einer FREMDEN Ontologie und ist publizierte API. Ein SE-fester Parser in contracts koennte
|
|
15
|
+
* das nicht mehr bedienen.
|
|
16
|
+
*
|
|
17
|
+
* Was hier bleibt, bleibt mit Grund: `attributeTypeOf` und `hydrateAttrValue` lesen
|
|
18
|
+
* `ELEMENT_ATTRIBUTES` und fuettern `RealRefSchema`/`TestRefSchema` — sie gehoeren zu den
|
|
19
|
+
* SCHEMATA, nicht zum Dialekt, und der Codec importiert sie von hier (CR-GC-334). Zwei
|
|
20
|
+
* Hydrationsregeln wuerden driften; es gibt genau eine.
|
|
21
|
+
*
|
|
22
|
+
* Der Dateiname bleibt absichtlich stehen — ein Rename traegt Kosten bei jedem Importeur.
|
|
9
23
|
*
|
|
10
24
|
* @sigloch/contracts/se
|
|
11
25
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { isValidTrace } from './meta-model.js';
|
|
26
|
+
import { ELEMENT_ATTRIBUTES } from './ontology.js';
|
|
14
27
|
// ---------------------------------------------------------------------------
|
|
15
28
|
// Extraction
|
|
16
29
|
// ---------------------------------------------------------------------------
|
|
@@ -21,40 +34,8 @@ export function extractFormatE(llmOutput) {
|
|
|
21
34
|
return m ? m[1].trim() : null;
|
|
22
35
|
}
|
|
23
36
|
// ---------------------------------------------------------------------------
|
|
24
|
-
//
|
|
37
|
+
// Attribut-Hydration — geteilt mit FormatECodec (CR-GC-334)
|
|
25
38
|
// ---------------------------------------------------------------------------
|
|
26
|
-
// CR-SM-266 D5: ABGELEITET statt abgeschrieben. Die Liste stand hier als zweite Kopie des
|
|
27
|
-
// TraceType-Enums und trug `produces` noch, als es dort schon entfernt war — genau die Drift,
|
|
28
|
-
// die eine doppelte Wahrheit erzeugt. Eine Quelle, keine Pflege.
|
|
29
|
-
const VALID_TRACE_TYPES = new Set(TraceType.options);
|
|
30
|
-
const OP_PREFIX = {
|
|
31
|
-
'+': 'add',
|
|
32
|
-
'-': 'remove',
|
|
33
|
-
'~': 'update',
|
|
34
|
-
'!': 'strict_add',
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* CR-SM-215: the target group is `(.+)` — Format-E allows fan-out
|
|
38
|
-
* `A -x-> B, C, D`, one edge per target. `graph-api-core`'s codec has always parsed
|
|
39
|
-
* it; this parser rejected it as `Invalid edge syntax`, so the same text produced
|
|
40
|
-
* different operations depending on which parser saw it.
|
|
41
|
-
*/
|
|
42
|
-
const EDGE_RE = /^([+\-~!])?\s*(\S+)\s+-(\w+)->\s+(.+?)\s*$/;
|
|
43
|
-
const NODE_RE = /^([+\-~!])?\s*(\S+?)(?:\|(.*))?$/;
|
|
44
|
-
/** CR-147: @key value attribute line (indented, below a node entry). */
|
|
45
|
-
const ATTR_RE = /^\s*@(\w+)\s+(.+)$/;
|
|
46
|
-
/**
|
|
47
|
-
* BOK-CR-026: hydrate a JSON object/array attribute value. The ontology's bindings
|
|
48
|
-
* (`realRef {file,symbol?,lang?}`, `testRefs [{file,tool,…}]`) are objects/arrays; kept as raw
|
|
49
|
-
* strings they fail RealRefSchema/TestRefSchema and the element reads as unbound.
|
|
50
|
-
* Only `{…}`/`[…]` are attempted — every other value stays the string it is, and a
|
|
51
|
-
* malformed literal falls back to the string rather than failing the whole parse.
|
|
52
|
-
*
|
|
53
|
-
* CR-GC-334: exported, because `FormatECodec` (graph-api-core) parses the SAME `@key value`
|
|
54
|
-
* lines and did NOT hydrate — the identical defect this function was written for, one
|
|
55
|
-
* package over. Two hydration rules would drift; there is one, and it lives here with the
|
|
56
|
-
* schemas it feeds.
|
|
57
|
-
*/
|
|
58
39
|
/**
|
|
59
40
|
* CR-SM-251: der deklarierte Typ des Attributs entscheidet, nicht die Schreibweise des Wertes.
|
|
60
41
|
*
|
|
@@ -69,6 +50,18 @@ export function attributeTypeOf(elementType, key) {
|
|
|
69
50
|
const specs = ELEMENT_ATTRIBUTES[elementType];
|
|
70
51
|
return specs?.find(s => s.key === key)?.type;
|
|
71
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* BOK-CR-026: hydrate a JSON object/array attribute value. The ontology's bindings
|
|
55
|
+
* (`realRef {file,symbol?,lang?}`, `testRefs [{file,tool,…}]`) are objects/arrays; kept as raw
|
|
56
|
+
* strings they fail RealRefSchema/TestRefSchema and the element reads as unbound.
|
|
57
|
+
* Only `{…}`/`[…]` are attempted — every other value stays the string it is, and a
|
|
58
|
+
* malformed literal falls back to the string rather than failing the whole parse.
|
|
59
|
+
*
|
|
60
|
+
* CR-GC-334: exported, because `FormatECodec` (graph-api-core) parses the SAME `@key value`
|
|
61
|
+
* lines and did NOT hydrate — the identical defect this function was written for, one
|
|
62
|
+
* package over. Two hydration rules would drift; there is one, and it lives here with the
|
|
63
|
+
* schemas it feeds.
|
|
64
|
+
*/
|
|
72
65
|
export function hydrateAttrValue(raw, declaredType) {
|
|
73
66
|
// CR-SM-251: `concept:true` kam als String "true" an, und alle 11 Vergleiche in
|
|
74
67
|
// rules.ts/conformance-rules.ts pruefen identitaetsscharf (`=== true`). Damit war jeder
|
|
@@ -95,261 +88,3 @@ export function hydrateAttrValue(raw, declaredType) {
|
|
|
95
88
|
return raw;
|
|
96
89
|
}
|
|
97
90
|
}
|
|
98
|
-
/** CR-148: Trace-type normalization aliases (source→target→from→to). */
|
|
99
|
-
const TRACE_NORMALIZE = {
|
|
100
|
-
FLOW: { SCHEMA: 'relation' }, // FLOW→SCHEMA io → relation
|
|
101
|
-
};
|
|
102
|
-
/** `### <TYPE>` — the node type section (CR-SM-216). */
|
|
103
|
-
const TYPE_SECTION_RE = /^###\s+([A-Za-z_]+)\s*$/;
|
|
104
|
-
/** Parse a Format E text block into validated operations. */
|
|
105
|
-
export function parseFormatE(input, options = {}) {
|
|
106
|
-
const operations = [];
|
|
107
|
-
const errors = [];
|
|
108
|
-
let section = null;
|
|
109
|
-
let currentType = null;
|
|
110
|
-
/** uid → type, from this text's node sections. */
|
|
111
|
-
const declared = new Map();
|
|
112
|
-
const typeOf = (uid) => declared.get(uid) ?? options.resolveType?.(uid);
|
|
113
|
-
/**
|
|
114
|
-
* CR-SM-266 B: die Kinds eines uid — erst aus DIESEM Text, dann aus dem Store.
|
|
115
|
-
*
|
|
116
|
-
* `kinds` reist in Format E als `@kinds a,b` und wird erst vom Konsumenten aufs
|
|
117
|
-
* Top-Level-Feld gehoben (graph-api-core, CR-195d). Hier steht es also noch in
|
|
118
|
-
* `op.attributes.kinds`, und genau dort wird es gelesen — sonst saehe eine Mutation, die
|
|
119
|
-
* REQ und satisfy-Kante in EINEM Block anlegt, die eigenen Kinds nicht. Seit CR-SM-320
|
|
120
|
-
* liegt es dort bereits als Liste (s. Attribut-Zeile unten), also ohne eigene Normalisierung.
|
|
121
|
-
*/
|
|
122
|
-
const kindsOf = (uid) => {
|
|
123
|
-
for (const op of operations) {
|
|
124
|
-
if ('elementType' in op && op.semanticId === uid && Array.isArray(op.attributes?.kinds)) {
|
|
125
|
-
return op.attributes.kinds;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return options.resolveKinds?.(uid);
|
|
129
|
-
};
|
|
130
|
-
for (const rawLine of input.split('\n')) {
|
|
131
|
-
const line = rawLine.trim();
|
|
132
|
-
if (!line || line.startsWith('//') || line.startsWith('#!'))
|
|
133
|
-
continue;
|
|
134
|
-
// CR-147: @attribute lines attach to the last node operation
|
|
135
|
-
const attrMatch = ATTR_RE.exec(line);
|
|
136
|
-
if (attrMatch) {
|
|
137
|
-
const lastOp = operations.length > 0 ? operations[operations.length - 1] : null;
|
|
138
|
-
if (lastOp && (lastOp.type === 'add_node' || lastOp.type === 'update_node' || lastOp.type === 'strict_add_node')) {
|
|
139
|
-
if (!lastOp.attributes)
|
|
140
|
-
lastOp.attributes = {};
|
|
141
|
-
const key = attrMatch[1];
|
|
142
|
-
const value = hydrateAttrValue(attrMatch[2].trim(), attributeTypeOf(lastOp.elementType, key));
|
|
143
|
-
// CR-SM-320 (ITEM-2026-007): `kinds` ist keine freie Eigenschaft, sondern die REQ-Spalte,
|
|
144
|
-
// die hier nur durchreist. Ein Einzelwert (`@kinds non-functional`) blieb als roher
|
|
145
|
-
// String liegen und wurde so persistiert — jede Sicht, die auf Listen-Mitgliedschaft
|
|
146
|
-
// filtert, verlor die REQ still. Die Form wird am PRODUZENTEN hergestellt, nicht bei
|
|
147
|
-
// jedem Leser einzeln: hier ist `kinds` immer eine Liste.
|
|
148
|
-
lastOp.attributes[key] = key === 'kinds' ? [...normalizeReqKinds(value)] : value;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
errors.push(`@attribute line without preceding node: "${line}"`);
|
|
152
|
-
}
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
// Section headers
|
|
156
|
-
if (/^##\s*nodes?\s*$/i.test(line)) {
|
|
157
|
-
section = 'nodes';
|
|
158
|
-
currentType = null;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (/^##\s*edges?\s*$/i.test(line)) {
|
|
162
|
-
section = 'edges';
|
|
163
|
-
currentType = null;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
// CR-SM-216: `### <TYPE>` carries the element type for the nodes below it.
|
|
167
|
-
const typeSection = TYPE_SECTION_RE.exec(line);
|
|
168
|
-
if (typeSection && section === 'nodes') {
|
|
169
|
-
const parsed = ElementType.safeParse(typeSection[1]);
|
|
170
|
-
if (parsed.success) {
|
|
171
|
-
currentType = parsed.data;
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
currentType = null;
|
|
175
|
-
errors.push(`Unknown element type section: "### ${typeSection[1]}"`);
|
|
176
|
-
}
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
// Skip other markdown headers
|
|
180
|
-
if (line.startsWith('#'))
|
|
181
|
-
continue;
|
|
182
|
-
// Edge: detect by arrow in the structural part only — before the description
|
|
183
|
-
// pipe. Node descriptions may legitimately contain '->' ("FUNC->FUNC compose");
|
|
184
|
-
// edges carry no pipe, so splitting on '|' disambiguates (CR-GC-247). Since
|
|
185
|
-
// CR-SM-215 widened the target group to `(.+?)`, this guard is what keeps a
|
|
186
|
-
// multi-word node description from being read as an edge.
|
|
187
|
-
const isEdgeLine = line.split('|', 1)[0].includes('->');
|
|
188
|
-
if (isEdgeLine || section === 'edges') {
|
|
189
|
-
const edgeMatch = isEdgeLine ? EDGE_RE.exec(line) : null;
|
|
190
|
-
if (edgeMatch) {
|
|
191
|
-
parseEdge(edgeMatch, typeOf, kindsOf, operations, errors);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
errors.push(`Invalid edge line: "${line}"`);
|
|
195
|
-
}
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
// Node
|
|
199
|
-
if (section === 'nodes') {
|
|
200
|
-
const nodeMatch = NODE_RE.exec(line);
|
|
201
|
-
if (!nodeMatch) {
|
|
202
|
-
errors.push(`Invalid node line: "${line}"`);
|
|
203
|
-
}
|
|
204
|
-
else if (!currentType) {
|
|
205
|
-
// CR-SM-216: no type section, no type. Guessing one from the id is what
|
|
206
|
-
// CR-230 punished; an error is the point.
|
|
207
|
-
errors.push(`Node "${nodeMatch[2]}" is not under a "### <TYPE>" section`);
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
declared.set(nodeMatch[2], currentType);
|
|
211
|
-
parseNode(nodeMatch, currentType, operations, errors);
|
|
212
|
-
}
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
// Unknown line (edges are handled above, nodes need their section)
|
|
216
|
-
if (line.length > 0)
|
|
217
|
-
errors.push(`Unrecognized line: "${line}"`);
|
|
218
|
-
}
|
|
219
|
-
return { operations, errors };
|
|
220
|
-
}
|
|
221
|
-
/** CR-148: Normalize trace type using alias table. */
|
|
222
|
-
function normalizeTraceType(srcType, tgtType, traceType) {
|
|
223
|
-
const aliases = TRACE_NORMALIZE[srcType];
|
|
224
|
-
if (aliases && aliases[tgtType] && traceType !== aliases[tgtType]) {
|
|
225
|
-
return aliases[tgtType];
|
|
226
|
-
}
|
|
227
|
-
return traceType;
|
|
228
|
-
}
|
|
229
|
-
function parseNode(m, elementType, ops, errors) {
|
|
230
|
-
const opChar = m[1] || '+';
|
|
231
|
-
const id = m[2];
|
|
232
|
-
const descr = m[3]?.trim();
|
|
233
|
-
const action = OP_PREFIX[opChar] ?? 'add';
|
|
234
|
-
if (!id) {
|
|
235
|
-
errors.push(`Node line without an id: "${m[0]}"`);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (action === 'remove') {
|
|
239
|
-
ops.push({ type: 'remove_node', semanticId: id });
|
|
240
|
-
}
|
|
241
|
-
else if (action === 'update') {
|
|
242
|
-
ops.push({ type: 'update_node', semanticId: id, elementType, description: descr });
|
|
243
|
-
}
|
|
244
|
-
else if (action === 'strict_add') {
|
|
245
|
-
ops.push({ type: 'strict_add_node', semanticId: id, elementType, description: descr });
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
ops.push({ type: 'add_node', semanticId: id, elementType, description: descr });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function parseEdge(m, typeOf, kindsOf, ops, errors) {
|
|
252
|
-
const opChar = m[1] || '+';
|
|
253
|
-
const sourceId = m[2];
|
|
254
|
-
const traceType = m[3];
|
|
255
|
-
const action = OP_PREFIX[opChar] ?? 'add';
|
|
256
|
-
if (!VALID_TRACE_TYPES.has(traceType)) {
|
|
257
|
-
errors.push(`Invalid trace type: "${traceType}"`);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const srcType = typeOf(sourceId);
|
|
261
|
-
if (!srcType) {
|
|
262
|
-
errors.push(`Cannot resolve type of "${sourceId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
const edgeType = action === 'remove'
|
|
266
|
-
? 'remove_edge'
|
|
267
|
-
: action === 'strict_add'
|
|
268
|
-
? 'strict_add_edge'
|
|
269
|
-
: 'add_edge';
|
|
270
|
-
// CR-SM-215: 1:n fan-out — `A -x-> B, C` is n independent edges. Validation runs
|
|
271
|
-
// per target (like `graph-api-core`'s codec), so one bad target does not discard
|
|
272
|
-
// its siblings.
|
|
273
|
-
const targets = m[4].split(',').map(t => t.trim()).filter(Boolean);
|
|
274
|
-
for (const targetId of targets) {
|
|
275
|
-
const tgtType = typeOf(targetId);
|
|
276
|
-
if (!tgtType) {
|
|
277
|
-
errors.push(`Cannot resolve type of "${targetId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
// CR-148: Normalize before the meta-model check (e.g. FLOW→SCHEMA io → relation)
|
|
281
|
-
const resolvedTraceType = normalizeTraceType(srcType, tgtType, traceType);
|
|
282
|
-
if (!isValidTrace({
|
|
283
|
-
source: srcType, target: tgtType, type: resolvedTraceType,
|
|
284
|
-
sourceKinds: kindsOf(sourceId), targetKinds: kindsOf(targetId),
|
|
285
|
-
})) {
|
|
286
|
-
errors.push(`Meta-model violation: ${srcType} -${traceType}-> ${tgtType} is not valid`);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
ops.push({
|
|
290
|
-
type: edgeType,
|
|
291
|
-
semanticId: `${sourceId}->${targetId}`,
|
|
292
|
-
sourceId,
|
|
293
|
-
targetId,
|
|
294
|
-
traceType: resolvedTraceType,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
|
-
// Serializer
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
/**
|
|
302
|
-
* Serialize an OntologyGraph to compact Format E text.
|
|
303
|
-
*
|
|
304
|
-
* CR-SM-216: nodes are written under `### <TYPE>` sections — the type is declared once
|
|
305
|
-
* per section instead of once per id. Measured on the graphcode SSOT graph (369
|
|
306
|
-
* elements), 12 section headers cost ~48 tokens where a per-node type attribute would
|
|
307
|
-
* have cost ~1476.
|
|
308
|
-
*/
|
|
309
|
-
export function serializeToFormatE(graph) {
|
|
310
|
-
const lines = [];
|
|
311
|
-
// Nodes, grouped by type
|
|
312
|
-
// CR-SM-266 D5: kein SESSION-Skip mehr — der Elementtyp existiert nicht.
|
|
313
|
-
const modelingElements = graph.elements;
|
|
314
|
-
if (modelingElements.length > 0) {
|
|
315
|
-
lines.push('## Nodes');
|
|
316
|
-
const byType = new Map();
|
|
317
|
-
for (const el of modelingElements) {
|
|
318
|
-
const group = byType.get(el.type);
|
|
319
|
-
if (group)
|
|
320
|
-
group.push(el);
|
|
321
|
-
else
|
|
322
|
-
byType.set(el.type, [el]);
|
|
323
|
-
}
|
|
324
|
-
for (const type of [...byType.keys()].sort()) {
|
|
325
|
-
lines.push(`### ${type}`);
|
|
326
|
-
for (const el of byType.get(type) ?? []) {
|
|
327
|
-
const descr = el.description ? `|${el.description}` : '';
|
|
328
|
-
lines.push(`+ ${el.id}${descr}`);
|
|
329
|
-
// CR-147: Serialize known attributes
|
|
330
|
-
if (el.attributes) {
|
|
331
|
-
for (const [k, v] of Object.entries(el.attributes)) {
|
|
332
|
-
if (v == null)
|
|
333
|
-
continue;
|
|
334
|
-
// BOK-CR-026: objects/arrays as JSON — String({}) collapses a realRef/testRefs
|
|
335
|
-
// binding to "[object Object]" and loses it on the next parse.
|
|
336
|
-
const text = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
337
|
-
if (text.length > 0)
|
|
338
|
-
lines.push(` @${k} ${text}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// Edges
|
|
345
|
-
// CR-SM-266 D5: kein audit-Filter mehr — `Trace.category` existiert nicht.
|
|
346
|
-
const modelingTraces = graph.traces;
|
|
347
|
-
if (modelingTraces.length > 0) {
|
|
348
|
-
lines.push('');
|
|
349
|
-
lines.push('## Edges');
|
|
350
|
-
for (const t of modelingTraces) {
|
|
351
|
-
lines.push(`+ ${t.source} -${t.type}-> ${t.target}`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return lines.join('\n');
|
|
355
|
-
}
|
|
@@ -72,4 +72,20 @@ export interface FunctionCriticality {
|
|
|
72
72
|
* Cases, dann stabil nach `funcId`. Eine Zeile je FUNC — auch fuer die mit 0, sonst waere die
|
|
73
73
|
* Abwesenheit eines Wertes von der Abwesenheit der Funktion nicht zu unterscheiden.
|
|
74
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* CR-SM-335 (ITEM-2026-096, zugesagt in CR-SM-313) — EINE Definition von "welche Ketten
|
|
77
|
+
* enthalten diese FUNC", als MENGEN.
|
|
78
|
+
*
|
|
79
|
+
* R-21 hielt fuer "teilen diese beiden EINE Kette" einen eigenen, lokalen Index in `rules.ts`,
|
|
80
|
+
* waehrend die ZAHL fuer seine Infrastruktur-Ausnahme aus `functionCriticality` kam. Zwei
|
|
81
|
+
* Stellen lasen dieselbe Kantenmenge nach derselben Definition (R-30: direkte
|
|
82
|
+
* `FCHAIN -compose-> FUNC`, keine Vererbung). Es gab keinen Widerspruch — beide taten
|
|
83
|
+
* dasselbe. Die Gefahr ist die kuenftige Aenderung an nur einer der beiden, und das ist
|
|
84
|
+
* genau die Drift-Klasse, gegen die `module-crossings.ts` gebaut wurde (CR-SM-276).
|
|
85
|
+
*
|
|
86
|
+
* Eine Rechnung liefert jetzt beides: `chainsByFunc` die Mengen, `functionCriticality` die
|
|
87
|
+
* Zahlen daraus. Eine FUNC ohne Kette taucht NICHT auf — `get` liefert `undefined`, und der
|
|
88
|
+
* Aufrufer entscheidet, ob das 0 oder "nicht gefragt" heisst.
|
|
89
|
+
*/
|
|
90
|
+
export declare function chainsByFunc(graph: OntologyGraph): Map<string, Set<string>>;
|
|
75
91
|
export declare function functionCriticality(graph: OntologyGraph): FunctionCriticality[];
|
|
@@ -6,15 +6,27 @@ import { indexOf } from './graph-index.js';
|
|
|
6
6
|
* Cases, dann stabil nach `funcId`. Eine Zeile je FUNC — auch fuer die mit 0, sonst waere die
|
|
7
7
|
* Abwesenheit eines Wertes von der Abwesenheit der Funktion nicht zu unterscheiden.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* CR-SM-335 (ITEM-2026-096, zugesagt in CR-SM-313) — EINE Definition von "welche Ketten
|
|
11
|
+
* enthalten diese FUNC", als MENGEN.
|
|
12
|
+
*
|
|
13
|
+
* R-21 hielt fuer "teilen diese beiden EINE Kette" einen eigenen, lokalen Index in `rules.ts`,
|
|
14
|
+
* waehrend die ZAHL fuer seine Infrastruktur-Ausnahme aus `functionCriticality` kam. Zwei
|
|
15
|
+
* Stellen lasen dieselbe Kantenmenge nach derselben Definition (R-30: direkte
|
|
16
|
+
* `FCHAIN -compose-> FUNC`, keine Vererbung). Es gab keinen Widerspruch — beide taten
|
|
17
|
+
* dasselbe. Die Gefahr ist die kuenftige Aenderung an nur einer der beiden, und das ist
|
|
18
|
+
* genau die Drift-Klasse, gegen die `module-crossings.ts` gebaut wurde (CR-SM-276).
|
|
19
|
+
*
|
|
20
|
+
* Eine Rechnung liefert jetzt beides: `chainsByFunc` die Mengen, `functionCriticality` die
|
|
21
|
+
* Zahlen daraus. Eine FUNC ohne Kette taucht NICHT auf — `get` liefert `undefined`, und der
|
|
22
|
+
* Aufrufer entscheidet, ob das 0 oder "nicht gefragt" heisst.
|
|
23
|
+
*/
|
|
24
|
+
export function chainsByFunc(graph) {
|
|
10
25
|
const idx = indexOf(graph);
|
|
11
|
-
const funcs = idx.elementsOfType('FUNC');
|
|
12
26
|
const typeOf = new Map(graph.elements.map((e) => [e.id, e.type]));
|
|
13
|
-
const funcIds = new Set(
|
|
14
|
-
const compose = idx.tracesOfType('compose');
|
|
15
|
-
// FUNC -> seine Ketten. R-30s Fassung, woertlich.
|
|
27
|
+
const funcIds = new Set(idx.elementsOfType('FUNC').map((f) => f.id));
|
|
16
28
|
const chainsOf = new Map();
|
|
17
|
-
for (const t of compose) {
|
|
29
|
+
for (const t of idx.tracesOfType('compose')) {
|
|
18
30
|
if (typeOf.get(t.source) !== 'FCHAIN' || !funcIds.has(t.target))
|
|
19
31
|
continue;
|
|
20
32
|
let s = chainsOf.get(t.target);
|
|
@@ -24,6 +36,15 @@ export function functionCriticality(graph) {
|
|
|
24
36
|
}
|
|
25
37
|
s.add(t.source);
|
|
26
38
|
}
|
|
39
|
+
return chainsOf;
|
|
40
|
+
}
|
|
41
|
+
export function functionCriticality(graph) {
|
|
42
|
+
const idx = indexOf(graph);
|
|
43
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
44
|
+
const typeOf = new Map(graph.elements.map((e) => [e.id, e.type]));
|
|
45
|
+
const compose = idx.tracesOfType('compose');
|
|
46
|
+
// FUNC -> seine Ketten. R-30s Fassung, woertlich — und dieselbe Rechnung, die R-21 liest.
|
|
47
|
+
const chainsOf = chainsByFunc(graph);
|
|
27
48
|
// FCHAIN -> seine Use Cases.
|
|
28
49
|
const ucsOf = new Map();
|
|
29
50
|
for (const t of compose) {
|