@sigloch/graphcode-client 1.5.1 → 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/index.d.ts CHANGED
@@ -18,6 +18,5 @@
18
18
  */
19
19
  export { HOST_SOCK_BASENAME, callHost, type ShimRequest, type ShimResponse, } from './host-client.js';
20
20
  export { MARKDOWN_VIEWS, VIEW_FILENAMES, type MarkdownView } from './view-catalog.js';
21
- export { GRAPHCODE_INCOSE_SCOPE, PHASE_GATE_RULES, PHASE_GATE_LABELS, PHASE_GATE_CREATIONS, IMPL_GATE_PHASE, IMPL_GATE_MILESTONES, IMPL_GATE_RULES, ABSENT_CREATION_PROVIDER, creationBlockingMsg, summarizeReadiness, computeReadiness, scoreReadiness, getFamilyRuleIds, type IncoseScope, type CreationCurrency, type CreationCurrencyProvider, type ReadinessDimension, type ReadinessGate, type ReadinessReport, } from './readiness.js';
22
- export { NEUTRAL_COMPLETENESS, COMPLETENESS_SLICES, scoreCompleteness, typeOf, type CGraph, type CNode, GateCompleteness, type CompletenessLeg, } from './readiness-completeness.js';
21
+ export { GRAPHCODE_INCOSE_SCOPE, MARK_LABELS, computeMarks, summarizeReadiness, computeReadiness, scoreReadiness, getFamilyRuleIds, type IncoseScope, type ReadinessDimension, type ReadinessMark, type ReadinessReport, } from './readiness.js';
23
22
  export * from './panels.js';
package/dist/index.js CHANGED
@@ -18,6 +18,5 @@
18
18
  */
19
19
  export { HOST_SOCK_BASENAME, callHost, } from './host-client.js';
20
20
  export { MARKDOWN_VIEWS, VIEW_FILENAMES } from './view-catalog.js';
21
- export { GRAPHCODE_INCOSE_SCOPE, PHASE_GATE_RULES, PHASE_GATE_LABELS, PHASE_GATE_CREATIONS, IMPL_GATE_PHASE, IMPL_GATE_MILESTONES, IMPL_GATE_RULES, ABSENT_CREATION_PROVIDER, creationBlockingMsg, summarizeReadiness, computeReadiness, scoreReadiness, getFamilyRuleIds, } from './readiness.js';
22
- export { NEUTRAL_COMPLETENESS, COMPLETENESS_SLICES, scoreCompleteness, typeOf, GateCompleteness, } from './readiness-completeness.js';
21
+ export { GRAPHCODE_INCOSE_SCOPE, MARK_LABELS, computeMarks, summarizeReadiness, computeReadiness, scoreReadiness, getFamilyRuleIds, } from './readiness.js';
23
22
  export * from './panels.js';
package/dist/panels.d.ts CHANGED
@@ -14,29 +14,15 @@
14
14
  *
15
15
  * @author andreas@siglochconsulting
16
16
  */
17
- import type { ReadinessReport, ReadinessGate, CreationCurrency, CreationCurrencyProvider } from './readiness.js';
17
+ import type { ReadinessReport, ReadinessMark } from './readiness.js';
18
18
  import type { RuleViolation, LiveUpdateEvent } from '@sigloch/contracts/harness';
19
19
  import type { Graph } from '@sigloch/graph-api-core';
20
- export interface GatePanel {
21
- id: string;
22
- label: string;
23
- passed: boolean;
24
- score: number;
25
- /** The blocking elements — the drill-down behind the traffic light. */
26
- blocking: string[];
27
- /**
28
- * Structural completeness (CR-GC-250): ONE value per gate (covered/total) for
29
- * the panel; `missing[]` is the on-click per-leg drill-down, same mechanic as
30
- * `blocking`. REQ-completeness-single-value.
31
- */
32
- completeness: ReadinessGate['completeness'];
33
- }
34
20
  export interface ReadinessPanel {
35
21
  compliancePct: number;
36
22
  totalElements: number;
37
23
  elementsWithErrors: number;
38
- phaseGates: GatePanel[];
39
- implGates: GatePanel[];
24
+ /** SRR, PDR, CDR, TRR, Bau — reached or not, with the findings that hold each (CR-SM-395). */
25
+ marks: ReadinessMark[];
40
26
  }
41
27
  export declare function readinessPanel(report: ReadinessReport): ReadinessPanel;
