@neurcode-ai/cli 0.9.63 → 0.9.65
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 +201 -0
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +273 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/control-plane.js +7 -7
- package/dist/commands/control-plane.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +108 -1
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/patch-apply.d.ts +2 -0
- package/dist/commands/patch-apply.d.ts.map +1 -1
- package/dist/commands/patch-apply.js +331 -19
- package/dist/commands/patch-apply.js.map +1 -1
- package/dist/commands/pilot-report.d.ts +9 -0
- package/dist/commands/pilot-report.d.ts.map +1 -0
- package/dist/commands/pilot-report.js +176 -0
- package/dist/commands/pilot-report.js.map +1 -0
- package/dist/commands/remediate-governance.d.ts +54 -0
- package/dist/commands/remediate-governance.d.ts.map +1 -0
- package/dist/commands/remediate-governance.js +375 -0
- package/dist/commands/remediate-governance.js.map +1 -0
- package/dist/commands/remediate.d.ts.map +1 -1
- package/dist/commands/remediate.js.map +1 -1
- package/dist/commands/replay.d.ts.map +1 -1
- package/dist/commands/replay.js +35 -5
- package/dist/commands/replay.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +336 -25
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/workspace.js +7 -7
- package/dist/commands/workspace.js.map +1 -1
- package/dist/daemon/server.d.ts +2 -2
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +2113 -32
- package/dist/daemon/server.js.map +1 -1
- package/dist/explainability/DeterminismClassifier.d.ts +34 -0
- package/dist/explainability/DeterminismClassifier.d.ts.map +1 -0
- package/dist/explainability/DeterminismClassifier.js +104 -0
- package/dist/explainability/DeterminismClassifier.js.map +1 -0
- package/dist/explainability/ViolationFormatter.d.ts +32 -0
- package/dist/explainability/ViolationFormatter.d.ts.map +1 -0
- package/dist/explainability/ViolationFormatter.js +252 -0
- package/dist/explainability/ViolationFormatter.js.map +1 -0
- package/dist/explainability/index.d.ts +15 -0
- package/dist/explainability/index.d.ts.map +1 -0
- package/dist/explainability/index.js +94 -0
- package/dist/explainability/index.js.map +1 -0
- package/dist/explainability/types.d.ts +37 -0
- package/dist/explainability/types.d.ts.map +1 -0
- package/dist/explainability/types.js +3 -0
- package/dist/explainability/types.js.map +1 -0
- package/dist/governance/canonical-pipeline.d.ts +38 -0
- package/dist/governance/canonical-pipeline.d.ts.map +1 -0
- package/dist/governance/canonical-pipeline.js +448 -0
- package/dist/governance/canonical-pipeline.js.map +1 -0
- package/dist/governance/structural-on-diff.d.ts +13 -0
- package/dist/governance/structural-on-diff.d.ts.map +1 -0
- package/dist/governance/structural-on-diff.js +35 -0
- package/dist/governance/structural-on-diff.js.map +1 -0
- package/dist/governance/structural-policy-merge.d.ts +14 -0
- package/dist/governance/structural-policy-merge.d.ts.map +1 -0
- package/dist/governance/structural-policy-merge.js +25 -0
- package/dist/governance/structural-policy-merge.js.map +1 -0
- package/dist/index.js +86 -4
- package/dist/index.js.map +1 -1
- package/dist/integrations/review-compression/index.d.ts +50 -0
- package/dist/integrations/review-compression/index.d.ts.map +1 -0
- package/dist/integrations/review-compression/index.js +158 -0
- package/dist/integrations/review-compression/index.js.map +1 -0
- package/dist/intent-engine/domain-taxonomy.d.ts +42 -0
- package/dist/intent-engine/domain-taxonomy.d.ts.map +1 -0
- package/dist/intent-engine/domain-taxonomy.js +534 -0
- package/dist/intent-engine/domain-taxonomy.js.map +1 -0
- package/dist/intent-engine/index.d.ts +1 -0
- package/dist/intent-engine/index.d.ts.map +1 -1
- package/dist/intent-engine/index.js +6 -1
- package/dist/intent-engine/index.js.map +1 -1
- package/dist/intent-engine/matcher.d.ts.map +1 -1
- package/dist/intent-engine/matcher.js +2 -0
- package/dist/intent-engine/matcher.js.map +1 -1
- package/dist/intent-engine/parser.d.ts.map +1 -1
- package/dist/intent-engine/parser.js +47 -0
- package/dist/intent-engine/parser.js.map +1 -1
- package/dist/intent-engine/semantic-expander.d.ts +104 -0
- package/dist/intent-engine/semantic-expander.d.ts.map +1 -0
- package/dist/intent-engine/semantic-expander.js +480 -0
- package/dist/intent-engine/semantic-expander.js.map +1 -0
- package/dist/patch-engine/diff.d.ts +1 -1
- package/dist/patch-engine/diff.js +1 -1
- package/dist/patch-engine/generator.d.ts +9 -0
- package/dist/patch-engine/generator.d.ts.map +1 -1
- package/dist/patch-engine/generator.js +375 -17
- package/dist/patch-engine/generator.js.map +1 -1
- package/dist/patch-engine/index.d.ts +25 -25
- package/dist/patch-engine/index.d.ts.map +1 -1
- package/dist/patch-engine/index.js +134 -87
- package/dist/patch-engine/index.js.map +1 -1
- package/dist/patch-engine/patterns.d.ts +1 -1
- package/dist/patch-engine/patterns.d.ts.map +1 -1
- package/dist/patch-engine/patterns.js +282 -41
- package/dist/patch-engine/patterns.js.map +1 -1
- package/dist/patch-engine/rollback.d.ts +31 -0
- package/dist/patch-engine/rollback.d.ts.map +1 -0
- package/dist/patch-engine/rollback.js +275 -0
- package/dist/patch-engine/rollback.js.map +1 -0
- package/dist/patch-engine/safety.d.ts +28 -0
- package/dist/patch-engine/safety.d.ts.map +1 -0
- package/dist/patch-engine/safety.js +122 -0
- package/dist/patch-engine/safety.js.map +1 -0
- package/dist/patch-engine/transaction.d.ts +52 -0
- package/dist/patch-engine/transaction.d.ts.map +1 -0
- package/dist/patch-engine/transaction.js +93 -0
- package/dist/patch-engine/transaction.js.map +1 -0
- package/dist/semantic/index.d.ts +14 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +30 -0
- package/dist/semantic/index.js.map +1 -0
- package/dist/semantic/tfidf-engine.d.ts +81 -0
- package/dist/semantic/tfidf-engine.d.ts.map +1 -0
- package/dist/semantic/tfidf-engine.js +278 -0
- package/dist/semantic/tfidf-engine.js.map +1 -0
- package/dist/semantic/vector-store.d.ts +108 -0
- package/dist/semantic/vector-store.d.ts.map +1 -0
- package/dist/semantic/vector-store.js +321 -0
- package/dist/semantic/vector-store.js.map +1 -0
- package/dist/structural-rules/context-severity.d.ts +46 -0
- package/dist/structural-rules/context-severity.d.ts.map +1 -0
- package/dist/structural-rules/context-severity.js +115 -0
- package/dist/structural-rules/context-severity.js.map +1 -0
- package/dist/structural-rules/distributed/DS001-saga-rollback-absence.d.ts +11 -0
- package/dist/structural-rules/distributed/DS001-saga-rollback-absence.d.ts.map +1 -0
- package/dist/structural-rules/distributed/DS001-saga-rollback-absence.js +212 -0
- package/dist/structural-rules/distributed/DS001-saga-rollback-absence.js.map +1 -0
- package/dist/structural-rules/distributed/DS002-missing-correlation-id.d.ts +11 -0
- package/dist/structural-rules/distributed/DS002-missing-correlation-id.d.ts.map +1 -0
- package/dist/structural-rules/distributed/DS002-missing-correlation-id.js +213 -0
- package/dist/structural-rules/distributed/DS002-missing-correlation-id.js.map +1 -0
- package/dist/structural-rules/distributed/index.d.ts +3 -0
- package/dist/structural-rules/distributed/index.d.ts.map +1 -0
- package/dist/structural-rules/distributed/index.js +8 -0
- package/dist/structural-rules/distributed/index.js.map +1 -0
- package/dist/structural-rules/engine.d.ts +25 -0
- package/dist/structural-rules/engine.d.ts.map +1 -0
- package/dist/structural-rules/engine.js +90 -0
- package/dist/structural-rules/engine.js.map +1 -0
- package/dist/structural-rules/index.d.ts +41 -0
- package/dist/structural-rules/index.d.ts.map +1 -0
- package/dist/structural-rules/index.js +141 -0
- package/dist/structural-rules/index.js.map +1 -0
- package/dist/structural-rules/python/PY001-asyncio-task-without-cancel.d.ts +11 -0
- package/dist/structural-rules/python/PY001-asyncio-task-without-cancel.d.ts.map +1 -0
- package/dist/structural-rules/python/PY001-asyncio-task-without-cancel.js +66 -0
- package/dist/structural-rules/python/PY001-asyncio-task-without-cancel.js.map +1 -0
- package/dist/structural-rules/python/PY002-unbounded-dict-singleton.d.ts +11 -0
- package/dist/structural-rules/python/PY002-unbounded-dict-singleton.d.ts.map +1 -0
- package/dist/structural-rules/python/PY002-unbounded-dict-singleton.js +135 -0
- package/dist/structural-rules/python/PY002-unbounded-dict-singleton.js.map +1 -0
- package/dist/structural-rules/python/PY003-broad-except-clause.d.ts +11 -0
- package/dist/structural-rules/python/PY003-broad-except-clause.d.ts.map +1 -0
- package/dist/structural-rules/python/PY003-broad-except-clause.js +86 -0
- package/dist/structural-rules/python/PY003-broad-except-clause.js.map +1 -0
- package/dist/structural-rules/python/PY004-swallowed-async-exception.d.ts +11 -0
- package/dist/structural-rules/python/PY004-swallowed-async-exception.d.ts.map +1 -0
- package/dist/structural-rules/python/PY004-swallowed-async-exception.js +167 -0
- package/dist/structural-rules/python/PY004-swallowed-async-exception.js.map +1 -0
- package/dist/structural-rules/python/PY005-fastapi-without-pydantic.d.ts +11 -0
- package/dist/structural-rules/python/PY005-fastapi-without-pydantic.d.ts.map +1 -0
- package/dist/structural-rules/python/PY005-fastapi-without-pydantic.js +154 -0
- package/dist/structural-rules/python/PY005-fastapi-without-pydantic.js.map +1 -0
- package/dist/structural-rules/python/PY006-blocking-io-in-async.d.ts +11 -0
- package/dist/structural-rules/python/PY006-blocking-io-in-async.d.ts.map +1 -0
- package/dist/structural-rules/python/PY006-blocking-io-in-async.js +130 -0
- package/dist/structural-rules/python/PY006-blocking-io-in-async.js.map +1 -0
- package/dist/structural-rules/python/PY007-sqlalchemy-session-leak.d.ts +11 -0
- package/dist/structural-rules/python/PY007-sqlalchemy-session-leak.d.ts.map +1 -0
- package/dist/structural-rules/python/PY007-sqlalchemy-session-leak.js +93 -0
- package/dist/structural-rules/python/PY007-sqlalchemy-session-leak.js.map +1 -0
- package/dist/structural-rules/python/PY008-celery-task-without-retry.d.ts +11 -0
- package/dist/structural-rules/python/PY008-celery-task-without-retry.d.ts.map +1 -0
- package/dist/structural-rules/python/PY008-celery-task-without-retry.js +154 -0
- package/dist/structural-rules/python/PY008-celery-task-without-retry.js.map +1 -0
- package/dist/structural-rules/python/PY009-unsafe-pickle-deserialization.d.ts +11 -0
- package/dist/structural-rules/python/PY009-unsafe-pickle-deserialization.d.ts.map +1 -0
- package/dist/structural-rules/python/PY009-unsafe-pickle-deserialization.js +133 -0
- package/dist/structural-rules/python/PY009-unsafe-pickle-deserialization.js.map +1 -0
- package/dist/structural-rules/python/PY010-leaked-aiohttp-session.d.ts +11 -0
- package/dist/structural-rules/python/PY010-leaked-aiohttp-session.d.ts.map +1 -0
- package/dist/structural-rules/python/PY010-leaked-aiohttp-session.js +80 -0
- package/dist/structural-rules/python/PY010-leaked-aiohttp-session.js.map +1 -0
- package/dist/structural-rules/rules/SR001-swallowed-async-rejection.d.ts +11 -0
- package/dist/structural-rules/rules/SR001-swallowed-async-rejection.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR001-swallowed-async-rejection.js +145 -0
- package/dist/structural-rules/rules/SR001-swallowed-async-rejection.js.map +1 -0
- package/dist/structural-rules/rules/SR002-unbounded-collection.d.ts +11 -0
- package/dist/structural-rules/rules/SR002-unbounded-collection.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR002-unbounded-collection.js +196 -0
- package/dist/structural-rules/rules/SR002-unbounded-collection.js.map +1 -0
- package/dist/structural-rules/rules/SR003-timer-without-cleanup.d.ts +11 -0
- package/dist/structural-rules/rules/SR003-timer-without-cleanup.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR003-timer-without-cleanup.js +148 -0
- package/dist/structural-rules/rules/SR003-timer-without-cleanup.js.map +1 -0
- package/dist/structural-rules/rules/SR004-request-boundary-no-validation.d.ts +11 -0
- package/dist/structural-rules/rules/SR004-request-boundary-no-validation.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR004-request-boundary-no-validation.js +162 -0
- package/dist/structural-rules/rules/SR004-request-boundary-no-validation.js.map +1 -0
- package/dist/structural-rules/rules/SR005-halfopen-probe-gate.d.ts +11 -0
- package/dist/structural-rules/rules/SR005-halfopen-probe-gate.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR005-halfopen-probe-gate.js +150 -0
- package/dist/structural-rules/rules/SR005-halfopen-probe-gate.js.map +1 -0
- package/dist/structural-rules/rules/SR006-fanout-error-sanitization.d.ts +11 -0
- package/dist/structural-rules/rules/SR006-fanout-error-sanitization.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR006-fanout-error-sanitization.js +161 -0
- package/dist/structural-rules/rules/SR006-fanout-error-sanitization.js.map +1 -0
- package/dist/structural-rules/rules/SR007-cross-request-error.d.ts +11 -0
- package/dist/structural-rules/rules/SR007-cross-request-error.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR007-cross-request-error.js +175 -0
- package/dist/structural-rules/rules/SR007-cross-request-error.js.map +1 -0
- package/dist/structural-rules/rules/SR008-background-task-orphan.d.ts +11 -0
- package/dist/structural-rules/rules/SR008-background-task-orphan.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR008-background-task-orphan.js +176 -0
- package/dist/structural-rules/rules/SR008-background-task-orphan.js.map +1 -0
- package/dist/structural-rules/rules/SR009-missing-retry-backoff.d.ts +11 -0
- package/dist/structural-rules/rules/SR009-missing-retry-backoff.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR009-missing-retry-backoff.js +168 -0
- package/dist/structural-rules/rules/SR009-missing-retry-backoff.js.map +1 -0
- package/dist/structural-rules/rules/SR010-retry-storm.d.ts +11 -0
- package/dist/structural-rules/rules/SR010-retry-storm.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR010-retry-storm.js +181 -0
- package/dist/structural-rules/rules/SR010-retry-storm.js.map +1 -0
- package/dist/structural-rules/rules/SR011-event-listener-leak.d.ts +11 -0
- package/dist/structural-rules/rules/SR011-event-listener-leak.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR011-event-listener-leak.js +208 -0
- package/dist/structural-rules/rules/SR011-event-listener-leak.js.map +1 -0
- package/dist/structural-rules/rules/SR012-promise-race-leak.d.ts +11 -0
- package/dist/structural-rules/rules/SR012-promise-race-leak.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR012-promise-race-leak.js +191 -0
- package/dist/structural-rules/rules/SR012-promise-race-leak.js.map +1 -0
- package/dist/structural-rules/rules/SR013-missing-idempotency-key.d.ts +11 -0
- package/dist/structural-rules/rules/SR013-missing-idempotency-key.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR013-missing-idempotency-key.js +219 -0
- package/dist/structural-rules/rules/SR013-missing-idempotency-key.js.map +1 -0
- package/dist/structural-rules/rules/SR014-mutable-closure-async.d.ts +11 -0
- package/dist/structural-rules/rules/SR014-mutable-closure-async.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR014-mutable-closure-async.js +208 -0
- package/dist/structural-rules/rules/SR014-mutable-closure-async.js.map +1 -0
- package/dist/structural-rules/rules/SR015-dangling-abort-controller.d.ts +11 -0
- package/dist/structural-rules/rules/SR015-dangling-abort-controller.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR015-dangling-abort-controller.js +190 -0
- package/dist/structural-rules/rules/SR015-dangling-abort-controller.js.map +1 -0
- package/dist/structural-rules/rules/SR016-unsafe-json-parse.d.ts +11 -0
- package/dist/structural-rules/rules/SR016-unsafe-json-parse.d.ts.map +1 -0
- package/dist/structural-rules/rules/SR016-unsafe-json-parse.js +187 -0
- package/dist/structural-rules/rules/SR016-unsafe-json-parse.js.map +1 -0
- package/dist/structural-rules/suppressions.d.ts +43 -0
- package/dist/structural-rules/suppressions.d.ts.map +1 -0
- package/dist/structural-rules/suppressions.js +115 -0
- package/dist/structural-rules/suppressions.js.map +1 -0
- package/dist/structural-rules/types.d.ts +43 -0
- package/dist/structural-rules/types.d.ts.map +1 -0
- package/dist/structural-rules/types.js +3 -0
- package/dist/structural-rules/types.js.map +1 -0
- package/dist/utils/advisory-signals.d.ts +5 -0
- package/dist/utils/advisory-signals.d.ts.map +1 -1
- package/dist/utils/advisory-signals.js +50 -12
- package/dist/utils/advisory-signals.js.map +1 -1
- package/dist/utils/ai-debt-budget.d.ts.map +1 -1
- package/dist/utils/ai-debt-budget.js +5 -2
- package/dist/utils/ai-debt-budget.js.map +1 -1
- package/dist/utils/brain-cache.d.ts +100 -0
- package/dist/utils/brain-cache.d.ts.map +1 -0
- package/dist/utils/brain-cache.js +346 -0
- package/dist/utils/brain-cache.js.map +1 -0
- package/dist/utils/cli-json.d.ts.map +1 -1
- package/dist/utils/cli-json.js +80 -12
- package/dist/utils/cli-json.js.map +1 -1
- package/dist/utils/execution-bus.d.ts +10 -0
- package/dist/utils/execution-bus.d.ts.map +1 -1
- package/dist/utils/execution-bus.js +16 -0
- package/dist/utils/execution-bus.js.map +1 -1
- package/dist/utils/governance-provenance.d.ts +95 -0
- package/dist/utils/governance-provenance.d.ts.map +1 -0
- package/dist/utils/governance-provenance.js +187 -0
- package/dist/utils/governance-provenance.js.map +1 -0
- package/dist/utils/pilot-metrics.d.ts +46 -0
- package/dist/utils/pilot-metrics.d.ts.map +1 -0
- package/dist/utils/pilot-metrics.js +240 -0
- package/dist/utils/pilot-metrics.js.map +1 -0
- package/dist/utils/policy-compiler.d.ts +6 -0
- package/dist/utils/policy-compiler.d.ts.map +1 -1
- package/dist/utils/policy-compiler.js +20 -0
- package/dist/utils/policy-compiler.js.map +1 -1
- package/dist/utils/replay-runtime.d.ts +34 -0
- package/dist/utils/replay-runtime.d.ts.map +1 -1
- package/dist/utils/replay-runtime.js +207 -0
- package/dist/utils/replay-runtime.js.map +1 -1
- package/dist/workspace/cross-repo-graph.d.ts +111 -0
- package/dist/workspace/cross-repo-graph.d.ts.map +1 -0
- package/dist/workspace/cross-repo-graph.js +450 -0
- package/dist/workspace/cross-repo-graph.js.map +1 -0
- package/dist/workspace/federated-context.d.ts +144 -0
- package/dist/workspace/federated-context.d.ts.map +1 -0
- package/dist/workspace/federated-context.js +347 -0
- package/dist/workspace/federated-context.js.map +1 -0
- package/dist/workspace/index.d.ts +38 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +48 -0
- package/dist/workspace/index.js.map +1 -0
- package/package.json +10 -10
package/dist/daemon/server.js
CHANGED
|
@@ -39,20 +39,187 @@ exports.startDaemon = startDaemon;
|
|
|
39
39
|
const http = __importStar(require("node:http"));
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
41
|
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const node_crypto_1 = require("node:crypto");
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const patch_engine_1 = require("../patch-engine");
|
|
45
|
+
const diff_1 = require("../patch-engine/diff");
|
|
42
46
|
const execution_bus_1 = require("../utils/execution-bus");
|
|
43
47
|
const runtime_events_1 = require("../utils/runtime-events");
|
|
44
48
|
const control_plane_1 = require("../utils/control-plane");
|
|
45
49
|
const workspace_runtime_1 = require("../utils/workspace-runtime");
|
|
50
|
+
const workspace_1 = require("../workspace");
|
|
51
|
+
const semantic_1 = require("../semantic");
|
|
52
|
+
const intent_engine_1 = require("../intent-engine");
|
|
46
53
|
const replay_runtime_1 = require("../utils/replay-runtime");
|
|
54
|
+
const contracts_1 = require("@neurcode-ai/contracts");
|
|
55
|
+
const telemetry_1 = require("@neurcode-ai/telemetry");
|
|
56
|
+
const governance_provenance_1 = require("../utils/governance-provenance");
|
|
57
|
+
const pilot_metrics_1 = require("../utils/pilot-metrics");
|
|
47
58
|
// ── Configuration ──────────────────────────────────────────────────────────────
|
|
48
|
-
exports.DAEMON_PORT = 4321;
|
|
49
|
-
exports.DAEMON_HOST = '127.0.0.1';
|
|
59
|
+
exports.DAEMON_PORT = Number.parseInt(process.env.NEURCODE_DAEMON_PORT || '4321', 10) || 4321;
|
|
60
|
+
exports.DAEMON_HOST = process.env.NEURCODE_DAEMON_HOST || '127.0.0.1';
|
|
50
61
|
const SSE_RETRY_MS = 3000;
|
|
62
|
+
const REQUEST_ID_HEADER = 'x-neurcode-request-id';
|
|
63
|
+
const ALLOW_NON_LOOPBACK = ['1', 'true', 'yes', 'on'].includes(String(process.env.NEURCODE_DAEMON_ALLOW_REMOTE || '').trim().toLowerCase());
|
|
51
64
|
const runtimeEventClients = new Map();
|
|
52
65
|
let runtimeEventClientSeq = 0;
|
|
53
66
|
let runtimeEventUnsubscribe = null;
|
|
54
67
|
let runtimeEventTailTimer = null;
|
|
55
68
|
let runtimeEventTailCursor = null;
|
|
69
|
+
const DAEMON_MAX_ERROR_HISTORY = 40;
|
|
70
|
+
const DAEMON_MAX_ROUTE_SAMPLE = 1000;
|
|
71
|
+
const daemonOpsMetrics = {
|
|
72
|
+
startedAt: new Date().toISOString(),
|
|
73
|
+
requestsTotal: 0,
|
|
74
|
+
requestsByMethod: {},
|
|
75
|
+
requestsByRoute: {},
|
|
76
|
+
failuresTotal: 0,
|
|
77
|
+
retriableFailuresTotal: 0,
|
|
78
|
+
stalePreviewRejections: 0,
|
|
79
|
+
rollbackStaleRejections: 0,
|
|
80
|
+
patchApplied: 0,
|
|
81
|
+
patchPartial: 0,
|
|
82
|
+
patchRejected: 0,
|
|
83
|
+
rollbackApplied: 0,
|
|
84
|
+
rollbackRejected: 0,
|
|
85
|
+
recentErrors: [],
|
|
86
|
+
};
|
|
87
|
+
function normalizeRoutePath(url) {
|
|
88
|
+
const pathOnly = url.split('?')[0]?.trim() || '/';
|
|
89
|
+
return pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
|
|
90
|
+
}
|
|
91
|
+
function incrementMetricCounter(record, key) {
|
|
92
|
+
record[key] = (record[key] || 0) + 1;
|
|
93
|
+
}
|
|
94
|
+
function recordDaemonRequest(url, method) {
|
|
95
|
+
daemonOpsMetrics.requestsTotal += 1;
|
|
96
|
+
incrementMetricCounter(daemonOpsMetrics.requestsByMethod, method.toUpperCase());
|
|
97
|
+
const route = normalizeRoutePath(url);
|
|
98
|
+
incrementMetricCounter(daemonOpsMetrics.requestsByRoute, route);
|
|
99
|
+
// Keep route cardinality bounded for long-lived daemon sessions.
|
|
100
|
+
const routeKeys = Object.keys(daemonOpsMetrics.requestsByRoute);
|
|
101
|
+
if (routeKeys.length > DAEMON_MAX_ROUTE_SAMPLE) {
|
|
102
|
+
const overflow = routeKeys
|
|
103
|
+
.sort((left, right) => (daemonOpsMetrics.requestsByRoute[left] || 0) - (daemonOpsMetrics.requestsByRoute[right] || 0))
|
|
104
|
+
.slice(0, routeKeys.length - DAEMON_MAX_ROUTE_SAMPLE);
|
|
105
|
+
for (const key of overflow) {
|
|
106
|
+
delete daemonOpsMetrics.requestsByRoute[key];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function recordDaemonFailure(sample) {
|
|
111
|
+
daemonOpsMetrics.failuresTotal += 1;
|
|
112
|
+
if (sample.retriable) {
|
|
113
|
+
daemonOpsMetrics.retriableFailuresTotal += 1;
|
|
114
|
+
}
|
|
115
|
+
daemonOpsMetrics.recentErrors.unshift({
|
|
116
|
+
at: new Date().toISOString(),
|
|
117
|
+
...sample,
|
|
118
|
+
});
|
|
119
|
+
if (daemonOpsMetrics.recentErrors.length > DAEMON_MAX_ERROR_HISTORY) {
|
|
120
|
+
daemonOpsMetrics.recentErrors.length = DAEMON_MAX_ERROR_HISTORY;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function recordPatchOutcome(status) {
|
|
124
|
+
if (status === 'applied') {
|
|
125
|
+
daemonOpsMetrics.patchApplied += 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (status === 'partial') {
|
|
129
|
+
daemonOpsMetrics.patchPartial += 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (status === 'stale_preview') {
|
|
133
|
+
daemonOpsMetrics.patchRejected += 1;
|
|
134
|
+
daemonOpsMetrics.stalePreviewRejections += 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (status === 'rollback_applied') {
|
|
138
|
+
daemonOpsMetrics.rollbackApplied += 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (status === 'rollback_stale') {
|
|
142
|
+
daemonOpsMetrics.rollbackRejected += 1;
|
|
143
|
+
daemonOpsMetrics.rollbackStaleRejections += 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (status === 'rollback_rejected') {
|
|
147
|
+
daemonOpsMetrics.rollbackRejected += 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
daemonOpsMetrics.patchRejected += 1;
|
|
151
|
+
}
|
|
152
|
+
function buildDaemonOperationalSummary(cwd) {
|
|
153
|
+
const uptimeSeconds = Math.max(0, Math.floor((Date.now() - new Date(daemonOpsMetrics.startedAt).getTime()) / 1000));
|
|
154
|
+
const lockPath = path.resolve(cwd, '.neurcode', 'executions', '.lock');
|
|
155
|
+
let lockPresent = false;
|
|
156
|
+
let lockAgeMs = null;
|
|
157
|
+
try {
|
|
158
|
+
const stat = fs.statSync(lockPath);
|
|
159
|
+
lockPresent = stat.isFile();
|
|
160
|
+
lockAgeMs = Date.now() - stat.mtimeMs;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
lockPresent = false;
|
|
164
|
+
lockAgeMs = null;
|
|
165
|
+
}
|
|
166
|
+
const executionQuery = (0, execution_bus_1.queryExecutions)(cwd, {
|
|
167
|
+
limit: 200,
|
|
168
|
+
status: 'all',
|
|
169
|
+
});
|
|
170
|
+
const items = executionQuery.items || [];
|
|
171
|
+
const activeExecutions = items.filter((item) => item.status !== 'completed' && item.status !== 'failed').length;
|
|
172
|
+
const recentFailures = items
|
|
173
|
+
.filter((item) => item.status === 'failed')
|
|
174
|
+
.slice(0, 10)
|
|
175
|
+
.map((item) => ({
|
|
176
|
+
id: item.id,
|
|
177
|
+
type: item.type,
|
|
178
|
+
source: item.source,
|
|
179
|
+
actor: item.actor,
|
|
180
|
+
completedAt: item.completedAt,
|
|
181
|
+
message: item.result?.message || null,
|
|
182
|
+
}));
|
|
183
|
+
const patchAttempts = daemonOpsMetrics.patchApplied + daemonOpsMetrics.patchPartial + daemonOpsMetrics.patchRejected;
|
|
184
|
+
const rollbackAttempts = daemonOpsMetrics.rollbackApplied + daemonOpsMetrics.rollbackRejected;
|
|
185
|
+
return {
|
|
186
|
+
uptimeSeconds,
|
|
187
|
+
activeExecutions,
|
|
188
|
+
sseClients: runtimeEventClients.size,
|
|
189
|
+
requestTotals: {
|
|
190
|
+
total: daemonOpsMetrics.requestsTotal,
|
|
191
|
+
failures: daemonOpsMetrics.failuresTotal,
|
|
192
|
+
retriableFailures: daemonOpsMetrics.retriableFailuresTotal,
|
|
193
|
+
byMethod: daemonOpsMetrics.requestsByMethod,
|
|
194
|
+
topRoutes: Object.entries(daemonOpsMetrics.requestsByRoute)
|
|
195
|
+
.sort((left, right) => right[1] - left[1])
|
|
196
|
+
.slice(0, 12)
|
|
197
|
+
.map(([route, count]) => ({ route, count })),
|
|
198
|
+
},
|
|
199
|
+
patchStats: {
|
|
200
|
+
attempts: patchAttempts,
|
|
201
|
+
applied: daemonOpsMetrics.patchApplied,
|
|
202
|
+
partial: daemonOpsMetrics.patchPartial,
|
|
203
|
+
rejected: daemonOpsMetrics.patchRejected,
|
|
204
|
+
stalePreviewRejections: daemonOpsMetrics.stalePreviewRejections,
|
|
205
|
+
successRate: patchAttempts > 0 ? Number((daemonOpsMetrics.patchApplied / patchAttempts).toFixed(4)) : null,
|
|
206
|
+
},
|
|
207
|
+
rollbackStats: {
|
|
208
|
+
attempts: rollbackAttempts,
|
|
209
|
+
applied: daemonOpsMetrics.rollbackApplied,
|
|
210
|
+
rejected: daemonOpsMetrics.rollbackRejected,
|
|
211
|
+
staleRejections: daemonOpsMetrics.rollbackStaleRejections,
|
|
212
|
+
successRate: rollbackAttempts > 0 ? Number((daemonOpsMetrics.rollbackApplied / rollbackAttempts).toFixed(4)) : null,
|
|
213
|
+
},
|
|
214
|
+
executionLock: {
|
|
215
|
+
path: lockPath,
|
|
216
|
+
present: lockPresent,
|
|
217
|
+
ageMs: lockAgeMs,
|
|
218
|
+
},
|
|
219
|
+
recentFailures,
|
|
220
|
+
recentErrors: daemonOpsMetrics.recentErrors,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
56
223
|
// ── Request helpers ────────────────────────────────────────────────────────────
|
|
57
224
|
function readBody(req) {
|
|
58
225
|
return new Promise((resolve, reject) => {
|
|
@@ -63,7 +230,18 @@ function readBody(req) {
|
|
|
63
230
|
});
|
|
64
231
|
}
|
|
65
232
|
function send(res, status, body) {
|
|
66
|
-
const
|
|
233
|
+
const requestIdRaw = res.getHeader(REQUEST_ID_HEADER);
|
|
234
|
+
const requestId = typeof requestIdRaw === 'string' && requestIdRaw.trim().length > 0
|
|
235
|
+
? requestIdRaw.trim()
|
|
236
|
+
: null;
|
|
237
|
+
const payloadBody = (requestId
|
|
238
|
+
&& body
|
|
239
|
+
&& typeof body === 'object'
|
|
240
|
+
&& !Array.isArray(body)
|
|
241
|
+
&& !Object.prototype.hasOwnProperty.call(body, 'requestId'))
|
|
242
|
+
? { ...body, requestId }
|
|
243
|
+
: body;
|
|
244
|
+
const payload = JSON.stringify(payloadBody);
|
|
67
245
|
res.writeHead(status, {
|
|
68
246
|
'Content-Type': 'application/json',
|
|
69
247
|
'Content-Length': Buffer.byteLength(payload),
|
|
@@ -73,8 +251,52 @@ function send(res, status, body) {
|
|
|
73
251
|
function success(res, data) {
|
|
74
252
|
send(res, 200, { success: true, data });
|
|
75
253
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
254
|
+
function defaultFailureCode(status, error) {
|
|
255
|
+
if (status === 400)
|
|
256
|
+
return contracts_1.DAEMON_ERROR_CODES.badRequest;
|
|
257
|
+
if (status === 401)
|
|
258
|
+
return contracts_1.DAEMON_ERROR_CODES.unauthorized;
|
|
259
|
+
if (status === 403)
|
|
260
|
+
return contracts_1.DAEMON_ERROR_CODES.forbidden;
|
|
261
|
+
if (status === 404) {
|
|
262
|
+
if (/no route for/i.test(error))
|
|
263
|
+
return contracts_1.DAEMON_ERROR_CODES.routeNotFound;
|
|
264
|
+
return contracts_1.DAEMON_ERROR_CODES.notFound;
|
|
265
|
+
}
|
|
266
|
+
if (status === 408)
|
|
267
|
+
return contracts_1.DAEMON_ERROR_CODES.timeout;
|
|
268
|
+
if (status === 409)
|
|
269
|
+
return contracts_1.DAEMON_ERROR_CODES.conflict;
|
|
270
|
+
if (status === 422)
|
|
271
|
+
return contracts_1.DAEMON_ERROR_CODES.validationFailed;
|
|
272
|
+
if (status === 429)
|
|
273
|
+
return contracts_1.DAEMON_ERROR_CODES.rateLimited;
|
|
274
|
+
if (status >= 500)
|
|
275
|
+
return contracts_1.DAEMON_ERROR_CODES.internalError;
|
|
276
|
+
return contracts_1.DAEMON_ERROR_CODES.unknown;
|
|
277
|
+
}
|
|
278
|
+
function failure(res, error, status = 500, options = {}) {
|
|
279
|
+
const code = options.code ?? defaultFailureCode(status, error);
|
|
280
|
+
const retriable = options.retriable ?? status >= 500;
|
|
281
|
+
const requestIdRaw = res.getHeader(REQUEST_ID_HEADER);
|
|
282
|
+
const requestId = typeof requestIdRaw === 'string' && requestIdRaw.trim().length > 0
|
|
283
|
+
? requestIdRaw.trim()
|
|
284
|
+
: null;
|
|
285
|
+
const route = res.__neurcodeRoutePath || '/unknown';
|
|
286
|
+
recordDaemonFailure({
|
|
287
|
+
route,
|
|
288
|
+
requestId,
|
|
289
|
+
code,
|
|
290
|
+
message: error,
|
|
291
|
+
retriable,
|
|
292
|
+
});
|
|
293
|
+
send(res, status, {
|
|
294
|
+
success: false,
|
|
295
|
+
error,
|
|
296
|
+
code,
|
|
297
|
+
retriable,
|
|
298
|
+
details: options.details ?? null,
|
|
299
|
+
});
|
|
78
300
|
}
|
|
79
301
|
function addCorsHeaders(res, req) {
|
|
80
302
|
// Wildcard is safe: daemon binds to 127.0.0.1 only and isLoopback() rejects
|
|
@@ -84,13 +306,294 @@ function addCorsHeaders(res, req) {
|
|
|
84
306
|
const requestedHeaders = Array.isArray(requestedHeadersRaw)
|
|
85
307
|
? requestedHeadersRaw.join(',')
|
|
86
308
|
: (requestedHeadersRaw || '');
|
|
87
|
-
const allowedHeaders = new Set(['content-type', 'x-neurcode-source', 'x-neurcode-actor']
|
|
309
|
+
const allowedHeaders = new Set(['content-type', 'x-neurcode-source', 'x-neurcode-actor', REQUEST_ID_HEADER]
|
|
88
310
|
.concat(requestedHeaders.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean)));
|
|
89
311
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
90
312
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
91
313
|
res.setHeader('Access-Control-Allow-Headers', [...allowedHeaders].join(', '));
|
|
92
314
|
res.setHeader('Access-Control-Max-Age', '86400');
|
|
93
315
|
}
|
|
316
|
+
function resolveGitRoot(cwd) {
|
|
317
|
+
const result = (0, node_child_process_1.spawnSync)('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
|
|
318
|
+
encoding: 'utf-8',
|
|
319
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
320
|
+
});
|
|
321
|
+
if (result.status !== 0)
|
|
322
|
+
return null;
|
|
323
|
+
const value = typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
324
|
+
return value.length > 0 ? value : null;
|
|
325
|
+
}
|
|
326
|
+
function captureGitDirtyPaths(cwd) {
|
|
327
|
+
const gitRoot = resolveGitRoot(cwd);
|
|
328
|
+
if (!gitRoot)
|
|
329
|
+
return null;
|
|
330
|
+
const statusResult = (0, node_child_process_1.spawnSync)('git', ['-C', cwd, 'status', '--porcelain=1', '-z', '--untracked-files=all'], {
|
|
331
|
+
encoding: 'utf-8',
|
|
332
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
333
|
+
});
|
|
334
|
+
if (statusResult.status !== 0 || typeof statusResult.stdout !== 'string')
|
|
335
|
+
return null;
|
|
336
|
+
const tokens = statusResult.stdout.split('\0').filter((entry) => entry.length > 0);
|
|
337
|
+
const dirty = new Set();
|
|
338
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
339
|
+
const token = tokens[index];
|
|
340
|
+
if (token.length < 4)
|
|
341
|
+
continue;
|
|
342
|
+
const status = token.slice(0, 2);
|
|
343
|
+
const filePath = token.slice(3).trim();
|
|
344
|
+
if (filePath.length > 0) {
|
|
345
|
+
dirty.add(path.resolve(gitRoot, filePath));
|
|
346
|
+
}
|
|
347
|
+
const renamedOrCopied = status.includes('R') || status.includes('C');
|
|
348
|
+
if (renamedOrCopied && index + 1 < tokens.length) {
|
|
349
|
+
index += 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return dirty;
|
|
353
|
+
}
|
|
354
|
+
function hashFileForDiff(absPath) {
|
|
355
|
+
try {
|
|
356
|
+
const stat = fs.statSync(absPath);
|
|
357
|
+
if (!stat.isFile())
|
|
358
|
+
return '<non-file>';
|
|
359
|
+
const content = fs.readFileSync(absPath);
|
|
360
|
+
return (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return '<missing>';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function captureDirtyFileFingerprints(cwd) {
|
|
367
|
+
const dirtyPaths = captureGitDirtyPaths(cwd);
|
|
368
|
+
if (!dirtyPaths)
|
|
369
|
+
return null;
|
|
370
|
+
const map = new Map();
|
|
371
|
+
for (const dirtyPath of dirtyPaths) {
|
|
372
|
+
map.set(dirtyPath, hashFileForDiff(dirtyPath));
|
|
373
|
+
}
|
|
374
|
+
return map;
|
|
375
|
+
}
|
|
376
|
+
function isAllowedPatchSideEffect(absPath, targetAbsPath, cwd) {
|
|
377
|
+
if (absPath === targetAbsPath)
|
|
378
|
+
return true;
|
|
379
|
+
const rel = path.relative(cwd, absPath);
|
|
380
|
+
if (!rel || rel.startsWith('..'))
|
|
381
|
+
return false;
|
|
382
|
+
if (rel === 'neurcode.policy.compiled.json')
|
|
383
|
+
return true;
|
|
384
|
+
return rel === '.neurcode' || rel.startsWith(`.neurcode${path.sep}`);
|
|
385
|
+
}
|
|
386
|
+
function collectUnexpectedPatchSideEffects(before, after, targetAbsPath, cwd) {
|
|
387
|
+
if (!before || !after)
|
|
388
|
+
return [];
|
|
389
|
+
const added = [...after].filter((entry) => !before.has(entry));
|
|
390
|
+
const unexpected = added
|
|
391
|
+
.filter((entry) => !isAllowedPatchSideEffect(entry, targetAbsPath, cwd))
|
|
392
|
+
.map((entry) => path.relative(cwd, entry).replace(/\\/g, '/'))
|
|
393
|
+
.filter((entry) => entry.length > 0)
|
|
394
|
+
.sort();
|
|
395
|
+
return unexpected;
|
|
396
|
+
}
|
|
397
|
+
function collectUnexpectedPatchMutations(before, after, targetAbsPath, cwd) {
|
|
398
|
+
if (!before || !after)
|
|
399
|
+
return [];
|
|
400
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
401
|
+
const unexpected = [];
|
|
402
|
+
for (const key of keys) {
|
|
403
|
+
const beforeHash = before.get(key) ?? '<missing>';
|
|
404
|
+
const afterHash = after.get(key) ?? '<missing>';
|
|
405
|
+
if (beforeHash === afterHash)
|
|
406
|
+
continue;
|
|
407
|
+
if (isAllowedPatchSideEffect(key, targetAbsPath, cwd))
|
|
408
|
+
continue;
|
|
409
|
+
const rel = path.relative(cwd, key).replace(/\\/g, '/');
|
|
410
|
+
if (rel.length > 0 && !rel.startsWith('..')) {
|
|
411
|
+
unexpected.push(rel);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return unexpected.sort();
|
|
415
|
+
}
|
|
416
|
+
function patternDescriptor(kind, confidence, manualReviewRequired) {
|
|
417
|
+
const labelByKind = {
|
|
418
|
+
missing_validation: 'API input validation guard',
|
|
419
|
+
missing_timeout_handling: 'Outbound request timeout guard',
|
|
420
|
+
unsafe_fetch_without_retries: 'Outbound request retry guard',
|
|
421
|
+
missing_idempotency_keys: 'Mutation idempotency-key guard',
|
|
422
|
+
unsafe_file_uploads: 'Upload MIME/size validation guard',
|
|
423
|
+
missing_auth_middleware: 'Route authentication middleware',
|
|
424
|
+
missing_rate_limiting: 'Route rate limiting middleware',
|
|
425
|
+
missing_token_expiry: 'JWT expiry enforcement',
|
|
426
|
+
unsafe_inner_html_usage: 'Unsafe DOM sink replacement',
|
|
427
|
+
unsafe_sensitive_logging: 'Sensitive log redaction',
|
|
428
|
+
db_in_ui: 'Service-layer boundary placeholder',
|
|
429
|
+
todo_fixme: 'TODO/FIXME debt marker removal',
|
|
430
|
+
};
|
|
431
|
+
const confidenceModel = confidence === 'high'
|
|
432
|
+
? 'high'
|
|
433
|
+
: confidence === 'medium'
|
|
434
|
+
? 'medium'
|
|
435
|
+
: 'low';
|
|
436
|
+
return {
|
|
437
|
+
kind,
|
|
438
|
+
label: labelByKind[kind] || kind,
|
|
439
|
+
deterministic: true,
|
|
440
|
+
confidenceModel,
|
|
441
|
+
advisoryOnly: confidenceModel === 'low',
|
|
442
|
+
manualReviewRequired,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function summarizeDiff(diff) {
|
|
446
|
+
let addedLines = 0;
|
|
447
|
+
let removedLines = 0;
|
|
448
|
+
for (const line of diff.split('\n')) {
|
|
449
|
+
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@'))
|
|
450
|
+
continue;
|
|
451
|
+
if (line.startsWith('+'))
|
|
452
|
+
addedLines += 1;
|
|
453
|
+
if (line.startsWith('-'))
|
|
454
|
+
removedLines += 1;
|
|
455
|
+
}
|
|
456
|
+
const changedLines = addedLines + removedLines;
|
|
457
|
+
return {
|
|
458
|
+
addedLines,
|
|
459
|
+
removedLines,
|
|
460
|
+
changedLines,
|
|
461
|
+
summary: `${changedLines} changed line(s): +${addedLines} / -${removedLines}`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function extractRequestInputUsage(content) {
|
|
465
|
+
const accessMatch = content.match(/\b(req|request)\.(body|params|query)\b/);
|
|
466
|
+
if (!accessMatch)
|
|
467
|
+
return null;
|
|
468
|
+
const receiver = accessMatch[1];
|
|
469
|
+
const field = accessMatch[2];
|
|
470
|
+
const fieldRegex = new RegExp(`\\b${receiver}\\.${field}\\.([A-Za-z_$][\\w$]*)\\b`, 'g');
|
|
471
|
+
const fields = [];
|
|
472
|
+
const seen = new Set();
|
|
473
|
+
let match = fieldRegex.exec(content);
|
|
474
|
+
while (match) {
|
|
475
|
+
const fieldName = match[1];
|
|
476
|
+
if (!seen.has(fieldName)) {
|
|
477
|
+
seen.add(fieldName);
|
|
478
|
+
fields.push(fieldName);
|
|
479
|
+
}
|
|
480
|
+
match = fieldRegex.exec(content);
|
|
481
|
+
}
|
|
482
|
+
return { receiver, field, fields };
|
|
483
|
+
}
|
|
484
|
+
function buildPatchPreviewReasoning(patternKind, targetPath, beforeContent) {
|
|
485
|
+
if (!patternKind)
|
|
486
|
+
return null;
|
|
487
|
+
if (patternKind === 'missing_validation') {
|
|
488
|
+
const usage = extractRequestInputUsage(beforeContent);
|
|
489
|
+
if (!usage) {
|
|
490
|
+
return {
|
|
491
|
+
summary: 'Adds deterministic API input validation guard.',
|
|
492
|
+
why: `This file accesses request input without a validation boundary check.`,
|
|
493
|
+
risk: 'Malformed input can cause runtime errors or unsafe processing paths.',
|
|
494
|
+
expectedOutcome: 'Invalid requests fail fast and valid requests continue unchanged.',
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const noun = usage.field === 'body' ? 'request body' : usage.field === 'params' ? 'route params' : 'query params';
|
|
498
|
+
const fieldSummary = usage.fields.length > 0 ? usage.fields.join(', ') : 'no explicit property access detected';
|
|
499
|
+
return {
|
|
500
|
+
summary: `Adds deterministic validation before reading ${usage.receiver}.${usage.field}.`,
|
|
501
|
+
why: `${targetPath} reads ${noun} fields (${fieldSummary}) before validation.`,
|
|
502
|
+
risk: `Without boundary validation, malformed ${noun} may propagate into handler logic.`,
|
|
503
|
+
expectedOutcome: `Invalid ${noun} returns HTTP 400 early; valid requests keep existing behavior.`,
|
|
504
|
+
fields: usage.fields,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (patternKind === 'db_in_ui') {
|
|
508
|
+
return {
|
|
509
|
+
summary: 'Suggests moving direct DB access behind a service boundary.',
|
|
510
|
+
why: `${targetPath} appears to perform direct data access in a non-service layer.`,
|
|
511
|
+
risk: 'Layering violations increase coupling and make behavior harder to govern.',
|
|
512
|
+
expectedOutcome: 'Patch inserts a deterministic placeholder to redirect to service-layer logic.',
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (patternKind === 'missing_auth_middleware') {
|
|
516
|
+
return {
|
|
517
|
+
summary: 'Adds deterministic authentication middleware to the route definition.',
|
|
518
|
+
why: `${targetPath} appears to expose a request handler without an auth middleware guard.`,
|
|
519
|
+
risk: 'Unauthenticated routes can expose sensitive behavior to unauthorized clients.',
|
|
520
|
+
expectedOutcome: 'Route execution is gated by requireAuth before handler logic runs.',
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (patternKind === 'missing_rate_limiting') {
|
|
524
|
+
return {
|
|
525
|
+
summary: 'Adds deterministic rate-limit middleware to the route definition.',
|
|
526
|
+
why: `${targetPath} appears to expose a request handler without rate limiting controls.`,
|
|
527
|
+
risk: 'Unbounded request rates can increase abuse, cost, and availability risks.',
|
|
528
|
+
expectedOutcome: 'Route applies rateLimitGuard before handler execution.',
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (patternKind === 'missing_timeout_handling') {
|
|
532
|
+
return {
|
|
533
|
+
summary: 'Adds deterministic timeout guard to outbound fetch call.',
|
|
534
|
+
why: `${targetPath} issues a fetch request without timeout protection.`,
|
|
535
|
+
risk: 'Unbounded network calls can hang request execution and degrade reliability under upstream latency.',
|
|
536
|
+
expectedOutcome: 'Fetch call aborts after timeout and fails fast instead of hanging.',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (patternKind === 'unsafe_fetch_without_retries') {
|
|
540
|
+
return {
|
|
541
|
+
summary: 'Wraps outbound fetch call in deterministic retry guard.',
|
|
542
|
+
why: `${targetPath} makes outbound network calls without transient failure retry handling.`,
|
|
543
|
+
risk: 'Single transient failures can become user-facing errors and increase instability.',
|
|
544
|
+
expectedOutcome: 'Transient upstream failures retry deterministically before failing.',
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (patternKind === 'missing_idempotency_keys') {
|
|
548
|
+
return {
|
|
549
|
+
summary: 'Adds deterministic idempotency-key guard for side-effecting requests.',
|
|
550
|
+
why: `${targetPath} appears to process payment/order-like mutations without idempotency key enforcement.`,
|
|
551
|
+
risk: 'Duplicate requests can cause repeated side effects (double charges/orders).',
|
|
552
|
+
expectedOutcome: 'Requests missing idempotency key fail early with explicit error.',
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (patternKind === 'unsafe_file_uploads') {
|
|
556
|
+
return {
|
|
557
|
+
summary: 'Adds deterministic MIME and size guards for uploaded files.',
|
|
558
|
+
why: `${targetPath} appears to process uploaded files without boundary checks.`,
|
|
559
|
+
risk: 'Unbounded or unsafe uploads increase security and stability risk.',
|
|
560
|
+
expectedOutcome: 'Invalid upload payloads are rejected before processing.',
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (patternKind === 'missing_token_expiry') {
|
|
564
|
+
return {
|
|
565
|
+
summary: 'Adds deterministic token expiry to JWT signing call.',
|
|
566
|
+
why: `${targetPath} signs JWT tokens without an expiresIn option.`,
|
|
567
|
+
risk: 'Long-lived tokens increase replay and account-compromise blast radius.',
|
|
568
|
+
expectedOutcome: 'Tokens gain explicit expiry to enforce credential rotation windows.',
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (patternKind === 'unsafe_inner_html_usage') {
|
|
572
|
+
return {
|
|
573
|
+
summary: 'Replaces unsafe innerHTML assignment with textContent.',
|
|
574
|
+
why: `${targetPath} writes HTML content directly into the DOM using innerHTML.`,
|
|
575
|
+
risk: 'innerHTML assignments can expose XSS vectors when input is not trusted.',
|
|
576
|
+
expectedOutcome: 'DOM assignment becomes text-only rendering with reduced injection risk.',
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (patternKind === 'unsafe_sensitive_logging') {
|
|
580
|
+
return {
|
|
581
|
+
summary: 'Removes deterministic sensitive logging line.',
|
|
582
|
+
why: `${targetPath} appears to log secret-bearing fields (token/authorization/password).`,
|
|
583
|
+
risk: 'Sensitive log content can leak credentials to observability or audit sinks.',
|
|
584
|
+
expectedOutcome: 'Sensitive logging path is replaced with a neutral warning placeholder.',
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (patternKind === 'todo_fixme') {
|
|
588
|
+
return {
|
|
589
|
+
summary: 'Removes TODO/FIXME marker matched by policy.',
|
|
590
|
+
why: `${targetPath} includes TODO/FIXME comments tracked as governance debt.`,
|
|
591
|
+
risk: 'Unresolved TODO markers can hide missing implementation or review debt.',
|
|
592
|
+
expectedOutcome: 'Patch removes the marker; implementation must still be verified separately.',
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
94
597
|
function isExecutionActionType(value) {
|
|
95
598
|
if (typeof value !== 'string')
|
|
96
599
|
return false;
|
|
@@ -102,7 +605,119 @@ function isExecutionActionType(value) {
|
|
|
102
605
|
|| value === 'policy-sync'
|
|
103
606
|
|| value === 'intent-update');
|
|
104
607
|
}
|
|
608
|
+
function asObjectRecord(value) {
|
|
609
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
610
|
+
return null;
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
function asObjectArray(value) {
|
|
614
|
+
if (!Array.isArray(value))
|
|
615
|
+
return [];
|
|
616
|
+
return value
|
|
617
|
+
.map((entry) => asObjectRecord(entry))
|
|
618
|
+
.filter((entry) => entry !== null);
|
|
619
|
+
}
|
|
620
|
+
function toLegacyViolation(entry, fallbackSeverity) {
|
|
621
|
+
const file = typeof entry.file === 'string' && entry.file.trim().length > 0
|
|
622
|
+
? entry.file.trim()
|
|
623
|
+
: '';
|
|
624
|
+
const message = typeof entry.message === 'string' && entry.message.trim().length > 0
|
|
625
|
+
? entry.message.trim()
|
|
626
|
+
: '';
|
|
627
|
+
if (!file || !message)
|
|
628
|
+
return null;
|
|
629
|
+
const severity = typeof entry.severity === 'string' && entry.severity.trim().length > 0
|
|
630
|
+
? entry.severity.trim()
|
|
631
|
+
: fallbackSeverity;
|
|
632
|
+
const rule = typeof entry.rule === 'string' && entry.rule.trim().length > 0
|
|
633
|
+
? entry.rule.trim()
|
|
634
|
+
: typeof entry.policy === 'string' && entry.policy.trim().length > 0
|
|
635
|
+
? entry.policy.trim()
|
|
636
|
+
: '';
|
|
637
|
+
return { file, message, severity, rule };
|
|
638
|
+
}
|
|
639
|
+
function normalizeVerifyPayloadForLegacyClients(payload) {
|
|
640
|
+
if (!payload)
|
|
641
|
+
return null;
|
|
642
|
+
const existingViolations = asObjectArray(payload.violations)
|
|
643
|
+
.map((entry) => toLegacyViolation(entry, 'warn'))
|
|
644
|
+
.filter((entry) => entry !== null);
|
|
645
|
+
const blockingItems = asObjectArray(payload.blockingItems)
|
|
646
|
+
.map((entry) => toLegacyViolation(entry, 'block'))
|
|
647
|
+
.filter((entry) => entry !== null);
|
|
648
|
+
const advisoryItems = asObjectArray(payload.advisoryItems)
|
|
649
|
+
.map((entry) => toLegacyViolation(entry, 'warn'))
|
|
650
|
+
.filter((entry) => entry !== null);
|
|
651
|
+
const warnings = asObjectArray(payload.warnings)
|
|
652
|
+
.map((entry) => toLegacyViolation(entry, 'warn'))
|
|
653
|
+
.filter((entry) => entry !== null);
|
|
654
|
+
const merged = [...existingViolations];
|
|
655
|
+
const canonicalSeverity = (value) => {
|
|
656
|
+
const normalized = value.trim().toLowerCase();
|
|
657
|
+
if (normalized === 'block' || normalized === 'critical' || normalized === 'high')
|
|
658
|
+
return 'block';
|
|
659
|
+
if (normalized === 'warn' || normalized === 'warning' || normalized === 'advisory' || normalized === 'medium' || normalized === 'low')
|
|
660
|
+
return 'warn';
|
|
661
|
+
return normalized;
|
|
662
|
+
};
|
|
663
|
+
const canonicalKey = (entry) => `${entry.file}::${entry.rule}::${entry.message}::${canonicalSeverity(entry.severity)}`;
|
|
664
|
+
const seen = new Set(merged.map((entry) => canonicalKey(entry)));
|
|
665
|
+
for (const item of [...blockingItems, ...advisoryItems, ...warnings]) {
|
|
666
|
+
const key = canonicalKey(item);
|
|
667
|
+
if (seen.has(key))
|
|
668
|
+
continue;
|
|
669
|
+
seen.add(key);
|
|
670
|
+
merged.push(item);
|
|
671
|
+
}
|
|
672
|
+
if (merged.length === 0)
|
|
673
|
+
return payload;
|
|
674
|
+
return {
|
|
675
|
+
...payload,
|
|
676
|
+
violations: merged,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function normalizeFixPayloadForLegacyClients(payload) {
|
|
680
|
+
if (!payload)
|
|
681
|
+
return null;
|
|
682
|
+
const suggestions = asObjectArray(payload.suggestions);
|
|
683
|
+
if (suggestions.length === 0)
|
|
684
|
+
return payload;
|
|
685
|
+
const deduped = [];
|
|
686
|
+
const seen = new Set();
|
|
687
|
+
for (const suggestion of suggestions) {
|
|
688
|
+
const file = typeof suggestion.file === 'string' ? suggestion.file.trim() : '';
|
|
689
|
+
const line = typeof suggestion.line === 'number' && Number.isFinite(suggestion.line)
|
|
690
|
+
? String(Math.floor(suggestion.line))
|
|
691
|
+
: '';
|
|
692
|
+
const message = typeof suggestion.message === 'string' ? suggestion.message.trim() : '';
|
|
693
|
+
const rule = typeof suggestion.rule === 'string'
|
|
694
|
+
? suggestion.rule.trim()
|
|
695
|
+
: typeof suggestion.policy === 'string'
|
|
696
|
+
? suggestion.policy.trim()
|
|
697
|
+
: '';
|
|
698
|
+
const confidence = typeof suggestion.confidence === 'string' ? suggestion.confidence.trim().toLowerCase() : '';
|
|
699
|
+
const patch = asObjectRecord(suggestion.patch);
|
|
700
|
+
const patchDiff = patch && typeof patch.diff === 'string' ? patch.diff : '';
|
|
701
|
+
const key = `${file}::${line}::${rule}::${message}::${confidence}::${(0, node_crypto_1.createHash)('sha1').update(patchDiff).digest('hex')}`;
|
|
702
|
+
if (seen.has(key))
|
|
703
|
+
continue;
|
|
704
|
+
seen.add(key);
|
|
705
|
+
deduped.push(suggestion);
|
|
706
|
+
}
|
|
707
|
+
if (deduped.length === suggestions.length)
|
|
708
|
+
return payload;
|
|
709
|
+
return {
|
|
710
|
+
...payload,
|
|
711
|
+
suggestions: deduped,
|
|
712
|
+
_normalization: {
|
|
713
|
+
...(asObjectRecord(payload._normalization) || {}),
|
|
714
|
+
suggestionsDeduped: suggestions.length - deduped.length,
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
105
718
|
function isLoopback(req) {
|
|
719
|
+
if (ALLOW_NON_LOOPBACK)
|
|
720
|
+
return true;
|
|
106
721
|
const addr = req.socket.remoteAddress ?? '';
|
|
107
722
|
return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
|
|
108
723
|
}
|
|
@@ -350,8 +965,9 @@ async function handleVerify(req, res) {
|
|
|
350
965
|
failure(res, run.execution.result?.message || 'verify execution produced no payload');
|
|
351
966
|
return;
|
|
352
967
|
}
|
|
968
|
+
const normalizedPayload = normalizeVerifyPayloadForLegacyClients(run.primaryPayload ?? null) ?? run.primaryPayload;
|
|
353
969
|
success(res, {
|
|
354
|
-
...
|
|
970
|
+
...normalizedPayload,
|
|
355
971
|
_execution: {
|
|
356
972
|
id: run.execution.id,
|
|
357
973
|
type: run.execution.type,
|
|
@@ -376,9 +992,11 @@ async function handleFix(req, res) {
|
|
|
376
992
|
failure(res, run.execution.result?.message || 'fix execution produced no payload');
|
|
377
993
|
return;
|
|
378
994
|
}
|
|
995
|
+
const normalizedFixPayload = normalizeFixPayloadForLegacyClients(run.primaryPayload) ?? run.primaryPayload;
|
|
996
|
+
const normalizedVerifyAfter = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
|
|
379
997
|
success(res, {
|
|
380
|
-
...
|
|
381
|
-
verifyAfter:
|
|
998
|
+
...normalizedFixPayload,
|
|
999
|
+
verifyAfter: normalizedVerifyAfter ?? null,
|
|
382
1000
|
_execution: {
|
|
383
1001
|
id: run.execution.id,
|
|
384
1002
|
type: run.execution.type,
|
|
@@ -403,9 +1021,11 @@ async function handleFixApplySafe(req, res) {
|
|
|
403
1021
|
failure(res, run.execution.result?.message || 'fix --apply-safe execution produced no payload');
|
|
404
1022
|
return;
|
|
405
1023
|
}
|
|
1024
|
+
const normalizedFixPayload = normalizeFixPayloadForLegacyClients(run.primaryPayload) ?? run.primaryPayload;
|
|
1025
|
+
const normalizedVerifyAfter = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
|
|
406
1026
|
success(res, {
|
|
407
|
-
...
|
|
408
|
-
verifyAfter:
|
|
1027
|
+
...normalizedFixPayload,
|
|
1028
|
+
verifyAfter: normalizedVerifyAfter ?? null,
|
|
409
1029
|
execution: run.execution,
|
|
410
1030
|
});
|
|
411
1031
|
}
|
|
@@ -423,24 +1043,36 @@ async function handlePatch(req, res) {
|
|
|
423
1043
|
failure(res, 'Missing or unsafe "file" field', 400);
|
|
424
1044
|
return;
|
|
425
1045
|
}
|
|
1046
|
+
const previewToken = typeof body.previewToken === 'string' && body.previewToken.trim().length > 0
|
|
1047
|
+
? body.previewToken.trim()
|
|
1048
|
+
: undefined;
|
|
1049
|
+
const cwd = process.cwd();
|
|
1050
|
+
const targetPath = file.trim();
|
|
1051
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
1052
|
+
const beforeDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1053
|
+
const beforeDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
426
1054
|
// Capture file content before patch to detect real change
|
|
427
|
-
const absPath = path.resolve(process.cwd(), file);
|
|
428
1055
|
let contentBefore = null;
|
|
429
1056
|
try {
|
|
430
1057
|
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
431
1058
|
}
|
|
432
1059
|
catch { /* file may not exist */ }
|
|
1060
|
+
const primaryArgs = ['patch', '--file', targetPath];
|
|
1061
|
+
if (previewToken) {
|
|
1062
|
+
primaryArgs.push('--preview-token', previewToken);
|
|
1063
|
+
}
|
|
433
1064
|
const run = await (0, execution_bus_1.runExecution)({
|
|
434
1065
|
type: 'patch',
|
|
435
1066
|
source: toSource(req),
|
|
436
1067
|
actor: toActor(req),
|
|
437
|
-
target:
|
|
438
|
-
cwd
|
|
1068
|
+
target: targetPath,
|
|
1069
|
+
cwd,
|
|
439
1070
|
reverify: true,
|
|
1071
|
+
primaryArgs,
|
|
440
1072
|
});
|
|
441
1073
|
const patchData = run.primaryPayload ?? {
|
|
442
1074
|
success: false,
|
|
443
|
-
file,
|
|
1075
|
+
file: targetPath,
|
|
444
1076
|
message: run.execution.result?.message || 'No applicable patch found',
|
|
445
1077
|
};
|
|
446
1078
|
// Validate that the file actually changed on disk
|
|
@@ -452,12 +1084,298 @@ async function handlePatch(req, res) {
|
|
|
452
1084
|
}
|
|
453
1085
|
catch { /* ignore read error */ }
|
|
454
1086
|
}
|
|
1087
|
+
const afterDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1088
|
+
const afterDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
1089
|
+
const sideEffects = collectUnexpectedPatchSideEffects(beforeDirtyPaths, afterDirtyPaths, absPath, cwd);
|
|
1090
|
+
const mutatedSideEffects = collectUnexpectedPatchMutations(beforeDirtyFingerprints, afterDirtyFingerprints, absPath, cwd);
|
|
1091
|
+
const combinedSideEffects = [...new Set([...sideEffects, ...mutatedSideEffects])].sort();
|
|
1092
|
+
const payloadFile = typeof patchData.file === 'string' ? patchData.file : '';
|
|
1093
|
+
const payloadTargetMatch = payloadFile.length > 0
|
|
1094
|
+
? path.resolve(cwd, payloadFile) === absPath
|
|
1095
|
+
: true;
|
|
1096
|
+
const patchSucceeded = patchData.success === true;
|
|
1097
|
+
const rawPatchStatus = typeof patchData.status === 'string' ? patchData.status : '';
|
|
1098
|
+
const patchStatus = rawPatchStatus === 'filesystem_changed_since_preview'
|
|
1099
|
+
? 'stale_preview'
|
|
1100
|
+
: !patchSucceeded
|
|
1101
|
+
? 'rejected'
|
|
1102
|
+
: changed && payloadTargetMatch && combinedSideEffects.length === 0
|
|
1103
|
+
? 'applied'
|
|
1104
|
+
: changed
|
|
1105
|
+
? 'partial'
|
|
1106
|
+
: 'rejected';
|
|
1107
|
+
const patchMessage = (() => {
|
|
1108
|
+
if (patchStatus === 'applied') {
|
|
1109
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1110
|
+
? patchData.message
|
|
1111
|
+
: `${contracts_1.STATUS_TERMS.safePatchApplied}`;
|
|
1112
|
+
}
|
|
1113
|
+
if (patchStatus === 'partial') {
|
|
1114
|
+
if (!payloadTargetMatch) {
|
|
1115
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: patch target mismatch detected between requested file and daemon payload file.`;
|
|
1116
|
+
}
|
|
1117
|
+
if (combinedSideEffects.length > 0) {
|
|
1118
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: patch introduced side effects in ${combinedSideEffects.length} additional file(s).`;
|
|
1119
|
+
}
|
|
1120
|
+
return `${contracts_1.STATUS_TERMS.safePatchApplied}. ${contracts_1.STATUS_TERMS.manualReviewRecommended}.`;
|
|
1121
|
+
}
|
|
1122
|
+
if (patchStatus === 'stale_preview') {
|
|
1123
|
+
return `${contracts_1.STATUS_TERMS.filesystemChangedSincePreview}. Regenerate patch preview and retry. ${contracts_1.STATUS_TERMS.retrySafe}.`;
|
|
1124
|
+
}
|
|
1125
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1126
|
+
? patchData.message
|
|
1127
|
+
: `${contracts_1.STATUS_TERMS.patchRejected}; no deterministic file-scoped change applied`;
|
|
1128
|
+
})();
|
|
1129
|
+
const reverifyRequired = patchStatus === 'applied' || patchStatus === 'partial';
|
|
1130
|
+
const stateLabel = patchStatus === 'stale_preview'
|
|
1131
|
+
? contracts_1.STATUS_TERMS.filesystemChangedSincePreview.toLowerCase()
|
|
1132
|
+
: (0, contracts_1.toPatchStateLabel)(patchStatus).toLowerCase();
|
|
1133
|
+
recordPatchOutcome(patchStatus);
|
|
1134
|
+
const normalizedVerifyPayload = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
|
|
1135
|
+
success(res, {
|
|
1136
|
+
patch: {
|
|
1137
|
+
...patchData,
|
|
1138
|
+
file: payloadFile || targetPath,
|
|
1139
|
+
success: patchStatus === 'applied',
|
|
1140
|
+
rawSuccess: patchData.success === true,
|
|
1141
|
+
changed,
|
|
1142
|
+
status: patchStatus,
|
|
1143
|
+
targetMatch: payloadTargetMatch,
|
|
1144
|
+
sideEffects: combinedSideEffects,
|
|
1145
|
+
message: patchMessage,
|
|
1146
|
+
reverifyRequired,
|
|
1147
|
+
stateLabel,
|
|
1148
|
+
previewTokenUsed: previewToken ? true : false,
|
|
1149
|
+
},
|
|
1150
|
+
verify: normalizedVerifyPayload ?? null,
|
|
1151
|
+
execution: run.execution,
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
async function handlePatchRollback(req, res) {
|
|
1155
|
+
let body = {};
|
|
1156
|
+
try {
|
|
1157
|
+
body = JSON.parse(await readBody(req));
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
failure(res, 'Invalid JSON body', 400);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const file = body.file;
|
|
1164
|
+
const receiptId = typeof body.receiptId === 'string' ? body.receiptId.trim() : '';
|
|
1165
|
+
if (!file || typeof file !== 'string' || file.includes('..')) {
|
|
1166
|
+
failure(res, 'Missing or unsafe "file" field', 400);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (!receiptId) {
|
|
1170
|
+
failure(res, 'Missing "receiptId" field', 400);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const cwd = process.cwd();
|
|
1174
|
+
const targetPath = file.trim();
|
|
1175
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
1176
|
+
const beforeDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1177
|
+
const beforeDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
1178
|
+
let contentBefore = null;
|
|
1179
|
+
try {
|
|
1180
|
+
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
1181
|
+
}
|
|
1182
|
+
catch { /* file may not exist */ }
|
|
1183
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
1184
|
+
type: 'patch',
|
|
1185
|
+
source: toSource(req),
|
|
1186
|
+
actor: toActor(req),
|
|
1187
|
+
target: targetPath,
|
|
1188
|
+
cwd,
|
|
1189
|
+
reverify: true,
|
|
1190
|
+
primaryArgs: ['patch', '--file', targetPath, '--rollback-receipt', receiptId, '--json'],
|
|
1191
|
+
});
|
|
1192
|
+
const patchData = run.primaryPayload ?? {
|
|
1193
|
+
success: false,
|
|
1194
|
+
file: targetPath,
|
|
1195
|
+
message: run.execution.result?.message || 'No rollback receipt could be applied',
|
|
1196
|
+
};
|
|
1197
|
+
let changed = false;
|
|
1198
|
+
if (patchData.success && contentBefore !== null) {
|
|
1199
|
+
try {
|
|
1200
|
+
const contentAfter = fs.readFileSync(absPath, 'utf-8');
|
|
1201
|
+
changed = contentAfter !== contentBefore;
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
// ignore read error
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const afterDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1208
|
+
const afterDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
1209
|
+
const sideEffects = collectUnexpectedPatchSideEffects(beforeDirtyPaths, afterDirtyPaths, absPath, cwd);
|
|
1210
|
+
const mutatedSideEffects = collectUnexpectedPatchMutations(beforeDirtyFingerprints, afterDirtyFingerprints, absPath, cwd);
|
|
1211
|
+
const combinedSideEffects = [...new Set([...sideEffects, ...mutatedSideEffects])].sort();
|
|
1212
|
+
const payloadFile = typeof patchData.file === 'string' ? patchData.file : '';
|
|
1213
|
+
const payloadTargetMatch = payloadFile.length > 0
|
|
1214
|
+
? path.resolve(cwd, payloadFile) === absPath
|
|
1215
|
+
: true;
|
|
1216
|
+
const rawStatus = typeof patchData.status === 'string' ? patchData.status : '';
|
|
1217
|
+
const rollbackStatus = rawStatus === 'rollback_applied'
|
|
1218
|
+
? 'rollback_applied'
|
|
1219
|
+
: rawStatus === 'rollback_stale' || rawStatus === 'filesystem_changed_since_patch'
|
|
1220
|
+
? 'rollback_stale'
|
|
1221
|
+
: 'rollback_rejected';
|
|
1222
|
+
const rollbackSucceeded = patchData.success === true && rollbackStatus === 'rollback_applied' && payloadTargetMatch && combinedSideEffects.length === 0;
|
|
1223
|
+
const rollbackMessage = (() => {
|
|
1224
|
+
if (rollbackSucceeded) {
|
|
1225
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1226
|
+
? patchData.message
|
|
1227
|
+
: contracts_1.STATUS_TERMS.rollbackApplied;
|
|
1228
|
+
}
|
|
1229
|
+
if (!payloadTargetMatch) {
|
|
1230
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: rollback receipt target mismatch detected.`;
|
|
1231
|
+
}
|
|
1232
|
+
if (combinedSideEffects.length > 0) {
|
|
1233
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: rollback side effects detected in ${combinedSideEffects.length} additional file(s).`;
|
|
1234
|
+
}
|
|
1235
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1236
|
+
? patchData.message
|
|
1237
|
+
: contracts_1.STATUS_TERMS.patchRejected;
|
|
1238
|
+
})();
|
|
1239
|
+
recordPatchOutcome(rollbackStatus);
|
|
455
1240
|
success(res, {
|
|
456
|
-
patch: {
|
|
457
|
-
|
|
1241
|
+
patch: {
|
|
1242
|
+
...patchData,
|
|
1243
|
+
file: payloadFile || targetPath,
|
|
1244
|
+
success: rollbackSucceeded,
|
|
1245
|
+
rawSuccess: patchData.success === true,
|
|
1246
|
+
changed,
|
|
1247
|
+
status: rollbackStatus,
|
|
1248
|
+
targetMatch: payloadTargetMatch,
|
|
1249
|
+
sideEffects: combinedSideEffects,
|
|
1250
|
+
message: rollbackMessage,
|
|
1251
|
+
reverifyRequired: rollbackSucceeded,
|
|
1252
|
+
stateLabel: rollbackSucceeded
|
|
1253
|
+
? contracts_1.STATUS_TERMS.rollbackApplied.toLowerCase()
|
|
1254
|
+
: rollbackStatus === 'rollback_stale'
|
|
1255
|
+
? contracts_1.STATUS_TERMS.filesystemChangedSincePreview.toLowerCase()
|
|
1256
|
+
: contracts_1.STATUS_TERMS.patchRejected.toLowerCase(),
|
|
1257
|
+
previewTokenUsed: false,
|
|
1258
|
+
},
|
|
1259
|
+
verify: normalizeVerifyPayloadForLegacyClients(run.verificationPayload) ?? null,
|
|
458
1260
|
execution: run.execution,
|
|
459
1261
|
});
|
|
460
1262
|
}
|
|
1263
|
+
async function handlePatchPreview(req, res) {
|
|
1264
|
+
let body = {};
|
|
1265
|
+
try {
|
|
1266
|
+
body = JSON.parse(await readBody(req));
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
failure(res, 'Invalid JSON body', 400);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const file = body.file;
|
|
1273
|
+
if (!file || typeof file !== 'string' || file.includes('..')) {
|
|
1274
|
+
failure(res, 'Missing or unsafe "file" field', 400);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const cwd = process.cwd();
|
|
1278
|
+
const targetPath = file.trim();
|
|
1279
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
1280
|
+
let contentBefore = '';
|
|
1281
|
+
try {
|
|
1282
|
+
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
1283
|
+
}
|
|
1284
|
+
catch {
|
|
1285
|
+
failure(res, `File not found: ${targetPath}`, 404);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const preview = (0, patch_engine_1.applyFirstMatchingPatch)(targetPath, contentBefore);
|
|
1289
|
+
if (!preview) {
|
|
1290
|
+
success(res, {
|
|
1291
|
+
success: false,
|
|
1292
|
+
file: targetPath,
|
|
1293
|
+
status: 'rejected',
|
|
1294
|
+
message: `No deterministic patch preview available for ${targetPath}`,
|
|
1295
|
+
beforeContent: contentBefore,
|
|
1296
|
+
afterContent: null,
|
|
1297
|
+
diff: null,
|
|
1298
|
+
changed: false,
|
|
1299
|
+
patternKind: null,
|
|
1300
|
+
patchConfidence: null,
|
|
1301
|
+
patchHash: null,
|
|
1302
|
+
previewToken: null,
|
|
1303
|
+
validation: null,
|
|
1304
|
+
recipe: null,
|
|
1305
|
+
pattern: null,
|
|
1306
|
+
whatChanges: null,
|
|
1307
|
+
rollbackPreviewDiff: null,
|
|
1308
|
+
whySafe: null,
|
|
1309
|
+
manualReviewRequired: true,
|
|
1310
|
+
supportedDeterministicPattern: false,
|
|
1311
|
+
reasoning: null,
|
|
1312
|
+
});
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const reasoning = buildPatchPreviewReasoning(preview.patternKind, targetPath, contentBefore);
|
|
1316
|
+
const rollbackPreviewDiff = (0, diff_1.generateUnifiedDiff)(targetPath, preview.updatedContent, contentBefore);
|
|
1317
|
+
const changeSummary = summarizeDiff(preview.diff);
|
|
1318
|
+
const manualReviewRequired = preview.patchConfidence === 'low'
|
|
1319
|
+
|| preview.validation.safe !== true
|
|
1320
|
+
|| preview.recipe.requiresManualReview === true;
|
|
1321
|
+
const pattern = patternDescriptor(preview.patternKind, preview.patchConfidence, manualReviewRequired);
|
|
1322
|
+
const whySafe = {
|
|
1323
|
+
deterministic: true,
|
|
1324
|
+
validationPassed: preview.validation.safe === true,
|
|
1325
|
+
confidence: preview.patchConfidence,
|
|
1326
|
+
checks: preview.validation.checks,
|
|
1327
|
+
reasonCodes: preview.validation.reasonCodes,
|
|
1328
|
+
};
|
|
1329
|
+
if (!preview.validation.safe) {
|
|
1330
|
+
success(res, {
|
|
1331
|
+
success: false,
|
|
1332
|
+
file: targetPath,
|
|
1333
|
+
status: 'rejected',
|
|
1334
|
+
message: `Patch preview rejected by deterministic safety validation (${preview.validation.reasonCodes.join(', ') || 'unknown'}).`,
|
|
1335
|
+
beforeContent: contentBefore,
|
|
1336
|
+
afterContent: null,
|
|
1337
|
+
diff: preview.diff,
|
|
1338
|
+
changed: false,
|
|
1339
|
+
patternKind: preview.patternKind,
|
|
1340
|
+
patchConfidence: preview.patchConfidence,
|
|
1341
|
+
patchHash: preview.patchHash,
|
|
1342
|
+
previewToken: preview.previewToken,
|
|
1343
|
+
validation: preview.validation,
|
|
1344
|
+
recipe: preview.recipe,
|
|
1345
|
+
pattern,
|
|
1346
|
+
whatChanges: changeSummary,
|
|
1347
|
+
rollbackPreviewDiff,
|
|
1348
|
+
whySafe,
|
|
1349
|
+
manualReviewRequired,
|
|
1350
|
+
supportedDeterministicPattern: true,
|
|
1351
|
+
reasoning,
|
|
1352
|
+
});
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
success(res, {
|
|
1356
|
+
success: true,
|
|
1357
|
+
file: targetPath,
|
|
1358
|
+
status: 'preview',
|
|
1359
|
+
message: 'Patch preview generated',
|
|
1360
|
+
beforeContent: contentBefore,
|
|
1361
|
+
afterContent: preview.updatedContent,
|
|
1362
|
+
diff: preview.diff,
|
|
1363
|
+
changed: contentBefore !== preview.updatedContent,
|
|
1364
|
+
patternKind: preview.patternKind,
|
|
1365
|
+
patchConfidence: preview.patchConfidence,
|
|
1366
|
+
patchHash: preview.patchHash,
|
|
1367
|
+
previewToken: preview.previewToken,
|
|
1368
|
+
validation: preview.validation,
|
|
1369
|
+
recipe: preview.recipe,
|
|
1370
|
+
pattern,
|
|
1371
|
+
whatChanges: changeSummary,
|
|
1372
|
+
rollbackPreviewDiff,
|
|
1373
|
+
whySafe,
|
|
1374
|
+
manualReviewRequired,
|
|
1375
|
+
supportedDeterministicPattern: true,
|
|
1376
|
+
reasoning,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
461
1379
|
async function handleExecute(req, res) {
|
|
462
1380
|
let body = {};
|
|
463
1381
|
try {
|
|
@@ -855,6 +1773,243 @@ async function handleExecuteWorkspace(req, res) {
|
|
|
855
1773
|
});
|
|
856
1774
|
success(res, result);
|
|
857
1775
|
}
|
|
1776
|
+
// ── Semantic Search Handlers ──────────────────────────────────────────────────
|
|
1777
|
+
/**
|
|
1778
|
+
* POST /workspaces/:id/semantic-search
|
|
1779
|
+
*
|
|
1780
|
+
* Body: { query: string, limit?: number, minScore?: number }
|
|
1781
|
+
* Returns: { results: SemanticSearchResult[], totalIndexed: number }
|
|
1782
|
+
*
|
|
1783
|
+
* Performs TF-IDF vector similarity search over the brain context index.
|
|
1784
|
+
* Fully deterministic — zero LLM calls, zero external dependencies.
|
|
1785
|
+
*/
|
|
1786
|
+
async function handleSemanticSearch(req, res, workspaceId) {
|
|
1787
|
+
const workspace = (0, workspace_runtime_1.getWorkspaceById)(workspaceId, process.cwd());
|
|
1788
|
+
if (!workspace) {
|
|
1789
|
+
failure(res, `Workspace not found: ${workspaceId}`, 404, { code: 'workspace.not_found' });
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
let body;
|
|
1793
|
+
try {
|
|
1794
|
+
body = JSON.parse(await readBody(req));
|
|
1795
|
+
}
|
|
1796
|
+
catch {
|
|
1797
|
+
failure(res, 'Invalid JSON body', 400, { code: 'bad_request' });
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const query = typeof body.query === 'string' ? body.query.trim() : '';
|
|
1801
|
+
if (!query) {
|
|
1802
|
+
failure(res, 'Missing required field: query', 400, { code: 'bad_request' });
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const limit = typeof body.limit === 'number' ? Math.min(50, Math.max(1, body.limit)) : 10;
|
|
1806
|
+
const minScore = typeof body.minScore === 'number' ? Math.max(0, body.minScore) : 0.01;
|
|
1807
|
+
// Use the first enabled repo root, or fall back to process.cwd()
|
|
1808
|
+
const rootPath = workspace.repositories.find((r) => r.enabled)?.rootPath ?? process.cwd();
|
|
1809
|
+
const scope = { orgId: null, projectId: null };
|
|
1810
|
+
const stats = (0, semantic_1.getSemanticIndexStats)(rootPath, scope);
|
|
1811
|
+
// Auto-build index if it doesn't exist yet
|
|
1812
|
+
if (!stats.exists || stats.documentCount === 0) {
|
|
1813
|
+
(0, semantic_1.buildSemanticIndex)(rootPath, scope);
|
|
1814
|
+
}
|
|
1815
|
+
const results = (0, semantic_1.semanticSearch)(rootPath, scope, query, { limit, minScore });
|
|
1816
|
+
const updatedStats = (0, semantic_1.getSemanticIndexStats)(rootPath, scope);
|
|
1817
|
+
success(res, {
|
|
1818
|
+
schemaVersion: 'neurcode.semantic-search.v1',
|
|
1819
|
+
workspaceId,
|
|
1820
|
+
workspaceName: workspace.name,
|
|
1821
|
+
query,
|
|
1822
|
+
results,
|
|
1823
|
+
totalIndexed: updatedStats.documentCount,
|
|
1824
|
+
indexBuiltAt: updatedStats.builtAt,
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* POST /workspaces/:id/semantic-index/build
|
|
1829
|
+
*
|
|
1830
|
+
* Forces a full semantic index rebuild from the current brain context.
|
|
1831
|
+
* Returns: { documentsIndexed: number, builtAt: string }
|
|
1832
|
+
*/
|
|
1833
|
+
async function handleBuildSemanticIndex(_req, res, workspaceId) {
|
|
1834
|
+
const workspace = (0, workspace_runtime_1.getWorkspaceById)(workspaceId, process.cwd());
|
|
1835
|
+
if (!workspace) {
|
|
1836
|
+
failure(res, `Workspace not found: ${workspaceId}`, 404, { code: 'workspace.not_found' });
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
const rootPath = workspace.repositories.find((r) => r.enabled)?.rootPath ?? process.cwd();
|
|
1840
|
+
const scope = { orgId: null, projectId: null };
|
|
1841
|
+
const count = (0, semantic_1.buildSemanticIndex)(rootPath, scope);
|
|
1842
|
+
const stats = (0, semantic_1.getSemanticIndexStats)(rootPath, scope);
|
|
1843
|
+
success(res, {
|
|
1844
|
+
schemaVersion: 'neurcode.semantic-index.v1',
|
|
1845
|
+
workspaceId,
|
|
1846
|
+
workspaceName: workspace.name,
|
|
1847
|
+
documentsIndexed: count,
|
|
1848
|
+
builtAt: stats.builtAt,
|
|
1849
|
+
sizeBytes: stats.sizeBytes,
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* POST /workspaces/:id/intent-expand
|
|
1854
|
+
*
|
|
1855
|
+
* Body: { intent: string, forceRefresh?: boolean }
|
|
1856
|
+
*
|
|
1857
|
+
* Returns a rich semantic governance artifact for the given intent.
|
|
1858
|
+
* The artifact is HMAC-signed and cached — subsequent calls return
|
|
1859
|
+
* the stored artifact without calling the LLM again.
|
|
1860
|
+
*
|
|
1861
|
+
* expansionMethod='llm' → LLM was called (first time only)
|
|
1862
|
+
* expansionMethod='keyword-fallback' → no LLM key configured
|
|
1863
|
+
* signature present → tamper-evident, auditable
|
|
1864
|
+
*/
|
|
1865
|
+
async function handleIntentExpand(req, res, workspaceId) {
|
|
1866
|
+
const workspace = (0, workspace_runtime_1.getWorkspaceById)(workspaceId, process.cwd());
|
|
1867
|
+
if (!workspace) {
|
|
1868
|
+
failure(res, `Workspace not found: ${workspaceId}`, 404, { code: 'workspace.not_found' });
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
let body;
|
|
1872
|
+
try {
|
|
1873
|
+
body = JSON.parse(await readBody(req));
|
|
1874
|
+
}
|
|
1875
|
+
catch {
|
|
1876
|
+
failure(res, 'Invalid JSON body', 400, { code: 'bad_request' });
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const intent = typeof body.intent === 'string' ? body.intent.trim() : '';
|
|
1880
|
+
if (!intent) {
|
|
1881
|
+
failure(res, 'Missing required field: intent', 400, { code: 'bad_request' });
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const forceRefresh = body.forceRefresh === true;
|
|
1885
|
+
const rootPath = workspace.repositories.find((r) => r.enabled)?.rootPath ?? process.cwd();
|
|
1886
|
+
// Check cache first (synchronous — no LLM call)
|
|
1887
|
+
if (!forceRefresh) {
|
|
1888
|
+
const cached = (0, intent_engine_1.loadCachedExpansion)(rootPath, intent);
|
|
1889
|
+
if (cached) {
|
|
1890
|
+
success(res, {
|
|
1891
|
+
schemaVersion: 'neurcode.intent-expansion.v1',
|
|
1892
|
+
workspaceId,
|
|
1893
|
+
workspaceName: workspace.name,
|
|
1894
|
+
cacheHit: true,
|
|
1895
|
+
expansion: cached,
|
|
1896
|
+
summary: (0, intent_engine_1.formatExpansionSummary)(cached),
|
|
1897
|
+
});
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
// Expand (may call LLM or use fallback)
|
|
1902
|
+
try {
|
|
1903
|
+
const expansion = await (0, intent_engine_1.expandIntent)(intent, { cwd: rootPath, forceRefresh });
|
|
1904
|
+
success(res, {
|
|
1905
|
+
schemaVersion: 'neurcode.intent-expansion.v1',
|
|
1906
|
+
workspaceId,
|
|
1907
|
+
workspaceName: workspace.name,
|
|
1908
|
+
cacheHit: false,
|
|
1909
|
+
expansion,
|
|
1910
|
+
summary: (0, intent_engine_1.formatExpansionSummary)(expansion),
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
catch (err) {
|
|
1914
|
+
failure(res, `Intent expansion failed: ${err instanceof Error ? err.message : String(err)}`, 500, { code: 'intent_expansion.failed' });
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
// ── Cross-Repo Context Handlers ──────────────────────────────────────────────
|
|
1918
|
+
/**
|
|
1919
|
+
* GET /workspaces/:id/cross-repo-graph
|
|
1920
|
+
*
|
|
1921
|
+
* Returns the detected cross-repo dependency graph for all repos in a workspace.
|
|
1922
|
+
* Used by Fix Center to show which services are coupled to the file being patched.
|
|
1923
|
+
*
|
|
1924
|
+
* Query params:
|
|
1925
|
+
* ?format=summary — human-readable summary (default: full JSON)
|
|
1926
|
+
*/
|
|
1927
|
+
async function handleGetCrossRepoGraph(req, res, workspaceId) {
|
|
1928
|
+
const workspace = (0, workspace_runtime_1.getWorkspaceById)(workspaceId, process.cwd());
|
|
1929
|
+
if (!workspace) {
|
|
1930
|
+
failure(res, `Workspace not found: ${workspaceId}`, 404, { code: 'workspace.not_found' });
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const repos = workspace.repositories.filter((r) => r.enabled);
|
|
1934
|
+
if (repos.length === 0) {
|
|
1935
|
+
success(res, {
|
|
1936
|
+
schemaVersion: 'neurcode.cross-repo-graph.v1',
|
|
1937
|
+
workspaceId,
|
|
1938
|
+
workspaceName: workspace.name,
|
|
1939
|
+
graph: { generatedAt: new Date().toISOString(), repos: [], edges: [], stats: { filesScanned: 0, edgesDetected: 0, byVia: {}, byConfidence: {} } },
|
|
1940
|
+
edgeCount: 0,
|
|
1941
|
+
});
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const graph = (0, workspace_1.buildCrossRepoGraph)({ repos });
|
|
1945
|
+
success(res, {
|
|
1946
|
+
schemaVersion: 'neurcode.cross-repo-graph.v1',
|
|
1947
|
+
workspaceId,
|
|
1948
|
+
workspaceName: workspace.name,
|
|
1949
|
+
graph,
|
|
1950
|
+
edgeCount: graph.edges.length,
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* POST /workspaces/:id/federated-context
|
|
1955
|
+
*
|
|
1956
|
+
* Builds the full federated context for a set of changed files in the primary repo.
|
|
1957
|
+
* This is the multi-repo blast radius analysis endpoint.
|
|
1958
|
+
*
|
|
1959
|
+
* Body: { primaryRepo: string; changedFiles: string[] }
|
|
1960
|
+
*
|
|
1961
|
+
* Returns:
|
|
1962
|
+
* - affectedDownstreamRepos: services that call the changed code
|
|
1963
|
+
* - relevantUpstreamRepos: services the changed code calls
|
|
1964
|
+
* - federatedBlindSpots: structural coupling invisible to code scanning
|
|
1965
|
+
* - summary.requiresCoordinatedDeploy: true if any high-confidence edge exists
|
|
1966
|
+
*/
|
|
1967
|
+
async function handleGetFederatedContext(req, res, workspaceId) {
|
|
1968
|
+
const workspace = (0, workspace_runtime_1.getWorkspaceById)(workspaceId, process.cwd());
|
|
1969
|
+
if (!workspace) {
|
|
1970
|
+
failure(res, `Workspace not found: ${workspaceId}`, 404, { code: 'workspace.not_found' });
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
let body = {};
|
|
1974
|
+
try {
|
|
1975
|
+
body = JSON.parse(await readBody(req));
|
|
1976
|
+
}
|
|
1977
|
+
catch {
|
|
1978
|
+
failure(res, 'Invalid JSON body', 400, { code: 'invalid_body' });
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const primaryRepo = asNonEmptyString(body.primaryRepo);
|
|
1982
|
+
if (!primaryRepo) {
|
|
1983
|
+
failure(res, 'Missing required field: primaryRepo', 400, { code: 'missing_field' });
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const changedFiles = Array.isArray(body.changedFiles)
|
|
1987
|
+
? body.changedFiles.filter((f) => typeof f === 'string')
|
|
1988
|
+
: [];
|
|
1989
|
+
if (changedFiles.length === 0) {
|
|
1990
|
+
failure(res, 'changedFiles must be a non-empty array of strings', 400, { code: 'missing_field' });
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
const repoExists = workspace.repositories.some((r) => r.name === primaryRepo);
|
|
1994
|
+
if (!repoExists) {
|
|
1995
|
+
failure(res, `Repo "${primaryRepo}" not found in workspace "${workspaceId}"`, 404, { code: 'repo.not_found' });
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const pkg = (0, workspace_1.buildFederatedContext)({
|
|
1999
|
+
workspaceName: workspace.name,
|
|
2000
|
+
repos: workspace.repositories,
|
|
2001
|
+
primaryRepoName: primaryRepo,
|
|
2002
|
+
changedFiles,
|
|
2003
|
+
});
|
|
2004
|
+
const { workspaceName: _wn, ...pkgRest } = pkg;
|
|
2005
|
+
success(res, {
|
|
2006
|
+
schemaVersion: 'neurcode.federated-context.v1',
|
|
2007
|
+
workspaceId,
|
|
2008
|
+
workspaceName: workspace.name,
|
|
2009
|
+
...pkgRest,
|
|
2010
|
+
humanSummary: (0, workspace_1.formatFederatedContextSummary)(pkg),
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
858
2013
|
async function handleReplayState(req, res) {
|
|
859
2014
|
const requestUrl = new URL(req.url || '/replay/state', 'http://localhost');
|
|
860
2015
|
const at = asNonEmptyString(requestUrl.searchParams.get('at')) || new Date().toISOString();
|
|
@@ -963,9 +2118,782 @@ async function handleRuntimeEventStream(req, res) {
|
|
|
963
2118
|
req.on('error', cleanup);
|
|
964
2119
|
res.on('close', cleanup);
|
|
965
2120
|
}
|
|
2121
|
+
// ── Governance findings handler ───────────────────────────────────────────────
|
|
2122
|
+
// ── Enterprise docs handlers ────────────────────────────────────────────────
|
|
2123
|
+
/** Returns a manifest of all enterprise docs with titles and slugs. */
|
|
2124
|
+
async function handleDocsManifest(res) {
|
|
2125
|
+
// Resolve docs directory relative to repo root (two levels up from packages/cli/dist or src)
|
|
2126
|
+
const possibleRoots = [
|
|
2127
|
+
path.resolve(__dirname, '..', '..', '..', '..', 'docs', 'enterprise'),
|
|
2128
|
+
path.resolve(process.cwd(), 'docs', 'enterprise'),
|
|
2129
|
+
];
|
|
2130
|
+
let docsDir = '';
|
|
2131
|
+
for (const candidate of possibleRoots) {
|
|
2132
|
+
if (fs.existsSync(candidate)) {
|
|
2133
|
+
docsDir = candidate;
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (!docsDir) {
|
|
2138
|
+
success(res, { docs: [], docsDir: null, error: 'docs/enterprise/ directory not found' });
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
const files = fs.readdirSync(docsDir).filter((f) => f.endsWith('.md')).sort();
|
|
2143
|
+
const docs = files.map((filename) => {
|
|
2144
|
+
const slug = filename.replace(/\.md$/, '');
|
|
2145
|
+
let title = slug;
|
|
2146
|
+
try {
|
|
2147
|
+
const firstLine = fs.readFileSync(path.join(docsDir, filename), 'utf8').split('\n')[0] || '';
|
|
2148
|
+
const match = firstLine.match(/^#+ (.+)/);
|
|
2149
|
+
if (match)
|
|
2150
|
+
title = match[1].trim();
|
|
2151
|
+
}
|
|
2152
|
+
catch { /* use slug as title */ }
|
|
2153
|
+
return { slug, filename, title };
|
|
2154
|
+
});
|
|
2155
|
+
success(res, { docs, docsDir });
|
|
2156
|
+
}
|
|
2157
|
+
catch (err) {
|
|
2158
|
+
failure(res, err instanceof Error ? err.message : String(err), 500);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
/** Returns the raw markdown content of a single enterprise doc file. */
|
|
2162
|
+
async function handleDocsContent(res, slug) {
|
|
2163
|
+
// Sanitize: only allow alphanumeric, dash, underscore — no path traversal
|
|
2164
|
+
if (!/^[\w-]+$/.test(slug)) {
|
|
2165
|
+
failure(res, 'Invalid doc slug', 400);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
const possibleRoots = [
|
|
2169
|
+
path.resolve(__dirname, '..', '..', '..', '..', 'docs', 'enterprise'),
|
|
2170
|
+
path.resolve(process.cwd(), 'docs', 'enterprise'),
|
|
2171
|
+
];
|
|
2172
|
+
let docFile = '';
|
|
2173
|
+
for (const rootDir of possibleRoots) {
|
|
2174
|
+
const candidate = path.join(rootDir, `${slug}.md`);
|
|
2175
|
+
if (fs.existsSync(candidate)) {
|
|
2176
|
+
docFile = candidate;
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!docFile) {
|
|
2181
|
+
failure(res, `Doc not found: ${slug}`, 404);
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
try {
|
|
2185
|
+
const content = fs.readFileSync(docFile, 'utf8');
|
|
2186
|
+
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
2187
|
+
res.end(content);
|
|
2188
|
+
}
|
|
2189
|
+
catch (err) {
|
|
2190
|
+
failure(res, err instanceof Error ? err.message : String(err), 500);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
// ── End enterprise docs handlers ────────────────────────────────────────────
|
|
2194
|
+
async function handleGovernanceFindings(req, res) {
|
|
2195
|
+
const cwd = process.cwd();
|
|
2196
|
+
const requestUrl = new URL(req.url || '/governance/findings', 'http://localhost');
|
|
2197
|
+
const limitRaw = parseInt(requestUrl.searchParams.get('limit') || '100', 10);
|
|
2198
|
+
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 500) : 100;
|
|
2199
|
+
const filterSeverity = requestUrl.searchParams.get('severity') || undefined;
|
|
2200
|
+
const filterRule = requestUrl.searchParams.get('rule') || undefined;
|
|
2201
|
+
const filterFile = requestUrl.searchParams.get('file') || undefined;
|
|
2202
|
+
const filterDeterminism = requestUrl.searchParams.get('determinism') || undefined;
|
|
2203
|
+
const candidatePaths = [
|
|
2204
|
+
path.resolve(cwd, '.neurcode', 'last-verify-output.json'),
|
|
2205
|
+
path.resolve(cwd, '.neurcode', 'verify-output.json'),
|
|
2206
|
+
];
|
|
2207
|
+
let rawFindings = [];
|
|
2208
|
+
let sourceFile = '';
|
|
2209
|
+
let envelope = {};
|
|
2210
|
+
for (const candidate of candidatePaths) {
|
|
2211
|
+
try {
|
|
2212
|
+
if (fs.existsSync(candidate)) {
|
|
2213
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
2214
|
+
if (parsed && typeof parsed === 'object') {
|
|
2215
|
+
envelope = parsed;
|
|
2216
|
+
const findings = parsed.findings;
|
|
2217
|
+
if (Array.isArray(findings)) {
|
|
2218
|
+
rawFindings = findings;
|
|
2219
|
+
}
|
|
2220
|
+
else {
|
|
2221
|
+
const violations = parsed.violations;
|
|
2222
|
+
if (Array.isArray(violations)) {
|
|
2223
|
+
rawFindings = violations.map((v) => ({
|
|
2224
|
+
findingId: v.ruleId || 'unknown',
|
|
2225
|
+
ruleId: v.ruleId || 'unknown',
|
|
2226
|
+
ruleName: v.message || 'Unknown finding',
|
|
2227
|
+
severity: v.severity || 'advisory',
|
|
2228
|
+
category: 'structural',
|
|
2229
|
+
determinism: 'deterministic-structural',
|
|
2230
|
+
file: v.file,
|
|
2231
|
+
line: v.line,
|
|
2232
|
+
message: v.message,
|
|
2233
|
+
explanation: v.explanation,
|
|
2234
|
+
source: 'structural-rule-engine',
|
|
2235
|
+
confidence: v.confidence || 1.0,
|
|
2236
|
+
remediable: true,
|
|
2237
|
+
remediationStatus: 'pending',
|
|
2238
|
+
}));
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
sourceFile = candidate;
|
|
2242
|
+
}
|
|
2243
|
+
break;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
catch {
|
|
2247
|
+
// continue to next candidate
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
// Filter
|
|
2251
|
+
let filtered = rawFindings;
|
|
2252
|
+
if (filterSeverity) {
|
|
2253
|
+
filtered = filtered.filter((f) => (f.severity || '') === filterSeverity);
|
|
2254
|
+
}
|
|
2255
|
+
if (filterRule) {
|
|
2256
|
+
filtered = filtered.filter((f) => {
|
|
2257
|
+
const ruleId = f.ruleId || '';
|
|
2258
|
+
const ruleName = f.ruleName || '';
|
|
2259
|
+
return ruleId.includes(filterRule) || ruleName.toLowerCase().includes(filterRule.toLowerCase());
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
if (filterFile) {
|
|
2263
|
+
filtered = filtered.filter((f) => {
|
|
2264
|
+
const filePath = f.file || '';
|
|
2265
|
+
return filePath.includes(filterFile);
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
if (filterDeterminism) {
|
|
2269
|
+
filtered = filtered.filter((f) => (f.determinism || '') === filterDeterminism);
|
|
2270
|
+
}
|
|
2271
|
+
const paginated = filtered.slice(0, limit);
|
|
2272
|
+
const blockingCount = rawFindings.filter((f) => {
|
|
2273
|
+
const sev = (f.severity || '').toLowerCase();
|
|
2274
|
+
return sev === 'critical' || sev === 'high' || sev === 'blocking' || sev === 'block';
|
|
2275
|
+
}).length;
|
|
2276
|
+
const advisoryCount = rawFindings.length - blockingCount;
|
|
2277
|
+
const meta = {
|
|
2278
|
+
verdict: envelope.verdict || (rawFindings.length === 0 ? 'PASS' : blockingCount > 0 ? 'FAIL' : 'WARN'),
|
|
2279
|
+
verifiedAt: envelope.verifiedAt || envelope.timestamp || null,
|
|
2280
|
+
projectRoot: envelope.projectRoot || cwd,
|
|
2281
|
+
planId: envelope.planId || null,
|
|
2282
|
+
schemaVersion: envelope.schemaVersion || null,
|
|
2283
|
+
fingerprint: envelope.fingerprint || null,
|
|
2284
|
+
totalFindings: rawFindings.length,
|
|
2285
|
+
blockingCount,
|
|
2286
|
+
advisoryCount,
|
|
2287
|
+
structuralCount: rawFindings.filter((f) => f.category === 'structural').length,
|
|
2288
|
+
semanticCount: rawFindings.filter((f) => f.category === 'semantic').length,
|
|
2289
|
+
sourceFile: sourceFile || null,
|
|
2290
|
+
filtered: filtered.length,
|
|
2291
|
+
returned: paginated.length,
|
|
2292
|
+
};
|
|
2293
|
+
success(res, { meta, findings: paginated });
|
|
2294
|
+
}
|
|
2295
|
+
// ── Governance overview handler ───────────────────────────────────────────────
|
|
2296
|
+
async function handleGovernanceOverview(_req, res) {
|
|
2297
|
+
const cwd = process.cwd();
|
|
2298
|
+
// Read last verify output
|
|
2299
|
+
let lastVerifyMeta = {};
|
|
2300
|
+
let lastVerifyFindings = [];
|
|
2301
|
+
const verifyPath = path.resolve(cwd, '.neurcode', 'last-verify-output.json');
|
|
2302
|
+
try {
|
|
2303
|
+
if (fs.existsSync(verifyPath)) {
|
|
2304
|
+
const parsed = JSON.parse(fs.readFileSync(verifyPath, 'utf8'));
|
|
2305
|
+
if (parsed && typeof parsed === 'object') {
|
|
2306
|
+
lastVerifyMeta = parsed;
|
|
2307
|
+
const findings = parsed.findings;
|
|
2308
|
+
const violations = parsed.violations;
|
|
2309
|
+
if (Array.isArray(findings))
|
|
2310
|
+
lastVerifyFindings = findings;
|
|
2311
|
+
else if (Array.isArray(violations))
|
|
2312
|
+
lastVerifyFindings = violations;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
catch { /* ignore */ }
|
|
2317
|
+
// Read provenance file for recent runs
|
|
2318
|
+
const provenancePaths = [
|
|
2319
|
+
path.resolve(cwd, '.neurcode', 'provenance.json'),
|
|
2320
|
+
path.resolve(cwd, '.neurcode', 'provenance-chain.json'),
|
|
2321
|
+
];
|
|
2322
|
+
let recentRuns = [];
|
|
2323
|
+
for (const pp of provenancePaths) {
|
|
2324
|
+
try {
|
|
2325
|
+
if (fs.existsSync(pp)) {
|
|
2326
|
+
const parsed = JSON.parse(fs.readFileSync(pp, 'utf8'));
|
|
2327
|
+
if (Array.isArray(parsed)) {
|
|
2328
|
+
recentRuns = parsed.slice(-20).reverse();
|
|
2329
|
+
}
|
|
2330
|
+
else if (parsed && typeof parsed === 'object') {
|
|
2331
|
+
const records = parsed.records;
|
|
2332
|
+
if (Array.isArray(records))
|
|
2333
|
+
recentRuns = records.slice(-20).reverse();
|
|
2334
|
+
}
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
catch { /* ignore */ }
|
|
2339
|
+
}
|
|
2340
|
+
// Compute top triggered rules
|
|
2341
|
+
const ruleFreq = {};
|
|
2342
|
+
for (const f of lastVerifyFindings) {
|
|
2343
|
+
const ruleId = f.ruleId || 'unknown';
|
|
2344
|
+
ruleFreq[ruleId] = (ruleFreq[ruleId] || 0) + 1;
|
|
2345
|
+
}
|
|
2346
|
+
const topRules = Object.entries(ruleFreq)
|
|
2347
|
+
.sort((a, b) => b[1] - a[1])
|
|
2348
|
+
.slice(0, 5)
|
|
2349
|
+
.map(([ruleId, count]) => ({ ruleId, count }));
|
|
2350
|
+
// Read remediation artifacts
|
|
2351
|
+
const remediationDir = path.resolve(cwd, '.neurcode', 'remediation');
|
|
2352
|
+
let remediationCount = 0;
|
|
2353
|
+
let remediationPending = 0;
|
|
2354
|
+
let remediationValidated = 0;
|
|
2355
|
+
try {
|
|
2356
|
+
if (fs.existsSync(remediationDir)) {
|
|
2357
|
+
const entries = fs.readdirSync(remediationDir);
|
|
2358
|
+
for (const entry of entries) {
|
|
2359
|
+
if (entry.endsWith('-request.json'))
|
|
2360
|
+
remediationCount++;
|
|
2361
|
+
if (entry.endsWith('-receipt.json'))
|
|
2362
|
+
remediationValidated++;
|
|
2363
|
+
}
|
|
2364
|
+
remediationPending = remediationCount - remediationValidated;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
catch { /* ignore */ }
|
|
2368
|
+
const blockingCount = lastVerifyFindings.filter((f) => {
|
|
2369
|
+
const sev = (f.severity || '').toLowerCase();
|
|
2370
|
+
return sev === 'critical' || sev === 'high' || sev === 'blocking' || sev === 'block';
|
|
2371
|
+
}).length;
|
|
2372
|
+
success(res, {
|
|
2373
|
+
lastVerify: {
|
|
2374
|
+
verdict: lastVerifyMeta.verdict || (lastVerifyFindings.length === 0 ? 'PASS' : blockingCount > 0 ? 'FAIL' : 'WARN'),
|
|
2375
|
+
verifiedAt: lastVerifyMeta.verifiedAt || lastVerifyMeta.timestamp || null,
|
|
2376
|
+
planId: lastVerifyMeta.planId || null,
|
|
2377
|
+
totalFindings: lastVerifyFindings.length,
|
|
2378
|
+
blockingCount,
|
|
2379
|
+
advisoryCount: lastVerifyFindings.length - blockingCount,
|
|
2380
|
+
topRules,
|
|
2381
|
+
},
|
|
2382
|
+
recentRuns: recentRuns.slice(0, 10),
|
|
2383
|
+
remediation: {
|
|
2384
|
+
total: remediationCount,
|
|
2385
|
+
pending: remediationPending,
|
|
2386
|
+
validated: remediationValidated,
|
|
2387
|
+
},
|
|
2388
|
+
projectRoot: cwd,
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
// ── Brain cache status handler ────────────────────────────────────────────────
|
|
2392
|
+
async function handleBrainCacheStatus(_req, res) {
|
|
2393
|
+
const cwd = process.cwd();
|
|
2394
|
+
const brainDir = path.resolve(cwd, '.neurcode', 'brain-cache');
|
|
2395
|
+
const manifestPath = path.resolve(brainDir, 'manifest.json');
|
|
2396
|
+
let manifest = null;
|
|
2397
|
+
let cacheSize = 0;
|
|
2398
|
+
let fileCount = 0;
|
|
2399
|
+
let staleCount = 0;
|
|
2400
|
+
let cacheExists = false;
|
|
2401
|
+
try {
|
|
2402
|
+
cacheExists = fs.existsSync(brainDir);
|
|
2403
|
+
if (cacheExists) {
|
|
2404
|
+
const entries = fs.readdirSync(brainDir);
|
|
2405
|
+
fileCount = entries.length;
|
|
2406
|
+
for (const entry of entries) {
|
|
2407
|
+
try {
|
|
2408
|
+
const stat = fs.statSync(path.resolve(brainDir, entry));
|
|
2409
|
+
cacheSize += stat.size;
|
|
2410
|
+
}
|
|
2411
|
+
catch { /* ignore */ }
|
|
2412
|
+
}
|
|
2413
|
+
if (fs.existsSync(manifestPath)) {
|
|
2414
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
2415
|
+
const files = manifest.files;
|
|
2416
|
+
if (Array.isArray(files)) {
|
|
2417
|
+
const now = Date.now();
|
|
2418
|
+
for (const f of files) {
|
|
2419
|
+
const cachedAt = f.cachedAt;
|
|
2420
|
+
if (typeof cachedAt === 'string') {
|
|
2421
|
+
const age = now - new Date(cachedAt).getTime();
|
|
2422
|
+
if (age > 24 * 60 * 60 * 1000)
|
|
2423
|
+
staleCount++;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
catch { /* ignore */ }
|
|
2431
|
+
// Check semantic index
|
|
2432
|
+
const semanticIndexPath = path.resolve(brainDir, 'semantic-index.json');
|
|
2433
|
+
const hasSemanticIndex = fs.existsSync(semanticIndexPath);
|
|
2434
|
+
let semanticIndexSize = 0;
|
|
2435
|
+
let semanticIndexAt = null;
|
|
2436
|
+
try {
|
|
2437
|
+
if (hasSemanticIndex) {
|
|
2438
|
+
const stat = fs.statSync(semanticIndexPath);
|
|
2439
|
+
semanticIndexSize = stat.size;
|
|
2440
|
+
const idx = JSON.parse(fs.readFileSync(semanticIndexPath, 'utf8'));
|
|
2441
|
+
semanticIndexAt = idx.indexedAt || null;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
catch { /* ignore */ }
|
|
2445
|
+
success(res, {
|
|
2446
|
+
cacheExists,
|
|
2447
|
+
brainDir,
|
|
2448
|
+
manifest: manifest
|
|
2449
|
+
? {
|
|
2450
|
+
schemaVersion: manifest.schemaVersion,
|
|
2451
|
+
createdAt: manifest.createdAt,
|
|
2452
|
+
updatedAt: manifest.updatedAt,
|
|
2453
|
+
projectId: manifest.projectId,
|
|
2454
|
+
fileCount: Array.isArray(manifest.files) ? manifest.files.length : fileCount,
|
|
2455
|
+
staleCount,
|
|
2456
|
+
stalePercent: fileCount > 0 ? Math.round((staleCount / fileCount) * 100) : 0,
|
|
2457
|
+
}
|
|
2458
|
+
: null,
|
|
2459
|
+
semanticIndex: {
|
|
2460
|
+
exists: hasSemanticIndex,
|
|
2461
|
+
sizeBytes: semanticIndexSize,
|
|
2462
|
+
indexedAt: semanticIndexAt,
|
|
2463
|
+
},
|
|
2464
|
+
cacheDirSizeBytes: cacheSize,
|
|
2465
|
+
cwd,
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
// ── Remediation status handler ────────────────────────────────────────────────
|
|
2469
|
+
async function handleRemediationStatus(_req, res) {
|
|
2470
|
+
const cwd = process.cwd();
|
|
2471
|
+
const remediationDir = path.resolve(cwd, '.neurcode', 'remediation');
|
|
2472
|
+
const artifacts = [];
|
|
2473
|
+
try {
|
|
2474
|
+
if (fs.existsSync(remediationDir)) {
|
|
2475
|
+
const entries = fs.readdirSync(remediationDir).sort().reverse();
|
|
2476
|
+
for (const entry of entries.slice(0, 50)) {
|
|
2477
|
+
if (!entry.endsWith('.json'))
|
|
2478
|
+
continue;
|
|
2479
|
+
try {
|
|
2480
|
+
const fullPath = path.resolve(remediationDir, entry);
|
|
2481
|
+
const stat = fs.statSync(fullPath);
|
|
2482
|
+
const raw = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
2483
|
+
const isReceipt = entry.endsWith('-receipt.json');
|
|
2484
|
+
const isRequest = entry.endsWith('-request.json');
|
|
2485
|
+
artifacts.push({
|
|
2486
|
+
filename: entry,
|
|
2487
|
+
type: isReceipt ? 'receipt' : isRequest ? 'request' : 'unknown',
|
|
2488
|
+
sizeBytes: stat.size,
|
|
2489
|
+
createdAt: stat.birthtime.toISOString(),
|
|
2490
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
2491
|
+
findingId: raw.findingId || raw.requestId || null,
|
|
2492
|
+
ruleId: raw.ruleId || null,
|
|
2493
|
+
status: isReceipt
|
|
2494
|
+
? (raw.overallStatus || 'validated')
|
|
2495
|
+
: 'pending',
|
|
2496
|
+
passed: isReceipt ? raw.passed || false : null,
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
catch { /* ignore */ }
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
catch { /* ignore */ }
|
|
2504
|
+
const requests = artifacts.filter((a) => a.type === 'request');
|
|
2505
|
+
const receipts = artifacts.filter((a) => a.type === 'receipt');
|
|
2506
|
+
const passedReceipts = receipts.filter((a) => a.passed === true);
|
|
2507
|
+
success(res, {
|
|
2508
|
+
remediationDir,
|
|
2509
|
+
exists: fs.existsSync(remediationDir),
|
|
2510
|
+
summary: {
|
|
2511
|
+
total: requests.length,
|
|
2512
|
+
pending: requests.length - receipts.length,
|
|
2513
|
+
validated: receipts.length,
|
|
2514
|
+
passed: passedReceipts.length,
|
|
2515
|
+
failed: receipts.length - passedReceipts.length,
|
|
2516
|
+
},
|
|
2517
|
+
artifacts,
|
|
2518
|
+
cwd,
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
// ── Pilot report handler ──────────────────────────────────────────────────────
|
|
2522
|
+
// Aggregates existing local artifacts only: governance JSONL telemetry, provenance
|
|
2523
|
+
// index/legacy chain, pilot-metrics.json, last-verify-output — same sources as
|
|
2524
|
+
// `neurcode pilot-report` (no new telemetry systems).
|
|
2525
|
+
function pilotReportWindowDays() {
|
|
2526
|
+
return 7;
|
|
2527
|
+
}
|
|
2528
|
+
function pilotReportCutoffIso(days) {
|
|
2529
|
+
const d = new Date();
|
|
2530
|
+
d.setDate(d.getDate() - days);
|
|
2531
|
+
return d.toISOString().slice(0, 10);
|
|
2532
|
+
}
|
|
2533
|
+
function filterTelemetryByWindow(events, fromDate, toDate) {
|
|
2534
|
+
return events.filter((ev) => {
|
|
2535
|
+
const day = ev.emittedAt.slice(0, 10);
|
|
2536
|
+
return day >= fromDate && day <= toDate;
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
function aggregateDeterministicFromTelemetry(events) {
|
|
2540
|
+
let sumDeterministic = 0;
|
|
2541
|
+
let sumFindings = 0;
|
|
2542
|
+
for (const ev of events) {
|
|
2543
|
+
if (ev.eventType !== 'governance.verify.completed')
|
|
2544
|
+
continue;
|
|
2545
|
+
const p = ev.payload;
|
|
2546
|
+
const hist = p.determinismHistogram;
|
|
2547
|
+
const count = p.governanceFindingCount;
|
|
2548
|
+
if (!hist || typeof count !== 'number')
|
|
2549
|
+
continue;
|
|
2550
|
+
sumFindings += count;
|
|
2551
|
+
sumDeterministic += hist['deterministic-structural'] ?? 0;
|
|
2552
|
+
}
|
|
2553
|
+
const deterministicPct = sumFindings > 0 ? sumDeterministic / sumFindings : 0;
|
|
2554
|
+
return { deterministicPct, sumDeterministic, sumFindings };
|
|
2555
|
+
}
|
|
2556
|
+
function governanceTrustFromPct(deterministicPct) {
|
|
2557
|
+
if (deterministicPct >= 0.7)
|
|
2558
|
+
return 'HIGH';
|
|
2559
|
+
if (deterministicPct >= 0.4)
|
|
2560
|
+
return 'MEDIUM';
|
|
2561
|
+
return 'LOW';
|
|
2562
|
+
}
|
|
2563
|
+
function advisoryCaughtInPilotWindow(cwd, from, to) {
|
|
2564
|
+
try {
|
|
2565
|
+
const p = path.join(cwd, '.neurcode', 'pilot-metrics.json');
|
|
2566
|
+
if (!fs.existsSync(p))
|
|
2567
|
+
return 0;
|
|
2568
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
2569
|
+
if (!Array.isArray(raw))
|
|
2570
|
+
return 0;
|
|
2571
|
+
return raw
|
|
2572
|
+
.filter((e) => e.date >= from && e.date <= to)
|
|
2573
|
+
.reduce((s, e) => s + (e.advisoryCaught ?? 0), 0);
|
|
2574
|
+
}
|
|
2575
|
+
catch {
|
|
2576
|
+
return 0;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function summarizeTelemetryLine(ev) {
|
|
2580
|
+
if (ev.eventType === 'governance.verify.completed') {
|
|
2581
|
+
const p = ev.payload;
|
|
2582
|
+
const verdict = String(p.verdict ?? '');
|
|
2583
|
+
const n = Number(p.governanceFindingCount ?? 0);
|
|
2584
|
+
const ri = p.replayIntegrityStatus;
|
|
2585
|
+
return {
|
|
2586
|
+
emittedAt: ev.emittedAt,
|
|
2587
|
+
eventType: ev.eventType,
|
|
2588
|
+
summary: `Verify ${verdict} · ${n} findings`,
|
|
2589
|
+
verdict,
|
|
2590
|
+
replayIntegrityStatus: ri ?? null,
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
if (ev.eventType === 'rule.trigger') {
|
|
2594
|
+
const p = ev.payload;
|
|
2595
|
+
return {
|
|
2596
|
+
emittedAt: ev.emittedAt,
|
|
2597
|
+
eventType: ev.eventType,
|
|
2598
|
+
summary: `Rule ${p.ruleId ?? '?'} · ${p.count ?? 0}×`,
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
return {
|
|
2602
|
+
emittedAt: ev.emittedAt,
|
|
2603
|
+
eventType: ev.eventType,
|
|
2604
|
+
summary: ev.eventType.replace(/\./g, ' '),
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
async function handlePilotReport(_req, res) {
|
|
2608
|
+
const cwd = process.cwd();
|
|
2609
|
+
const days = pilotReportWindowDays();
|
|
2610
|
+
const periodTo = new Date().toISOString().slice(0, 10);
|
|
2611
|
+
const periodFrom = pilotReportCutoffIso(days);
|
|
2612
|
+
// Canonical governance telemetry (JSONL) — same reader as CLI pilot-report
|
|
2613
|
+
const telemetryEnvelopes = (0, telemetry_1.readGovernanceTelemetryEvents)(cwd);
|
|
2614
|
+
const telemetryWindow = filterTelemetryByWindow(telemetryEnvelopes, periodFrom, periodTo);
|
|
2615
|
+
const verifyCompletedInWindow = telemetryWindow.filter((e) => e.eventType === 'governance.verify.completed');
|
|
2616
|
+
// Legacy JSON telemetry (optional fallback counts for older repos)
|
|
2617
|
+
const telemetryPaths = [
|
|
2618
|
+
path.resolve(cwd, '.neurcode', 'telemetry.json'),
|
|
2619
|
+
path.resolve(cwd, '.neurcode', 'telemetry-events.json'),
|
|
2620
|
+
];
|
|
2621
|
+
let legacyEvents = [];
|
|
2622
|
+
for (const tp of telemetryPaths) {
|
|
2623
|
+
try {
|
|
2624
|
+
if (fs.existsSync(tp)) {
|
|
2625
|
+
const raw = JSON.parse(fs.readFileSync(tp, 'utf8'));
|
|
2626
|
+
if (Array.isArray(raw))
|
|
2627
|
+
legacyEvents = raw;
|
|
2628
|
+
else if (raw && typeof raw === 'object') {
|
|
2629
|
+
const evts = raw.events;
|
|
2630
|
+
if (Array.isArray(evts))
|
|
2631
|
+
legacyEvents = evts;
|
|
2632
|
+
}
|
|
2633
|
+
break;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
catch { /* ignore */ }
|
|
2637
|
+
}
|
|
2638
|
+
const legacyVerifyEvents = legacyEvents.filter((e) => e.type === 'verify' || e.eventType === 'verify');
|
|
2639
|
+
// Provenance index (preferred) + legacy flat chain files
|
|
2640
|
+
const provIndex = (0, governance_provenance_1.loadProvenanceIndex)(cwd);
|
|
2641
|
+
const provenancePaths = [
|
|
2642
|
+
path.resolve(cwd, '.neurcode', 'provenance.json'),
|
|
2643
|
+
path.resolve(cwd, '.neurcode', 'provenance-chain.json'),
|
|
2644
|
+
];
|
|
2645
|
+
let legacyRuns = [];
|
|
2646
|
+
for (const pp of provenancePaths) {
|
|
2647
|
+
try {
|
|
2648
|
+
if (fs.existsSync(pp)) {
|
|
2649
|
+
const raw = JSON.parse(fs.readFileSync(pp, 'utf8'));
|
|
2650
|
+
if (Array.isArray(raw))
|
|
2651
|
+
legacyRuns = raw;
|
|
2652
|
+
else if (raw && typeof raw === 'object') {
|
|
2653
|
+
const records = raw.records;
|
|
2654
|
+
if (Array.isArray(records))
|
|
2655
|
+
legacyRuns = records;
|
|
2656
|
+
}
|
|
2657
|
+
break;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
catch { /* ignore */ }
|
|
2661
|
+
}
|
|
2662
|
+
const indexRecords = [...provIndex.records].sort((a, b) => (a.runAt < b.runAt ? -1 : a.runAt > b.runAt ? 1 : 0));
|
|
2663
|
+
const runsForRates = indexRecords.length > 0
|
|
2664
|
+
? indexRecords.map((r) => ({ verdict: r.verdict, verifiedAt: r.runAt, blockingCount: 0, fingerprint: r.fingerprint }))
|
|
2665
|
+
: legacyRuns.map((r) => ({
|
|
2666
|
+
verdict: r.verdict,
|
|
2667
|
+
verifiedAt: r.verifiedAt || r.timestamp,
|
|
2668
|
+
blockingCount: r.blockingCount || 0,
|
|
2669
|
+
fingerprint: r.fingerprint,
|
|
2670
|
+
}));
|
|
2671
|
+
const passRuns = runsForRates.filter((r) => String(r.verdict).toUpperCase() === 'PASS');
|
|
2672
|
+
const failRuns = runsForRates.filter((r) => {
|
|
2673
|
+
const v = String(r.verdict).toUpperCase();
|
|
2674
|
+
return v === 'FAIL' || v === 'BLOCK';
|
|
2675
|
+
});
|
|
2676
|
+
const warnRuns = runsForRates.filter((r) => String(r.verdict).toUpperCase() === 'WARN');
|
|
2677
|
+
const total = runsForRates.length;
|
|
2678
|
+
const passRate = total > 0 ? Math.round((passRuns.length / total) * 100) : 0;
|
|
2679
|
+
const failRate = total > 0 ? Math.round((failRuns.length / total) * 100) : 0;
|
|
2680
|
+
const warnRate = total > 0 ? Math.round((warnRuns.length / total) * 100) : 0;
|
|
2681
|
+
// Top rules: legacy deep records + telemetry rollup (merged)
|
|
2682
|
+
const globalRuleFreq = {};
|
|
2683
|
+
for (const run of legacyRuns) {
|
|
2684
|
+
const violations = run.violations;
|
|
2685
|
+
const findings = run.findings;
|
|
2686
|
+
const items = Array.isArray(violations) ? violations : Array.isArray(findings) ? findings : [];
|
|
2687
|
+
for (const item of items) {
|
|
2688
|
+
const ruleId = item.ruleId || 'unknown';
|
|
2689
|
+
globalRuleFreq[ruleId] = (globalRuleFreq[ruleId] || 0) + 1;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
const rollup = (0, telemetry_1.rollupRulePrecisionFromEvents)(telemetryWindow);
|
|
2693
|
+
const telemetryTop = [...(0, telemetry_1.highTrustRuleLeaderboard)(rollup, 10)];
|
|
2694
|
+
for (const r of telemetryTop) {
|
|
2695
|
+
globalRuleFreq[r.ruleId] = (globalRuleFreq[r.ruleId] || 0) + r.triggerCount;
|
|
2696
|
+
}
|
|
2697
|
+
const topRules = Object.entries(globalRuleFreq)
|
|
2698
|
+
.sort((a, b) => b[1] - a[1])
|
|
2699
|
+
.slice(0, 10)
|
|
2700
|
+
.map(([ruleId, count]) => ({ ruleId, count }));
|
|
2701
|
+
const topRulesTelemetry = telemetryTop.map((r) => ({ ruleId: r.ruleId, triggerCount: r.triggerCount }));
|
|
2702
|
+
// Last verify output for current state
|
|
2703
|
+
let currentVerdict = 'UNKNOWN';
|
|
2704
|
+
let lastVerifiedAt = null;
|
|
2705
|
+
let deterministicFindingCount = 0;
|
|
2706
|
+
const verifyPath = path.resolve(cwd, '.neurcode', 'last-verify-output.json');
|
|
2707
|
+
try {
|
|
2708
|
+
if (fs.existsSync(verifyPath)) {
|
|
2709
|
+
const vf = JSON.parse(fs.readFileSync(verifyPath, 'utf8'));
|
|
2710
|
+
currentVerdict = vf.verdict || 'UNKNOWN';
|
|
2711
|
+
lastVerifiedAt = vf.verifiedAt || vf.timestamp || null;
|
|
2712
|
+
const fArr = Array.isArray(vf.findings) ? vf.findings : [];
|
|
2713
|
+
deterministicFindingCount = fArr.filter((f) => {
|
|
2714
|
+
const det = f.determinism || '';
|
|
2715
|
+
return det.startsWith('deterministic');
|
|
2716
|
+
}).length;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
catch { /* ignore */ }
|
|
2720
|
+
// Trend: last 7 runs chronological (oldest → newest for charts)
|
|
2721
|
+
const trendSource = indexRecords.length > 0 ? indexRecords : legacyRuns;
|
|
2722
|
+
const recentSlice = trendSource.slice(-7);
|
|
2723
|
+
const recentTrend = recentSlice.map((r) => {
|
|
2724
|
+
if ('runAt' in r && typeof r.runAt === 'string') {
|
|
2725
|
+
const row = r;
|
|
2726
|
+
return {
|
|
2727
|
+
verifiedAt: row.runAt,
|
|
2728
|
+
verdict: row.verdict,
|
|
2729
|
+
blockingCount: 0,
|
|
2730
|
+
fingerprint: row.fingerprint,
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
const row = r;
|
|
2734
|
+
return {
|
|
2735
|
+
verifiedAt: row.verifiedAt || row.timestamp,
|
|
2736
|
+
verdict: row.verdict,
|
|
2737
|
+
blockingCount: row.blockingCount || 0,
|
|
2738
|
+
fingerprint: row.fingerprint,
|
|
2739
|
+
};
|
|
2740
|
+
});
|
|
2741
|
+
const { deterministicPct, sumDeterministic, sumFindings } = aggregateDeterministicFromTelemetry(telemetryWindow);
|
|
2742
|
+
const deterministicRatioPct = Math.round(deterministicPct * 100);
|
|
2743
|
+
const governanceTrust = governanceTrustFromPct(deterministicPct);
|
|
2744
|
+
// Replay integrity distribution from verify.completed telemetry payloads
|
|
2745
|
+
let replayExact = 0;
|
|
2746
|
+
let replayBounded = 0;
|
|
2747
|
+
let replayUnknown = 0;
|
|
2748
|
+
let lastReplayIntegrityStatus = null;
|
|
2749
|
+
for (const ev of verifyCompletedInWindow) {
|
|
2750
|
+
const p = ev.payload;
|
|
2751
|
+
const ri = p.replayIntegrityStatus;
|
|
2752
|
+
if (ri === 'exact')
|
|
2753
|
+
replayExact += 1;
|
|
2754
|
+
else if (ri === 'bounded-degradation')
|
|
2755
|
+
replayBounded += 1;
|
|
2756
|
+
else
|
|
2757
|
+
replayUnknown += 1;
|
|
2758
|
+
if (!lastReplayIntegrityStatus && ri)
|
|
2759
|
+
lastReplayIntegrityStatus = ri;
|
|
2760
|
+
}
|
|
2761
|
+
for (let i = verifyCompletedInWindow.length - 1; i >= 0; i--) {
|
|
2762
|
+
const p = verifyCompletedInWindow[i].payload;
|
|
2763
|
+
const ri = p.replayIntegrityStatus;
|
|
2764
|
+
if (ri) {
|
|
2765
|
+
lastReplayIntegrityStatus = ri;
|
|
2766
|
+
break;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
// Provenance fingerprint integrity sample (last N index entries)
|
|
2770
|
+
const SAMPLE = 12;
|
|
2771
|
+
let sampleOk = 0;
|
|
2772
|
+
let sampleFail = 0;
|
|
2773
|
+
let sampleTried = 0;
|
|
2774
|
+
for (const entry of provIndex.records.slice(0, SAMPLE)) {
|
|
2775
|
+
const rec = (0, governance_provenance_1.loadProvenanceRecord)(cwd, entry.runId);
|
|
2776
|
+
if (!rec)
|
|
2777
|
+
continue;
|
|
2778
|
+
sampleTried += 1;
|
|
2779
|
+
if ((0, governance_provenance_1.verifyProvenanceIntegrity)(rec))
|
|
2780
|
+
sampleOk += 1;
|
|
2781
|
+
else
|
|
2782
|
+
sampleFail += 1;
|
|
2783
|
+
}
|
|
2784
|
+
const pilotSummary = (0, pilot_metrics_1.generatePilotSummary)(cwd, days);
|
|
2785
|
+
const fingerprintedInWindow = provIndex.records.filter((r) => {
|
|
2786
|
+
const d = r.runAt.slice(0, 10);
|
|
2787
|
+
return d >= periodFrom && d <= periodTo;
|
|
2788
|
+
}).length;
|
|
2789
|
+
const recentGovernanceEvents = telemetryEnvelopes
|
|
2790
|
+
.slice(-20)
|
|
2791
|
+
.reverse()
|
|
2792
|
+
.map(summarizeTelemetryLine);
|
|
2793
|
+
const operationalRisks = [];
|
|
2794
|
+
if (failRate >= 40) {
|
|
2795
|
+
operationalRisks.push({ level: 'high', label: 'Elevated failure rate', detail: `${failRate}% of recorded governance runs failed or blocked in the provenance window.` });
|
|
2796
|
+
}
|
|
2797
|
+
else if (failRate >= 15) {
|
|
2798
|
+
operationalRisks.push({ level: 'medium', label: 'Moderate failure rate', detail: `${failRate}% of runs failed or blocked — review top rules and intent scope.` });
|
|
2799
|
+
}
|
|
2800
|
+
if (pilotSummary.suppressionRate >= 0.25) {
|
|
2801
|
+
operationalRisks.push({
|
|
2802
|
+
level: 'medium',
|
|
2803
|
+
label: 'Suppression activity',
|
|
2804
|
+
detail: `Suppression rate ~${Math.round(pilotSummary.suppressionRate * 100)}% in pilot-metrics window — triage false positives vs policy.`,
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
if (deterministicRatioPct < 40 && sumFindings > 0) {
|
|
2808
|
+
operationalRisks.push({
|
|
2809
|
+
level: 'medium',
|
|
2810
|
+
label: 'Deterministic signal mix',
|
|
2811
|
+
detail: `${deterministicRatioPct}% of findings in telemetry window are deterministic-structural — semantic/advisory dominates.`,
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
if (replayBounded > replayExact && verifyCompletedInWindow.length >= 3) {
|
|
2815
|
+
operationalRisks.push({
|
|
2816
|
+
level: 'low',
|
|
2817
|
+
label: 'Replay bounded degradation',
|
|
2818
|
+
detail: 'Some verify completions reported bounded-degradation replay integrity — expected under truncation or large-repo limits.',
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
if (operationalRisks.length === 0) {
|
|
2822
|
+
operationalRisks.push({
|
|
2823
|
+
level: 'low',
|
|
2824
|
+
label: 'No acute risk flags',
|
|
2825
|
+
detail: 'Within normal bands for this snapshot — keep running verify and monitoring pilot-metrics.',
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
const verifyEventCount = verifyCompletedInWindow.length + legacyVerifyEvents.length;
|
|
2829
|
+
const telemetryEventCount = telemetryEnvelopes.length;
|
|
2830
|
+
success(res, {
|
|
2831
|
+
period: { days, from: periodFrom, to: periodTo },
|
|
2832
|
+
summary: {
|
|
2833
|
+
currentVerdict,
|
|
2834
|
+
lastVerifiedAt,
|
|
2835
|
+
totalRuns: total,
|
|
2836
|
+
passRate,
|
|
2837
|
+
failRate,
|
|
2838
|
+
warnRate,
|
|
2839
|
+
deterministicFindingCount,
|
|
2840
|
+
verifyEventCount,
|
|
2841
|
+
},
|
|
2842
|
+
deterministic: {
|
|
2843
|
+
ratioPct: deterministicRatioPct,
|
|
2844
|
+
sumDeterministic,
|
|
2845
|
+
sumFindings,
|
|
2846
|
+
windowDays: days,
|
|
2847
|
+
},
|
|
2848
|
+
governanceTrust,
|
|
2849
|
+
pilotSummary: {
|
|
2850
|
+
totalVerifyRuns: pilotSummary.totalVerifyRuns,
|
|
2851
|
+
averagePassRate: pilotSummary.averagePassRate,
|
|
2852
|
+
suppressionRate: pilotSummary.suppressionRate,
|
|
2853
|
+
totalBlockingCaught: pilotSummary.totalBlockingCaught,
|
|
2854
|
+
totalStructuralCaught: pilotSummary.totalStructuralCaught,
|
|
2855
|
+
advisoryCaught: advisoryCaughtInPilotWindow(cwd, periodFrom, periodTo),
|
|
2856
|
+
aiDebtTrend: pilotSummary.aiDebtTrend,
|
|
2857
|
+
topViolatedRules: pilotSummary.topViolatedRules,
|
|
2858
|
+
},
|
|
2859
|
+
provenanceCoverage: {
|
|
2860
|
+
indexedRuns: provIndex.records.length,
|
|
2861
|
+
fingerprintedInWindow,
|
|
2862
|
+
integritySampleVerified: sampleOk,
|
|
2863
|
+
integritySampleFailed: sampleFail,
|
|
2864
|
+
integritySampleTried: sampleTried,
|
|
2865
|
+
},
|
|
2866
|
+
replayIntegrity: {
|
|
2867
|
+
exact: replayExact,
|
|
2868
|
+
boundedDegradation: replayBounded,
|
|
2869
|
+
unknown: replayUnknown,
|
|
2870
|
+
lastStatus: lastReplayIntegrityStatus,
|
|
2871
|
+
},
|
|
2872
|
+
topRules,
|
|
2873
|
+
topRulesTelemetry,
|
|
2874
|
+
recentTrend,
|
|
2875
|
+
recentGovernanceEvents,
|
|
2876
|
+
operationalRisks,
|
|
2877
|
+
governance: {
|
|
2878
|
+
provenanceRecords: provIndex.records.length > 0 ? provIndex.records.length : legacyRuns.length,
|
|
2879
|
+
telemetryEvents: telemetryEventCount,
|
|
2880
|
+
legacyTelemetryEventCount: legacyEvents.length,
|
|
2881
|
+
cwd,
|
|
2882
|
+
},
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
966
2885
|
// ── Server factory ─────────────────────────────────────────────────────────────
|
|
967
2886
|
function createDaemonServer() {
|
|
968
2887
|
const server = http.createServer(async (req, res) => {
|
|
2888
|
+
const incomingRequestIdRaw = req.headers[REQUEST_ID_HEADER];
|
|
2889
|
+
const incomingRequestId = Array.isArray(incomingRequestIdRaw)
|
|
2890
|
+
? incomingRequestIdRaw[0]
|
|
2891
|
+
: incomingRequestIdRaw;
|
|
2892
|
+
const requestId = (typeof incomingRequestId === 'string'
|
|
2893
|
+
&& incomingRequestId.trim().length > 0)
|
|
2894
|
+
? incomingRequestId.trim().slice(0, 128)
|
|
2895
|
+
: `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2896
|
+
res.setHeader(REQUEST_ID_HEADER, requestId);
|
|
969
2897
|
addCorsHeaders(res, req);
|
|
970
2898
|
if (req.method === 'OPTIONS') {
|
|
971
2899
|
res.writeHead(204);
|
|
@@ -978,6 +2906,9 @@ function createDaemonServer() {
|
|
|
978
2906
|
}
|
|
979
2907
|
const url = req.url ?? '/';
|
|
980
2908
|
const method = req.method ?? 'GET';
|
|
2909
|
+
const normalizedRoutePath = normalizeRoutePath(url);
|
|
2910
|
+
recordDaemonRequest(normalizedRoutePath, method);
|
|
2911
|
+
res.__neurcodeRoutePath = normalizedRoutePath;
|
|
981
2912
|
try {
|
|
982
2913
|
if (method === 'GET' && url === '/health') {
|
|
983
2914
|
let version = '0.0.0';
|
|
@@ -986,10 +2917,12 @@ function createDaemonServer() {
|
|
|
986
2917
|
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? version;
|
|
987
2918
|
}
|
|
988
2919
|
catch { /* ignore */ }
|
|
2920
|
+
const operational = buildDaemonOperationalSummary(process.cwd());
|
|
989
2921
|
send(res, 200, {
|
|
990
2922
|
ok: true,
|
|
991
2923
|
version,
|
|
992
2924
|
cwd: process.cwd(),
|
|
2925
|
+
operational,
|
|
993
2926
|
executionBus: {
|
|
994
2927
|
schemaVersion: 'neurcode.execution.v1',
|
|
995
2928
|
supportedActions: ['verify', 'fix', 'patch', 'apply-safe', 'reverify', 'policy-sync', 'intent-update'],
|
|
@@ -998,6 +2931,11 @@ function createDaemonServer() {
|
|
|
998
2931
|
schemaVersion: 'neurcode.runtime-event.v1',
|
|
999
2932
|
streamPath: '/events/stream',
|
|
1000
2933
|
},
|
|
2934
|
+
compatibility: {
|
|
2935
|
+
runtimeContractVersion: contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION,
|
|
2936
|
+
cliJsonContractVersion: contracts_1.CLI_JSON_CONTRACT_VERSION,
|
|
2937
|
+
statusVocabularyVersion: contracts_1.STATUS_VOCABULARY_VERSION,
|
|
2938
|
+
},
|
|
1001
2939
|
controlPlane: {
|
|
1002
2940
|
schemaVersion: 'neurcode.control-plane.v1',
|
|
1003
2941
|
path: '/control-plane',
|
|
@@ -1013,6 +2951,14 @@ function createDaemonServer() {
|
|
|
1013
2951
|
});
|
|
1014
2952
|
return;
|
|
1015
2953
|
}
|
|
2954
|
+
if (method === 'GET' && (url === '/ops/summary' || url.startsWith('/ops/summary?'))) {
|
|
2955
|
+
success(res, {
|
|
2956
|
+
schemaVersion: 'neurcode.daemon.ops.v1',
|
|
2957
|
+
generatedAt: new Date().toISOString(),
|
|
2958
|
+
operational: buildDaemonOperationalSummary(process.cwd()),
|
|
2959
|
+
});
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
1016
2962
|
if (method === 'GET' && url.startsWith('/executions')) {
|
|
1017
2963
|
if (url === '/executions' || url.startsWith('/executions?')) {
|
|
1018
2964
|
await handleListExecutions(req, res);
|
|
@@ -1053,11 +2999,11 @@ function createDaemonServer() {
|
|
|
1053
2999
|
await handleGetControlPlane(req, res);
|
|
1054
3000
|
return;
|
|
1055
3001
|
}
|
|
1056
|
-
if (method === 'POST' && url === '/control-plane/preview') {
|
|
3002
|
+
if (method === 'POST' && (url === '/control-plane/preview' || url.startsWith('/control-plane/preview?'))) {
|
|
1057
3003
|
await handlePreviewControlPlaneUpdate(req, res);
|
|
1058
3004
|
return;
|
|
1059
3005
|
}
|
|
1060
|
-
if (method === 'PUT' && url === '/control-plane') {
|
|
3006
|
+
if (method === 'PUT' && (url === '/control-plane' || url.startsWith('/control-plane?'))) {
|
|
1061
3007
|
await handleApplyControlPlaneUpdate(req, res);
|
|
1062
3008
|
return;
|
|
1063
3009
|
}
|
|
@@ -1075,32 +3021,62 @@ function createDaemonServer() {
|
|
|
1075
3021
|
await handleGetWorkspaceRuntime(req, res, decodeURIComponent(runtimeMatch[1]));
|
|
1076
3022
|
return;
|
|
1077
3023
|
}
|
|
3024
|
+
// Cross-repo graph: GET /workspaces/:id/cross-repo-graph
|
|
3025
|
+
const crossRepoGraphMatch = url.match(/^\/workspaces\/([^/]+)\/cross-repo-graph(?:\?.*)?$/);
|
|
3026
|
+
if (crossRepoGraphMatch) {
|
|
3027
|
+
await handleGetCrossRepoGraph(req, res, decodeURIComponent(crossRepoGraphMatch[1]));
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
1078
3030
|
const detailMatch = url.match(/^\/workspaces\/([^/?]+)(?:\?.*)?$/);
|
|
1079
3031
|
if (detailMatch) {
|
|
1080
3032
|
await handleGetWorkspace(req, res, decodeURIComponent(detailMatch[1]));
|
|
1081
3033
|
return;
|
|
1082
3034
|
}
|
|
1083
3035
|
}
|
|
1084
|
-
if (method === 'POST' && url === '/workspaces') {
|
|
3036
|
+
if (method === 'POST' && (url === '/workspaces' || url.startsWith('/workspaces?'))) {
|
|
1085
3037
|
await handleCreateWorkspace(req, res);
|
|
1086
3038
|
return;
|
|
1087
3039
|
}
|
|
1088
|
-
const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate
|
|
3040
|
+
const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate(?:\?.*)?$/);
|
|
1089
3041
|
if (method === 'POST' && activateMatch) {
|
|
1090
3042
|
await handleActivateWorkspace(req, res, decodeURIComponent(activateMatch[1]));
|
|
1091
3043
|
return;
|
|
1092
3044
|
}
|
|
1093
|
-
const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories
|
|
3045
|
+
const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories(?:\?.*)?$/);
|
|
1094
3046
|
if (method === 'POST' && addRepoMatch) {
|
|
1095
3047
|
await handleAddWorkspaceRepository(req, res, decodeURIComponent(addRepoMatch[1]));
|
|
1096
3048
|
return;
|
|
1097
3049
|
}
|
|
1098
|
-
|
|
3050
|
+
// Federated context: POST /workspaces/:id/federated-context
|
|
3051
|
+
const federatedContextMatch = url.match(/^\/workspaces\/([^/]+)\/federated-context(?:\?.*)?$/);
|
|
3052
|
+
if (method === 'POST' && federatedContextMatch) {
|
|
3053
|
+
await handleGetFederatedContext(req, res, decodeURIComponent(federatedContextMatch[1]));
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
// Semantic search: POST /workspaces/:id/semantic-search
|
|
3057
|
+
const semanticSearchMatch = url.match(/^\/workspaces\/([^/]+)\/semantic-search(?:\?.*)?$/);
|
|
3058
|
+
if (method === 'POST' && semanticSearchMatch) {
|
|
3059
|
+
await handleSemanticSearch(req, res, decodeURIComponent(semanticSearchMatch[1]));
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
// Semantic index build: POST /workspaces/:id/semantic-index/build
|
|
3063
|
+
const semanticIndexBuildMatch = url.match(/^\/workspaces\/([^/]+)\/semantic-index\/build(?:\?.*)?$/);
|
|
3064
|
+
if (method === 'POST' && semanticIndexBuildMatch) {
|
|
3065
|
+
await handleBuildSemanticIndex(req, res, decodeURIComponent(semanticIndexBuildMatch[1]));
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
// Intent expansion: POST /workspaces/:id/intent-expand
|
|
3069
|
+
const intentExpandMatch = url.match(/^\/workspaces\/([^/]+)\/intent-expand(?:\?.*)?$/);
|
|
3070
|
+
if (method === 'POST' && intentExpandMatch) {
|
|
3071
|
+
await handleIntentExpand(req, res, decodeURIComponent(intentExpandMatch[1]));
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const updateWorkspaceMatch = url.match(/^\/workspaces\/([^/?]+)(?:\?.*)?$/);
|
|
1099
3075
|
if (method === 'PUT' && updateWorkspaceMatch) {
|
|
1100
3076
|
await handleUpdateWorkspace(req, res, decodeURIComponent(updateWorkspaceMatch[1]));
|
|
1101
3077
|
return;
|
|
1102
3078
|
}
|
|
1103
|
-
if (method === 'POST' && url === '/workspaces/execute') {
|
|
3079
|
+
if (method === 'POST' && (url === '/workspaces/execute' || url.startsWith('/workspaces/execute?'))) {
|
|
1104
3080
|
await handleExecuteWorkspace(req, res);
|
|
1105
3081
|
return;
|
|
1106
3082
|
}
|
|
@@ -1128,38 +3104,130 @@ function createDaemonServer() {
|
|
|
1128
3104
|
return;
|
|
1129
3105
|
}
|
|
1130
3106
|
}
|
|
1131
|
-
|
|
3107
|
+
// ── Governance findings & overview (new surfaces) ───────────────────────
|
|
3108
|
+
if (method === 'GET' && (url === '/governance/findings' || url.startsWith('/governance/findings?'))) {
|
|
3109
|
+
await handleGovernanceFindings(req, res);
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
if (method === 'GET' && (url === '/governance/overview' || url.startsWith('/governance/overview?'))) {
|
|
3113
|
+
await handleGovernanceOverview(req, res);
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
if (method === 'GET' && (url === '/brain/cache-status' || url.startsWith('/brain/cache-status?'))) {
|
|
3117
|
+
await handleBrainCacheStatus(req, res);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (method === 'GET' && (url === '/remediation/status' || url.startsWith('/remediation/status?'))) {
|
|
3121
|
+
await handleRemediationStatus(req, res);
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (method === 'GET' && (url === '/pilot-report' || url.startsWith('/pilot-report?'))) {
|
|
3125
|
+
await handlePilotReport(req, res);
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
// ── Enterprise docs serving (renders from docs/enterprise/ on filesystem) ─
|
|
3129
|
+
if (method === 'GET' && url === '/docs/enterprise') {
|
|
3130
|
+
await handleDocsManifest(res);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
const docsContentMatch = url.match(/^\/docs\/enterprise\/([^/?]+)(?:\?.*)?$/);
|
|
3134
|
+
if (method === 'GET' && docsContentMatch) {
|
|
3135
|
+
await handleDocsContent(res, decodeURIComponent(docsContentMatch[1]));
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
if (method === 'POST' && (url === '/execute' || url.startsWith('/execute?'))) {
|
|
1132
3139
|
await handleExecute(req, res);
|
|
1133
3140
|
return;
|
|
1134
3141
|
}
|
|
1135
|
-
if (method === 'POST' && url === '/verify') {
|
|
3142
|
+
if (method === 'POST' && (url === '/verify' || url.startsWith('/verify?'))) {
|
|
1136
3143
|
await handleVerify(req, res);
|
|
1137
3144
|
return;
|
|
1138
3145
|
}
|
|
1139
|
-
if (method === 'POST' && url === '/fix') {
|
|
3146
|
+
if (method === 'POST' && (url === '/fix' || url.startsWith('/fix?'))) {
|
|
1140
3147
|
await handleFix(req, res);
|
|
1141
3148
|
return;
|
|
1142
3149
|
}
|
|
1143
|
-
if (method === 'POST' && url === '/fix/apply-safe') {
|
|
3150
|
+
if (method === 'POST' && (url === '/fix/apply-safe' || url.startsWith('/fix/apply-safe?'))) {
|
|
1144
3151
|
await handleFixApplySafe(req, res);
|
|
1145
3152
|
return;
|
|
1146
3153
|
}
|
|
1147
|
-
if (method === 'POST' && url === '/patch') {
|
|
3154
|
+
if (method === 'POST' && (url === '/patch/preview' || url.startsWith('/patch/preview?'))) {
|
|
3155
|
+
await handlePatchPreview(req, res);
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (method === 'POST' && (url === '/patch/rollback' || url.startsWith('/patch/rollback?'))) {
|
|
3159
|
+
await handlePatchRollback(req, res);
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
if (method === 'POST' && (url === '/patch' || url.startsWith('/patch?'))) {
|
|
1148
3163
|
await handlePatch(req, res);
|
|
1149
3164
|
return;
|
|
1150
3165
|
}
|
|
1151
3166
|
failure(res, `No route for ${method} ${url}`, 404);
|
|
1152
3167
|
}
|
|
1153
3168
|
catch (err) {
|
|
1154
|
-
|
|
3169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3170
|
+
if (/execution lock busy|EEXIST: file already exists, open '.*\/\.lock'/.test(message)) {
|
|
3171
|
+
failure(res, 'Execution lock busy. Another daemon action is running; retry this request.', 409, {
|
|
3172
|
+
code: 'daemon.execution_lock_busy',
|
|
3173
|
+
retriable: true,
|
|
3174
|
+
details: { cause: message },
|
|
3175
|
+
});
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
failure(res, message, 500);
|
|
1155
3179
|
}
|
|
1156
3180
|
});
|
|
1157
3181
|
return server;
|
|
1158
3182
|
}
|
|
1159
3183
|
// ── Start function ─────────────────────────────────────────────────────────────
|
|
3184
|
+
function validateDaemonStartup(cwd) {
|
|
3185
|
+
const errors = [];
|
|
3186
|
+
const warnings = [];
|
|
3187
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] || '0', 10);
|
|
3188
|
+
if (!Number.isFinite(nodeMajor) || nodeMajor < 18) {
|
|
3189
|
+
errors.push(`Node.js >= 18 is required (detected ${process.versions.node}).`);
|
|
3190
|
+
}
|
|
3191
|
+
try {
|
|
3192
|
+
const stat = fs.statSync(cwd);
|
|
3193
|
+
if (!stat.isDirectory()) {
|
|
3194
|
+
errors.push(`Current working directory is not a directory: ${cwd}`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
catch (error) {
|
|
3198
|
+
errors.push(`Cannot access working directory ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3199
|
+
}
|
|
3200
|
+
try {
|
|
3201
|
+
const runtimeDir = path.resolve(cwd, '.neurcode');
|
|
3202
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
3203
|
+
fs.accessSync(runtimeDir, fs.constants.R_OK | fs.constants.W_OK);
|
|
3204
|
+
}
|
|
3205
|
+
catch (error) {
|
|
3206
|
+
errors.push(`Runtime state directory .neurcode is not writable in ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3207
|
+
}
|
|
3208
|
+
const apiUrl = process.env.NEURCODE_API_URL || process.env.VITE_API_URL;
|
|
3209
|
+
if (!apiUrl) {
|
|
3210
|
+
warnings.push('NEURCODE_API_URL is not set; cloud compatibility probes may rely on default localhost API settings.');
|
|
3211
|
+
}
|
|
3212
|
+
if (ALLOW_NON_LOOPBACK) {
|
|
3213
|
+
warnings.push('Remote daemon access is enabled via NEURCODE_DAEMON_ALLOW_REMOTE. Ensure network access is restricted and trusted.');
|
|
3214
|
+
}
|
|
3215
|
+
return { errors, warnings };
|
|
3216
|
+
}
|
|
1160
3217
|
function startDaemon() {
|
|
3218
|
+
const cwd = process.cwd();
|
|
3219
|
+
const startupValidation = validateDaemonStartup(cwd);
|
|
3220
|
+
for (const warning of startupValidation.warnings) {
|
|
3221
|
+
console.warn(`⚠️ Daemon startup warning: ${warning}`);
|
|
3222
|
+
}
|
|
3223
|
+
if (startupValidation.errors.length > 0) {
|
|
3224
|
+
for (const error of startupValidation.errors) {
|
|
3225
|
+
console.error(`❌ Daemon startup validation error: ${error}`);
|
|
3226
|
+
}
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
1161
3229
|
const server = createDaemonServer();
|
|
1162
|
-
startRuntimeEventTailer(
|
|
3230
|
+
startRuntimeEventTailer(cwd);
|
|
1163
3231
|
if (!runtimeEventUnsubscribe) {
|
|
1164
3232
|
runtimeEventUnsubscribe = (0, runtime_events_1.onRuntimeEvent)((event) => {
|
|
1165
3233
|
broadcastRuntimeEvent(event);
|
|
@@ -1181,6 +3249,8 @@ function startDaemon() {
|
|
|
1181
3249
|
console.log(` POST /verify → execution bus: verify`);
|
|
1182
3250
|
console.log(` POST /fix → execution bus: fix + reverify`);
|
|
1183
3251
|
console.log(` POST /fix/apply-safe → execution bus: apply-safe + reverify`);
|
|
3252
|
+
console.log(` POST /patch/preview → deterministic patch preview (before/after diff)`);
|
|
3253
|
+
console.log(` POST /patch/rollback → deterministic rollback apply by receipt`);
|
|
1184
3254
|
console.log(` POST /patch → execution bus: patch + reverify`);
|
|
1185
3255
|
console.log(` POST /execute → unified execution endpoint`);
|
|
1186
3256
|
console.log(` GET /executions → execution history`);
|
|
@@ -1189,6 +3259,7 @@ function startDaemon() {
|
|
|
1189
3259
|
console.log(` GET /executions/:id/diff → verification + patch inspection`);
|
|
1190
3260
|
console.log(` GET /events → runtime event history`);
|
|
1191
3261
|
console.log(` GET /events/stream → SSE deterministic governance runtime`);
|
|
3262
|
+
console.log(` GET /ops/summary → daemon operational health + reliability metrics`);
|
|
1192
3263
|
console.log(` GET /control-plane → governance control-plane state + snapshots`);
|
|
1193
3264
|
console.log(` POST /control-plane/preview → deterministic config impact preview`);
|
|
1194
3265
|
console.log(` PUT /control-plane → apply deterministic governance config update`);
|
|
@@ -1201,11 +3272,21 @@ function startDaemon() {
|
|
|
1201
3272
|
console.log(` POST /workspaces/:id/activate → set active workspace`);
|
|
1202
3273
|
console.log(` POST /workspaces/:id/repositories → add repository to workspace`);
|
|
1203
3274
|
console.log(` POST /workspaces/execute → workspace-scoped deterministic execution`);
|
|
3275
|
+
console.log(` GET /workspaces/:id/cross-repo-graph → detected cross-repo dependency edges`);
|
|
3276
|
+
console.log(` POST /workspaces/:id/federated-context → multi-repo blast radius analysis`);
|
|
3277
|
+
console.log(` POST /workspaces/:id/semantic-search → TF-IDF vector similarity file search`);
|
|
3278
|
+
console.log(` POST /workspaces/:id/semantic-index/build → rebuild semantic index from brain context`);
|
|
3279
|
+
console.log(` POST /workspaces/:id/intent-expand → signed semantic intent governance artifact`);
|
|
1204
3280
|
console.log(` GET /replay/state → deterministic governance state replay`);
|
|
1205
3281
|
console.log(` GET /replay/execution/:id → deterministic execution replay`);
|
|
1206
3282
|
console.log(` GET /replay/workspace/:id → deterministic workspace replay`);
|
|
1207
3283
|
console.log(` GET /replay/timeline → deterministic governance timeline replay`);
|
|
1208
|
-
console.log(
|
|
3284
|
+
console.log(` GET /governance/findings → canonical governance findings (last verify output)`);
|
|
3285
|
+
console.log(` GET /governance/overview → governance posture summary`);
|
|
3286
|
+
console.log(` GET /brain/cache-status → brain cache manifest and freshness`);
|
|
3287
|
+
console.log(` GET /remediation/status → remediation artifacts and receipts`);
|
|
3288
|
+
console.log(` GET /pilot-report → governance health metrics and trend`);
|
|
3289
|
+
console.log(`\n CWD: ${cwd}`);
|
|
1209
3290
|
console.log(` Press Ctrl+C to stop.\n`);
|
|
1210
3291
|
});
|
|
1211
3292
|
const stop = () => {
|