@sigloch/contracts 0.7.0 → 1.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.
@@ -72,36 +72,24 @@ export const TestRefSchema = z.object({
72
72
  level: z.string().optional(),
73
73
  });
74
74
  /**
75
- * Code binding for a FUNC element (CR-GC-205 Item 5, ontology bump 3.6.0).
76
- * Resolves a FUNC node to the concrete code symbol that realizes it, enabling
77
- * graph<->code conformance: every non-concept/non-external FUNC must resolve to a
78
- * real symbol in its allocated MOD's file, and (via the consumer's LSP-backed
79
- * check) every cross-module-called symbol must itself be a FUNC node.
80
- * - `file` : implementation file path, e.g. `src/harness.ts` (the symbol's home).
81
- * - `symbol` : the exported symbol (function/method/class) name realizing the FUNC.
82
- * - `lang` : optional language id (default `ts`) — selects the LSP/engine the
83
- * conformance check drives, so the binding is language-agnostic.
84
- * Stored under `OntologyElement.attributes.codeRef` (additive, opt-in validation).
75
+ * Realization binding for an element (CR-228, ontology bump 3.9.0) — unifies the
76
+ * byte-identical former `codeRef` (FUNC code symbol) and `schemaRef` (SCHEMA
77
+ * Zod export), and extends to MOD (physical part → CAD/geometry artefact). The
78
+ * *type* of the pointing element disambiguates what kind of realization it is:
79
+ * FUNC→code, SCHEMA→Zod-def, physical MOD→CAD. TEST keeps its own `testRef`
80
+ * (a TEST is not just located but *executed* — the runner is the extra value).
81
+ * - `file` : realization file path, e.g. `src/harness.ts`, `part.step`.
82
+ * - `symbol` : the realizing symbol (function/class/Zod export). Optional a
83
+ * geometry artefact (CAD) has no symbol; file-exists is the binding.
84
+ * - `lang` : optional language/format id (default `ts`) selects the
85
+ * conformance engine, so the binding is language-agnostic.
86
+ * Stored under `OntologyElement.attributes.realRef` (additive, opt-in validation).
87
+ * The free-text SCHEMA `contract` attribute stays a human description, not the
88
+ * conformance basis: RC-01/RC-03 resolve this binding, RC-04 checks it is parsed.
85
89
  */
