@nforma.ai/nforma 0.2.1
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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formal expected value extraction for P->F residual layer
|
|
3
|
+
* Parses formal_ref strings and loads parameter values from spec files
|
|
4
|
+
* Fail-open: returns null on any error (missing file, bad parse, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a formal_ref string into its components
|
|
14
|
+
* Formats:
|
|
15
|
+
* "spec:{module}/{file}:{param}" -> { type: 'spec', module, file, param }
|
|
16
|
+
* "requirement:{id}" -> { type: 'requirement', id }
|
|
17
|
+
* null/invalid -> null
|
|
18
|
+
* @param {string} formalRef
|
|
19
|
+
* @returns {object|null}
|
|
20
|
+
*/
|
|
21
|
+
function parseFormalRef(formalRef) {
|
|
22
|
+
if (!formalRef || typeof formalRef !== 'string') return null;
|
|
23
|
+
|
|
24
|
+
if (formalRef.startsWith('requirement:')) {
|
|
25
|
+
return { type: 'requirement', id: formalRef.slice('requirement:'.length) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (formalRef.startsWith('spec:')) {
|
|
29
|
+
const rest = formalRef.slice('spec:'.length);
|
|
30
|
+
// Format: module/file:param
|
|
31
|
+
const colonIdx = rest.lastIndexOf(':');
|
|
32
|
+
if (colonIdx === -1) {
|
|
33
|
+
// No param key — invariant reference (e.g., "spec:safety/invariant-consistency")
|
|
34
|
+
return { type: 'spec', path: rest, param: null };
|
|
35
|
+
}
|
|
36
|
+
const filePath = rest.slice(0, colonIdx);
|
|
37
|
+
const param = rest.slice(colonIdx + 1);
|
|
38
|
+
return { type: 'spec', path: filePath, param: param || null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract formal expected value from a spec file
|
|
46
|
+
* Supports .cfg files (line-based key=value) and .json files
|
|
47
|
+
* @param {string} formalRef - Formal reference string
|
|
48
|
+
* @param {object} [options]
|
|
49
|
+
* @param {string} [options.specDir] - Override default spec directory (for testing)
|
|
50
|
+
* @returns {*} The parameter value, or null if not found
|
|
51
|
+
*/
|
|
52
|
+
function extractFormalExpected(formalRef, options = {}) {
|
|
53
|
+
const parsed = parseFormalRef(formalRef);
|
|
54
|
+
if (!parsed) return null;
|
|
55
|
+
|
|
56
|
+
// Requirements are text, not numeric — return null
|
|
57
|
+
if (parsed.type === 'requirement') return null;
|
|
58
|
+
|
|
59
|
+
// Spec without param key — invariant reference, no extractable value
|
|
60
|
+
if (parsed.type === 'spec' && !parsed.param) return null;
|
|
61
|
+
|
|
62
|
+
const specDir = options.specDir || path.resolve(process.cwd(), '.planning/formal/spec');
|
|
63
|
+
const filePath = path.join(specDir, parsed.path);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
67
|
+
|
|
68
|
+
// JSON files
|
|
69
|
+
if (filePath.endsWith('.json')) {
|
|
70
|
+
const data = JSON.parse(content);
|
|
71
|
+
const val = data[parsed.param];
|
|
72
|
+
return val !== undefined ? val : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// CFG files (TLA+ config format: key = value)
|
|
76
|
+
for (const line of content.split('\n')) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
// Skip comments and empty lines
|
|
79
|
+
if (!trimmed || trimmed.startsWith('\\*') || trimmed.startsWith('SPECIFICATION') ||
|
|
80
|
+
trimmed.startsWith('CONSTANTS') || trimmed.startsWith('SYMMETRY') ||
|
|
81
|
+
trimmed.startsWith('INVARIANT') || trimmed.startsWith('PROPERTY') ||
|
|
82
|
+
trimmed.startsWith('CHECK_DEADLOCK')) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Match key = value patterns
|
|
86
|
+
const match = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
87
|
+
if (match && match[1] === parsed.param) {
|
|
88
|
+
const rawVal = match[2].trim();
|
|
89
|
+
// Try to parse as number
|
|
90
|
+
const num = Number(rawVal);
|
|
91
|
+
if (!isNaN(num) && rawVal !== '') return num;
|
|
92
|
+
// Return as string
|
|
93
|
+
return rawVal;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null; // Param not found in file
|
|
98
|
+
} catch {
|
|
99
|
+
// Fail-open: file not found, parse error, etc.
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { extractFormalExpected, parseFormalRef };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate deterministic fingerprint for a drift using formal parameter key
|
|
5
|
+
* Fingerprint depends only on the parameter key, not measured values or timestamps
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} drift - Drift object with formal_parameter_key and other optional fields
|
|
8
|
+
* @returns {string} - 16-char hex fingerprint
|
|
9
|
+
* @throws {Error} - If formal_parameter_key is missing or empty
|
|
10
|
+
*/
|
|
11
|
+
function fingerprintDrift(drift) {
|
|
12
|
+
// Validate that formal_parameter_key exists and is non-empty
|
|
13
|
+
if (!drift.formal_parameter_key || typeof drift.formal_parameter_key !== 'string' || drift.formal_parameter_key.trim() === '') {
|
|
14
|
+
throw new Error('formal_parameter_key required (non-empty string)');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Deterministic hash of the formal parameter key
|
|
18
|
+
return crypto.createHash('sha256')
|
|
19
|
+
.update(drift.formal_parameter_key)
|
|
20
|
+
.digest('hex')
|
|
21
|
+
.slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { fingerprintDrift };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize and hash an error message
|
|
5
|
+
* Strips timestamps, line numbers, and lowercases for stable hashing
|
|
6
|
+
*
|
|
7
|
+
* @param {string} msg - Error message text
|
|
8
|
+
* @returns {string} - 16-char hex hash
|
|
9
|
+
*/
|
|
10
|
+
function hashMessage(msg) {
|
|
11
|
+
const normalized = (msg || '')
|
|
12
|
+
.replace(/\d{4}-\d{2}-\d{2}T[\d:.Z]+/g, 'TIMESTAMP') // ISO8601 timestamps
|
|
13
|
+
.replace(/:\d+/g, ':LINE') // Line numbers
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.trim();
|
|
16
|
+
|
|
17
|
+
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate deterministic fingerprint for an issue using hierarchical strategy:
|
|
22
|
+
* exception_type -> function_name -> message_hash
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} issue - Issue object with optional exception_type, function_name, message
|
|
25
|
+
* @returns {string} - 16-char hex fingerprint
|
|
26
|
+
*/
|
|
27
|
+
function fingerprintIssue(issue) {
|
|
28
|
+
// Normalize exception type
|
|
29
|
+
const exceptionType = (issue.exception_type || 'unknown').toLowerCase();
|
|
30
|
+
|
|
31
|
+
// Normalize function name (replace non-alphanumeric with underscore)
|
|
32
|
+
const functionName = (issue.function_name || 'unknown')
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9_]/g, '_');
|
|
35
|
+
|
|
36
|
+
// Hash the message
|
|
37
|
+
const msgHash = hashMessage(issue.message);
|
|
38
|
+
|
|
39
|
+
// Combine components with colon separator
|
|
40
|
+
const combined = `${exceptionType}:${functionName}:${msgHash}`;
|
|
41
|
+
|
|
42
|
+
// Final deterministic hash
|
|
43
|
+
return crypto.createHash('sha256').update(combined).digest('hex').slice(0, 16);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { fingerprintIssue, hashMessage };
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure data functions for formal verification browsing.
|
|
5
|
+
* No blessed dependency — all functions are testable in isolation.
|
|
6
|
+
* Consumers: bin/nForma.cjs (blessed TUI)
|
|
7
|
+
*
|
|
8
|
+
* Data sources (project-relative paths via basePath):
|
|
9
|
+
* .planning/formal/model-registry.json — formal model inventory
|
|
10
|
+
* .planning/formal/traceability-matrix.json — requirement-property links
|
|
11
|
+
* .planning/formal/unit-test-coverage.json — test coverage sidecar
|
|
12
|
+
* .planning/formal/state-space-report.json — variable domains & risk
|
|
13
|
+
* .planning/formal/check-results.ndjson — verification results
|
|
14
|
+
* .planning/formal/requirements.json — requirement definitions
|
|
15
|
+
* .planning/formal/tla/*.tla — TLA+ source files
|
|
16
|
+
* .planning/formal/alloy/*.als — Alloy source files
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Data loaders (fail-open: return empty on missing/corrupt files)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function loadJSON(basePath, relPath) {
|
|
27
|
+
const p = path.join(basePath || process.cwd(), relPath);
|
|
28
|
+
if (!fs.existsSync(p)) return null;
|
|
29
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadModelRegistry(basePath) {
|
|
33
|
+
return loadJSON(basePath, '.planning/formal/model-registry.json') || { models: {} };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadTraceabilityMatrix(basePath) {
|
|
37
|
+
return loadJSON(basePath, '.planning/formal/traceability-matrix.json') || { requirements: {}, properties: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadUnitTestCoverage(basePath) {
|
|
41
|
+
return loadJSON(basePath, '.planning/formal/unit-test-coverage.json') || { requirements: {} };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadStateSpaceReport(basePath) {
|
|
45
|
+
return loadJSON(basePath, '.planning/formal/state-space-report.json') || { models: {} };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadRequirements(basePath) {
|
|
49
|
+
return loadJSON(basePath, '.planning/formal/requirements.json') || { requirements: [] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// TLA+ / Alloy source parsers (lightweight — extract variables & properties)
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function parseTLAVariables(content) {
|
|
57
|
+
const vars = [];
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
let inVarBlock = false;
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < lines.length; i++) {
|
|
62
|
+
const line = lines[i];
|
|
63
|
+
if (/^VARIABLES?\b/.test(line.trim())) {
|
|
64
|
+
inVarBlock = true;
|
|
65
|
+
// Variables may be on same line: VARIABLES a, b, c
|
|
66
|
+
const inline = line.replace(/^VARIABLES?\s*/, '').trim();
|
|
67
|
+
if (inline) {
|
|
68
|
+
for (const v of inline.split(',')) {
|
|
69
|
+
const name = v.trim().replace(/\\.*$/, '').trim();
|
|
70
|
+
if (name) vars.push({ name, comment: extractInlineComment(line) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (inVarBlock) {
|
|
76
|
+
if (line.trim() === '' || /^[A-Z]/.test(line.trim()) && !/^\s/.test(line)) {
|
|
77
|
+
// End of variable block (blank line or new definition)
|
|
78
|
+
if (!/^\s/.test(line) && line.trim() !== '') inVarBlock = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const v of line.split(',')) {
|
|
82
|
+
const name = v.trim().replace(/\\.*$/, '').trim();
|
|
83
|
+
if (name) vars.push({ name, comment: extractInlineComment(line) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return vars;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractInlineComment(line) {
|
|
91
|
+
const match = line.match(/\\[*]\s*(.+)$/);
|
|
92
|
+
return match ? match[1].trim() : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseTLAProperties(content) {
|
|
96
|
+
const props = [];
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
let pendingReqs = [];
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i].trim();
|
|
102
|
+
// Collect @requirement annotations
|
|
103
|
+
const reqMatch = line.match(/@requirement\s+(.+)/);
|
|
104
|
+
if (reqMatch) {
|
|
105
|
+
pendingReqs.push(...reqMatch[1].split(/[,\s]+/).filter(Boolean));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Property definition: Name ==
|
|
109
|
+
const propMatch = line.match(/^(\w+)\s*==/);
|
|
110
|
+
if (propMatch) {
|
|
111
|
+
const name = propMatch[1];
|
|
112
|
+
// Skip infrastructure definitions
|
|
113
|
+
if (!['vars', 'Init', 'Next', 'Spec', 'Fairness', 'Symmetry'].includes(name)) {
|
|
114
|
+
props.push({
|
|
115
|
+
name,
|
|
116
|
+
requirements: pendingReqs.length > 0 ? [...pendingReqs] : [],
|
|
117
|
+
line: i + 1,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
pendingReqs = [];
|
|
121
|
+
} else if (!line.startsWith('\\*') && !line.startsWith('(*') && line !== '') {
|
|
122
|
+
// Non-comment, non-empty line clears pending annotations
|
|
123
|
+
if (!reqMatch) pendingReqs = [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return props;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseAlloyConstructs(content) {
|
|
130
|
+
const constructs = [];
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
let pendingReqs = [];
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
const line = lines[i].trim();
|
|
136
|
+
const reqMatch = line.match(/@requirement\s+(.+)/);
|
|
137
|
+
if (reqMatch) {
|
|
138
|
+
pendingReqs.push(...reqMatch[1].split(/[,\s]+/).filter(Boolean));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Alloy construct: sig, fact, assert, pred, fun, check, run
|
|
142
|
+
const cMatch = line.match(/^(sig|fact|assert|pred|fun|check|run)\s+(\w+)/);
|
|
143
|
+
if (cMatch) {
|
|
144
|
+
constructs.push({
|
|
145
|
+
kind: cMatch[1],
|
|
146
|
+
name: cMatch[2],
|
|
147
|
+
requirements: pendingReqs.length > 0 ? [...pendingReqs] : [],
|
|
148
|
+
line: i + 1,
|
|
149
|
+
});
|
|
150
|
+
pendingReqs = [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return constructs;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseAlloySigs(content) {
|
|
157
|
+
const sigs = [];
|
|
158
|
+
const lines = content.split('\n');
|
|
159
|
+
for (let i = 0; i < lines.length; i++) {
|
|
160
|
+
const line = lines[i].trim();
|
|
161
|
+
const sigMatch = line.match(/^(?:abstract\s+|one\s+|lone\s+)?sig\s+(\w+)(?:\s+extends\s+(\w+))?/);
|
|
162
|
+
if (sigMatch) {
|
|
163
|
+
const fields = [];
|
|
164
|
+
// Parse fields in the sig body
|
|
165
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
166
|
+
const fl = lines[j].trim();
|
|
167
|
+
if (fl.startsWith('}')) break;
|
|
168
|
+
const fieldMatch = fl.match(/^(\w+)\s*:\s*(.+?)(?:,|$)/);
|
|
169
|
+
if (fieldMatch) {
|
|
170
|
+
fields.push({ name: fieldMatch[1], type: fieldMatch[2].trim().replace(/,\s*$/, '') });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
sigs.push({
|
|
174
|
+
name: sigMatch[1],
|
|
175
|
+
extends: sigMatch[2] || null,
|
|
176
|
+
fields,
|
|
177
|
+
line: i + 1,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return sigs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Aggregation functions
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build a comprehensive model summary from all data sources.
|
|
190
|
+
* Returns array of model objects with parsed details.
|
|
191
|
+
*/
|
|
192
|
+
function buildModelIndex(basePath) {
|
|
193
|
+
const registry = loadModelRegistry(basePath);
|
|
194
|
+
const stateSpace = loadStateSpaceReport(basePath);
|
|
195
|
+
const matrix = loadTraceabilityMatrix(basePath);
|
|
196
|
+
const base = basePath || process.cwd();
|
|
197
|
+
|
|
198
|
+
const models = [];
|
|
199
|
+
|
|
200
|
+
for (const [modelPath, entry] of Object.entries(registry.models || {})) {
|
|
201
|
+
const absPath = path.join(base, modelPath);
|
|
202
|
+
const exists = fs.existsSync(absPath);
|
|
203
|
+
const ext = path.extname(modelPath).toLowerCase();
|
|
204
|
+
const formalism = ext === '.tla' ? 'TLA+' : ext === '.als' ? 'Alloy' : ext === '.pm' ? 'PRISM' : 'Unknown';
|
|
205
|
+
|
|
206
|
+
let variables = [];
|
|
207
|
+
let properties = [];
|
|
208
|
+
let constructs = [];
|
|
209
|
+
let sigs = [];
|
|
210
|
+
|
|
211
|
+
if (exists) {
|
|
212
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
213
|
+
if (formalism === 'TLA+') {
|
|
214
|
+
variables = parseTLAVariables(content);
|
|
215
|
+
properties = parseTLAProperties(content);
|
|
216
|
+
} else if (formalism === 'Alloy') {
|
|
217
|
+
constructs = parseAlloyConstructs(content);
|
|
218
|
+
sigs = parseAlloySigs(content);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// State-space info
|
|
223
|
+
const ssEntry = (stateSpace.models || {})[modelPath] || null;
|
|
224
|
+
|
|
225
|
+
// Properties from traceability matrix
|
|
226
|
+
const matrixProps = [];
|
|
227
|
+
for (const [key, prop] of Object.entries(matrix.properties || {})) {
|
|
228
|
+
if (prop.model_file === modelPath) {
|
|
229
|
+
matrixProps.push(prop);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
models.push({
|
|
234
|
+
path: modelPath,
|
|
235
|
+
formalism,
|
|
236
|
+
exists,
|
|
237
|
+
description: entry.description || '',
|
|
238
|
+
version: entry.version || null,
|
|
239
|
+
requirements: entry.requirements || [],
|
|
240
|
+
variables,
|
|
241
|
+
properties, // TLA+ properties
|
|
242
|
+
constructs, // Alloy constructs
|
|
243
|
+
sigs, // Alloy sigs
|
|
244
|
+
stateSpace: ssEntry,
|
|
245
|
+
matrixProperties: matrixProps,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return models;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build a test coverage index: requirement → test cases.
|
|
254
|
+
*/
|
|
255
|
+
function buildTestIndex(basePath) {
|
|
256
|
+
const utc = loadUnitTestCoverage(basePath);
|
|
257
|
+
const reqs = loadRequirements(basePath);
|
|
258
|
+
const reqMap = {};
|
|
259
|
+
|
|
260
|
+
for (const r of (reqs.requirements || [])) {
|
|
261
|
+
reqMap[r.id] = {
|
|
262
|
+
id: r.id,
|
|
263
|
+
text: r.text || '',
|
|
264
|
+
category: r.category || 'Uncategorized',
|
|
265
|
+
status: r.status || 'Unknown',
|
|
266
|
+
covered: false,
|
|
267
|
+
testCases: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const [reqId, entry] of Object.entries(utc.requirements || {})) {
|
|
272
|
+
if (!reqMap[reqId]) {
|
|
273
|
+
reqMap[reqId] = { id: reqId, text: '', category: 'Unknown', status: 'Unknown', covered: false, testCases: [] };
|
|
274
|
+
}
|
|
275
|
+
reqMap[reqId].covered = entry.covered || false;
|
|
276
|
+
reqMap[reqId].testCases = (entry.test_cases || []).map(tc => ({
|
|
277
|
+
file: tc.test_file || '',
|
|
278
|
+
name: tc.test_name || '',
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return reqMap;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Build a variable index across all models.
|
|
287
|
+
* Returns array of { variable, model, domain, cardinality, bounded, risk }.
|
|
288
|
+
*/
|
|
289
|
+
function buildVariableIndex(basePath) {
|
|
290
|
+
const models = buildModelIndex(basePath);
|
|
291
|
+
const stateSpace = loadStateSpaceReport(basePath);
|
|
292
|
+
const variables = [];
|
|
293
|
+
|
|
294
|
+
for (const model of models) {
|
|
295
|
+
if (model.formalism === 'TLA+') {
|
|
296
|
+
const ssVars = ((stateSpace.models || {})[model.path] || {}).variables || [];
|
|
297
|
+
const ssMap = {};
|
|
298
|
+
for (const sv of ssVars) { ssMap[sv.name] = sv; }
|
|
299
|
+
|
|
300
|
+
for (const v of model.variables) {
|
|
301
|
+
const sv = ssMap[v.name] || {};
|
|
302
|
+
variables.push({
|
|
303
|
+
name: v.name,
|
|
304
|
+
model: model.path,
|
|
305
|
+
formalism: 'TLA+',
|
|
306
|
+
comment: v.comment,
|
|
307
|
+
domain: sv.domain || null,
|
|
308
|
+
cardinality: sv.cardinality || null,
|
|
309
|
+
bounded: sv.bounded != null ? sv.bounded : null,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} else if (model.formalism === 'Alloy') {
|
|
313
|
+
for (const sig of model.sigs) {
|
|
314
|
+
for (const field of sig.fields) {
|
|
315
|
+
variables.push({
|
|
316
|
+
name: `${sig.name}.${field.name}`,
|
|
317
|
+
model: model.path,
|
|
318
|
+
formalism: 'Alloy',
|
|
319
|
+
comment: null,
|
|
320
|
+
domain: field.type,
|
|
321
|
+
cardinality: null,
|
|
322
|
+
bounded: true, // Alloy is always bounded
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return variables;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Build an interconnection graph: nodes (reqs, models, tests, properties)
|
|
334
|
+
* and edges showing how they link together.
|
|
335
|
+
*/
|
|
336
|
+
function buildInterconnectionGraph(basePath) {
|
|
337
|
+
const models = buildModelIndex(basePath);
|
|
338
|
+
const testIndex = buildTestIndex(basePath);
|
|
339
|
+
const matrix = loadTraceabilityMatrix(basePath);
|
|
340
|
+
|
|
341
|
+
const nodes = { requirements: {}, models: {}, properties: {}, tests: {} };
|
|
342
|
+
const edges = [];
|
|
343
|
+
|
|
344
|
+
// Requirements
|
|
345
|
+
for (const [reqId, entry] of Object.entries(testIndex)) {
|
|
346
|
+
nodes.requirements[reqId] = {
|
|
347
|
+
type: 'requirement',
|
|
348
|
+
id: reqId,
|
|
349
|
+
text: entry.text,
|
|
350
|
+
category: entry.category,
|
|
351
|
+
status: entry.status,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Models + edges to requirements
|
|
356
|
+
for (const model of models) {
|
|
357
|
+
const shortPath = path.basename(model.path);
|
|
358
|
+
nodes.models[model.path] = {
|
|
359
|
+
type: 'model',
|
|
360
|
+
id: model.path,
|
|
361
|
+
shortName: shortPath,
|
|
362
|
+
formalism: model.formalism,
|
|
363
|
+
varCount: model.variables.length + model.sigs.reduce((n, s) => n + s.fields.length, 0),
|
|
364
|
+
propCount: model.properties.length + model.constructs.length,
|
|
365
|
+
riskLevel: model.stateSpace ? model.stateSpace.risk_level : null,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
for (const reqId of model.requirements) {
|
|
369
|
+
edges.push({ from: reqId, to: model.path, type: 'req-model' });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Properties → requirements
|
|
373
|
+
for (const prop of model.properties) {
|
|
374
|
+
const propKey = `${model.path}::${prop.name}`;
|
|
375
|
+
nodes.properties[propKey] = {
|
|
376
|
+
type: 'property',
|
|
377
|
+
id: propKey,
|
|
378
|
+
name: prop.name,
|
|
379
|
+
model: model.path,
|
|
380
|
+
};
|
|
381
|
+
for (const reqId of prop.requirements) {
|
|
382
|
+
edges.push({ from: reqId, to: propKey, type: 'req-property' });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const c of model.constructs) {
|
|
386
|
+
const cKey = `${model.path}::${c.name}`;
|
|
387
|
+
nodes.properties[cKey] = {
|
|
388
|
+
type: 'property',
|
|
389
|
+
id: cKey,
|
|
390
|
+
name: c.name,
|
|
391
|
+
kind: c.kind,
|
|
392
|
+
model: model.path,
|
|
393
|
+
};
|
|
394
|
+
for (const reqId of c.requirements) {
|
|
395
|
+
edges.push({ from: reqId, to: cKey, type: 'req-property' });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Tests → requirements
|
|
401
|
+
for (const [reqId, entry] of Object.entries(testIndex)) {
|
|
402
|
+
for (const tc of entry.testCases) {
|
|
403
|
+
const testKey = `${tc.file}::${tc.name}`;
|
|
404
|
+
if (!nodes.tests[testKey]) {
|
|
405
|
+
nodes.tests[testKey] = { type: 'test', id: testKey, file: tc.file, name: tc.name };
|
|
406
|
+
}
|
|
407
|
+
edges.push({ from: reqId, to: testKey, type: 'req-test' });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { nodes, edges };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Build a summary dashboard of the formal verification ecosystem.
|
|
416
|
+
*/
|
|
417
|
+
function buildFormalSummary(basePath) {
|
|
418
|
+
const models = buildModelIndex(basePath);
|
|
419
|
+
const testIndex = buildTestIndex(basePath);
|
|
420
|
+
const stateSpace = loadStateSpaceReport(basePath);
|
|
421
|
+
|
|
422
|
+
const tlaModels = models.filter(m => m.formalism === 'TLA+');
|
|
423
|
+
const alloyModels = models.filter(m => m.formalism === 'Alloy');
|
|
424
|
+
|
|
425
|
+
// Count variables
|
|
426
|
+
let totalVars = 0;
|
|
427
|
+
let unboundedVars = 0;
|
|
428
|
+
for (const m of models) {
|
|
429
|
+
if (m.formalism === 'TLA+') totalVars += m.variables.length;
|
|
430
|
+
if (m.formalism === 'Alloy') totalVars += m.sigs.reduce((n, s) => n + s.fields.length, 0);
|
|
431
|
+
}
|
|
432
|
+
const ssModels = Object.values(stateSpace.models || {});
|
|
433
|
+
for (const ss of ssModels) {
|
|
434
|
+
for (const v of (ss.variables || [])) {
|
|
435
|
+
if (!v.bounded) unboundedVars++;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Count properties
|
|
440
|
+
let totalProps = 0;
|
|
441
|
+
let annotatedProps = 0;
|
|
442
|
+
for (const m of models) {
|
|
443
|
+
for (const p of m.properties) {
|
|
444
|
+
totalProps++;
|
|
445
|
+
if (p.requirements.length > 0) annotatedProps++;
|
|
446
|
+
}
|
|
447
|
+
for (const c of m.constructs) {
|
|
448
|
+
totalProps++;
|
|
449
|
+
if (c.requirements.length > 0) annotatedProps++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Risk distribution
|
|
454
|
+
const byRisk = { MINIMAL: 0, LOW: 0, MODERATE: 0, HIGH: 0 };
|
|
455
|
+
for (const m of models) {
|
|
456
|
+
if (m.stateSpace && m.stateSpace.risk_level) {
|
|
457
|
+
byRisk[m.stateSpace.risk_level] = (byRisk[m.stateSpace.risk_level] || 0) + 1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Test coverage
|
|
462
|
+
const totalReqs = Object.keys(testIndex).length;
|
|
463
|
+
const coveredReqs = Object.values(testIndex).filter(t => t.covered).length;
|
|
464
|
+
const totalTests = Object.values(testIndex).reduce((n, t) => n + t.testCases.length, 0);
|
|
465
|
+
|
|
466
|
+
// All unique requirements across models
|
|
467
|
+
const allModelReqs = new Set();
|
|
468
|
+
for (const m of models) {
|
|
469
|
+
for (const r of m.requirements) allModelReqs.add(r);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
models: {
|
|
474
|
+
total: models.length,
|
|
475
|
+
tla: tlaModels.length,
|
|
476
|
+
alloy: alloyModels.length,
|
|
477
|
+
other: models.length - tlaModels.length - alloyModels.length,
|
|
478
|
+
},
|
|
479
|
+
variables: {
|
|
480
|
+
total: totalVars,
|
|
481
|
+
unbounded: unboundedVars,
|
|
482
|
+
},
|
|
483
|
+
properties: {
|
|
484
|
+
total: totalProps,
|
|
485
|
+
annotated: annotatedProps,
|
|
486
|
+
unannotated: totalProps - annotatedProps,
|
|
487
|
+
},
|
|
488
|
+
risk: byRisk,
|
|
489
|
+
coverage: {
|
|
490
|
+
totalReqs,
|
|
491
|
+
coveredReqs,
|
|
492
|
+
totalTests,
|
|
493
|
+
modelLinkedReqs: allModelReqs.size,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Exports
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
module.exports = {
|
|
503
|
+
loadModelRegistry,
|
|
504
|
+
loadTraceabilityMatrix,
|
|
505
|
+
loadUnitTestCoverage,
|
|
506
|
+
loadStateSpaceReport,
|
|
507
|
+
loadRequirements,
|
|
508
|
+
parseTLAVariables,
|
|
509
|
+
parseTLAProperties,
|
|
510
|
+
parseAlloyConstructs,
|
|
511
|
+
parseAlloySigs,
|
|
512
|
+
buildModelIndex,
|
|
513
|
+
buildTestIndex,
|
|
514
|
+
buildVariableIndex,
|
|
515
|
+
buildInterconnectionGraph,
|
|
516
|
+
buildFormalSummary,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
module.exports._pure = module.exports;
|