42
28
  export interface RecommendationItem {
@@ -85,11 +71,12 @@ export interface ViolationGroup {
85
71
  export declare function groupViolations(violations: RuleViolation[], elementLimit?: number): ViolationGroup[];
86
72
  export type Freshness = 'live' | 'stale' | 'absent';
87
73
  /**
88
- * Two artifact kinds with TWO distinct staleness mechanisms (CR-GC-222):
74
+ * Two artifact kinds (CR-GC-222):
89
75
  * - `render` — a deterministic `docs/views/*.md` projection; stale via file mtime vs graph.
90
- * - `analysis` — judgment work (FMEA/ConOps/Trade/…); stale when the analyzed SCOPE moved,
91
- * NEVER via mtime. The single `staleVsGraph` boolean was the defect (it applied
92
- * an mtime check to analyses that don't refresh on mtime).
76
+ * - `analysis` — judgment work (FMEA/ConOps/Trade/Assumption Review); carried out or not. Its
77
+ * only record is the stamp under `SYS.attributes.analysisFreshness` — there is no
78
+ * "stale" for an analysis (CR-SM-395: the currency computation is gone; it ran
79
+ * without effect in the product path since CR-GC-259).
93
80
  */
94
81
  export type ArtifactKind = 'render' | 'analysis';
95
82
  /** INCOSE/SE-standard vs graphcode-specific — so non-INCOSE rows are not mislabeled as INCOSE. */
@@ -103,15 +90,16 @@ export interface ArtifactCatalogEntry {
103
90
  }
104
91
  /**
105
92
  * The canonical INCOSE-tab artifact catalog (readiness-artifact-model.md §6). Renders are the
106
- * deterministic CR-GC-220 views; the 5 analysis ids are exactly CR-GC-221's creation keys
107
- * (`conops`/`assumption-review`/`fmea`/`trade`/`implplan`). `irr` is renamed to "Assumption
108
- * Review" and grouped graphcode-specific (it is not a standard INCOSE artifact).
93
+ * deterministic CR-GC-220 views; the four analysis ids are exactly contracts' `AnalysisArtifactId`
94
+ * (`conops`/`assumption-review`/`fmea`/`trade`). `irr` is renamed to "Assumption Review" and
95
+ * grouped graphcode-specific (it is not a standard INCOSE artifact).
96
+ *
97
+ * CR-SM-395: the implementation plan is a RENDER. The plan is the set of open CRs, not a stamp;
98
+ * its document (`implplan.md`) is a projection of the CR/MS structure like any other view.
109
99
  */
110
100
  export declare const ARTIFACT_CATALOG: readonly ArtifactCatalogEntry[];
111
101
  /** Classify a RENDER artifact. `exists` = doc present; `staleVsGraph` = graph changed since render. */
112
102
  export declare function artifactFreshness(exists: boolean, staleVsGraph: boolean): Freshness;
113
- /** Classify an ANALYSIS artifact from its scope-currency — 🟢 current / 🟡 stale / 🔴 absent. */
114
- export declare function analysisFreshness(currency: CreationCurrency): Freshness;
115
103
  export interface ArtifactStatus {
116
104
  id: string;
117
105
  label: string;
@@ -119,15 +107,13 @@ export interface ArtifactStatus {
119
107
  group: ArtifactGroup;
120
108
  freshness: Freshness;
121
109
  }
122
- /** One artifact's freshness signal: render rows carry exists/staleVsGraph, analysis rows carry currency. */
110
+ /** One artifact's signal. An analysis row reads `exists` only: the stamp is there, or it is not. */
123
111
  export interface ArtifactSignal {
124
112
  id: string;
125
- /** RENDER: a materialized doc is present. */
113
+ /** RENDER: a materialized doc is present. ANALYSIS: the analysis is on record (stamp present). */
126
114
  exists?: boolean;
127
- /** RENDER: the graph changed since the doc was generated (mtime). */
115
+ /** RENDER only: the graph changed since the doc was generated (mtime). */
128
116
  staleVsGraph?: boolean;
129
- /** ANALYSIS: scope-currency of the judgment work (the ONLY signal read for analysis rows). */
130
- currency?: CreationCurrency;
131
117
  }
132
118
  export interface ArtifactsPanel {
133
119
  artifacts: ArtifactStatus[];
@@ -136,39 +122,19 @@ export interface ArtifactsPanel {
136
122
  absentCount: number;
137
123
  }
138
124
  /**
139
- * Shape artifact freshness signals into the tab view-model. The catalog decides each row's
140
- * kind/group/label; the kind decides WHICH mechanism classifies it — render rows go through
141
- * the mtime path, analysis rows through the scope-currency path (so a creation is NEVER
142
- * classified by mtime, even if a caller passes `staleVsGraph` — it is ignored). Renderer groups
143
- * by `group` to show "INCOSE / SE-standard" and "graphcode-specific" separately.
125
+ * Shape artifact signals into the tab view-model. The catalog decides each row's
126
+ * kind/group/label; the kind decides how it is classified — a render row by mtime, an analysis
127
+ * row by presence alone (`staleVsGraph` is ignored there: an analysis is NEVER classified by
128
+ * mtime). Renderer groups by `group` to show "INCOSE / SE-standard" and "graphcode-specific"
129
+ * separately.
144
130
  */
145
131
  export declare function artifactsPanel(signals: ArtifactSignal[]): ArtifactsPanel;
146
132
  /**
147
- * Build the CreationCurrencyProvider that CR-GC-221's readiness scorer consumes (CR-GC-222
148
- * delivers the `analysis`-currency to 221). Maps each creation id → its scope-currency; an
149
- * unknown/never-analyzed creation reads as 🔴 absent.
133
+ * The signals of the four analysis artifacts, read off the graph: an analysis is on record iff
134
+ * its stamp under `SYS.attributes.analysisFreshness.<id>` is valid (CR-SM-227 — the same
135
+ * condition AF-01..04 check). No comparison against the live graphVersion (CR-SM-395).
150
136
  */
151
- export declare function creationCurrencyProvider(analysis: Array<{
152
- id: string;
153
- currency: CreationCurrency;
154
- }>): CreationCurrencyProvider;
155
- /**
156
- * Real scope-currency for the 5 analysis artifacts (CR-SM-227) — replaces the
157
- * `ABSENT_CREATION_PROVIDER` placeholder (CR-GVE-225 F10: GVE showed `absent`
158
- * correctly because no scope-currency check existed yet). Reads the
159
- * `graphVersion` freshness stamp AF-01..05 (contracts) check for presence of
160
- * (`SYS.attributes.analysisFreshness.<id>`) and classifies it against the
161
- * live graphVersion: missing/invalid stamp → absent; stamp older than the
162
- * current graphVersion → stale; stamp at (or ahead of) it → current.
163
- * Grober Graph-Versions-Stempel only, no scope-hash — CR-SM-227's declared
164
- * first-cut limitation ("Achtung" section), not full scope-precise staleness.
165
- */
166
- export declare function computeAnalysisCurrency(graph: Pick<Graph, 'nodes'>, currentGraphVersion: number): Array<{
167
- id: string;
168
- currency: CreationCurrency;
169
- }>;
170
- /** Convenience: build the real CreationCurrencyProvider straight off the graph + its live version. */
171
- export declare function analysisCreationCurrencyProvider(graph: Pick<Graph, 'nodes'>, currentGraphVersion: number): CreationCurrencyProvider;
137
+ export declare function analysisSignals(graph: Pick<Graph, 'nodes'>): ArtifactSignal[];
172
138
  export interface ImpactPanel {
173
139
  root: string;
174
140
  blastRadiusNodes: number;
package/dist/panels.js CHANGED
@@ -1,14 +1,10 @@
1
1
  import { AnalysisArtifactId, AnalysisFreshnessStampSchema } from '@sigloch/contracts/se';
2
- function toGatePanel(g) {
3
- return { id: g.id, label: g.label, passed: g.passed, score: g.score, blocking: g.blocking, completeness: g.completeness };
4
- }
5
2
  export function readinessPanel(report) {
6
3
  return {
7
4
  compliancePct: Math.round(report.compliance.score * 1000) / 10,
8
5
  totalElements: report.compliance.totalElements,
9
6
  elementsWithErrors: report.compliance.elementsWithErrors,
10
- phaseGates: report.phaseGates.map(toGatePanel),
11
- implGates: report.implGates.map(toGatePanel),
7
+ marks: report.marks,
12
8
  };
13
9
  }
14
10
  const SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
@@ -70,9 +66,12 @@ export function groupViolations(violations, elementLimit = 10) {
70
66
  }
71
67
  /**
72
68
  * The canonical INCOSE-tab artifact catalog (readiness-artifact-model.md §6). Renders are the
73
- * deterministic CR-GC-220 views; the 5 analysis ids are exactly CR-GC-221's creation keys
74
- * (`conops`/`assumption-review`/`fmea`/`trade`/`implplan`). `irr` is renamed to "Assumption
75
- * Review" and grouped graphcode-specific (it is not a standard INCOSE artifact).
69
+ * deterministic CR-GC-220 views; the four analysis ids are exactly contracts' `AnalysisArtifactId`
70
+ * (`conops`/`assumption-review`/`fmea`/`trade`). `irr` is renamed to "Assumption Review" and
71
+ * grouped graphcode-specific (it is not a standard INCOSE artifact).
72
+ *
73
+ * CR-SM-395: the implementation plan is a RENDER. The plan is the set of open CRs, not a stamp;
74
+ * its document (`implplan.md`) is a projection of the CR/MS structure like any other view.
76
75
  */
77
76
  export const ARTIFACT_CATALOG = [
78
77
  // Renders — INCOSE/SE-standard deterministic views (mtime-classified).
@@ -86,11 +85,11 @@ export const ARTIFACT_CATALOG = [
86
85
  { id: 'intplan', label: 'Integration & Test Plan', kind: 'render', group: 'incose' },
87
86
  { id: 'changelog', label: 'Change Log', kind: 'render', group: 'incose' },
88
87
  { id: 'references', label: 'Requirements Traceability', kind: 'render', group: 'incose' },
89
- // Creations — judgment work (scope-classified, NEVER mtime).
88
+ { id: 'implplan', label: 'Implementation Plan', kind: 'render', group: 'incose' },
89
+ // Analyses — judgment work (carried out or not, NEVER mtime).
90
90
  { id: 'conops', label: 'Concept of Operations', kind: 'analysis', group: 'incose' },
91
91
  { id: 'fmea', label: 'FMEA', kind: 'analysis', group: 'incose' },
92
92
  { id: 'trade', label: 'Trade Study', kind: 'analysis', group: 'incose' },
93
- { id: 'implplan', label: 'Implementation Plan', kind: 'analysis', group: 'incose' },
94
93
  { id: 'assumption-review', label: 'Assumption Review', kind: 'analysis', group: 'graphcode' },
95
94
  ];
96
95
  /** Classify a RENDER artifact. `exists` = doc present; `staleVsGraph` = graph changed since render. */
@@ -99,25 +98,19 @@ export function artifactFreshness(exists, staleVsGraph) {
99
98
  return 'absent';
100
99
  return staleVsGraph ? 'stale' : 'live';
101
100
  }
102
- /** Classify an ANALYSIS artifact from its scope-currency — 🟢 current / 🟡 stale / 🔴 absent. */
103
- export function analysisFreshness(currency) {
104
- return currency === 'current' ? 'live' : currency === 'stale' ? 'stale' : 'absent';
105
- }
106
101
  /**
107
- * Shape artifact freshness signals into the tab view-model. The catalog decides each row's
108
- * kind/group/label; the kind decides WHICH mechanism classifies it — render rows go through
109
- * the mtime path, analysis rows through the scope-currency path (so a creation is NEVER
110
- * classified by mtime, even if a caller passes `staleVsGraph` — it is ignored). Renderer groups
111
- * by `group` to show "INCOSE / SE-standard" and "graphcode-specific" separately.
102
+ * Shape artifact signals into the tab view-model. The catalog decides each row's
103
+ * kind/group/label; the kind decides how it is classified — a render row by mtime, an analysis
104
+ * row by presence alone (`staleVsGraph` is ignored there: an analysis is NEVER classified by
105
+ * mtime). Renderer groups by `group` to show "INCOSE / SE-standard" and "graphcode-specific"
106
+ * separately.
112
107
  */
113
108
  export function artifactsPanel(signals) {
114
109
  const byId = new Map(ARTIFACT_CATALOG.map((e) => [e.id, e]));
115
110
  const statuses = signals.map((s) => {
116
111
  const entry = byId.get(s.id);
117
112
  const kind = entry?.kind ?? 'render';
118
- const freshness = kind === 'analysis'
119
- ? analysisFreshness(s.currency ?? 'absent')
120
- : artifactFreshness(s.exists ?? false, s.staleVsGraph ?? false);
113
+ const freshness = artifactFreshness(s.exists ?? false, kind === 'render' && (s.staleVsGraph ?? false));
121
114
  return { id: s.id, label: entry?.label ?? s.id, kind, group: entry?.group ?? 'graphcode', freshness };
122
115
  });
123
116
  return {
@@ -128,41 +121,14 @@ export function artifactsPanel(signals) {
128
121
  };
129
122
  }
130
123
  /**
131
- * Build the CreationCurrencyProvider that CR-GC-221's readiness scorer consumes (CR-GC-222
132
- * delivers the `analysis`-currency to 221). Maps each creation id → its scope-currency; an
133
- * unknown/never-analyzed creation reads as 🔴 absent.
134
- */
135
- export function creationCurrencyProvider(analysis) {
136
- const m = new Map(analysis.map((a) => [a.id, a.currency]));
137
- return (creation) => m.get(creation) ?? 'absent';
138
- }
139
- /**
140
- * Real scope-currency for the 5 analysis artifacts (CR-SM-227) — replaces the
141
- * `ABSENT_CREATION_PROVIDER` placeholder (CR-GVE-225 F10: GVE showed `absent`
142
- * correctly because no scope-currency check existed yet). Reads the
143
- * `graphVersion` freshness stamp AF-01..05 (contracts) check for presence of
144
- * (`SYS.attributes.analysisFreshness.<id>`) and classifies it against the
145
- * live graphVersion: missing/invalid stamp → absent; stamp older than the
146
- * current graphVersion → stale; stamp at (or ahead of) it → current.
147
- * Grober Graph-Versions-Stempel only, no scope-hash — CR-SM-227's declared
148
- * first-cut limitation ("Achtung" section), not full scope-precise staleness.
124
+ * The signals of the four analysis artifacts, read off the graph: an analysis is on record iff
125
+ * its stamp under `SYS.attributes.analysisFreshness.<id>` is valid (CR-SM-227 — the same
126
+ * condition AF-01..04 check). No comparison against the live graphVersion (CR-SM-395).
149
127
  */
150
- export function computeAnalysisCurrency(graph, currentGraphVersion) {
128
+ export function analysisSignals(graph) {
151
129
  const sys = graph.nodes.find((n) => n.type === 'SYS');
152
130
  const stamps = (sys?.attributes?.analysisFreshness ?? {});
153
- return AnalysisArtifactId.options.map((id) => {
154
- const parsed = AnalysisFreshnessStampSchema.safeParse(stamps[id]);
155
- const currency = !parsed.success
156
- ? 'absent'
157
- : parsed.data.graphVersion >= currentGraphVersion
158
- ? 'current'
159
- : 'stale';
160
- return { id, currency };
161
- });
162
- }
163
- /** Convenience: build the real CreationCurrencyProvider straight off the graph + its live version. */
164
- export function analysisCreationCurrencyProvider(graph, currentGraphVersion) {
165
- return creationCurrencyProvider(computeAnalysisCurrency(graph, currentGraphVersion));
131
+ return AnalysisArtifactId.options.map((id) => ({ id, exists: AnalysisFreshnessStampSchema.safeParse(stamps[id]).success }));
166
132
  }
167
133
  export function impactPanel(impact) {
168
134
  return { root: impact.rootId, blastRadiusNodes: impact.nodeCount, blastRadiusEdges: impact.edgeCount };
@@ -182,7 +148,7 @@ export function healthPanel(health) {
182
148
  const DOMAIN_TO_PANELS = {
183
149
  graph: ['graph', 'impact', 'artifacts'],
184
150
  rules: ['recommendations', 'readiness'],
185
- readiness: ['readiness', 'implGates', 'artifacts'],
151
+ readiness: ['readiness', 'artifacts'],
186
152
  suggestions: ['recommendations'],
187
153
  };
188
154
  /** Which panel ids a viewer should refresh for this invalidation event. */
@@ -1,48 +1,48 @@
1
1
  /**
2
- * ReadinessScorer — CR-GC-107 (compliance) + CR-GC-125 (readiness model).
2
+ * Readiness — compliance and the marks (CR-GC-107, CR-GC-125; rebuilt CR-SM-395).
3
3
  *
4
- * Computes readiness for a graphcode graph purely from `@sigloch/contracts`
5
- * V3_RULES violations (via `harness.evaluateRules()`, the L2 gate — see
6
- * FCHAIN-apply-gate) PLUS the MS milestone nodes and element status. This is
7
- * the in-repo deliverable that proves readiness is measured against the FAMILY
8
- * rule base, NOT the aimprove predecessor rules (BQ-2.0.0 / INCOSE-style
9
- * BQ-06/BQ-02). The foreign BQ path is explicitly NOT used; it is removed from
10
- * family readiness scoring.
4
+ * Computed purely from rule violations of `@sigloch/contracts` (the family rule base, never a
5
+ * foreign rule set) and the stage each rule carries at its definition. There is no gate
6
+ * computation of its own in this package any more.
11
7
  *
12
- * REQ-readiness-model (CR-GC-125) — the model, defined here AND in the graph:
8
+ * THE MARKS. The rule catalog orders its sets in twelve stages (`STAGE_SETS`); a mark lies
9
+ * between two stages: SRR after stage 4, PDR after 7, CDR after 8, TRR after 9, Bau after 12.
10
+ * A mark is REACHED iff no finding holds it. A finding holds a mark when its rule lies at or
11
+ * before the mark's stage — or at every stage (`'immer'`) — and it is
12
+ * - an error, or
13
+ * - a finding of an existence rule: a set the stage requires is missing.
14
+ * Other warnings do not hold a mark; "free of warnings" is a separate statement.
13
15
  *
14
- * INCOSE-Artifact-Scope = LEAN, refined (CR-GC-226, supersedes the CR-GC-125
15
- * `lean = no artifacts` stance). graphcode is a headless QM-level governance
16
- * harness, not an ASIL-rated system. The graph is the SSOT and every RENDER
17
- * (SRS/SDD/RTM/ICD/…) is a DETERMINISTIC PROJECTION of it (CR-GC-220) — not a
18
- * separate hand-authored deliverable. CREATION artifacts (ConOps, FMEA,
19
- * Assumption Review, Trade, Impl Plan) ARE in scope as lightweight judgment
20
- * inputs and gate preconditions — tracked in the artifact tab (CR-GC-222) and
21
- * required by the gates (CR-GC-221). Only ASIL-D-grade evidence (full FMEA
22
- * S/O/D rigor, ASIL isolation proof) stays out of scope — graphcode carries no
23
- * `asil`-rated elements. Refined model: docs/proposals/readiness-artifact-model.md §3.
16
+ * The mark Bau additionally is held by every warning of the stages 10 to 12 (plan, binding,
17
+ * conformance): those findings ARE the work list of the build, and the mark reads "nothing left
18
+ * to plan, bind or reconcile". It opens again with every change.
24
19
  *
25
- * Phase-Readiness — 4 INCOSE technical-review gates (SRR/PDR/CDR/TRR), each a
26
- * DISJOINT + EXHAUSTIVE partition of the element-level V3_RULES. A gate is
27
- * `passed` iff none of its rules fire an error-severity violation; warnings/
28
- * info are advisory (`open`). The four gates collectively == element
29
- * compliance, so nothing is measured outside V3_RULES.
20
+ * A NOTE (severity `info`, e.g. MS-03, VR-01) holds no mark, at no stage (CR-SM-396).
30
21
  *
31
- * Implementation-Readiness — 4 program/build acceptance gates
32
- * (SAR/FCA/SVR/FRR), each bound to a milestone tier (MS-1..MS-4). A gate is
33
- * `ready` iff every CR assigned to its milestone (CR -relation-> MS) is
34
- * status=done AND the milestone scope (MS -compose-> X) carries no
35
- * error-severity violation. Derived from the MS nodes + element status, i.e.
36
- * the MS-01/MS-02 milestone-rule concerns — the remaining 2 of the 17 rules.
22
+ * AN ACCEPTED FINDING HOLDS ITS MARK (CR-SM-396). The rules keep firing on a finding the human
23
+ * accepted with a reason (`acceptedFindings`), and the marks are computed from all of them: an
24
+ * accepted AF-05 ("unbuilt, no order") still holds Bau. Acceptance takes a finding out of the
25
+ * work focus; it is no way to read a mark as reached. Callers pass the findings UNFILTERED.
37
26
  *
38
- * Host/dashboard wiring (panels) lives in MOD-dashboard (CR-GC-115); this module
39
- * provides the scoring primitive only — no dashboard, no renderer.
27
+ * Why "0 of 0" cannot read as reached: every mandatory set is demanded by an existence rule of
28
+ * the stage before it (no system → R-33, no use case → R-17, …). An empty set is therefore either
29
+ * voluntary, or the existence rule before it is open and holds the mark. Stage 8 (schema) carries
30
+ * no existence rule of its own (CR-SM-396): a data flow without a schema is a grammar error
31
+ * (R-18, every stage), and no data flow at all is held by R-31 (stage 6).
32
+ *
33
+ * What is gone with CR-SM-395: the three-valued gate state and its labels, the gate score, the
34
+ * gate completeness and its legs (`COMPLETENESS_SLICES`), the four build gates bound to milestone
35
+ * ids, the required creations per gate and the currency of analysis artifacts. The assignment
36
+ * rule → mark is `ALL_RULE_DEFS[].mark` in contracts, not a list here.
37
+ *
38
+ * Host/dashboard wiring (panels) lives in `panels.ts`; this module provides the scoring
39
+ * primitive only — no dashboard, no renderer.
40
40
  *
41
41
  * @author andreas@siglochconsulting
42
42
  */
43
43
  import type { Graph } from '@sigloch/graph-api-core';
44
+ import { type MarkType } from '@sigloch/contracts/se';
44
45
  import type { RuleViolation } from '@sigloch/contracts/harness';
45
- import { type GateCompleteness } from './readiness-completeness.js';
46
46
  /**
47
47
  * A readiness dimension score in [0, 1].
48
48
  * 0 = fully non-compliant, 1 = fully compliant.
@@ -59,111 +59,30 @@ export interface ReadinessDimension {
59
59
  }
60
60
  /**
61
61
  * INCOSE artifact scope for the system under governance.
62
- * `lean` — graph = SSOT; renders are deterministic projections, creations are
63
- * lightweight judgment inputs + gate preconditions (graphcode's stance,
64
- * refined CR-GC-226). NOT "no artifacts".
62
+ * `lean` — graph = SSOT; renders are deterministic projections, analyses are lightweight
63
+ * judgment inputs (graphcode's stance, refined CR-GC-226). NOT "no artifacts".
65
64
  * `full` — ASIL-D-grade evidence (full FMEA S/O/D rigor, ASIL isolation) (not graphcode).
66
65
  */
67
66
  export type IncoseScope = 'lean' | 'full';
68
- /** graphcode's declared scope — see the module header for the rationale. */
67
+ /** graphcode's declared scope. */
69
68
  export declare const GRAPHCODE_INCOSE_SCOPE: IncoseScope;
70
- /**
71
- * Which rules the impl gates own: the milestone- and change-level family. The impl
72
- * gates ARE the milestone/CR burndown (SAR/FCA/SVR/FRR ↔ MS-1..MS-4), so a rule about
73
- * a milestone or a change request belongs there and not on a design review.
74
- *
75
- * Derived from the prefix over the contracts catalog (CR-GC-312) — a new `MS-`/`CR-R`
76
- * rule lands here without an edit. Before, this was `['MS-01','MS-02']` written out,
77
- * frozen at the two rules that existed when it was authored.
78
- */
79
- export declare const IMPL_GATE_RULES: readonly string[];
80
- /**
81
- * Phase-Readiness gates (INCOSE technical reviews) → the rules each gate owns.
82
- * DISJOINT + EXHAUSTIVE over the rule catalog; asserted by TEST-readiness-model
83
- * against SE_DESCRIPTOR at runtime, never a hardcoded count.
84
- *
85
- * DERIVED from `RULE_TO_PHASE` (CR-GC-312). This used to be a hand-written literal —
86
- * and contracts had already replaced it: CR-SM-226 removed `emergentPhase`/`phaseScore`
87
- * as "a third, undocumented phase construct next to RULE_TO_DIMENSION and the (then
88
- * still ad-hoc) phase-gate grouping", introducing `RULE_TO_PHASE` as the one
89
- * replacement, "exported so no consumer reinvents it". That ad-hoc grouping was THIS
90
- * object, and the replacement was never carried through here.
91
- *
92
- * The cost of leaving it: the literal and `RULE_TO_PHASE` had drifted apart on 21 of
93
- * 33 shared rules — R-01 SRR vs TRR, R-08 TRR vs PDR, RD-01..03 CDR vs SRR, and so on.
94
- * Two live gate assignments for the same rules, and readiness was scored against the
95
- * frozen one. Deriving moves those rules onto the assignment contracts declares; gate
96
- * scores change accordingly, which is the correction, not a side effect.
97
- */
98
- export declare const PHASE_GATE_RULES: Record<string, readonly string[]>;
99
- /** Human labels for the phase gates. */
100
- export declare const PHASE_GATE_LABELS: Record<string, string>;
101
- /**
102
- * Creation artifacts each phase gate requires (CR-GC-221): the judgment work
103
- * ("rule-green ≠ analysis-done") a rule-clean structure cannot prove was ever
104
- * done. A gate is only `passed` if its required creations are 🟢-current.
105
- * TRR carries none (tests are inline in the graph, not a separate creation).
106
- */
107
- export declare const PHASE_GATE_CREATIONS: Record<string, readonly string[]>;
108
- /** Impl gate → the phase whose creations it inherits as an anti-vacuous-green precondition. */
109
- export declare const IMPL_GATE_PHASE: Record<string, string>;
110
- /** Currency of a creation artifact: 🟢 current / 🟡 stale / 🔴 absent. */
111
- export type CreationCurrency = 'current' | 'stale' | 'absent';
112
- /**
113
- * Resolves whether a creation artifact is current/stale/absent (CR-GC-221 = the
114
- * INTERFACE; CR-GC-222's classifier supplies the real implementation). When no
115
- * provider is passed to `computeReadiness`, creation enforcement is OFF
116
- * (back-compat) — the gates still report their `creationArtifacts`, but currency
117
- * is not checked until 222 wires a provider in.
118
- */
119
- export type CreationCurrencyProvider = (creation: string) => CreationCurrency;
120
- /** Default stub provider (CR-GC-221, "Default 🔴"): everything absent until CR-GC-222 lands. */
121
- export declare const ABSENT_CREATION_PROVIDER: CreationCurrencyProvider;
122
- /** Format a "<Creation> not performed (<gate> creation)" blocking message.
123
- * Exported so the help layer (CR-GC-228) can identify creation blockers in
124
- * `ReadinessGate.blocking[]` without re-deriving the format (single source). */
125
- export declare function creationBlockingMsg(creation: string, gateId: string): string;
126
- /**
127
- * Implementation-Readiness gates (program/build acceptance) → milestone tier.
128
- * Ready iff every CR assigned to the MS is done + the MS scope is error-clean.
129
- */
130
- export declare const IMPL_GATE_MILESTONES: Record<string, {
131
- ms: string;
132
- label: string;
133
- }>;
134
- /**
135
- * A single readiness gate outcome.
136
- * `passed` is the hard verdict (no blocking/error item); `score` ∈ [0,1] is a
137
- * continuous maturity indicator; `open` items are advisory (warnings/CR debt).
138
- */
139
- export interface ReadinessGate {
140
- /** Gate id — SRR/PDR/CDR/TRR (phase) or SAR/FCA/SVR/FRR (impl). */
141
- id: string;
142
- /** Human-readable review name. */
69
+ /** Human labels for the marks. */
70
+ export declare const MARK_LABELS: Readonly<Record<MarkType, string>>;
71
+ /** One mark: reached or not, and the findings that hold it. */
72
+ export interface ReadinessMark {
73
+ id: MarkType;
74
+ /** Human-readable name. */
143
75
  label: string;
144
- /** True iff no blocking (error-severity / not-done) item remains. */
145
- passed: boolean;
146
- /** Continuous maturity in [0, 1]. */
147
- score: number;
148
- /** Blocking items that hold the gate closed (errors / open CRs / scope errors). */
149
- blocking: string[];
150
- /** Advisory items surfaced but not blocking (warnings, info, scope notes). */
151
- open: string[];
152
- /** Creation artifacts this gate requires (CR-GC-221) — judgment work, not rules. */
153
- creationArtifacts: string[];
154
- /**
155
- * Structural completeness of the derivation chain owned by this gate
156
- * (CR-GC-250) — orthogonal to violation severity. `covered/total` over the
157
- * DRIVING population, so absence counts (0 FCHAIN → all UCs incomplete → SRR
158
- * red). One value per gate for the dashboard; `missing[]` is the on-click
159
- * per-leg detail. A gate cannot pass while `covered < total`. Impl gates carry
160
- * a neutral `{0,0,[]}` (completeness is a phase-gate concept).
161
- */
162
- completeness: GateCompleteness;
76
+ /** True iff `holding` is empty. */
77
+ reached: boolean;
78
+ /** The findings that hold the mark — the answer to "why not". */
79
+ holding: RuleViolation[];
163
80
  }
81
+ /** The marks in order, each with the findings that hold it. */
82
+ export declare function computeMarks(violations: RuleViolation[]): ReadinessMark[];
164
83
  /**
165
- * Full readiness snapshot derived from V3_RULES evaluation + MS + status.
166
- * All violation ruleIds are contracts rule-IDs (R-xx / RD-xx / MS-xx) — never BQ-*.
84
+ * Full readiness snapshot derived from rule violations.
85
+ * All violation ruleIds are contracts rule-IDs — never a foreign rule set.
167
86
  */
168
87
  export interface ReadinessReport {
169
88
  /**
@@ -173,48 +92,32 @@ export interface ReadinessReport {
173
92
  compliance: ReadinessDimension;
174
93
  /** Declared INCOSE artifact scope for this system (graphcode = `lean`). */
175
94
  incoseScope: IncoseScope;
176
- /** Phase-Readiness gates (INCOSE technical reviews) in lifecycle order. */
177
- phaseGates: ReadinessGate[];
178
- /** Implementation-Readiness gates (program/build acceptance) in tier order. */
179
- implGates: ReadinessGate[];
180
- /**
181
- * Per-contracts-rule-ID violation counts. Keys are exactly the
182
- * `SE_DESCRIPTOR.rules` members that fired. Foreign BQ-* IDs cannot appear —
183
- * the scorer delegates exclusively to the V3 rule engine.
184
- */
95
+ /** The marks SRR, PDR, CDR, TRR, Bau — in order. */
96
+ marks: ReadinessMark[];
97
+ /** Per-contracts-rule-ID violation counts. */
185
98
  violationsByRule: Record<string, number>;
186
99
  /** All raw violations, ordered by severity (error → warning → info). */
187
100
  violations: RuleViolation[];
188
- /**
189
- * Whether creation-artifact currency was ENFORCED in this report (CR-GC-259).
190
- * `off` means no `CreationCurrencyProvider` was supplied, so every gate's
191
- * `creationArtifacts` was REPORTED but never checked — a gate can read
192
- * `passed` with a never-performed FMEA/ConOps behind it. Made explicit because
193
- * a silently-unenforced gate is the same false-green class CR-GC-250 closed:
194
- * a consumer cannot distinguish "no creation blockers" from "not checked".
195
- * graphcode itself has no classifier yet (the real provider is fed by the
196
- * dashboard), so the product path reports `off` until one lands.
197
- */
198
- creationEnforcement: 'on' | 'off';
199
101
  /** Timestamp (ISO 8601) when this report was computed. */
200
102
  computedAt: string;
201
103
  }
202
104
  /**
203
105
  * Summary projection of a ReadinessReport (CR-GC-203 item 2): drops the heavy
204
- * per-element lists — `violations` and every gate's `blocking`/`open` — while
205
- * keeping scores, counts and `violationsByRule`. `graph_readiness` returns this
106
+ * per-element lists — `violations` and every mark's `holding` — while keeping
107
+ * `reached`, counts and `violationsByRule`. `graph_readiness` returns this
206
108
  * by default; `detail:true` returns the full lists. On a fully-red graph the
207
- * full report inlined every blocking element (86k+ chars, past the MCP tool
109
+ * full report inlined every finding (86k+ chars, past the MCP tool
208
110
  * result limit) forcing a file-spill; the summary stays small.
209
111
  */
210
112
  export declare function summarizeReadiness(report: ReadinessReport): ReadinessReport;
211
113
  /**
212
114
  * Compute readiness from a violations array + the full graph (nodes + edges).
213
115
  *
214
- * @param violations - from `harness.evaluateRules()` (contracts V3 rule IDs)
116
+ * @param violations - from `harness.evaluateRules()` (contracts rule IDs), UNFILTERED: a finding
117
+ * the human accepted with a reason still holds its mark (CR-SM-396).
215
118
  * @param graph - from `harness.getGraph()` (elements + traces)
216
119
  */
217
- export declare function computeReadiness(violations: RuleViolation[], graph: Pick<Graph, 'nodes' | 'edges'>, creationCurrency?: CreationCurrencyProvider): ReadinessReport;
120
+ export declare function computeReadiness(violations: RuleViolation[], graph: Pick<Graph, 'nodes' | 'edges'>): ReadinessReport;
218
121
  /**
219
122
  * Convenience wrapper: runs `evaluateRules()` + `getGraph()` on the harness
220
123
  * and returns the full ReadinessReport.
package/dist/readiness.js CHANGED
@@ -1,278 +1,68 @@
1
1
  import { SE_DESCRIPTOR } from '@sigloch/graph-api-core';
2
- import { CODE_CONFORMANCE_RULES, RULE_TO_PHASE } from '@sigloch/contracts/se';
3
- import { scoreCompleteness, NEUTRAL_COMPLETENESS, } from './readiness-completeness.js';
4
- /** graphcode's declared scope — see the module header for the rationale. */
2
+ import { ALL_RULE_DEFS, CODE_CONFORMANCE_RULES, Mark, MARK_STAGE } from '@sigloch/contracts/se';
3
+ /** graphcode's declared scope. */
5
4
  export const GRAPHCODE_INCOSE_SCOPE = 'lean';
6
- /**
7
- * Which rules the impl gates own: the milestone- and change-level family. The impl
8
- * gates ARE the milestone/CR burndown (SAR/FCA/SVR/FRR ↔ MS-1..MS-4), so a rule about
9
- * a milestone or a change request belongs there and not on a design review.
10
- *
11
- * Derived from the prefix over the contracts catalog (CR-GC-312) — a new `MS-`/`CR-R`
12
- * rule lands here without an edit. Before, this was `['MS-01','MS-02']` written out,
13
- * frozen at the two rules that existed when it was authored.
14
- */
15
- export const IMPL_GATE_RULES = Object.keys(RULE_TO_PHASE)
16
- .filter((id) => /^(MS-|CR-R)/.test(id))
17
- .sort();
18
- /**
19
- * `RULE_TO_PHASE` covers the whole catalog including the CODING profile (`BQ-` and
20
- * `ND-` prefixes). The readiness model is the SE profile only — `SE_DESCRIPTOR.rules`
21
- * deliberately excludes those, so a gate must not claim one and then never see it fire.
22
- */
23
- const isCodingProfile = (id) => /^(BQ-|ND-)/.test(id);
24
- /**
25
- * Which conformance rule sits on which gate. RC-* are deliberately outside
26
- * `ALL_RULE_DEFS` (they need `CodeFacts` from a repo checkout, which contracts has no
27
- * access to), so `RULE_TO_PHASE` cannot carry them and this is the one legitimate
28
- * local addition.
29
- *
30
- * Still not hand-picked: each RC rule inherits the gate of the PRESENCE rule it
31
- * resolves — the Presence/Resolution split contracts documents (R-19/R-20/R-26 declare
32
- * a binding, RC-01/02/03/04 check it actually resolves on disk). RC-05 (cross-module
33
- * import drift) has no presence partner; it is module coupling, so it follows R-23.
34
- *
35
- * CR-SM-262: RC-06 folgt RC-05 aus demselben Grund. Ein Praesenz-Partner kaeme nicht in
36
- * Frage — die Regel prueft ausschliesslich `external === true`, und genau diese Knoten sind
37
- * von R-20 AUSGENOMMEN ("exempt from the R-20 realRef-binding requirement", ELEMENT_ATTRIBUTES).
38
- * Sie loest also keine Praesenzforderung auf, sondern stellt eine eigene: das Paket, auf das
39
- * eine externe Bindung zeigt, muss in den dependencies stehen. Ihr Gegenstand ist damit die
40
- * Paket-Abhaengigkeit, die groebste Form von Modul-Kopplung — dieselbe Kategorie wie RC-05.
41
- */
42
- const RC_PRESENCE_PARTNER = {
43
- 'RC-01': 'R-20', // FUNC realRef → declared symbol
44
- 'RC-02': 'R-19', // testRefs → runnable test cases
45
- 'RC-03': 'R-26', // SCHEMA realRef → declared export
46
- 'RC-04': 'R-26', // SCHEMA realRef parsed at its interface
47
- 'RC-05': 'R-23', // cross-module import the graph does not document
48
- 'RC-06': 'R-23', // external binding on a package the consumer does not depend on
49
- 'RC-07': 'CR-R01', // CR node agrees with docs/cr — the CR presence rule's gate (CR-SM-329)
50
- 'RC-08': 'R-26', // SCHEMA realRef is a Zod schema, not a type (CR-SM-358)
51
- 'RC-09': 'R-26', // SCHEMA parsed only at its modelled interface — code → model (CR-SM-358)
52
- 'RC-10': 'R-23', // MOD without resolvable files — RC-05 is blind there, module coupling (CR-SM-377)
53
- };
54
- /**
55
- * Phase-Readiness gates (INCOSE technical reviews) → the rules each gate owns.
56
- * DISJOINT + EXHAUSTIVE over the rule catalog; asserted by TEST-readiness-model
57
- * against SE_DESCRIPTOR at runtime, never a hardcoded count.
58
- *
59
- * DERIVED from `RULE_TO_PHASE` (CR-GC-312). This used to be a hand-written literal —
60
- * and contracts had already replaced it: CR-SM-226 removed `emergentPhase`/`phaseScore`
61
- * as "a third, undocumented phase construct next to RULE_TO_DIMENSION and the (then
62
- * still ad-hoc) phase-gate grouping", introducing `RULE_TO_PHASE` as the one
63
- * replacement, "exported so no consumer reinvents it". That ad-hoc grouping was THIS
64
- * object, and the replacement was never carried through here.
65
- *
66
- * The cost of leaving it: the literal and `RULE_TO_PHASE` had drifted apart on 21 of
67
- * 33 shared rules — R-01 SRR vs TRR, R-08 TRR vs PDR, RD-01..03 CDR vs SRR, and so on.
68
- * Two live gate assignments for the same rules, and readiness was scored against the
69
- * frozen one. Deriving moves those rules onto the assignment contracts declares; gate
70
- * scores change accordingly, which is the correction, not a side effect.
71
- */
72
- export const PHASE_GATE_RULES = (() => {
73
- const implOwned = new Set(IMPL_GATE_RULES);
74
- const gates = { SRR: [], PDR: [], CDR: [], TRR: [] };
75
- for (const [ruleId, gate] of Object.entries(RULE_TO_PHASE)) {
76
- if (implOwned.has(ruleId))
77
- continue; // the impl axis owns these
78
- if (isCodingProfile(ruleId))
79
- continue; // not part of the SE readiness model
80
- gates[gate]?.push(ruleId);
81
- }
82
- for (const [rcId, partner] of Object.entries(RC_PRESENCE_PARTNER)) {
83
- const gate = RULE_TO_PHASE[partner];
84
- if (gate)
85
- gates[gate].push(rcId);
86
- }
87
- for (const ids of Object.values(gates))
88
- ids.sort();
89
- return gates;
90
- })();
91
- /** Human labels for the phase gates. */
92
- export const PHASE_GATE_LABELS = {
5
+ // ---------------------------------------------------------------------------
6
+ // Marks (CR-SM-395)
7
+ // ---------------------------------------------------------------------------
8
+ /** Human labels for the marks. */
9
+ export const MARK_LABELS = {
93
10
  SRR: 'System Requirements Review',
94
11
  PDR: 'Preliminary Design Review',
95
12
  CDR: 'Critical Design Review',
96
13
  TRR: 'Test Readiness Review',
14
+ Bau: 'Bau',
97
15
  };
98
- /**
99
- * Creation artifacts each phase gate requires (CR-GC-221): the judgment work
100
- * ("rule-green ≠ analysis-done") a rule-clean structure cannot prove was ever
101
- * done. A gate is only `passed` if its required creations are 🟢-current.
102
- * TRR carries none (tests are inline in the graph, not a separate creation).
103
- */
104
- export const PHASE_GATE_CREATIONS = {
105
- SRR: ['conops', 'assumption-review'],
106
- PDR: ['fmea', 'trade'],
107
- CDR: ['implplan'],
108
- TRR: [],
109
- };
110
- /** Human labels for creation artifacts (used in blocking messages). */
111
- const CREATION_LABELS = {
112
- conops: 'ConOps',
113
- 'assumption-review': 'Assumption Review',
114
- fmea: 'FMEA',
115
- trade: 'Trade study',
116
- implplan: 'Impl plan',
117
- };
118
- /** Impl gate → the phase whose creations it inherits as an anti-vacuous-green precondition. */
119
- export const IMPL_GATE_PHASE = {
120
- SAR: 'SRR',
121
- FCA: 'PDR',
122
- SVR: 'CDR',
123
- FRR: 'TRR',
124
- };
125
- /** Default stub provider (CR-GC-221, "Default 🔴"): everything absent until CR-GC-222 lands. */
126
- export const ABSENT_CREATION_PROVIDER = () => 'absent';
127
- /** Format a "<Creation> not performed (<gate> creation)" blocking message.
128
- * Exported so the help layer (CR-GC-228) can identify creation blockers in
129
- * `ReadinessGate.blocking[]` without re-deriving the format (single source). */
130
- export function creationBlockingMsg(creation, gateId) {
131
- return `${CREATION_LABELS[creation] ?? creation} not performed (${gateId} creation)`;
16
+ const RULES = new Map(ALL_RULE_DEFS.map((r) => [r.id, r]));
17
+ /** Does this finding hold this mark? See the module header for the rule. */
18
+ function holds(v, mark) {
19
+ if (v.severity === 'info')
20
+ return false; // a note holds no mark (CR-SM-396)
21
+ const rule = RULES.get(v.ruleId);
22
+ // A finding of a rule the catalog does not know cannot be placed on the ladder: like `'immer'`.
23
+ const stage = rule?.stage ?? 'immer';
24
+ if (stage !== 'immer' && stage > MARK_STAGE[mark])
25
+ return false;
26
+ if (v.severity === 'error' || rule?.role === 'existence')
27
+ return true;
28
+ return mark === 'Bau' && stage !== 'immer' && stage > MARK_STAGE.TRR;
29
+ }
30
+ /** The marks in order, each with the findings that hold it. */
31
+ export function computeMarks(violations) {
32
+ return Mark.options.map((id) => {
33
+ const holding = violations.filter((v) => holds(v, id));
34
+ return { id, label: MARK_LABELS[id], reached: holding.length === 0, holding };
35
+ });
132
36
  }
133
- /**
134
- * Implementation-Readiness gates (program/build acceptance) → milestone tier.
135
- * Ready iff every CR assigned to the MS is done + the MS scope is error-clean.
136
- */
137
- export const IMPL_GATE_MILESTONES = {
138
- SAR: { ms: 'MS-1-specification', label: 'System Acceptance Review' },
139
- FCA: { ms: 'MS-2-coding-vv', label: 'Functional Configuration Audit' },
140
- SVR: { ms: 'MS-3-mvp-readiness', label: 'System Verification Review' },
141
- FRR: { ms: 'MS-4-mvp2', label: 'Functional Readiness Review' },
142
- };
143
37
  /**
144
38
  * Summary projection of a ReadinessReport (CR-GC-203 item 2): drops the heavy
145
- * per-element lists — `violations` and every gate's `blocking`/`open` — while
146
- * keeping scores, counts and `violationsByRule`. `graph_readiness` returns this
39
+ * per-element lists — `violations` and every mark's `holding` — while keeping
40
+ * `reached`, counts and `violationsByRule`. `graph_readiness` returns this
147
41
  * by default; `detail:true` returns the full lists. On a fully-red graph the
148
- * full report inlined every blocking element (86k+ chars, past the MCP tool
42
+ * full report inlined every finding (86k+ chars, past the MCP tool
149
43
  * result limit) forcing a file-spill; the summary stays small.
150
44
  */
151
45
  export function summarizeReadiness(report) {
152
- const stripGate = (g) => ({
153
- ...g,
154
- blocking: [],
155
- open: [],
156
- // Keep the one-value covered/total for the dashboard; drop the per-leg drill-down.
157
- completeness: { ...g.completeness, missing: [] },
158
- });
159
46
  return {
160
47
  ...report,
161
- phaseGates: report.phaseGates.map(stripGate),
162
- implGates: report.implGates.map(stripGate),
48
+ marks: report.marks.map((m) => ({ ...m, holding: [] })),
163
49
  violations: [],
164
50
  };
165
51
  }
166
52
  // ---------------------------------------------------------------------------
167
53
  // Scorer
168
54
  // ---------------------------------------------------------------------------
169
- /** Group violations by element-id for per-element error lookups. */
170
- function elementsWithErrorSet(violations) {
171
- return new Set(violations
172
- .filter((v) => v.severity === 'error' && v.elementId !== undefined)
173
- .map((v) => v.elementId));
174
- }
175
- /**
176
- * Score one phase gate from the violations whose ruleId it owns, plus its
177
- * required creation artifacts (CR-GC-221): a gate is only `passed` if it is
178
- * rule-clean AND every required creation is 🟢-current. When `currency` is
179
- * undefined, creation enforcement is OFF (back-compat) — `creationArtifacts`
180
- * is still reported. Creations affect `passed`/`blocking`, not the rule `score`.
181
- */
182
- function scorePhaseGate(id, ruleIds, violations, creations, graph, currency) {
183
- const owned = violations.filter((v) => ruleIds.includes(v.ruleId));
184
- const errors = owned.filter((v) => v.severity === 'error');
185
- const rulesWithError = new Set(errors.map((v) => v.ruleId));
186
- const ruleBlocking = errors.map((v) => `${v.ruleId}: ${v.message}`);
187
- const open = owned.filter((v) => v.severity !== 'error').map((v) => `${v.ruleId}: ${v.message}`);
188
- // Phase gate requires every creation 🟢-current — stale OR absent blocks.
189
- const creationBlocking = currency
190
- ? creations.filter((c) => currency(c) !== 'current').map((c) => creationBlockingMsg(c, id))
191
- : [];
192
- // Structural completeness (CR-GC-250): one aggregated blocking line, not N warnings.
193
- // CR-SM-226: reads off the SAME violations array (the rule stream), no re-walk.
194
- const completeness = scoreCompleteness(id, graph, violations);
195
- const complete = completeness.covered === completeness.total;
196
- const completenessBlocking = complete
197
- ? []
198
- : [`${id} completeness ${completeness.covered}/${completeness.total} — ${completeness.missing.join('; ')}`];
199
- const blocking = [...ruleBlocking, ...creationBlocking, ...completenessBlocking];
200
- // Maturity = share of the gate's hard checks with no error, floored by completeness coverage:
201
- // a gate cannot read green while a mandated chain leg is empty.
202
- const ruleScore = ruleIds.length > 0 ? (ruleIds.length - rulesWithError.size) / ruleIds.length : 1;
203
- const completenessRatio = completeness.total > 0 ? completeness.covered / completeness.total : 1;
204
- return {
205
- id,
206
- label: PHASE_GATE_LABELS[id] ?? id,
207
- passed: rulesWithError.size === 0 && creationBlocking.length === 0 && complete,
208
- score: Math.min(ruleScore, completenessRatio),
209
- blocking,
210
- open,
211
- creationArtifacts: [...creations],
212
- completeness,
213
- };
214
- }
215
- /**
216
- * Score one implementation gate from its milestone's CRs + scope (MS + status),
217
- * plus an anti-vacuous-green creation check (CR-GC-221): the gate additionally
218
- * blocks when a creation required by its phase is 🔴 **absent** — even with no
219
- * representing CR (a never-done FMEA must not pass silently). Only `absent`
220
- * blocks here (not `stale`); enforcement is off when `currency` is undefined.
221
- */
222
- function scoreImplGate(id, ms, label, graph, elementsWithError, creations, currency) {
223
- const msNode = graph.nodes.find((n) => n.uid === ms);
224
- if (!msNode) {
225
- return {
226
- id,
227
- label,
228
- passed: false,
229
- score: 0,
230
- blocking: [`milestone ${ms} missing`],
231
- open: [],
232
- creationArtifacts: [...creations],
233
- completeness: NEUTRAL_COMPLETENESS,
234
- };
235
- }
236
- const nodeById = new Map(graph.nodes.map((n) => [n.uid, n]));
237
- // CRs assigned to this milestone: CR -relation-> MS (the MS-01 scope semantics).
238
- const crs = graph.edges
239
- .filter((e) => e.edgeType === 'relation' && e.targetId === ms)
240
- .map((e) => nodeById.get(e.sourceId))
241
- .filter((n) => !!n && n.type === 'CR');
242
- const openCrs = crs.filter((c) => c.attributes?.status !== 'done');
243
- // Milestone scope: composed elements (MS -compose-> X) must be error-clean.
244
- const scopeErrors = graph.edges
245
- .filter((e) => e.sourceId === ms && e.edgeType === 'compose')
246
- .map((e) => e.targetId)
247
- .filter((id2) => elementsWithError.has(id2));
248
- // Anti-vacuous-green: a phase creation that is 🔴 absent blocks the impl gate
249
- // even when every CR is done and the scope is clean (the analysis was never done).
250
- const creationBlocking = currency
251
- ? creations.filter((c) => currency(c) === 'absent').map((c) => creationBlockingMsg(c, id))
252
- : [];
253
- const blocking = [
254
- ...openCrs.map((c) => `${c.uid} not done`),
255
- ...scopeErrors.map((id2) => `${id2} has an error-severity violation`),
256
- ...creationBlocking,
257
- ];
258
- const msStatus = msNode.attributes?.status;
259
- const score = crs.length > 0
260
- ? (crs.length - openCrs.length) / crs.length
261
- : msStatus === 'done' || msStatus === 'reviewed' ? 1 : 0;
262
- return { id, label, passed: blocking.length === 0, score, blocking, open: [], creationArtifacts: [...creations], completeness: NEUTRAL_COMPLETENESS };
263
- }
264
55
  /**
265
56
  * Compute readiness from a violations array + the full graph (nodes + edges).
266
57
  *
267
- * @param violations - from `harness.evaluateRules()` (contracts V3 rule IDs)
58
+ * @param violations - from `harness.evaluateRules()` (contracts rule IDs), UNFILTERED: a finding
59
+ * the human accepted with a reason still holds its mark (CR-SM-396).
268
60
  * @param graph - from `harness.getGraph()` (elements + traces)
269
61
  */
270
- export function computeReadiness(violations, graph, creationCurrency) {
62
+ export function computeReadiness(violations, graph) {
271
63
  const totalElements = graph.nodes.length;
272
- const elementsWithError = elementsWithErrorSet(violations);
273
- const elementsWithErrors = elementsWithError.size;
64
+ const elementsWithErrors = new Set(violations.filter((v) => v.severity === 'error' && v.elementId !== undefined).map((v) => v.elementId)).size;
274
65
  const complianceScore = totalElements > 0 ? (totalElements - elementsWithErrors) / totalElements : 1;
275
- // Violation counts keyed by contracts rule-ID (never BQ-*).
276
66
  const violationsByRule = {};
277
67
  for (const v of violations) {
278
68
  violationsByRule[v.ruleId] = (violationsByRule[v.ruleId] ?? 0) + 1;
@@ -280,8 +70,6 @@ export function computeReadiness(violations, graph, creationCurrency) {
280
70
  // Sort: error → warning → info.
281
71
  const severityRank = { error: 0, warning: 1, info: 2 };
282
72
  const sorted = [...violations].sort((a, b) => (severityRank[a.severity] ?? 3) - (severityRank[b.severity] ?? 3));
283
- const phaseGates = Object.entries(PHASE_GATE_RULES).map(([id, ruleIds]) => scorePhaseGate(id, ruleIds, violations, PHASE_GATE_CREATIONS[id] ?? [], graph, creationCurrency));
284
- const implGates = Object.entries(IMPL_GATE_MILESTONES).map(([id, { ms, label }]) => scoreImplGate(id, ms, label, graph, elementsWithError, PHASE_GATE_CREATIONS[IMPL_GATE_PHASE[id]] ?? [], creationCurrency));
285
73
  return {
286
74
  compliance: {
287
75
  score: complianceScore,
@@ -290,11 +78,9 @@ export function computeReadiness(violations, graph, creationCurrency) {
290
78
  elementsWithErrors,
291
79
  },
292
80
  incoseScope: GRAPHCODE_INCOSE_SCOPE,
293
- phaseGates,
294
- implGates,
81
+ marks: computeMarks(violations),
295
82
  violationsByRule,
296
83
  violations: sorted,
297
- creationEnforcement: creationCurrency ? 'on' : 'off',
298
84
  computedAt: new Date().toISOString(),
299
85
  };
300
86
  }
@@ -306,9 +92,7 @@ export function computeReadiness(violations, graph, creationCurrency) {
306
92
  * directly without importing its class (avoids circular dep risk in consumers).
307
93
  */
308
94
  export function scoreReadiness(harness) {
309
- const violations = harness.evaluateRules();
310
- const graph = harness.getGraph();
311
- return computeReadiness(violations, graph);
95
+ return computeReadiness(harness.evaluateRules(), harness.getGraph());
312
96
  }
313
97
  /**
314
98
  * The authoritative set of family rule-IDs (derived at runtime from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/graphcode-client",
3
- "version": "1.5.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,11 +40,11 @@
40
40
  "zod": "^4.3.6"
41
41
  },
42
42
  "peerDependencies": {
43
- "@sigloch/contracts": ">=10 <11",
43
+ "@sigloch/contracts": ">=11 <12",
44
44
  "@sigloch/graph-api-core": ">=5 <6"
45
45
  },
46
46
  "devDependencies": {
47
- "@sigloch/contracts": ">=10 <11",
47
+ "@sigloch/contracts": ">=11 <12",
48
48
  "@sigloch/graph-api-core": ">=5.4 <6"
49
49
  }
50
50
  }
@@ -1,71 +0,0 @@
1
- /**
2
- * readiness-completeness.ts — the COMPLETENESS dimension of readiness (CR-GC-250,
3
- * split out of readiness.ts by CR-GC-260; rebuilt on the rule stream CR-SM-226).
4
- *
5
- * Its own module because it answers a different question than the rest of readiness:
6
- * compliance/gates ask "does any element violate a rule", completeness asks "is the
7
- * structural chain covered at all" — the absence case rules cannot see (0 FCHAIN → a
8
- * per-element warning rule fires 0 times and the gate reads green).
9
- *
10
- * CR-SM-226: every leg now reads off a named `@sigloch/contracts` rule instead of
11
- * re-walking the graph locally. The former local out-edge/actor-boundary/testRefs/
12
- * codeRef/leaf-function helpers were a SECOND, drifting implementation of exactly
13
- * what R-15/UC-01/UC-03/FC-04/SC-04/R-19/R-20 already check in `@sigloch/contracts/se`
14
- * — deleted. The rule engine stays the single source of truth; this file only
15
- * re-groups its violations by the DRIVING population per leg, so an EMPTY
16
- * population still counts against the gate (the vacuous-complete case a
17
- * per-element warning rule cannot see on its own — closed for real by
18
- * `@sigloch/contracts/se`'s R-28 Ebenen-Präsenz, CR-SM-226; die Regel selbst ist mit CR-SM-247 entfallen).
19
- *
20
- * BROWSER-SAFE — no `node:*` import may enter this file: the panels layer builds on it.
21
- *
22
- * @author andreas@siglochconsulting
23
- */
24
- import { z } from 'zod/v4';
25
- import type { Graph } from '@sigloch/graph-api-core';
26
- import type { RuleViolation } from '@sigloch/contracts/harness';
27
- export type CGraph = Pick<Graph, 'nodes' | 'edges'>;
28
- export type CNode = Graph['nodes'][number];
29
- /** A gate's structural completeness over its driving population — ein Zod-Vertrag (CR-SM-361). */
30
- export declare const GateCompleteness: z.ZodObject<{
31
- covered: z.ZodNumber;
32
- total: z.ZodNumber;
33
- missing: z.ZodArray<z.ZodString>;
34
- }, z.core.$strip>;
35
- export type GateCompleteness = z.infer<typeof GateCompleteness>;
36
- /** Neutral completeness for gates that carry no chain slice (impl gates). */
37
- export declare const NEUTRAL_COMPLETENESS: GateCompleteness;
38
- export declare const typeOf: (byId: Map<string, CNode>, id: string) => string | undefined;
39
- /**
40
- * One completeness leg: every node of `sourceType` (the driving population)
41
- * must carry NO violation of `ruleId` (its `@sigloch/contracts` rule twin),
42
- * else it is a `missing` source for the owning gate. Cardinality legs use the
43
- * id `"<SRC>→<TGT>"` so the drift test can match them against the
44
- * meta-model's `1..*` patterns.
45
- */
46
- export interface CompletenessLeg {
47
- id: string;
48
- sourceType: string;
49
- ruleId: string;
50
- }
51
- /**
52
- * Phase-gate → chain legs that must be complete AT THAT PHASE, each backed by
53
- * its `@sigloch/contracts` rule twin (CR-SM-226):
54
- * SRR: SYS→UC ≈ R-17 (SYS must have compose; not UC-typed specifically —
55
- * no dedicated SYS→UC rule exists, R-17 is the closest twin),
56
- * UC→FCHAIN = UC-03, UC→REQ = UC-01.
57
- * PDR: FCHAIN→FUNC = R-15, FCHAIN-actor-bounded = FC-04 (trigger+consumer).
58
- * CDR: FLOW→SCHEMA = R-18 (seit CR-SM-271: die Untergrenze ist Grammatik,
59
- * `FLOW -relation-> SCHEMA [1..1]` — SC-04 existiert nicht mehr; ein
60
- * R-18-Befund am FLOW heisst "Kanten ungrammatisch", inkl. fehlendem
61
- * oder doppeltem SCHEMA).
62
- * TRR: TEST.testRefs = R-19, FUNC.realRef = R-20.
63
- */
64
- export declare const COMPLETENESS_SLICES: Record<string, CompletenessLeg[]>;
65
- /**
66
- * Score one phase gate's structural completeness over its driving population.
67
- * `total === 0` (no source elements to cover) → vacuously complete (ratio 1).
68
- * `violations` is the full rule-stream output (e.g. `harness.evaluateRules()`);
69
- * only the leg's own `ruleId` is consulted per node.
70
- */
71
- export declare function scoreCompleteness(gateId: string, graph: CGraph, violations: RuleViolation[]): GateCompleteness;
@@ -1,100 +0,0 @@
1
- /**
2
- * readiness-completeness.ts — the COMPLETENESS dimension of readiness (CR-GC-250,
3
- * split out of readiness.ts by CR-GC-260; rebuilt on the rule stream CR-SM-226).
4
- *
5
- * Its own module because it answers a different question than the rest of readiness:
6
- * compliance/gates ask "does any element violate a rule", completeness asks "is the
7
- * structural chain covered at all" — the absence case rules cannot see (0 FCHAIN → a
8
- * per-element warning rule fires 0 times and the gate reads green).
9
- *
10
- * CR-SM-226: every leg now reads off a named `@sigloch/contracts` rule instead of
11
- * re-walking the graph locally. The former local out-edge/actor-boundary/testRefs/
12
- * codeRef/leaf-function helpers were a SECOND, drifting implementation of exactly
13
- * what R-15/UC-01/UC-03/FC-04/SC-04/R-19/R-20 already check in `@sigloch/contracts/se`
14
- * — deleted. The rule engine stays the single source of truth; this file only
15
- * re-groups its violations by the DRIVING population per leg, so an EMPTY
16
- * population still counts against the gate (the vacuous-complete case a
17
- * per-element warning rule cannot see on its own — closed for real by
18
- * `@sigloch/contracts/se`'s R-28 Ebenen-Präsenz, CR-SM-226; die Regel selbst ist mit CR-SM-247 entfallen).
19
- *
20
- * BROWSER-SAFE — no `node:*` import may enter this file: the panels layer builds on it.
21
- *
22
- * @author andreas@siglochconsulting
23
- */
24
- import { z } from 'zod/v4';
25
- /** A gate's structural completeness over its driving population — ein Zod-Vertrag (CR-SM-361). */
26
- export const GateCompleteness = z.object({
27
- /** Source elements whose chain leg is complete. */
28
- covered: z.number().int().nonnegative(),
29
- /** Total source elements evaluated across the gate's legs (the denominator). */
30
- total: z.number().int().nonnegative(),
31
- /** Per-leg misses as `"<leg>: <uid>"` — the on-click drill-down detail. */
32
- missing: z.array(z.string()),
33
- });
34
- /** Neutral completeness for gates that carry no chain slice (impl gates). */
35
- export const NEUTRAL_COMPLETENESS = GateCompleteness.parse({ covered: 0, total: 0, missing: [] });
36
- export const typeOf = (byId, id) => byId.get(id)?.type;
37
- /**
38
- * Phase-gate → chain legs that must be complete AT THAT PHASE, each backed by
39
- * its `@sigloch/contracts` rule twin (CR-SM-226):
40
- * SRR: SYS→UC ≈ R-17 (SYS must have compose; not UC-typed specifically —
41
- * no dedicated SYS→UC rule exists, R-17 is the closest twin),
42
- * UC→FCHAIN = UC-03, UC→REQ = UC-01.
43
- * PDR: FCHAIN→FUNC = R-15, FCHAIN-actor-bounded = FC-04 (trigger+consumer).
44
- * CDR: FLOW→SCHEMA = R-18 (seit CR-SM-271: die Untergrenze ist Grammatik,
45
- * `FLOW -relation-> SCHEMA [1..1]` — SC-04 existiert nicht mehr; ein
46
- * R-18-Befund am FLOW heisst "Kanten ungrammatisch", inkl. fehlendem
47
- * oder doppeltem SCHEMA).
48
- * TRR: TEST.testRefs = R-19, FUNC.realRef = R-20.
49
- */
50
- export const COMPLETENESS_SLICES = {
51
- // Behavioral spine exists: system has use cases, every UC derives a chain AND requirements.
52
- SRR: [
53
- { id: 'SYS→UC', sourceType: 'SYS', ruleId: 'R-17' },
54
- { id: 'UC→FCHAIN', sourceType: 'UC', ruleId: 'UC-03' },
55
- { id: 'UC→REQ', sourceType: 'UC', ruleId: 'UC-01' },
56
- ],
57
- // Functional architecture: every chain has functions AND is actor-bounded (trigger + consumer).
58
- PDR: [
59
- { id: 'FCHAIN→FUNC', sourceType: 'FCHAIN', ruleId: 'R-15' },
60
- { id: 'FCHAIN-actor-bounded', sourceType: 'FCHAIN', ruleId: 'FC-04' },
61
- ],
62
- // Interfaces carry a data contract: every FLOW has a SCHEMA (CR-GC-106 schema-before-code).
63
- // CR-SM-271: SC-04 entfiel — der Fall meldet als R-18 (Untergrenzen-Bein) am FLOW.
64
- CDR: [
65
- { id: 'FLOW→SCHEMA', sourceType: 'FLOW', ruleId: 'R-18' },
66
- ],
67
- // Binding: tests are runnable and functions carry code — concept/external exemptions
68
- // are R-19/R-20's own business now (single source of truth), not re-decided here.
69
- TRR: [
70
- { id: 'TEST.testRefs', sourceType: 'TEST', ruleId: 'R-19' },
71
- { id: 'FUNC.realRef', sourceType: 'FUNC', ruleId: 'R-20' },
72
- ],
73
- };
74
- /**
75
- * Score one phase gate's structural completeness over its driving population.
76
- * `total === 0` (no source elements to cover) → vacuously complete (ratio 1).
77
- * `violations` is the full rule-stream output (e.g. `harness.evaluateRules()`);
78
- * only the leg's own `ruleId` is consulted per node.
79
- */
80
- export function scoreCompleteness(gateId, graph, violations) {
81
- const legs = COMPLETENESS_SLICES[gateId] ?? [];
82
- let covered = 0;
83
- let total = 0;
84
- const missing = [];
85
- for (const leg of legs) {
86
- const violatingIds = new Set(violations
87
- .filter((v) => v.ruleId === leg.ruleId && v.elementId !== undefined)
88
- .map((v) => v.elementId));
89
- for (const node of graph.nodes) {
90
- if (node.type !== leg.sourceType)
91
- continue;
92
- total += 1;
93
- if (violatingIds.has(node.uid))
94
- missing.push(`${leg.id}: ${node.uid}`);
95
- else
96
- covered += 1;
97
- }
98
- }
99
- return GateCompleteness.parse({ covered, total, missing }); // CR-SM-361: der Erzeuger prueft seinen Vertrag
100
- }