86
- export const CodeRefSchema = z.object({
90
+ export const RealRefSchema = z.object({
87
91
  file: RepoRelativePathSchema,
88
- symbol: z.string(),
89
- lang: z.string().optional(),
90
- });
91
- /**
92
- * Binding of a SCHEMA node to the Zod schema that defines it (CR-211), analogous
93
- * to CodeRefSchema for FUNC:
94
- * - `file` : the source file declaring the Zod schema, e.g. `src/se/ontology.ts`.
95
- * - `symbol` : the exported schema symbol, e.g. `CodeRefSchema`.
96
- * - `lang` : optional language id (default `ts`).
97
- * Stored under `OntologyElement.attributes.schemaRef`. The free-text `contract`
98
- * attribute stays as a human description but is no longer the conformance basis:
99
- * RC-03 resolves this binding to a declared export, RC-04 checks it is parsed at
100
- * the modelled interface.
101
- */
102
- export const SchemaRefSchema = z.object({
103
- file: RepoRelativePathSchema,
104
- symbol: z.string(),
92
+ symbol: z.string().optional(),
105
93
  lang: z.string().optional(),
106
94
  });
107
95
  /**
@@ -188,8 +176,8 @@ export const ELEMENT_ATTRIBUTES = {
188
176
  { key: 'timingBudgetMs', type: 'number', description: 'NFR timing budget in ms' },
189
177
  { key: 'measuredMs', type: 'number', description: 'Measured execution time in ms' },
190
178
  { key: 'sourceFile', type: 'string', description: 'Implementation source file' },
191
- { key: 'codeRef', type: 'object', description: 'Code binding {file, symbol, lang?} — the symbol that realizes this FUNC; see CodeRefSchema (CR-GC-205)' },
192
- { key: 'external', type: 'boolean', description: 'Externally-realized FUNC (e.g. a renderer in another package): exempt from the R-20 codeRef-binding requirement (CR-GC-205)' },
179
+ { key: 'realRef', type: 'object', description: 'Realization binding {file, symbol?, lang?} — the code symbol that realizes this FUNC; see RealRefSchema (CR-228, was codeRef)' },
180
+ { key: 'external', type: 'boolean', description: 'Externally-realized FUNC (e.g. a renderer in another package): exempt from the R-20 realRef-binding requirement (CR-GC-205)' },
193
181
  { key: 'concept', type: 'boolean', description: 'Concept-only FUNC: specified, no implementation yet; exempt from R-20 (CR-GC-205)' },
194
182
  ],
195
183
  UC: [
@@ -206,11 +194,15 @@ export const ELEMENT_ATTRIBUTES = {
206
194
  ],
207
195
  MOD: [
208
196
  { key: 'path', type: 'string', description: 'Source file or glob the module owns, e.g. src/harness.ts — anchors the MOD<->file mapping for graph<->code conformance (CR-GC-205)' },
197
+ { key: 'kind', type: 'string', description: "Module kind, e.g. 'physical' for a machine part (Bauteil) — physical MODs are realized by a CAD/geometry realRef (R-27), logical MODs by their FUNCs' code (CR-191/228)" },
198
+ { key: 'realRef', type: 'object', description: 'Realization binding {file, symbol?, lang?} — CAD/geometry artefact realizing a physical MOD; see RealRefSchema (CR-228, symbol optional)' },
199
+ { key: 'external', type: 'boolean', description: 'Externally-realized MOD: exempt from the R-27 realRef-presence requirement (CR-228)' },
200
+ { key: 'concept', type: 'boolean', description: 'Concept-only MOD: modelled, no realization yet; exempt from R-27 (CR-228)' },
209
201
  ],
210
202
  SCHEMA: [
211
203
  { key: 'contract', type: 'string', description: 'Free-text contract description, e.g. "@sigloch/contracts LiveUpdateEventSchema" — human hint only, not the conformance basis (CR-211)' },
212
- { key: 'schemaRef', type: 'object', description: 'Schema binding {file, symbol, lang?} — the Zod export defining this SCHEMA; see SchemaRefSchema (CR-211)' },
213
- { key: 'external', type: 'boolean', description: 'Externally-defined SCHEMA (e.g. a foreign-API contract): exempt from the schemaRef-presence requirement (CR-211)' },
214
- { key: 'concept', type: 'boolean', description: 'Concept-only SCHEMA: modelled, no Zod export yet; exempt from the schemaRef-presence requirement (CR-211)' },
204
+ { key: 'realRef', type: 'object', description: 'Realization binding {file, symbol?, lang?} — the Zod export defining this SCHEMA; see RealRefSchema (CR-228, was schemaRef)' },
205
+ { key: 'external', type: 'boolean', description: 'Externally-defined SCHEMA (e.g. a foreign-API contract): exempt from the realRef-presence requirement (CR-211)' },
206
+ { key: 'concept', type: 'boolean', description: 'Concept-only SCHEMA: modelled, no Zod export yet; exempt from the realRef-presence requirement (CR-211)' },
215
207
  ],
216
208
  };
@@ -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-01..03)
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,9 +37,11 @@ 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
- 'R-22': 'alloc', 'R-23': 'alloc', 'R-26': 'schema',
44
+ 'R-22': 'alloc', 'R-23': 'alloc', 'R-26': 'schema', 'R-27': 'arch',
43
45
  // uc
44
46
  'UC-01': 'uc', 'UC-02': 'uc', 'UC-03': 'uc', 'UC-04': 'uc',
45
47
  'UC-05': 'uc', 'UC-06': 'uc',
@@ -52,7 +54,7 @@ export const RULE_TO_DIMENSION = {
52
54
  // ver
53
55
  'R-01': 'ver', 'R-05': 'ver',
54
56
  // schema
55
- 'SC-01': 'schema', 'SC-02': 'schema', 'SC-03': 'schema',
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
package/dist/se/rules.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @sigloch/contracts/se — single source of truth for SE validation rules.
5
5
  */
6
6
  import { z } from 'zod/v4';
7
- import { ElementType, TraceType, TestRefSchema, CodeRefSchema, SchemaRefSchema } from './ontology.js';
7
+ import { ElementType, TraceType, TestRefSchema, RealRefSchema } from './ontology.js';
8
8
  import { isValidTrace } from './meta-model.js';
9
9
  export const RuleSeverity = z.enum(['error', 'warning', 'info']);
10
10
  /** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
@@ -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) {
@@ -581,10 +633,10 @@ function testMustHaveRunnableBinding(graph) {
581
633
  }));
582
634
  }
583
635
  // ---------------------------------------------------------------------------
584
- // R-20: FUNC code binding (CR-GC-205 Item 5, extended by CR-210) a FUNC that
585
- // is not explicitly concept-only (attributes.concept === true) or externally
586
- // realized (attributes.external === true) counts as BOUND when it EITHER carries
587
- // a valid codeRef {file, symbol} OR is a blackbox parent whose compose→FUNC
636
+ // R-20: FUNC realRef binding (CR-GC-205 Item 5, extended by CR-210, unified CR-228)
637
+ // — a FUNC that is not explicitly concept-only (attributes.concept === true) or
638
+ // externally realized (attributes.external === true) counts as BOUND when it EITHER
639
+ // carries a valid realRef {file, symbol?} OR is a blackbox parent whose compose→FUNC
588
640
  // children are ALL (recursively) bound — the parent is realized by its children
589
641
  // (e.g. FUNC-gesture-capture, gve CR-GVE-150). A partially-bound parent fires
590
642
  // R-20 naming the still-unbound leaves in the fix_hint, so drift on a child is
@@ -598,7 +650,7 @@ function funcMustHaveCodeBinding(graph) {
598
650
  const composeFuncChildren = (id) => graph.traces
599
651
  .filter(t => t.source === id && t.type === 'compose' && funcById.has(t.target))
600
652
  .map(t => t.target);
601
- const hasCodeRef = (el) => CodeRefSchema.safeParse(el.attributes?.codeRef).success;
653
+ const hasCodeRef = (el) => RealRefSchema.safeParse(el.attributes?.realRef).success;
602
654
  const isExempt = (el) => el.attributes?.concept === true || el.attributes?.external === true;
603
655
  // A FUNC is bound iff it is exempt, has a codeRef, or is a parent whose
604
656
  // compose→FUNC children are all bound (recursive, cycle-safe).
@@ -662,8 +714,8 @@ function funcMustHaveCodeBinding(graph) {
662
714
  rule_id: 'R-20',
663
715
  severity: 'warning',
664
716
  element_id: fn.id,
665
- message: `${fn.id} is a FUNC without a valid codeRef binding`,
666
- fix_hint: 'Add attributes.codeRef {file, symbol, lang?}, or set attributes.concept:true (spec-only) / attributes.external:true (realized in another package)',
717
+ message: `${fn.id} is a FUNC without a valid realRef binding`,
718
+ fix_hint: 'Add attributes.realRef {file, symbol, lang?}, or set attributes.concept:true (spec-only) / attributes.external:true (realized in another package)',
667
719
  context: { element_type: fn.type, element_name: fn.name },
668
720
  });
669
721
  }
@@ -800,28 +852,52 @@ function modMustHaveAllocatedFunc(graph) {
800
852
  // MOD→satisfy→REQ, behavioral NFRs FCHAIN→satisfy→REQ. R-18 now flags any residual
801
853
  // REQ→MOD allocate edge as an invalid trace pattern.
802
854
  // ---------------------------------------------------------------------------
803
- // R-26: SCHEMA schemaRef presence (CR-211) — a SCHEMA that is not explicitly
804
- // concept-only or external should carry a valid schemaRef {file, symbol} so its
805
- // Zod definition is machine-resolvable (RC-03 then checks it resolves, RC-04 that
806
- // it is parsed at the interface). WARNING, not error: the 9 currently-unbound
807
- // SCHEMAs on the reference model must not turn readiness red before any binding
808
- // exists — the presence signal mirrors R-20 (codeRef) / R-19 (testRef). Symbol
809
- // RESOLUTION is out of scope here (pure, no I/O — that is RC-03's job).
855
+ // R-26: SCHEMA realRef presence (CR-211, unified CR-228) — a SCHEMA that is not
856
+ // explicitly concept-only or external should carry a valid realRef {file, symbol?}
857
+ // so its Zod definition is machine-resolvable (RC-03 then checks it resolves, RC-04
858
+ // that it is parsed at the interface). WARNING, not error: currently-unbound SCHEMAs
859
+ // on the reference model must not turn readiness red before any binding exists — the
860
+ // presence signal mirrors R-20 (FUNC realRef) / R-19 (testRef). Symbol RESOLUTION is
861
+ // out of scope here (pure, no I/O — that is RC-03's job).
810
862
  // ---------------------------------------------------------------------------
811
863
  function schemaMustHaveSchemaRef(graph) {
812
864
  return graph.elements
813
865
  .filter(e => e.type === 'SCHEMA')
814
866
  .filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
815
- .filter(e => !SchemaRefSchema.safeParse(e.attributes?.schemaRef).success)
867
+ .filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
816
868
  .map(sc => ({
817
869
  rule_id: 'R-26',
818
870
  severity: 'warning',
819
871
  element_id: sc.id,
820
- message: `${sc.id} is a SCHEMA without a valid schemaRef binding`,
821
- fix_hint: 'Add attributes.schemaRef {file, symbol, lang?}, or set attributes.concept:true (spec-only) / attributes.external:true (foreign-API contract)',
872
+ message: `${sc.id} is a SCHEMA without a valid realRef binding`,
873
+ fix_hint: 'Add attributes.realRef {file, symbol, lang?}, or set attributes.concept:true (spec-only) / attributes.external:true (foreign-API contract)',
822
874
  context: { element_type: sc.type, element_name: sc.name },
823
875
  }));
824
876
  }
877
+ // ---------------------------------------------------------------------------
878
+ // R-27: physical MOD realRef presence (CR-228) — the MOD arm of the unified
879
+ // "element must have a realRef" rule (FUNC=R-20, SCHEMA=R-26, physical MOD=R-27).
880
+ // A physical MOD (kind='physical') is a Bauteil realized by a CAD/geometry
881
+ // artefact, not by code (RT-01: FUNCs never allocate directly to it). Logical/SW
882
+ // MODs are realized through their FUNCs' code (R-20) and are NOT in scope here.
883
+ // WARNING (presence signal like R-20/R-26); concept/external MODs are exempt.
884
+ // Symbol is optional on a realRef (a geometry file has none) — presence = a valid
885
+ // realRef with a file. RESOLUTION (file on disk) is a consumer/RC concern.
886
+ // ---------------------------------------------------------------------------
887
+ function physicalModMustHaveRealRef(graph) {
888
+ return graph.elements
889
+ .filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical')
890
+ .filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
891
+ .filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
892
+ .map(mod => ({
893
+ rule_id: 'R-27',
894
+ severity: 'warning',
895
+ element_id: mod.id,
896
+ message: `${mod.id} is a physical MOD without a valid realRef (CAD/geometry) binding`,
897
+ fix_hint: 'Add attributes.realRef {file, symbol?, lang?} pointing at the CAD/geometry artefact, or set attributes.concept:true / attributes.external:true',
898
+ context: { element_type: mod.type, element_name: mod.name },
899
+ }));
900
+ }
825
901
  export const V3_RULES = [
826
902
  { id: 'R-01', name: 'REQ must have verification', severity: 'error', evaluate: reqMustHaveVerification },
827
903
  { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq },
@@ -837,14 +913,16 @@ export const V3_RULES = [
837
913
  { id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular },
838
914
  { id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern },
839
915
  { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding },
840
- { id: 'R-20', name: 'FUNC code binding', severity: 'warning', evaluate: funcMustHaveCodeBinding },
916
+ { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding },
841
917
  { id: 'R-21', name: 'FUNC↔FUNC connection needs integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest },
842
918
  { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated },
843
919
  { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc },
844
- { id: 'R-26', name: 'SCHEMA must have schemaRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef },
920
+ { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef },
921
+ { id: 'R-27', name: 'Physical MOD must have realRef', severity: 'warning', evaluate: physicalModMustHaveRealRef },
845
922
  { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement },
846
923
  { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency },
847
924
  { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition },
925
+ { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth },
848
926
  { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope },
849
927
  { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency },
850
928
  ];
@@ -1,11 +1,19 @@
1
1
  /**
2
- * SC-01..SC-03 — Schema Quality rules (CR-122).
3
- * Complement V3 structural rules with content checks on SCHEMA elements.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "0.7.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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;
@@ -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
- }