@nforma.ai/nforma 0.2.1 → 0.29.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/README.md +2 -2
- package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
- package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
- package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
- package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
- package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
- package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
- package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
- package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
- package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
- package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
- package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
- package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
- package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
- package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
- package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
- package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
- package/bin/accept-debug-invariant.cjs +2 -2
- package/bin/account-manager.cjs +10 -10
- package/bin/aggregate-requirements.cjs +1 -1
- package/bin/analyze-assumptions.cjs +3 -3
- package/bin/analyze-state-space.cjs +14 -14
- package/bin/assumption-register.cjs +146 -0
- package/bin/attribute-trace-divergence.cjs +1 -1
- package/bin/auth-drivers/gh-cli.cjs +1 -1
- package/bin/auth-drivers/pool.cjs +1 -1
- package/bin/autoClosePtoF.cjs +3 -3
- package/bin/budget-tracker.cjs +77 -0
- package/bin/build-layer-manifest.cjs +153 -0
- package/bin/call-quorum-slot.cjs +3 -3
- package/bin/ccr-secure-config.cjs +5 -5
- package/bin/check-bundled-sdks.cjs +1 -1
- package/bin/check-mcp-health.cjs +1 -1
- package/bin/check-provider-health.cjs +6 -6
- package/bin/check-spec-sync.cjs +26 -26
- package/bin/check-trace-schema-drift.cjs +5 -5
- package/bin/conformance-schema.cjs +2 -2
- package/bin/cross-layer-dashboard.cjs +297 -0
- package/bin/design-impact.cjs +377 -0
- package/bin/detect-coverage-gaps.cjs +7 -7
- package/bin/failure-mode-catalog.cjs +227 -0
- package/bin/failure-taxonomy.cjs +177 -0
- package/bin/formal-scope-scan.cjs +179 -0
- package/bin/gate-a-grounding.cjs +334 -0
- package/bin/gate-b-abstraction.cjs +243 -0
- package/bin/gate-c-validation.cjs +166 -0
- package/bin/generate-formal-specs.cjs +17 -17
- package/bin/generate-petri-net.cjs +3 -3
- package/bin/generate-tla-cfg.cjs +5 -5
- package/bin/git-heatmap.cjs +571 -0
- package/bin/harness-diagnostic.cjs +326 -0
- package/bin/hazard-model.cjs +261 -0
- package/bin/install-formal-tools.cjs +1 -1
- package/bin/install.js +184 -139
- package/bin/instrumentation-map.cjs +178 -0
- package/bin/invariant-catalog.cjs +437 -0
- package/bin/issue-classifier.cjs +2 -2
- package/bin/load-baseline-requirements.cjs +4 -4
- package/bin/manage-agents-core.cjs +32 -32
- package/bin/migrate-to-slots.cjs +39 -39
- package/bin/mismatch-register.cjs +217 -0
- package/bin/nForma.cjs +176 -81
- package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
- package/bin/observe-config.cjs +8 -0
- package/bin/observe-debt-writer.cjs +1 -1
- package/bin/observe-handler-deps.cjs +356 -0
- package/bin/observe-handler-grafana.cjs +2 -17
- package/bin/observe-handler-internal.cjs +5 -5
- package/bin/observe-handler-logstash.cjs +2 -17
- package/bin/observe-handler-prometheus.cjs +2 -17
- package/bin/observe-handler-upstream.cjs +251 -0
- package/bin/observe-handlers.cjs +12 -33
- package/bin/observe-render.cjs +68 -22
- package/bin/observe-utils.cjs +37 -0
- package/bin/observed-fsm.cjs +324 -0
- package/bin/planning-paths.cjs +6 -0
- package/bin/polyrepo.cjs +1 -1
- package/bin/probe-quorum-slots.cjs +1 -1
- package/bin/promote-gate-maturity.cjs +274 -0
- package/bin/promote-model.cjs +1 -1
- package/bin/propose-debug-invariants.cjs +1 -1
- package/bin/quorum-cache.cjs +144 -0
- package/bin/quorum-consensus-gate.cjs +1 -1
- package/bin/quorum-preflight.cjs +89 -0
- package/bin/quorum-slot-dispatch.cjs +6 -6
- package/bin/requirements-core.cjs +1 -1
- package/bin/review-mcp-logs.cjs +1 -1
- package/bin/risk-heatmap.cjs +151 -0
- package/bin/run-account-manager-tlc.cjs +4 -4
- package/bin/run-account-pool-alloy.cjs +2 -2
- package/bin/run-alloy.cjs +2 -2
- package/bin/run-audit-alloy.cjs +2 -2
- package/bin/run-breaker-tlc.cjs +3 -3
- package/bin/run-formal-check.cjs +9 -9
- package/bin/run-formal-verify.cjs +30 -9
- package/bin/run-installer-alloy.cjs +2 -2
- package/bin/run-oscillation-tlc.cjs +4 -4
- package/bin/run-phase-tlc.cjs +1 -1
- package/bin/run-protocol-tlc.cjs +4 -4
- package/bin/run-quorum-composition-alloy.cjs +2 -2
- package/bin/run-sensitivity-sweep.cjs +2 -2
- package/bin/run-stop-hook-tlc.cjs +3 -3
- package/bin/run-tlc.cjs +21 -21
- package/bin/run-transcript-alloy.cjs +2 -2
- package/bin/secrets.cjs +5 -5
- package/bin/security-sweep.cjs +238 -0
- package/bin/sensitivity-report.cjs +3 -3
- package/bin/set-secret.cjs +5 -5
- package/bin/setup-telemetry-cron.sh +3 -3
- package/bin/stall-detector.cjs +126 -0
- package/bin/state-candidates.cjs +206 -0
- package/bin/sync-baseline-requirements.cjs +1 -1
- package/bin/telemetry-collector.cjs +1 -1
- package/bin/test-changed.cjs +111 -0
- package/bin/test-recipe-gen.cjs +250 -0
- package/bin/trace-corpus-stats.cjs +211 -0
- package/bin/unified-mcp-server.mjs +3 -3
- package/bin/update-scoreboard.cjs +1 -1
- package/bin/validate-memory.cjs +2 -2
- package/bin/validate-traces.cjs +10 -10
- package/bin/verify-quorum-health.cjs +66 -5
- package/bin/xstate-to-tla.cjs +4 -4
- package/bin/xstate-trace-walker.cjs +3 -3
- package/commands/{qgsd → nf}/add-phase.md +3 -3
- package/commands/{qgsd → nf}/add-requirement.md +3 -3
- package/commands/{qgsd → nf}/add-todo.md +3 -3
- package/commands/{qgsd → nf}/audit-milestone.md +4 -4
- package/commands/{qgsd → nf}/check-todos.md +3 -3
- package/commands/{qgsd → nf}/cleanup.md +3 -3
- package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
- package/commands/{qgsd → nf}/complete-milestone.md +9 -9
- package/commands/{qgsd → nf}/debug.md +9 -9
- package/commands/{qgsd → nf}/discuss-phase.md +3 -3
- package/commands/{qgsd → nf}/execute-phase.md +15 -15
- package/commands/{qgsd → nf}/fix-tests.md +3 -3
- package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
- package/commands/{qgsd → nf}/health.md +3 -3
- package/commands/{qgsd → nf}/help.md +3 -3
- package/commands/{qgsd → nf}/insert-phase.md +3 -3
- package/commands/nf/join-discord.md +18 -0
- package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
- package/commands/{qgsd → nf}/map-codebase.md +7 -7
- package/commands/{qgsd → nf}/map-requirements.md +3 -3
- package/commands/{qgsd → nf}/mcp-restart.md +3 -3
- package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
- package/commands/{qgsd → nf}/mcp-setup.md +63 -63
- package/commands/{qgsd → nf}/mcp-status.md +3 -3
- package/commands/{qgsd → nf}/mcp-update.md +7 -7
- package/commands/{qgsd → nf}/new-milestone.md +8 -8
- package/commands/{qgsd → nf}/new-project.md +8 -8
- package/commands/{qgsd → nf}/observe.md +49 -16
- package/commands/{qgsd → nf}/pause-work.md +3 -3
- package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
- package/commands/{qgsd → nf}/plan-phase.md +6 -6
- package/commands/{qgsd → nf}/polyrepo.md +2 -2
- package/commands/{qgsd → nf}/progress.md +3 -3
- package/commands/{qgsd → nf}/queue.md +2 -2
- package/commands/{qgsd → nf}/quick.md +8 -8
- package/commands/{qgsd → nf}/quorum-test.md +10 -10
- package/commands/{qgsd → nf}/quorum.md +36 -86
- package/commands/{qgsd → nf}/reapply-patches.md +2 -2
- package/commands/{qgsd → nf}/remove-phase.md +3 -3
- package/commands/{qgsd → nf}/research-phase.md +12 -12
- package/commands/{qgsd → nf}/resume-work.md +3 -3
- package/commands/nf/review-requirements.md +31 -0
- package/commands/{qgsd → nf}/set-profile.md +3 -3
- package/commands/{qgsd → nf}/settings.md +6 -6
- package/commands/{qgsd → nf}/solve.md +35 -35
- package/commands/{qgsd → nf}/sync-baselines.md +4 -4
- package/commands/{qgsd → nf}/triage.md +10 -10
- package/commands/{qgsd → nf}/update.md +3 -3
- package/commands/{qgsd → nf}/verify-work.md +5 -5
- package/hooks/dist/config-loader.js +188 -32
- package/hooks/dist/conformance-schema.cjs +2 -2
- package/hooks/dist/gsd-context-monitor.js +118 -13
- package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
- package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
- package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +6 -4
- package/scripts/build-hooks.js +13 -6
- package/scripts/secret-audit.sh +1 -1
- package/scripts/verify-hooks-sync.cjs +90 -0
- package/templates/{qgsd.json → nf.json} +4 -4
- package/commands/qgsd/join-discord.md +0 -18
- package/hooks/dist/qgsd-session-start.js +0 -122
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
// Traceability (3) — generate-traceability-matrix.cjs (requirements <-> properties matrix)
|
|
22
22
|
// check-coverage-guard.cjs (coverage regression guard vs baseline)
|
|
23
23
|
// analyze-state-space.cjs (state-space risk classification per TLA+ model)
|
|
24
|
+
// Gates (3) — gate-a-grounding.cjs, gate-b-abstraction.cjs, gate-c-validation.cjs
|
|
24
25
|
// Registry (N) — custom check commands from model-registry.json
|
|
25
26
|
// ─────────────────────────────────────────────────────────────
|
|
26
|
-
// Total:
|
|
27
|
+
// Total: 37+ steps (dynamic — registry can add more)
|
|
27
28
|
//
|
|
28
29
|
// Usage:
|
|
29
30
|
// node bin/run-formal-verify.cjs # all 28 steps
|
|
30
31
|
// node bin/run-formal-verify.cjs --concurrent # run tool groups in parallel (old behavior)
|
|
31
|
-
//
|
|
32
|
+
// NF_FORMAL_CONCURRENT=1 node bin/run-formal-verify.cjs # same via env var
|
|
32
33
|
// node bin/run-formal-verify.cjs --only=generate # source extraction only (2 steps)
|
|
33
34
|
// node bin/run-formal-verify.cjs --only=tla # TLA+ only (10 steps)
|
|
34
35
|
// node bin/run-formal-verify.cjs --only=alloy # Alloy only (8 steps)
|
|
@@ -63,7 +64,7 @@ for (const arg of process.argv.slice(2)) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// ── Runner picker maps ─────────────────────────────────────────────────────────
|
|
66
|
-
// Maps known
|
|
67
|
+
// Maps known nForma model names to their specialized runners. Unknown models
|
|
67
68
|
// fall back to generic runners (run-tlc.cjs, run-alloy.cjs, run-prism.cjs).
|
|
68
69
|
|
|
69
70
|
const TLA_RUNNER_MAP = {
|
|
@@ -328,9 +329,9 @@ const STATIC_STEPS = [
|
|
|
328
329
|
// ─ Source extraction — must run first so generated specs are fresh ──────────
|
|
329
330
|
{
|
|
330
331
|
tool: 'generate', id: 'generate:tla-from-xstate',
|
|
331
|
-
label: 'Generate TLA+ spec (
|
|
332
|
+
label: 'Generate TLA+ spec (NFQuorum_xstate.tla) + TLC model config from XState machine (xstate-to-tla)',
|
|
332
333
|
type: 'node', script: 'xstate-to-tla.cjs',
|
|
333
|
-
args: ['src/machines/
|
|
334
|
+
args: ['src/machines/nf-workflow.machine.ts', '--module=NFQuorum', '--config=.planning/formal/tla/guards/nf-workflow.json'],
|
|
334
335
|
},
|
|
335
336
|
{
|
|
336
337
|
tool: 'generate', id: 'generate:alloy-prism-specs',
|
|
@@ -394,6 +395,26 @@ const STATIC_STEPS = [
|
|
|
394
395
|
type: 'node', script: 'analyze-state-space.cjs', args: [],
|
|
395
396
|
nonCritical: true,
|
|
396
397
|
},
|
|
398
|
+
|
|
399
|
+
// ─ Gates — cross-layer alignment checks ───────────────────────────────────
|
|
400
|
+
{
|
|
401
|
+
tool: 'gates', id: 'gates:gate-a',
|
|
402
|
+
label: 'Gate A -- L1-L2 grounding alignment score',
|
|
403
|
+
type: 'node', script: 'gate-a-grounding.cjs', args: [],
|
|
404
|
+
nonCritical: true,
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
tool: 'gates', id: 'gates:gate-b',
|
|
408
|
+
label: 'Gate B -- L2-L3 traceability alignment score',
|
|
409
|
+
type: 'node', script: 'gate-b-abstraction.cjs', args: [],
|
|
410
|
+
nonCritical: true,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
tool: 'gates', id: 'gates:gate-c',
|
|
414
|
+
label: 'Gate C -- L3-TC validation alignment score',
|
|
415
|
+
type: 'node', script: 'gate-c-validation.cjs', args: [],
|
|
416
|
+
nonCritical: true,
|
|
417
|
+
},
|
|
397
418
|
];
|
|
398
419
|
|
|
399
420
|
// Discover dynamic model steps from ROOT/.planning/formal/
|
|
@@ -412,7 +433,7 @@ process.stdout.write(TAG + ' Discovered models: ' + uniqueDynamicSteps.length +
|
|
|
412
433
|
const argv = process.argv.slice(2);
|
|
413
434
|
const onlyArg = argv.find(a => a.startsWith('--only='));
|
|
414
435
|
const only = onlyArg ? onlyArg.split('=')[1] : null;
|
|
415
|
-
const concurrent = argv.includes('--concurrent') || process.env.
|
|
436
|
+
const concurrent = argv.includes('--concurrent') || process.env.NF_FORMAL_CONCURRENT === '1';
|
|
416
437
|
|
|
417
438
|
const steps = only
|
|
418
439
|
? STEPS.filter(s => s.tool === only || s.id === only)
|
|
@@ -421,7 +442,7 @@ const steps = only
|
|
|
421
442
|
if (only && steps.length === 0) {
|
|
422
443
|
process.stderr.write(
|
|
423
444
|
TAG + ' Unknown --only value: ' + only + '\n' +
|
|
424
|
-
TAG + ' Valid values: tla, alloy, prism, petri, generate, ci, uppaal, registry, or a step id\n'
|
|
445
|
+
TAG + ' Valid values: tla, alloy, prism, petri, generate, ci, uppaal, gates, registry, or a step id\n'
|
|
425
446
|
);
|
|
426
447
|
process.exit(1);
|
|
427
448
|
}
|
|
@@ -550,7 +571,7 @@ async function runOnce() {
|
|
|
550
571
|
fs.writeFileSync(ndjsonPath, '', 'utf8');
|
|
551
572
|
|
|
552
573
|
process.stdout.write(TAG + ' ' + HR + '\n');
|
|
553
|
-
process.stdout.write(TAG + '
|
|
574
|
+
process.stdout.write(TAG + ' nForma Formal Verification Suite\n');
|
|
554
575
|
if (only) {
|
|
555
576
|
process.stdout.write(TAG + ' Filter: --only=' + only + '\n');
|
|
556
577
|
}
|
|
@@ -641,7 +662,7 @@ if (watchArg) {
|
|
|
641
662
|
// by spawning with a custom cwd. __dirname-relative paths would always point
|
|
642
663
|
// to the real repo's src/machines/ regardless of spawn cwd, breaking isolation.
|
|
643
664
|
const machineDir = path.join(process.cwd(), 'src', 'machines');
|
|
644
|
-
const machineName = '
|
|
665
|
+
const machineName = 'nf-workflow.machine.ts';
|
|
645
666
|
let debounceTimer = null;
|
|
646
667
|
let running = false; // concurrent-run guard
|
|
647
668
|
let watcher = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-installer-alloy.cjs
|
|
4
|
-
// Invokes Alloy 6 JAR headless for
|
|
4
|
+
// Invokes Alloy 6 JAR headless for nForma installer and taxonomy specs (GAP-7, GAP-8).
|
|
5
5
|
// Requirements: GAP-7, GAP-8
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
|
|
17
17
|
|
|
18
18
|
const { spawnSync } = require('child_process');
|
|
19
|
-
const JAVA_HEAP_MAX = process.env.
|
|
19
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-oscillation-tlc.cjs
|
|
4
|
-
// Invokes TLC model checker for
|
|
4
|
+
// Invokes TLC model checker for nForma oscillation and convergence TLA+ specifications.
|
|
5
5
|
// Requirements: GAP-1, GAP-5
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
15
15
|
|
|
16
16
|
const { spawnSync } = require('child_process');
|
|
17
|
-
const JAVA_HEAP_MAX = process.env.
|
|
17
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
@@ -129,8 +129,8 @@ if (!fs.existsSync(jarPath)) {
|
|
|
129
129
|
|
|
130
130
|
// ── 4. Resolve spec and config paths ─────────────────────────────────────────
|
|
131
131
|
const specFileName = configName === 'MCoscillation'
|
|
132
|
-
? '
|
|
133
|
-
: '
|
|
132
|
+
? 'NFOscillation.tla'
|
|
133
|
+
: 'NFConvergence.tla';
|
|
134
134
|
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', specFileName);
|
|
135
135
|
const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
136
136
|
|
package/bin/run-phase-tlc.cjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// NOTE: Uses spawnSync (no shell) for safe subprocess invocation -- no exec().
|
|
16
16
|
|
|
17
17
|
const { spawnSync } = require('child_process');
|
|
18
|
-
const JAVA_HEAP_MAX = process.env.
|
|
18
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
package/bin/run-protocol-tlc.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-protocol-tlc.cjs
|
|
4
|
-
// Invokes TLC model checker for
|
|
4
|
+
// Invokes TLC model checker for nForma protocol termination TLA+ specifications.
|
|
5
5
|
// Requirements: GAP-2, GAP-6
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
15
15
|
|
|
16
16
|
const { spawnSync } = require('child_process');
|
|
17
|
-
const JAVA_HEAP_MAX = process.env.
|
|
17
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
@@ -129,8 +129,8 @@ if (!fs.existsSync(jarPath)) {
|
|
|
129
129
|
|
|
130
130
|
// ── 4. Resolve spec and config paths ─────────────────────────────────────────
|
|
131
131
|
const specFileName = configName === 'MCdeliberation'
|
|
132
|
-
? '
|
|
133
|
-
: '
|
|
132
|
+
? 'NFDeliberation.tla'
|
|
133
|
+
: 'NFPreFilter.tla';
|
|
134
134
|
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', specFileName);
|
|
135
135
|
const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
136
136
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-quorum-composition-alloy.cjs
|
|
4
|
-
// Invokes Alloy 6 JAR headless for the
|
|
4
|
+
// Invokes Alloy 6 JAR headless for the nForma quorum composition model.
|
|
5
5
|
// Requirements: SPEC-03
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
|
|
13
13
|
|
|
14
14
|
const { spawnSync } = require('child_process');
|
|
15
|
-
const JAVA_HEAP_MAX = process.env.
|
|
15
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Always exits 0.
|
|
14
14
|
|
|
15
15
|
const { spawnSync } = require('child_process');
|
|
16
|
-
const JAVA_HEAP_MAX = process.env.
|
|
16
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const os = require('os');
|
|
@@ -67,7 +67,7 @@ function runTLCSweep(maxSize) {
|
|
|
67
67
|
const overrideCfg = baseCfg.replace(/MaxSize\s*=\s*\d+/, 'MaxSize = ' + maxSize);
|
|
68
68
|
fs.writeFileSync(tmpCfg, overrideCfg, 'utf8');
|
|
69
69
|
|
|
70
|
-
const tlaFile = path.join(__dirname, '..', '.planning', 'formal', 'tla', '
|
|
70
|
+
const tlaFile = path.join(__dirname, '..', '.planning', 'formal', 'tla', 'NFQuorum.tla');
|
|
71
71
|
process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
|
|
72
72
|
const javaResult = spawnSync('java', [
|
|
73
73
|
'-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-stop-hook-tlc.cjs
|
|
4
|
-
// Invokes TLC model checker for the
|
|
4
|
+
// Invokes TLC model checker for the nForma Stop hook TLA+ specification.
|
|
5
5
|
// Requirements: SPEC-01
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
14
14
|
|
|
15
15
|
const { spawnSync } = require('child_process');
|
|
16
|
-
const JAVA_HEAP_MAX = process.env.
|
|
16
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
@@ -118,7 +118,7 @@ if (!fs.existsSync(jarPath)) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// ── 4. Resolve spec and config paths ─────────────────────────────────────────
|
|
121
|
-
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', '
|
|
121
|
+
const specPath = path.join(ROOT, '.planning', 'formal', 'tla', 'NFStopHook.tla');
|
|
122
122
|
const cfgPath = path.join(ROOT, '.planning', 'formal', 'tla', configName + '.cfg');
|
|
123
123
|
|
|
124
124
|
// LivenessProperty1/2/3 requires -workers 1 (TLC multi-worker liveness bug in v1.8.0)
|
package/bin/run-tlc.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-tlc.cjs
|
|
4
|
-
// Invokes TLC model checker for the
|
|
4
|
+
// Invokes TLC model checker for the nForma formal TLA+ specification.
|
|
5
5
|
// Requirements: TLA-04
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// - .planning/formal/tla/tla2tools.jar (see .planning/formal/tla/README.md for download command)
|
|
15
15
|
|
|
16
16
|
const { spawnSync } = require('child_process');
|
|
17
|
-
const JAVA_HEAP_MAX = process.env.
|
|
17
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
|
@@ -298,52 +298,52 @@ if (require.main === module) {
|
|
|
298
298
|
// Map config names to their corresponding spec files.
|
|
299
299
|
// Static map for known exceptions; auto-discovery handles the rest.
|
|
300
300
|
const SPEC_MAP = {
|
|
301
|
-
'MCMCPEnv': '
|
|
302
|
-
'MCsafety': '
|
|
303
|
-
'MCliveness': '
|
|
304
|
-
'
|
|
305
|
-
'MCrecruiting-liveness': '
|
|
306
|
-
'MCrecruiting-safety': '
|
|
301
|
+
'MCMCPEnv': 'NFMCPEnv.tla',
|
|
302
|
+
'MCsafety': 'NFQuorum.tla',
|
|
303
|
+
'MCliveness': 'NFQuorum.tla',
|
|
304
|
+
'MCNFQuorum': 'NFQuorum_xstate.tla',
|
|
305
|
+
'MCrecruiting-liveness': 'NFRecruiting.tla',
|
|
306
|
+
'MCrecruiting-safety': 'NFRecruiting.tla',
|
|
307
307
|
'MCTUINavigation': 'TUINavigation.tla',
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
-
// Auto-discover spec file: (1) check SPEC_MAP, (2) scan cfg header for
|
|
311
|
-
// (3) try naming conventions, (4) fall back to
|
|
310
|
+
// Auto-discover spec file: (1) check SPEC_MAP, (2) scan cfg header for nForma*.tla ref,
|
|
311
|
+
// (3) try naming conventions, (4) fall back to NFQuorum.tla
|
|
312
312
|
function resolveSpecFile(cfgName) {
|
|
313
313
|
if (SPEC_MAP[cfgName]) return SPEC_MAP[cfgName];
|
|
314
314
|
|
|
315
315
|
const tlaDir = path.join(ROOT, '.planning', 'formal', 'tla');
|
|
316
316
|
|
|
317
|
-
// Strategy 1: read cfg header for
|
|
317
|
+
// Strategy 1: read cfg header for nForma*.tla reference (with or without .tla suffix)
|
|
318
318
|
try {
|
|
319
319
|
const cfgContent = fs.readFileSync(path.join(tlaDir, cfgName + '.cfg'), 'utf8');
|
|
320
320
|
const headerLines = cfgContent.split('\n').slice(0, 5).join('\n');
|
|
321
|
-
// Match
|
|
322
|
-
const refMatch = headerLines.match(/
|
|
321
|
+
// Match NF*.tla or "for nForma*." (without .tla extension)
|
|
322
|
+
const refMatch = headerLines.match(/NF\w+\.tla/) || headerLines.match(/NF\w+/);
|
|
323
323
|
if (refMatch) {
|
|
324
324
|
const candidate = refMatch[0].endsWith('.tla') ? refMatch[0] : refMatch[0] + '.tla';
|
|
325
325
|
if (fs.existsSync(path.join(tlaDir, candidate))) return candidate;
|
|
326
326
|
}
|
|
327
327
|
} catch (_) { /* fall through */ }
|
|
328
328
|
|
|
329
|
-
// Strategy 2: naming convention — strip MC prefix, normalize hyphens, find matching
|
|
329
|
+
// Strategy 2: naming convention — strip MC prefix, normalize hyphens, find matching NF*.tla
|
|
330
330
|
const stripped = cfgName.replace(/^MC/, '').toLowerCase().replace(/-/g, '');
|
|
331
331
|
try {
|
|
332
332
|
const allTla = fs.readdirSync(tlaDir).filter(f => f.endsWith('.tla') && !f.includes('TTrace'));
|
|
333
|
-
const
|
|
333
|
+
const nfFiles = allTla.filter(f => f.startsWith('NF'));
|
|
334
334
|
const normalize = (s) => s.toLowerCase().replace(/-/g, '');
|
|
335
|
-
// Exact match against
|
|
336
|
-
const match =
|
|
335
|
+
// Exact match against nForma-prefixed files
|
|
336
|
+
const match = nfFiles.find(f => normalize(f.replace('NF', '').replace('.tla', '')) === stripped);
|
|
337
337
|
if (match) return match;
|
|
338
|
-
// Fuzzy substring match against
|
|
339
|
-
const fuzzy =
|
|
338
|
+
// Fuzzy substring match against nForma-prefixed files
|
|
339
|
+
const fuzzy = nfFiles.find(f => normalize(f).includes(stripped));
|
|
340
340
|
if (fuzzy) return fuzzy;
|
|
341
|
-
// Fallback: check non-
|
|
341
|
+
// Fallback: check non-nForma-prefixed files (exact match on stripped name)
|
|
342
342
|
const nonPrefixed = allTla.find(f => normalize(f.replace('.tla', '')) === stripped);
|
|
343
343
|
if (nonPrefixed) return nonPrefixed;
|
|
344
344
|
} catch (_) { /* fall through */ }
|
|
345
345
|
|
|
346
|
-
return '
|
|
346
|
+
return 'NFQuorum.tla';
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
const specFile = resolveSpecFile(configName);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// bin/run-transcript-alloy.cjs
|
|
4
|
-
// Invokes Alloy 6 JAR headless for
|
|
4
|
+
// Invokes Alloy 6 JAR headless for nForma transcript scanning spec (GAP-4).
|
|
5
5
|
// Requirements: GAP-4
|
|
6
6
|
//
|
|
7
7
|
// Usage:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
|
|
16
16
|
|
|
17
17
|
const { spawnSync } = require('child_process');
|
|
18
|
-
const JAVA_HEAP_MAX = process.env.
|
|
18
|
+
const JAVA_HEAP_MAX = process.env.NF_JAVA_HEAP_MAX || '512m';
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { writeCheckResult } = require('./write-check-result.cjs');
|
package/bin/secrets.cjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const SERVICE = '
|
|
5
|
+
const SERVICE = 'nforma';
|
|
6
6
|
|
|
7
|
-
const INDEX_PATH = path.join(os.homedir(), '.claude', '
|
|
7
|
+
const INDEX_PATH = path.join(os.homedir(), '.claude', 'nf-key-index.json');
|
|
8
8
|
|
|
9
9
|
// Read the key index (no keychain access needed — just a JSON file)
|
|
10
10
|
function readIndex() {
|
|
@@ -84,7 +84,7 @@ async function syncToClaudeJson(service) {
|
|
|
84
84
|
try {
|
|
85
85
|
credentials = await list(service);
|
|
86
86
|
} catch (e) {
|
|
87
|
-
process.stderr.write('[
|
|
87
|
+
process.stderr.write('[nf-secrets] keytar unavailable: ' + e.message + '\n');
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -100,7 +100,7 @@ async function syncToClaudeJson(service) {
|
|
|
100
100
|
try {
|
|
101
101
|
raw = fs.readFileSync(claudeJsonPath, 'utf8');
|
|
102
102
|
} catch (e) {
|
|
103
|
-
process.stderr.write('[
|
|
103
|
+
process.stderr.write('[nf-secrets] ~/.claude.json not found, skipping sync\n');
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -108,7 +108,7 @@ async function syncToClaudeJson(service) {
|
|
|
108
108
|
try {
|
|
109
109
|
claudeJson = JSON.parse(raw);
|
|
110
110
|
} catch (e) {
|
|
111
|
-
process.stderr.write('[
|
|
111
|
+
process.stderr.write('[nf-secrets] ~/.claude.json is invalid JSON, skipping sync\n');
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* bin/security-sweep.cjs — Standalone security scanner for nForma.
|
|
6
|
+
*
|
|
7
|
+
* Scans git-tracked files for hardcoded secrets, debug artifacts, and API keys.
|
|
8
|
+
* Produces structured findings with file:line references for VERIFICATION.md.
|
|
9
|
+
*
|
|
10
|
+
* Advisory only — exit code 0 always. Never blocks.
|
|
11
|
+
*
|
|
12
|
+
* Exports: SECRET_PATTERNS, scanFile, scanDirectory, formatReport
|
|
13
|
+
* CLI: node bin/security-sweep.cjs [--json] [--cwd /path]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
// spawnSync is used (not exec) to avoid shell injection — arguments are passed as an array
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
// ─── Secret Patterns ────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const SECRET_PATTERNS = [
|
|
24
|
+
{ name: 'AWS Access Key', pattern: /AKIA[A-Z0-9]{16}/, severity: 'high' },
|
|
25
|
+
{ name: 'GitHub Token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/, severity: 'high' },
|
|
26
|
+
{ name: 'GitHub PAT', pattern: /github_pat_[A-Za-z0-9_]{22,}/, severity: 'high' },
|
|
27
|
+
{ name: 'Stripe Live Key', pattern: /[sr]k_live_[A-Za-z0-9]{20,}/, severity: 'high' },
|
|
28
|
+
{ name: 'OpenAI Key', pattern: /sk-[A-Za-z0-9]{32,}/, severity: 'high' },
|
|
29
|
+
{ name: 'Generic API Key Assignment', pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]{10,}['"]/i, severity: 'medium' },
|
|
30
|
+
{ name: 'Generic Secret Assignment', pattern: /(?:secret|password)\s*[:=]\s*['"][^'"]{8,}['"]/i, severity: 'medium' },
|
|
31
|
+
{ name: 'Debugger Statement', pattern: /^\s*debugger\s*;?\s*$/, severity: 'low' },
|
|
32
|
+
{ name: 'Sensitive Console.log', pattern: /console\.log\(.*(?:password|secret|token|key|api_key)/i, severity: 'low' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Words that indicate a line is a test fixture / not a real secret
|
|
36
|
+
const TEST_INDICATOR_WORDS = ['test', 'mock', 'fake', 'example', 'dummy', 'fixture', 'placeholder', 'todo'];
|
|
37
|
+
|
|
38
|
+
// File patterns to exclude from scanning
|
|
39
|
+
const EXCLUDE_PATTERNS = [
|
|
40
|
+
/\.test\./,
|
|
41
|
+
/\.spec\./,
|
|
42
|
+
/security-sweep\.cjs$/,
|
|
43
|
+
/\.md$/,
|
|
44
|
+
/\.json$/,
|
|
45
|
+
/\.jsonl$/,
|
|
46
|
+
/node_modules\//,
|
|
47
|
+
/\.planning\/\.quorum-cache\//,
|
|
48
|
+
/package-lock\.json$/,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ─── scanFile ────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Scan file content line-by-line against SECRET_PATTERNS.
|
|
55
|
+
* @param {string} filePath - Relative or absolute file path (for reporting).
|
|
56
|
+
* @param {string} content - File content to scan.
|
|
57
|
+
* @returns {Array<{file:string, line:number, column:number, pattern_name:string, severity:string, match:string}>}
|
|
58
|
+
*/
|
|
59
|
+
function scanFile(filePath, content) {
|
|
60
|
+
if (!content || typeof content !== 'string') return [];
|
|
61
|
+
|
|
62
|
+
// Skip binary content (null bytes in first 512 chars)
|
|
63
|
+
if (content.slice(0, 512).includes('\0')) return [];
|
|
64
|
+
|
|
65
|
+
const findings = [];
|
|
66
|
+
const lines = content.split('\n');
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
const lineLower = line.toLowerCase();
|
|
71
|
+
|
|
72
|
+
// Skip lines containing test indicator words
|
|
73
|
+
if (TEST_INDICATOR_WORDS.some(w => lineLower.includes(w))) continue;
|
|
74
|
+
|
|
75
|
+
for (const pat of SECRET_PATTERNS) {
|
|
76
|
+
const m = pat.pattern.exec(line);
|
|
77
|
+
if (m) {
|
|
78
|
+
findings.push({
|
|
79
|
+
file: filePath,
|
|
80
|
+
line: i + 1,
|
|
81
|
+
column: m.index + 1,
|
|
82
|
+
pattern_name: pat.name,
|
|
83
|
+
severity: pat.severity,
|
|
84
|
+
match: m[0].length > 40 ? m[0].slice(0, 37) + '...' : m[0],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── scanDirectory ───────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Scan git-tracked files in a directory.
|
|
97
|
+
* @param {string} cwd - Working directory to scan.
|
|
98
|
+
* @param {Object} [options]
|
|
99
|
+
* @param {string[]} [options.excludePatterns] - Additional glob patterns to exclude.
|
|
100
|
+
* @param {number} [options.maxFiles=500] - Max files to scan.
|
|
101
|
+
* @returns {{findings: Array, files_scanned: number, duration_ms: number}}
|
|
102
|
+
*/
|
|
103
|
+
function scanDirectory(cwd, options = {}) {
|
|
104
|
+
const maxFiles = options.maxFiles || 500;
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
// Get tracked files via git ls-files (spawnSync, not exec — no shell injection)
|
|
108
|
+
let files = [];
|
|
109
|
+
try {
|
|
110
|
+
const result = spawnSync('git', ['ls-files'], {
|
|
111
|
+
cwd,
|
|
112
|
+
encoding: 'utf8',
|
|
113
|
+
timeout: 5000,
|
|
114
|
+
});
|
|
115
|
+
if (result.status === 0 && result.stdout) {
|
|
116
|
+
files = result.stdout.split('\n').filter(f => f.trim());
|
|
117
|
+
}
|
|
118
|
+
} catch (_) {
|
|
119
|
+
return { findings: [], files_scanned: 0, duration_ms: Date.now() - startTime };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Filter out excluded patterns
|
|
123
|
+
files = files.filter(f => !EXCLUDE_PATTERNS.some(rx => rx.test(f)));
|
|
124
|
+
|
|
125
|
+
// Apply additional exclude patterns from options
|
|
126
|
+
if (options.excludePatterns && Array.isArray(options.excludePatterns)) {
|
|
127
|
+
for (const pat of options.excludePatterns) {
|
|
128
|
+
try {
|
|
129
|
+
const rx = new RegExp(pat);
|
|
130
|
+
files = files.filter(f => !rx.test(f));
|
|
131
|
+
} catch (_) {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Cap file count
|
|
136
|
+
if (files.length > maxFiles) {
|
|
137
|
+
files = files.slice(0, maxFiles);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const allFindings = [];
|
|
141
|
+
let scanned = 0;
|
|
142
|
+
|
|
143
|
+
for (const relPath of files) {
|
|
144
|
+
const absPath = path.join(cwd, relPath);
|
|
145
|
+
try {
|
|
146
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
147
|
+
// Skip binary files (null bytes in first 512 bytes)
|
|
148
|
+
if (content.slice(0, 512).includes('\0')) continue;
|
|
149
|
+
scanned++;
|
|
150
|
+
const findings = scanFile(relPath, content);
|
|
151
|
+
allFindings.push(...findings);
|
|
152
|
+
} catch (_) {
|
|
153
|
+
// Skip unreadable files silently
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
findings: allFindings,
|
|
159
|
+
files_scanned: scanned,
|
|
160
|
+
duration_ms: Date.now() - startTime,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── formatReport ────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Format scan results as a markdown section for VERIFICATION.md.
|
|
168
|
+
* @param {{findings: Array, files_scanned: number, duration_ms: number}} scanResult
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
function formatReport(scanResult) {
|
|
172
|
+
const { findings, files_scanned, duration_ms } = scanResult;
|
|
173
|
+
const lines = [];
|
|
174
|
+
|
|
175
|
+
lines.push('## Security Sweep');
|
|
176
|
+
lines.push('');
|
|
177
|
+
lines.push(`**Scanned:** ${files_scanned} files in ${duration_ms}ms`);
|
|
178
|
+
|
|
179
|
+
if (!findings || findings.length === 0) {
|
|
180
|
+
lines.push('**Findings:** 0');
|
|
181
|
+
lines.push('');
|
|
182
|
+
lines.push('No hardcoded secrets, debug artifacts, or API keys detected.');
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const high = findings.filter(f => f.severity === 'high').length;
|
|
187
|
+
const medium = findings.filter(f => f.severity === 'medium').length;
|
|
188
|
+
const low = findings.filter(f => f.severity === 'low').length;
|
|
189
|
+
|
|
190
|
+
lines.push(`**Findings:** ${findings.length} (${high} high, ${medium} medium, ${low} low)`);
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push('| Severity | File | Line | Pattern | Match |');
|
|
193
|
+
lines.push('|----------|------|------|---------|-------|');
|
|
194
|
+
|
|
195
|
+
for (const f of findings) {
|
|
196
|
+
lines.push(`| ${f.severity} | ${f.file} | ${f.line} | ${f.pattern_name} | ${f.match} |`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push('_Advisory: Review findings and confirm whether they are genuine secrets or false positives._');
|
|
201
|
+
|
|
202
|
+
return lines.join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
if (require.main === module) {
|
|
208
|
+
const args = process.argv.slice(2);
|
|
209
|
+
const jsonFlag = args.includes('--json');
|
|
210
|
+
const cwdIdx = args.indexOf('--cwd');
|
|
211
|
+
const cwd = cwdIdx >= 0 && args[cwdIdx + 1] ? args[cwdIdx + 1] : process.cwd();
|
|
212
|
+
|
|
213
|
+
const result = scanDirectory(cwd);
|
|
214
|
+
|
|
215
|
+
if (jsonFlag) {
|
|
216
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
217
|
+
} else {
|
|
218
|
+
process.stdout.write(formatReport(result) + '\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Log conformance event (best-effort)
|
|
222
|
+
try {
|
|
223
|
+
const planningPaths = require('./planning-paths.cjs');
|
|
224
|
+
const conformancePath = planningPaths.resolveWithFallback(cwd, 'conformance-events');
|
|
225
|
+
const event = {
|
|
226
|
+
ts: new Date().toISOString(),
|
|
227
|
+
action: 'security_sweep',
|
|
228
|
+
files_scanned: result.files_scanned,
|
|
229
|
+
findings_count: result.findings.length,
|
|
230
|
+
duration_ms: result.duration_ms,
|
|
231
|
+
};
|
|
232
|
+
fs.appendFileSync(conformancePath, JSON.stringify(event) + '\n');
|
|
233
|
+
} catch (_) {}
|
|
234
|
+
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { SECRET_PATTERNS, scanFile, scanDirectory, formatReport };
|