chainlesschain 0.81.0 → 0.143.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +62 -0
- package/src/commands/activitypub.js +61 -0
- package/src/commands/agent-network.js +254 -1
- package/src/commands/agent.js +117 -0
- package/src/commands/audit.js +302 -0
- package/src/commands/automation.js +271 -1
- package/src/commands/bi.js +61 -0
- package/src/commands/bm25.js +78 -0
- package/src/commands/browse.js +64 -0
- package/src/commands/ccron.js +78 -0
- package/src/commands/codegen.js +224 -0
- package/src/commands/collab.js +341 -0
- package/src/commands/compliance.js +1075 -0
- package/src/commands/compt.js +78 -0
- package/src/commands/consol.js +231 -0
- package/src/commands/cowork.js +263 -0
- package/src/commands/crosschain.js +62 -0
- package/src/commands/dao.js +62 -0
- package/src/commands/dbevo.js +284 -0
- package/src/commands/dev.js +252 -0
- package/src/commands/did.js +358 -0
- package/src/commands/dlp.js +61 -0
- package/src/commands/economy.js +56 -0
- package/src/commands/encrypt.js +341 -0
- package/src/commands/evolution.js +56 -0
- package/src/commands/evomap.js +61 -0
- package/src/commands/export.js +256 -1
- package/src/commands/fflag.js +178 -0
- package/src/commands/fusion.js +258 -0
- package/src/commands/git.js +45 -0
- package/src/commands/governance.js +325 -0
- package/src/commands/hardening.js +411 -0
- package/src/commands/hmemory.js +56 -0
- package/src/commands/hook.js +148 -0
- package/src/commands/import.js +252 -0
- package/src/commands/incentive.js +322 -0
- package/src/commands/inference.js +42 -0
- package/src/commands/infra.js +244 -0
- package/src/commands/instinct.js +260 -0
- package/src/commands/ipfs.js +318 -0
- package/src/commands/itbudget.js +45 -0
- package/src/commands/kg.js +387 -0
- package/src/commands/llm.js +263 -0
- package/src/commands/lowcode.js +44 -0
- package/src/commands/matrix.js +62 -0
- package/src/commands/mcp.js +221 -0
- package/src/commands/mcpscaf.js +41 -0
- package/src/commands/meminj.js +41 -0
- package/src/commands/memory.js +248 -0
- package/src/commands/multimodal.js +296 -0
- package/src/commands/nlprog.js +356 -0
- package/src/commands/nostr.js +62 -0
- package/src/commands/note.js +244 -0
- package/src/commands/ops.js +354 -0
- package/src/commands/orchestrate.js +166 -0
- package/src/commands/orchgov.js +45 -0
- package/src/commands/org.js +277 -0
- package/src/commands/p2p.js +390 -0
- package/src/commands/pdfp.js +78 -0
- package/src/commands/perception.js +290 -0
- package/src/commands/perf.js +39 -0
- package/src/commands/perm.js +45 -0
- package/src/commands/permmem.js +251 -0
- package/src/commands/pipeline.js +57 -1
- package/src/commands/planmode.js +45 -0
- package/src/commands/plugin-ecosystem.js +273 -0
- package/src/commands/pqc.js +393 -0
- package/src/commands/promcomp.js +82 -0
- package/src/commands/quantization.js +351 -0
- package/src/commands/rcache.js +271 -0
- package/src/commands/recommend.js +382 -0
- package/src/commands/runtime.js +307 -0
- package/src/commands/scim.js +262 -0
- package/src/commands/seshhook.js +41 -0
- package/src/commands/seshsearch.js +41 -0
- package/src/commands/seshtail.js +41 -0
- package/src/commands/seshu.js +41 -0
- package/src/commands/session.js +258 -0
- package/src/commands/sganal.js +78 -0
- package/src/commands/siem.js +40 -0
- package/src/commands/skill.js +267 -1
- package/src/commands/slotfill.js +41 -0
- package/src/commands/social.js +290 -0
- package/src/commands/sso.js +186 -1
- package/src/commands/svccont.js +45 -0
- package/src/commands/sync.js +256 -0
- package/src/commands/tech.js +338 -0
- package/src/commands/tenant.js +351 -0
- package/src/commands/tms.js +45 -0
- package/src/commands/tokens.js +269 -0
- package/src/commands/topiccls.js +45 -0
- package/src/commands/trust.js +249 -0
- package/src/commands/uprof.js +45 -0
- package/src/commands/vcheck.js +78 -0
- package/src/commands/wallet.js +277 -0
- package/src/commands/webfetch.js +41 -0
- package/src/commands/workflow.js +171 -0
- package/src/commands/zkp.js +62 -0
- package/src/harness/prompt-compressor.js +331 -0
- package/src/index.js +65 -1
- package/src/lib/a2a-protocol.js +105 -0
- package/src/lib/activitypub-bridge.js +105 -0
- package/src/lib/agent-coordinator.js +325 -0
- package/src/lib/agent-economy.js +105 -0
- package/src/lib/agent-network.js +387 -0
- package/src/lib/agent-router.js +395 -0
- package/src/lib/aiops.js +478 -0
- package/src/lib/app-builder.js +105 -0
- package/src/lib/audit-logger.js +379 -0
- package/src/lib/automation-engine.js +330 -0
- package/src/lib/autonomous-agent.js +105 -0
- package/src/lib/autonomous-developer.js +350 -0
- package/src/lib/bi-engine.js +105 -0
- package/src/lib/bm25-search.js +81 -0
- package/src/lib/browser-automation.js +105 -0
- package/src/lib/code-agent.js +323 -0
- package/src/lib/collaboration-governance.js +364 -0
- package/src/lib/community-governance.js +436 -0
- package/src/lib/compliance-framework-reporter.js +105 -0
- package/src/lib/compliance-manager.js +434 -0
- package/src/lib/compression-telemetry.js +81 -0
- package/src/lib/content-recommendation.js +469 -0
- package/src/lib/content-recommender.js +105 -0
- package/src/lib/cowork-cron.js +81 -0
- package/src/lib/cowork-task-runner.js +105 -0
- package/src/lib/cross-chain.js +105 -0
- package/src/lib/crypto-manager.js +350 -0
- package/src/lib/dao-governance.js +105 -0
- package/src/lib/dbevo.js +338 -0
- package/src/lib/decentral-infra.js +340 -0
- package/src/lib/did-manager.js +367 -0
- package/src/lib/dlp-engine.js +105 -0
- package/src/lib/evolution-system.js +105 -0
- package/src/lib/evomap-manager.js +105 -0
- package/src/lib/execution-backend.js +105 -0
- package/src/lib/feature-flags.js +85 -0
- package/src/lib/git-integration.js +105 -0
- package/src/lib/hardening-manager.js +348 -0
- package/src/lib/hierarchical-memory.js +105 -0
- package/src/lib/hook-manager.js +380 -0
- package/src/lib/inference-network.js +105 -0
- package/src/lib/instinct-manager.js +332 -0
- package/src/lib/ipfs-storage.js +334 -0
- package/src/lib/iteration-budget.js +105 -0
- package/src/lib/knowledge-exporter.js +381 -0
- package/src/lib/knowledge-graph.js +432 -0
- package/src/lib/knowledge-importer.js +379 -0
- package/src/lib/llm-providers.js +391 -0
- package/src/lib/matrix-bridge.js +105 -0
- package/src/lib/mcp-registry.js +333 -0
- package/src/lib/mcp-scaffold.js +81 -0
- package/src/lib/memory-injection.js +81 -0
- package/src/lib/memory-manager.js +330 -0
- package/src/lib/multimodal.js +346 -0
- package/src/lib/nl-programming.js +343 -0
- package/src/lib/nostr-bridge.js +105 -0
- package/src/lib/note-versioning.js +327 -0
- package/src/lib/orchestrator.js +105 -0
- package/src/lib/org-manager.js +323 -0
- package/src/lib/p2p-manager.js +387 -0
- package/src/lib/pdf-parser.js +81 -0
- package/src/lib/perception.js +346 -0
- package/src/lib/perf-tuning.js +109 -1
- package/src/lib/permanent-memory.js +320 -0
- package/src/lib/permission-engine.js +81 -0
- package/src/lib/pipeline-orchestrator.js +105 -0
- package/src/lib/plan-mode.js +81 -0
- package/src/lib/plugin-ecosystem.js +377 -0
- package/src/lib/pqc-manager.js +368 -0
- package/src/lib/prompt-compressor.js +1 -10
- package/src/lib/protocol-fusion.js +417 -0
- package/src/lib/quantization.js +325 -0
- package/src/lib/response-cache.js +327 -0
- package/src/lib/scim-manager.js +329 -0
- package/src/lib/service-container.js +81 -0
- package/src/lib/session-consolidator.js +105 -0
- package/src/lib/session-hooks.js +81 -0
- package/src/lib/session-manager.js +329 -0
- package/src/lib/session-search.js +81 -0
- package/src/lib/session-tail.js +81 -0
- package/src/lib/session-usage.js +83 -0
- package/src/lib/siem-exporter.js +105 -0
- package/src/lib/skill-loader.js +377 -0
- package/src/lib/slot-filler.js +81 -0
- package/src/lib/social-graph-analytics.js +81 -0
- package/src/lib/social-graph.js +81 -0
- package/src/lib/social-manager.js +326 -0
- package/src/lib/sso-manager.js +332 -0
- package/src/lib/sub-agent-registry.js +110 -0
- package/src/lib/sync-manager.js +326 -0
- package/src/lib/task-model-selector.js +81 -0
- package/src/lib/tech-learning-engine.js +369 -0
- package/src/lib/tenant-saas.js +460 -0
- package/src/lib/threat-intel.js +335 -0
- package/src/lib/todo-manager.js +105 -0
- package/src/lib/token-incentive.js +293 -0
- package/src/lib/token-tracker.js +329 -0
- package/src/lib/topic-classifier.js +105 -0
- package/src/lib/trust-security.js +390 -0
- package/src/lib/ueba.js +389 -0
- package/src/lib/universal-runtime.js +325 -0
- package/src/lib/user-profile.js +81 -0
- package/src/lib/version-checker.js +81 -0
- package/src/lib/wallet-manager.js +326 -0
- package/src/lib/web-fetch.js +81 -0
- package/src/lib/workflow-engine.js +322 -0
- package/src/lib/zkp-engine.js +105 -0
|
@@ -598,3 +598,472 @@ export function _resetState() {
|
|
|
598
598
|
_userRecs.clear();
|
|
599
599
|
_seq = 0;
|
|
600
600
|
}
|
|
601
|
+
|
|
602
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
603
|
+
* Phase 48 V2 — Profile Maturity + Feed Channel Lifecycle
|
|
604
|
+
* Strictly additive. Legacy surface above is preserved.
|
|
605
|
+
* ═════════════════════════════════════════════════════════════ */
|
|
606
|
+
|
|
607
|
+
export const PROFILE_MATURITY_V2 = Object.freeze({
|
|
608
|
+
ONBOARDING: "onboarding",
|
|
609
|
+
ACTIVE: "active",
|
|
610
|
+
DORMANT: "dormant",
|
|
611
|
+
RETIRED: "retired",
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
export const FEED_LIFECYCLE_V2 = Object.freeze({
|
|
615
|
+
DRAFT: "draft",
|
|
616
|
+
ACTIVE: "active",
|
|
617
|
+
PAUSED: "paused",
|
|
618
|
+
ARCHIVED: "archived",
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const PROFILE_TRANSITIONS_V2 = new Map([
|
|
622
|
+
["onboarding", new Set(["active", "retired"])],
|
|
623
|
+
["active", new Set(["dormant", "retired"])],
|
|
624
|
+
["dormant", new Set(["active", "retired"])],
|
|
625
|
+
]);
|
|
626
|
+
const PROFILE_TERMINALS_V2 = new Set(["retired"]);
|
|
627
|
+
|
|
628
|
+
const FEED_TRANSITIONS_V2 = new Map([
|
|
629
|
+
["draft", new Set(["active", "archived"])],
|
|
630
|
+
["active", new Set(["paused", "archived"])],
|
|
631
|
+
["paused", new Set(["active", "archived"])],
|
|
632
|
+
]);
|
|
633
|
+
const FEED_TERMINALS_V2 = new Set(["archived"]);
|
|
634
|
+
|
|
635
|
+
export const REC_DEFAULT_MAX_ACTIVE_PROFILES_PER_SEGMENT = 10000;
|
|
636
|
+
export const REC_DEFAULT_MAX_ACTIVE_FEEDS_PER_CURATOR = 20;
|
|
637
|
+
export const REC_DEFAULT_PROFILE_IDLE_MS = 90 * 86400000; // 90 days
|
|
638
|
+
export const REC_DEFAULT_FEED_STALE_MS = 30 * 86400000; // 30 days
|
|
639
|
+
|
|
640
|
+
let _maxActiveProfilesPerSegmentV2 =
|
|
641
|
+
REC_DEFAULT_MAX_ACTIVE_PROFILES_PER_SEGMENT;
|
|
642
|
+
let _maxActiveFeedsPerCuratorV2 = REC_DEFAULT_MAX_ACTIVE_FEEDS_PER_CURATOR;
|
|
643
|
+
let _profileIdleMsV2 = REC_DEFAULT_PROFILE_IDLE_MS;
|
|
644
|
+
let _feedStaleMsV2 = REC_DEFAULT_FEED_STALE_MS;
|
|
645
|
+
|
|
646
|
+
const _profileStatesV2 = new Map(); // profileId → V2 record
|
|
647
|
+
const _feedStatesV2 = new Map(); // feedId → V2 record
|
|
648
|
+
|
|
649
|
+
function _positiveIntV2(n, label) {
|
|
650
|
+
const num = Number(n);
|
|
651
|
+
if (!Number.isFinite(num) || num <= 0) {
|
|
652
|
+
throw new Error(`${label} must be a positive integer`);
|
|
653
|
+
}
|
|
654
|
+
return Math.floor(num);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function _validProfileStatusV2(s) {
|
|
658
|
+
return (
|
|
659
|
+
s === "onboarding" || s === "active" || s === "dormant" || s === "retired"
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function _validFeedStatusV2(s) {
|
|
664
|
+
return s === "draft" || s === "active" || s === "paused" || s === "archived";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function getDefaultMaxActiveProfilesPerSegmentV2() {
|
|
668
|
+
return REC_DEFAULT_MAX_ACTIVE_PROFILES_PER_SEGMENT;
|
|
669
|
+
}
|
|
670
|
+
export function getMaxActiveProfilesPerSegmentV2() {
|
|
671
|
+
return _maxActiveProfilesPerSegmentV2;
|
|
672
|
+
}
|
|
673
|
+
export function setMaxActiveProfilesPerSegmentV2(n) {
|
|
674
|
+
_maxActiveProfilesPerSegmentV2 = _positiveIntV2(
|
|
675
|
+
n,
|
|
676
|
+
"maxActiveProfilesPerSegment",
|
|
677
|
+
);
|
|
678
|
+
return _maxActiveProfilesPerSegmentV2;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function getDefaultMaxActiveFeedsPerCuratorV2() {
|
|
682
|
+
return REC_DEFAULT_MAX_ACTIVE_FEEDS_PER_CURATOR;
|
|
683
|
+
}
|
|
684
|
+
export function getMaxActiveFeedsPerCuratorV2() {
|
|
685
|
+
return _maxActiveFeedsPerCuratorV2;
|
|
686
|
+
}
|
|
687
|
+
export function setMaxActiveFeedsPerCuratorV2(n) {
|
|
688
|
+
_maxActiveFeedsPerCuratorV2 = _positiveIntV2(n, "maxActiveFeedsPerCurator");
|
|
689
|
+
return _maxActiveFeedsPerCuratorV2;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function getDefaultProfileIdleMsV2() {
|
|
693
|
+
return REC_DEFAULT_PROFILE_IDLE_MS;
|
|
694
|
+
}
|
|
695
|
+
export function getProfileIdleMsV2() {
|
|
696
|
+
return _profileIdleMsV2;
|
|
697
|
+
}
|
|
698
|
+
export function setProfileIdleMsV2(ms) {
|
|
699
|
+
_profileIdleMsV2 = _positiveIntV2(ms, "profileIdleMs");
|
|
700
|
+
return _profileIdleMsV2;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function getDefaultFeedStaleMsV2() {
|
|
704
|
+
return REC_DEFAULT_FEED_STALE_MS;
|
|
705
|
+
}
|
|
706
|
+
export function getFeedStaleMsV2() {
|
|
707
|
+
return _feedStaleMsV2;
|
|
708
|
+
}
|
|
709
|
+
export function setFeedStaleMsV2(ms) {
|
|
710
|
+
_feedStaleMsV2 = _positiveIntV2(ms, "feedStaleMs");
|
|
711
|
+
return _feedStaleMsV2;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* ── Profile V2 ─────────────────────────────────────────────── */
|
|
715
|
+
|
|
716
|
+
export function registerProfileV2(db, config = {}) {
|
|
717
|
+
void db;
|
|
718
|
+
const profileId = String(config.profileId || "").trim();
|
|
719
|
+
if (!profileId) throw new Error("profileId is required");
|
|
720
|
+
const segment = String(config.segment || "").trim();
|
|
721
|
+
if (!segment) throw new Error("segment is required");
|
|
722
|
+
if (_profileStatesV2.has(profileId)) {
|
|
723
|
+
throw new Error(`Profile already registered in V2: ${profileId}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const now = Number(config.now ?? Date.now());
|
|
727
|
+
const initialStatus = config.initialStatus || "onboarding";
|
|
728
|
+
if (!_validProfileStatusV2(initialStatus)) {
|
|
729
|
+
throw new Error(`Invalid initial status: ${initialStatus}`);
|
|
730
|
+
}
|
|
731
|
+
if (PROFILE_TERMINALS_V2.has(initialStatus)) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`Cannot register profile in terminal status '${initialStatus}'`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (initialStatus === "active") {
|
|
738
|
+
let activeCount = 0;
|
|
739
|
+
for (const rec of _profileStatesV2.values()) {
|
|
740
|
+
if (rec.segment === segment && rec.status === "active") {
|
|
741
|
+
activeCount += 1;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (activeCount >= _maxActiveProfilesPerSegmentV2) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
`Max active profiles per segment reached (${_maxActiveProfilesPerSegmentV2})`,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const record = {
|
|
752
|
+
profileId,
|
|
753
|
+
segment,
|
|
754
|
+
ownerId: config.ownerId ? String(config.ownerId) : null,
|
|
755
|
+
status: initialStatus,
|
|
756
|
+
metadata: config.metadata ? { ...config.metadata } : {},
|
|
757
|
+
createdAt: now,
|
|
758
|
+
updatedAt: now,
|
|
759
|
+
lastActivityAt: now,
|
|
760
|
+
reason: null,
|
|
761
|
+
};
|
|
762
|
+
_profileStatesV2.set(profileId, record);
|
|
763
|
+
return { ...record, metadata: { ...record.metadata } };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function getProfileV2(profileId) {
|
|
767
|
+
const rec = _profileStatesV2.get(String(profileId || ""));
|
|
768
|
+
if (!rec) return null;
|
|
769
|
+
return { ...rec, metadata: { ...rec.metadata } };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function setProfileMaturityV2(db, profileId, newStatus, patch = {}) {
|
|
773
|
+
void db;
|
|
774
|
+
const id = String(profileId || "");
|
|
775
|
+
const record = _profileStatesV2.get(id);
|
|
776
|
+
if (!record) throw new Error(`Profile not registered in V2: ${id}`);
|
|
777
|
+
if (!_validProfileStatusV2(newStatus)) {
|
|
778
|
+
throw new Error(`Invalid profile status: ${newStatus}`);
|
|
779
|
+
}
|
|
780
|
+
if (PROFILE_TERMINALS_V2.has(record.status)) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`Profile is in terminal status '${record.status}' and cannot transition`,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const allowed = PROFILE_TRANSITIONS_V2.get(record.status);
|
|
786
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
787
|
+
throw new Error(`Invalid transition: ${record.status} → ${newStatus}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (newStatus === "active" && record.status !== "active") {
|
|
791
|
+
let activeCount = 0;
|
|
792
|
+
for (const rec of _profileStatesV2.values()) {
|
|
793
|
+
if (rec.segment === record.segment && rec.status === "active") {
|
|
794
|
+
activeCount += 1;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (activeCount >= _maxActiveProfilesPerSegmentV2) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`Max active profiles per segment reached (${_maxActiveProfilesPerSegmentV2})`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
record.status = newStatus;
|
|
805
|
+
record.updatedAt = Number(patch.now ?? Date.now());
|
|
806
|
+
if (patch.reason !== undefined) record.reason = patch.reason;
|
|
807
|
+
if (patch.metadata && typeof patch.metadata === "object") {
|
|
808
|
+
record.metadata = { ...record.metadata, ...patch.metadata };
|
|
809
|
+
}
|
|
810
|
+
return { ...record, metadata: { ...record.metadata } };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function activateProfile(db, profileId, reason) {
|
|
814
|
+
return setProfileMaturityV2(db, profileId, "active", { reason });
|
|
815
|
+
}
|
|
816
|
+
export function dormantProfile(db, profileId, reason) {
|
|
817
|
+
return setProfileMaturityV2(db, profileId, "dormant", { reason });
|
|
818
|
+
}
|
|
819
|
+
export function retireProfile(db, profileId, reason) {
|
|
820
|
+
return setProfileMaturityV2(db, profileId, "retired", { reason });
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function touchProfileActivity(profileId) {
|
|
824
|
+
const rec = _profileStatesV2.get(String(profileId || ""));
|
|
825
|
+
if (!rec) throw new Error(`Profile not registered in V2: ${profileId}`);
|
|
826
|
+
rec.lastActivityAt = Date.now();
|
|
827
|
+
return { ...rec, metadata: { ...rec.metadata } };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/* ── Feed V2 ────────────────────────────────────────────────── */
|
|
831
|
+
|
|
832
|
+
export function registerFeedV2(db, config = {}) {
|
|
833
|
+
void db;
|
|
834
|
+
const feedId = String(config.feedId || "").trim();
|
|
835
|
+
if (!feedId) throw new Error("feedId is required");
|
|
836
|
+
const curatorId = String(config.curatorId || "").trim();
|
|
837
|
+
if (!curatorId) throw new Error("curatorId is required");
|
|
838
|
+
const name = String(config.name || "").trim();
|
|
839
|
+
if (!name) throw new Error("name is required");
|
|
840
|
+
if (_feedStatesV2.has(feedId)) {
|
|
841
|
+
throw new Error(`Feed already registered in V2: ${feedId}`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const now = Number(config.now ?? Date.now());
|
|
845
|
+
const initialStatus = config.initialStatus || "draft";
|
|
846
|
+
if (!_validFeedStatusV2(initialStatus)) {
|
|
847
|
+
throw new Error(`Invalid initial status: ${initialStatus}`);
|
|
848
|
+
}
|
|
849
|
+
if (FEED_TERMINALS_V2.has(initialStatus)) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Cannot register feed in terminal status '${initialStatus}'`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (initialStatus === "active") {
|
|
856
|
+
let activeCount = 0;
|
|
857
|
+
for (const rec of _feedStatesV2.values()) {
|
|
858
|
+
if (rec.curatorId === curatorId && rec.status === "active") {
|
|
859
|
+
activeCount += 1;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (activeCount >= _maxActiveFeedsPerCuratorV2) {
|
|
863
|
+
throw new Error(
|
|
864
|
+
`Max active feeds per curator reached (${_maxActiveFeedsPerCuratorV2})`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const record = {
|
|
870
|
+
feedId,
|
|
871
|
+
curatorId,
|
|
872
|
+
name,
|
|
873
|
+
topics: Array.isArray(config.topics) ? [...config.topics] : [],
|
|
874
|
+
status: initialStatus,
|
|
875
|
+
metadata: config.metadata ? { ...config.metadata } : {},
|
|
876
|
+
createdAt: now,
|
|
877
|
+
updatedAt: now,
|
|
878
|
+
lastPublishedAt: now,
|
|
879
|
+
reason: null,
|
|
880
|
+
};
|
|
881
|
+
_feedStatesV2.set(feedId, record);
|
|
882
|
+
return {
|
|
883
|
+
...record,
|
|
884
|
+
topics: [...record.topics],
|
|
885
|
+
metadata: { ...record.metadata },
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export function getFeedV2(feedId) {
|
|
890
|
+
const rec = _feedStatesV2.get(String(feedId || ""));
|
|
891
|
+
if (!rec) return null;
|
|
892
|
+
return {
|
|
893
|
+
...rec,
|
|
894
|
+
topics: [...rec.topics],
|
|
895
|
+
metadata: { ...rec.metadata },
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function setFeedStatusV2(db, feedId, newStatus, patch = {}) {
|
|
900
|
+
void db;
|
|
901
|
+
const id = String(feedId || "");
|
|
902
|
+
const record = _feedStatesV2.get(id);
|
|
903
|
+
if (!record) throw new Error(`Feed not registered in V2: ${id}`);
|
|
904
|
+
if (!_validFeedStatusV2(newStatus)) {
|
|
905
|
+
throw new Error(`Invalid feed status: ${newStatus}`);
|
|
906
|
+
}
|
|
907
|
+
if (FEED_TERMINALS_V2.has(record.status)) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Feed is in terminal status '${record.status}' and cannot transition`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
const allowed = FEED_TRANSITIONS_V2.get(record.status);
|
|
913
|
+
if (!allowed || !allowed.has(newStatus)) {
|
|
914
|
+
throw new Error(`Invalid transition: ${record.status} → ${newStatus}`);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (newStatus === "active" && record.status !== "active") {
|
|
918
|
+
let activeCount = 0;
|
|
919
|
+
for (const rec of _feedStatesV2.values()) {
|
|
920
|
+
if (rec.curatorId === record.curatorId && rec.status === "active") {
|
|
921
|
+
activeCount += 1;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (activeCount >= _maxActiveFeedsPerCuratorV2) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
`Max active feeds per curator reached (${_maxActiveFeedsPerCuratorV2})`,
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
record.status = newStatus;
|
|
932
|
+
record.updatedAt = Number(patch.now ?? Date.now());
|
|
933
|
+
if (patch.reason !== undefined) record.reason = patch.reason;
|
|
934
|
+
if (patch.metadata && typeof patch.metadata === "object") {
|
|
935
|
+
record.metadata = { ...record.metadata, ...patch.metadata };
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
...record,
|
|
939
|
+
topics: [...record.topics],
|
|
940
|
+
metadata: { ...record.metadata },
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function activateFeed(db, feedId, reason) {
|
|
945
|
+
return setFeedStatusV2(db, feedId, "active", { reason });
|
|
946
|
+
}
|
|
947
|
+
export function pauseFeed(db, feedId, reason) {
|
|
948
|
+
return setFeedStatusV2(db, feedId, "paused", { reason });
|
|
949
|
+
}
|
|
950
|
+
export function archiveFeed(db, feedId, reason) {
|
|
951
|
+
return setFeedStatusV2(db, feedId, "archived", { reason });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function touchFeedPublish(feedId) {
|
|
955
|
+
const rec = _feedStatesV2.get(String(feedId || ""));
|
|
956
|
+
if (!rec) throw new Error(`Feed not registered in V2: ${feedId}`);
|
|
957
|
+
rec.lastPublishedAt = Date.now();
|
|
958
|
+
return {
|
|
959
|
+
...rec,
|
|
960
|
+
topics: [...rec.topics],
|
|
961
|
+
metadata: { ...rec.metadata },
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/* ── Counts ─────────────────────────────────────────────────── */
|
|
966
|
+
|
|
967
|
+
export function getActiveProfileCount(segment) {
|
|
968
|
+
let n = 0;
|
|
969
|
+
for (const rec of _profileStatesV2.values()) {
|
|
970
|
+
if (rec.status !== "active") continue;
|
|
971
|
+
if (segment !== undefined && rec.segment !== String(segment)) continue;
|
|
972
|
+
n += 1;
|
|
973
|
+
}
|
|
974
|
+
return n;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function getActiveFeedCount(curatorId) {
|
|
978
|
+
let n = 0;
|
|
979
|
+
for (const rec of _feedStatesV2.values()) {
|
|
980
|
+
if (rec.status !== "active") continue;
|
|
981
|
+
if (curatorId !== undefined && rec.curatorId !== String(curatorId)) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
n += 1;
|
|
985
|
+
}
|
|
986
|
+
return n;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/* ── Auto-flip Bulk Ops ─────────────────────────────────────── */
|
|
990
|
+
|
|
991
|
+
export function autoDormantIdleProfiles(db, nowMs) {
|
|
992
|
+
void db;
|
|
993
|
+
const now = Number(nowMs ?? Date.now());
|
|
994
|
+
const flipped = [];
|
|
995
|
+
for (const rec of _profileStatesV2.values()) {
|
|
996
|
+
if (rec.status !== "active") continue;
|
|
997
|
+
if (now - rec.lastActivityAt > _profileIdleMsV2) {
|
|
998
|
+
rec.status = "dormant";
|
|
999
|
+
rec.updatedAt = now;
|
|
1000
|
+
rec.reason = "idle";
|
|
1001
|
+
flipped.push(rec.profileId);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return flipped;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export function autoArchiveStaleFeeds(db, nowMs) {
|
|
1008
|
+
void db;
|
|
1009
|
+
const now = Number(nowMs ?? Date.now());
|
|
1010
|
+
const flipped = [];
|
|
1011
|
+
for (const rec of _feedStatesV2.values()) {
|
|
1012
|
+
if (rec.status !== "active" && rec.status !== "paused") continue;
|
|
1013
|
+
if (now - rec.lastPublishedAt > _feedStaleMsV2) {
|
|
1014
|
+
rec.status = "archived";
|
|
1015
|
+
rec.updatedAt = now;
|
|
1016
|
+
rec.reason = "stale";
|
|
1017
|
+
flipped.push(rec.feedId);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return flipped;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/* ── Stats V2 ───────────────────────────────────────────────── */
|
|
1024
|
+
|
|
1025
|
+
export function getRecommendationStatsV2() {
|
|
1026
|
+
const profilesByStatus = {
|
|
1027
|
+
onboarding: 0,
|
|
1028
|
+
active: 0,
|
|
1029
|
+
dormant: 0,
|
|
1030
|
+
retired: 0,
|
|
1031
|
+
};
|
|
1032
|
+
const feedsByStatus = {
|
|
1033
|
+
draft: 0,
|
|
1034
|
+
active: 0,
|
|
1035
|
+
paused: 0,
|
|
1036
|
+
archived: 0,
|
|
1037
|
+
};
|
|
1038
|
+
for (const rec of _profileStatesV2.values()) {
|
|
1039
|
+
if (profilesByStatus[rec.status] !== undefined) {
|
|
1040
|
+
profilesByStatus[rec.status] += 1;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const rec of _feedStatesV2.values()) {
|
|
1044
|
+
if (feedsByStatus[rec.status] !== undefined) {
|
|
1045
|
+
feedsByStatus[rec.status] += 1;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
totalProfilesV2: _profileStatesV2.size,
|
|
1050
|
+
totalFeedsV2: _feedStatesV2.size,
|
|
1051
|
+
maxActiveProfilesPerSegment: _maxActiveProfilesPerSegmentV2,
|
|
1052
|
+
maxActiveFeedsPerCurator: _maxActiveFeedsPerCuratorV2,
|
|
1053
|
+
profileIdleMs: _profileIdleMsV2,
|
|
1054
|
+
feedStaleMs: _feedStaleMsV2,
|
|
1055
|
+
profilesByStatus,
|
|
1056
|
+
feedsByStatus,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/* ── Reset V2 (tests) ───────────────────────────────────────── */
|
|
1061
|
+
|
|
1062
|
+
export function _resetStateV2() {
|
|
1063
|
+
_profileStatesV2.clear();
|
|
1064
|
+
_feedStatesV2.clear();
|
|
1065
|
+
_maxActiveProfilesPerSegmentV2 = REC_DEFAULT_MAX_ACTIVE_PROFILES_PER_SEGMENT;
|
|
1066
|
+
_maxActiveFeedsPerCuratorV2 = REC_DEFAULT_MAX_ACTIVE_FEEDS_PER_CURATOR;
|
|
1067
|
+
_profileIdleMsV2 = REC_DEFAULT_PROFILE_IDLE_MS;
|
|
1068
|
+
_feedStaleMsV2 = REC_DEFAULT_FEED_STALE_MS;
|
|
1069
|
+
}
|
|
@@ -203,3 +203,108 @@ export class CLIContentRecommender {
|
|
|
203
203
|
return matrix;
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
// ===== V2 Surface: Content Recommender governance overlay (CLI v0.139.0) =====
|
|
209
|
+
export const RECOMMENDER_PROFILE_MATURITY_V2 = Object.freeze({
|
|
210
|
+
PENDING: "pending", ACTIVE: "active", STALE: "stale", ARCHIVED: "archived",
|
|
211
|
+
});
|
|
212
|
+
export const RECOMMENDATION_JOB_LIFECYCLE_V2 = Object.freeze({
|
|
213
|
+
QUEUED: "queued", RUNNING: "running", COMPLETED: "completed", FAILED: "failed", CANCELLED: "cancelled",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const _crpTrans = new Map([
|
|
217
|
+
[RECOMMENDER_PROFILE_MATURITY_V2.PENDING, new Set([RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE, RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED])],
|
|
218
|
+
[RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE, new Set([RECOMMENDER_PROFILE_MATURITY_V2.STALE, RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED])],
|
|
219
|
+
[RECOMMENDER_PROFILE_MATURITY_V2.STALE, new Set([RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE, RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED])],
|
|
220
|
+
[RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED, new Set()],
|
|
221
|
+
]);
|
|
222
|
+
const _crpTerminal = new Set([RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED]);
|
|
223
|
+
const _crjTrans = new Map([
|
|
224
|
+
[RECOMMENDATION_JOB_LIFECYCLE_V2.QUEUED, new Set([RECOMMENDATION_JOB_LIFECYCLE_V2.RUNNING, RECOMMENDATION_JOB_LIFECYCLE_V2.CANCELLED])],
|
|
225
|
+
[RECOMMENDATION_JOB_LIFECYCLE_V2.RUNNING, new Set([RECOMMENDATION_JOB_LIFECYCLE_V2.COMPLETED, RECOMMENDATION_JOB_LIFECYCLE_V2.FAILED, RECOMMENDATION_JOB_LIFECYCLE_V2.CANCELLED])],
|
|
226
|
+
[RECOMMENDATION_JOB_LIFECYCLE_V2.COMPLETED, new Set()],
|
|
227
|
+
[RECOMMENDATION_JOB_LIFECYCLE_V2.FAILED, new Set()],
|
|
228
|
+
[RECOMMENDATION_JOB_LIFECYCLE_V2.CANCELLED, new Set()],
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const _crpsV2 = new Map();
|
|
232
|
+
const _crjsV2 = new Map();
|
|
233
|
+
let _crpMaxActivePerOwner = 8;
|
|
234
|
+
let _crpMaxPendingJobsPerProfile = 10;
|
|
235
|
+
let _crpIdleMs = 7 * 24 * 60 * 60 * 1000;
|
|
236
|
+
let _crjStuckMs = 5 * 60 * 1000;
|
|
237
|
+
|
|
238
|
+
function _crpPos(n, lbl) { const v = Math.floor(Number(n)); if (!Number.isFinite(v) || v <= 0) throw new Error(`${lbl} must be positive integer`); return v; }
|
|
239
|
+
|
|
240
|
+
export function setMaxActiveRecommenderProfilesPerOwnerV2(n) { _crpMaxActivePerOwner = _crpPos(n, "maxActiveRecommenderProfilesPerOwner"); }
|
|
241
|
+
export function getMaxActiveRecommenderProfilesPerOwnerV2() { return _crpMaxActivePerOwner; }
|
|
242
|
+
export function setMaxPendingRecommendationJobsPerProfileV2(n) { _crpMaxPendingJobsPerProfile = _crpPos(n, "maxPendingRecommendationJobsPerProfile"); }
|
|
243
|
+
export function getMaxPendingRecommendationJobsPerProfileV2() { return _crpMaxPendingJobsPerProfile; }
|
|
244
|
+
export function setRecommenderProfileIdleMsV2(n) { _crpIdleMs = _crpPos(n, "recommenderProfileIdleMs"); }
|
|
245
|
+
export function getRecommenderProfileIdleMsV2() { return _crpIdleMs; }
|
|
246
|
+
export function setRecommendationJobStuckMsV2(n) { _crjStuckMs = _crpPos(n, "recommendationJobStuckMs"); }
|
|
247
|
+
export function getRecommendationJobStuckMsV2() { return _crjStuckMs; }
|
|
248
|
+
|
|
249
|
+
export function _resetStateContentRecommenderV2() {
|
|
250
|
+
_crpsV2.clear(); _crjsV2.clear();
|
|
251
|
+
_crpMaxActivePerOwner = 8; _crpMaxPendingJobsPerProfile = 10;
|
|
252
|
+
_crpIdleMs = 7 * 24 * 60 * 60 * 1000; _crjStuckMs = 5 * 60 * 1000;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function registerRecommenderProfileV2({ id, owner, strategy, metadata } = {}) {
|
|
256
|
+
if (!id || typeof id !== "string") throw new Error("id is required");
|
|
257
|
+
if (!owner || typeof owner !== "string") throw new Error("owner is required");
|
|
258
|
+
if (_crpsV2.has(id)) throw new Error(`recommender profile ${id} already registered`);
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
const p = { id, owner, strategy: strategy || "tfidf", status: RECOMMENDER_PROFILE_MATURITY_V2.PENDING, createdAt: now, updatedAt: now, activatedAt: null, archivedAt: null, lastTouchedAt: now, metadata: { ...(metadata || {}) } };
|
|
261
|
+
_crpsV2.set(id, p);
|
|
262
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
263
|
+
}
|
|
264
|
+
function _crpCheckP(from, to) { const a = _crpTrans.get(from); if (!a || !a.has(to)) throw new Error(`invalid recommender profile transition ${from} → ${to}`); }
|
|
265
|
+
function _crpCountActive(owner) { let n = 0; for (const p of _crpsV2.values()) if (p.owner === owner && p.status === RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE) n++; return n; }
|
|
266
|
+
|
|
267
|
+
export function activateRecommenderProfileV2(id) {
|
|
268
|
+
const p = _crpsV2.get(id); if (!p) throw new Error(`recommender profile ${id} not found`);
|
|
269
|
+
_crpCheckP(p.status, RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE);
|
|
270
|
+
const recovery = p.status === RECOMMENDER_PROFILE_MATURITY_V2.STALE;
|
|
271
|
+
if (!recovery) { const c = _crpCountActive(p.owner); if (c >= _crpMaxActivePerOwner) throw new Error(`max active recommender profiles per owner (${_crpMaxActivePerOwner}) reached for ${p.owner}`); }
|
|
272
|
+
const now = Date.now(); p.status = RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE; p.updatedAt = now; p.lastTouchedAt = now; if (!p.activatedAt) p.activatedAt = now;
|
|
273
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
274
|
+
}
|
|
275
|
+
export function staleRecommenderProfileV2(id) { const p = _crpsV2.get(id); if (!p) throw new Error(`recommender profile ${id} not found`); _crpCheckP(p.status, RECOMMENDER_PROFILE_MATURITY_V2.STALE); p.status = RECOMMENDER_PROFILE_MATURITY_V2.STALE; p.updatedAt = Date.now(); return { ...p, metadata: { ...p.metadata } }; }
|
|
276
|
+
export function archiveRecommenderProfileV2(id) { const p = _crpsV2.get(id); if (!p) throw new Error(`recommender profile ${id} not found`); _crpCheckP(p.status, RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED); const now = Date.now(); p.status = RECOMMENDER_PROFILE_MATURITY_V2.ARCHIVED; p.updatedAt = now; if (!p.archivedAt) p.archivedAt = now; return { ...p, metadata: { ...p.metadata } }; }
|
|
277
|
+
export function touchRecommenderProfileV2(id) { const p = _crpsV2.get(id); if (!p) throw new Error(`recommender profile ${id} not found`); if (_crpTerminal.has(p.status)) throw new Error(`cannot touch terminal recommender profile ${id}`); const now = Date.now(); p.lastTouchedAt = now; p.updatedAt = now; return { ...p, metadata: { ...p.metadata } }; }
|
|
278
|
+
export function getRecommenderProfileV2(id) { const p = _crpsV2.get(id); if (!p) return null; return { ...p, metadata: { ...p.metadata } }; }
|
|
279
|
+
export function listRecommenderProfilesV2() { return [..._crpsV2.values()].map((p) => ({ ...p, metadata: { ...p.metadata } })); }
|
|
280
|
+
|
|
281
|
+
function _crjCountPending(profileId) { let n = 0; for (const j of _crjsV2.values()) if (j.profileId === profileId && (j.status === RECOMMENDATION_JOB_LIFECYCLE_V2.QUEUED || j.status === RECOMMENDATION_JOB_LIFECYCLE_V2.RUNNING)) n++; return n; }
|
|
282
|
+
|
|
283
|
+
export function createRecommendationJobV2({ id, profileId, query, metadata } = {}) {
|
|
284
|
+
if (!id || typeof id !== "string") throw new Error("id is required");
|
|
285
|
+
if (!profileId || typeof profileId !== "string") throw new Error("profileId is required");
|
|
286
|
+
if (_crjsV2.has(id)) throw new Error(`recommendation job ${id} already exists`);
|
|
287
|
+
if (!_crpsV2.has(profileId)) throw new Error(`recommender profile ${profileId} not found`);
|
|
288
|
+
const pending = _crjCountPending(profileId);
|
|
289
|
+
if (pending >= _crpMaxPendingJobsPerProfile) throw new Error(`max pending recommendation jobs per profile (${_crpMaxPendingJobsPerProfile}) reached for ${profileId}`);
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const j = { id, profileId, query: query || "", status: RECOMMENDATION_JOB_LIFECYCLE_V2.QUEUED, createdAt: now, updatedAt: now, startedAt: null, settledAt: null, metadata: { ...(metadata || {}) } };
|
|
292
|
+
_crjsV2.set(id, j);
|
|
293
|
+
return { ...j, metadata: { ...j.metadata } };
|
|
294
|
+
}
|
|
295
|
+
function _crjCheckJ(from, to) { const a = _crjTrans.get(from); if (!a || !a.has(to)) throw new Error(`invalid recommendation job transition ${from} → ${to}`); }
|
|
296
|
+
export function startRecommendationJobV2(id) { const j = _crjsV2.get(id); if (!j) throw new Error(`recommendation job ${id} not found`); _crjCheckJ(j.status, RECOMMENDATION_JOB_LIFECYCLE_V2.RUNNING); const now = Date.now(); j.status = RECOMMENDATION_JOB_LIFECYCLE_V2.RUNNING; j.updatedAt = now; if (!j.startedAt) j.startedAt = now; return { ...j, metadata: { ...j.metadata } }; }
|
|
297
|
+
export function completeRecommendationJobV2(id) { const j = _crjsV2.get(id); if (!j) throw new Error(`recommendation job ${id} not found`); _crjCheckJ(j.status, RECOMMENDATION_JOB_LIFECYCLE_V2.COMPLETED); const now = Date.now(); j.status = RECOMMENDATION_JOB_LIFECYCLE_V2.COMPLETED; j.updatedAt = now; if (!j.settledAt) j.settledAt = now; return { ...j, metadata: { ...j.metadata } }; }
|
|
298
|
+
export function failRecommendationJobV2(id, reason) { const j = _crjsV2.get(id); if (!j) throw new Error(`recommendation job ${id} not found`); _crjCheckJ(j.status, RECOMMENDATION_JOB_LIFECYCLE_V2.FAILED); const now = Date.now(); j.status = RECOMMENDATION_JOB_LIFECYCLE_V2.FAILED; j.updatedAt = now; if (!j.settledAt) j.settledAt = now; if (reason) j.metadata.failReason = String(reason); return { ...j, metadata: { ...j.metadata } }; }
|
|
299
|
+
export function cancelRecommendationJobV2(id, reason) { const j = _crjsV2.get(id); if (!j) throw new Error(`recommendation job ${id} not found`); _crjCheckJ(j.status, RECOMMENDATION_JOB_LIFECYCLE_V2.CANCELLED); const now = Date.now(); j.status = RECOMMENDATION_JOB_LIFECYCLE_V2.CANCELLED; j.updatedAt = now; if (!j.settledAt) j.settledAt = now; if (reason) j.metadata.cancelReason = String(reason); return { ...j, metadata: { ...j.metadata } }; }
|
|
300
|
+
export function getRecommendationJobV2(id) { const j = _crjsV2.get(id); if (!j) return null; return { ...j, metadata: { ...j.metadata } }; }
|
|
301
|
+
export function listRecommendationJobsV2() { return [..._crjsV2.values()].map((j) => ({ ...j, metadata: { ...j.metadata } })); }
|
|
302
|
+
|
|
303
|
+
export function autoStaleIdleRecommenderProfilesV2({ now } = {}) { const t = now ?? Date.now(); const flipped = []; for (const p of _crpsV2.values()) if (p.status === RECOMMENDER_PROFILE_MATURITY_V2.ACTIVE && (t - p.lastTouchedAt) >= _crpIdleMs) { p.status = RECOMMENDER_PROFILE_MATURITY_V2.STALE; p.updatedAt = t; flipped.push(p.id); } return { flipped, count: flipped.length }; }
|
|
304
|
+
export function autoFailStuckRecommendationJobsV2({ now } = {}) { const t = now ?? Date.now(); const flipped = []; for (const j of _crjsV2.values()) if (j.status === RECOMMENDATION_JOB_LIFECYCLE_V2.RUNNING && j.startedAt != null && (t - j.startedAt) >= _crjStuckMs) { j.status = RECOMMENDATION_JOB_LIFECYCLE_V2.FAILED; j.updatedAt = t; if (!j.settledAt) j.settledAt = t; j.metadata.failReason = "auto-fail-stuck"; flipped.push(j.id); } return { flipped, count: flipped.length }; }
|
|
305
|
+
|
|
306
|
+
export function getContentRecommenderGovStatsV2() {
|
|
307
|
+
const profilesByStatus = {}; for (const s of Object.values(RECOMMENDER_PROFILE_MATURITY_V2)) profilesByStatus[s] = 0; for (const p of _crpsV2.values()) profilesByStatus[p.status]++;
|
|
308
|
+
const jobsByStatus = {}; for (const s of Object.values(RECOMMENDATION_JOB_LIFECYCLE_V2)) jobsByStatus[s] = 0; for (const j of _crjsV2.values()) jobsByStatus[j.status]++;
|
|
309
|
+
return { totalRecommenderProfilesV2: _crpsV2.size, totalRecommendationJobsV2: _crjsV2.size, maxActiveRecommenderProfilesPerOwner: _crpMaxActivePerOwner, maxPendingRecommendationJobsPerProfile: _crpMaxPendingJobsPerProfile, recommenderProfileIdleMs: _crpIdleMs, recommendationJobStuckMs: _crjStuckMs, profilesByStatus, jobsByStatus };
|
|
310
|
+
}
|
package/src/lib/cowork-cron.js
CHANGED
|
@@ -472,3 +472,84 @@ function _minuteKey(date) {
|
|
|
472
472
|
function _secondKey(date) {
|
|
473
473
|
return `${_minuteKey(date)}-${date.getSeconds()}`;
|
|
474
474
|
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
// =====================================================================
|
|
478
|
+
// Cowork Cron V2 governance overlay
|
|
479
|
+
// =====================================================================
|
|
480
|
+
export const CCRON_PROFILE_MATURITY_V2 = Object.freeze({ PENDING: "pending", ACTIVE: "active", PAUSED: "paused", ARCHIVED: "archived" });
|
|
481
|
+
export const CCRON_TICK_LIFECYCLE_V2 = Object.freeze({ QUEUED: "queued", RUNNING: "running", COMPLETED: "completed", FAILED: "failed", CANCELLED: "cancelled" });
|
|
482
|
+
const _ccronPTrans = new Map([
|
|
483
|
+
[CCRON_PROFILE_MATURITY_V2.PENDING, new Set([CCRON_PROFILE_MATURITY_V2.ACTIVE, CCRON_PROFILE_MATURITY_V2.ARCHIVED])],
|
|
484
|
+
[CCRON_PROFILE_MATURITY_V2.ACTIVE, new Set([CCRON_PROFILE_MATURITY_V2.PAUSED, CCRON_PROFILE_MATURITY_V2.ARCHIVED])],
|
|
485
|
+
[CCRON_PROFILE_MATURITY_V2.PAUSED, new Set([CCRON_PROFILE_MATURITY_V2.ACTIVE, CCRON_PROFILE_MATURITY_V2.ARCHIVED])],
|
|
486
|
+
[CCRON_PROFILE_MATURITY_V2.ARCHIVED, new Set()],
|
|
487
|
+
]);
|
|
488
|
+
const _ccronPTerminal = new Set([CCRON_PROFILE_MATURITY_V2.ARCHIVED]);
|
|
489
|
+
const _ccronTTrans = new Map([
|
|
490
|
+
[CCRON_TICK_LIFECYCLE_V2.QUEUED, new Set([CCRON_TICK_LIFECYCLE_V2.RUNNING, CCRON_TICK_LIFECYCLE_V2.CANCELLED])],
|
|
491
|
+
[CCRON_TICK_LIFECYCLE_V2.RUNNING, new Set([CCRON_TICK_LIFECYCLE_V2.COMPLETED, CCRON_TICK_LIFECYCLE_V2.FAILED, CCRON_TICK_LIFECYCLE_V2.CANCELLED])],
|
|
492
|
+
[CCRON_TICK_LIFECYCLE_V2.COMPLETED, new Set()],
|
|
493
|
+
[CCRON_TICK_LIFECYCLE_V2.FAILED, new Set()],
|
|
494
|
+
[CCRON_TICK_LIFECYCLE_V2.CANCELLED, new Set()],
|
|
495
|
+
]);
|
|
496
|
+
const _ccronPsV2 = new Map();
|
|
497
|
+
const _ccronTsV2 = new Map();
|
|
498
|
+
let _ccronMaxActive = 6, _ccronMaxPending = 15, _ccronIdleMs = 30 * 24 * 60 * 60 * 1000, _ccronStuckMs = 60 * 1000;
|
|
499
|
+
function _ccronPos(n, label) { const v = Math.floor(Number(n)); if (!Number.isFinite(v) || v <= 0) throw new Error(`${label} must be positive integer`); return v; }
|
|
500
|
+
function _ccronCheckP(from, to) { const a = _ccronPTrans.get(from); if (!a || !a.has(to)) throw new Error(`invalid ccron profile transition ${from} → ${to}`); }
|
|
501
|
+
function _ccronCheckT(from, to) { const a = _ccronTTrans.get(from); if (!a || !a.has(to)) throw new Error(`invalid ccron tick transition ${from} → ${to}`); }
|
|
502
|
+
function _ccronCountActive(owner) { let c = 0; for (const p of _ccronPsV2.values()) if (p.owner === owner && p.status === CCRON_PROFILE_MATURITY_V2.ACTIVE) c++; return c; }
|
|
503
|
+
function _ccronCountPending(profileId) { let c = 0; for (const t of _ccronTsV2.values()) if (t.profileId === profileId && (t.status === CCRON_TICK_LIFECYCLE_V2.QUEUED || t.status === CCRON_TICK_LIFECYCLE_V2.RUNNING)) c++; return c; }
|
|
504
|
+
export function setMaxActiveCcronProfilesPerOwnerV2(n) { _ccronMaxActive = _ccronPos(n, "maxActiveCcronProfilesPerOwner"); }
|
|
505
|
+
export function getMaxActiveCcronProfilesPerOwnerV2() { return _ccronMaxActive; }
|
|
506
|
+
export function setMaxPendingCcronTicksPerProfileV2(n) { _ccronMaxPending = _ccronPos(n, "maxPendingCcronTicksPerProfile"); }
|
|
507
|
+
export function getMaxPendingCcronTicksPerProfileV2() { return _ccronMaxPending; }
|
|
508
|
+
export function setCcronProfileIdleMsV2(n) { _ccronIdleMs = _ccronPos(n, "ccronProfileIdleMs"); }
|
|
509
|
+
export function getCcronProfileIdleMsV2() { return _ccronIdleMs; }
|
|
510
|
+
export function setCcronTickStuckMsV2(n) { _ccronStuckMs = _ccronPos(n, "ccronTickStuckMs"); }
|
|
511
|
+
export function getCcronTickStuckMsV2() { return _ccronStuckMs; }
|
|
512
|
+
export function _resetStateCoworkCronV2() { _ccronPsV2.clear(); _ccronTsV2.clear(); _ccronMaxActive = 6; _ccronMaxPending = 15; _ccronIdleMs = 30 * 24 * 60 * 60 * 1000; _ccronStuckMs = 60 * 1000; }
|
|
513
|
+
export function registerCcronProfileV2({ id, owner, expr, metadata } = {}) {
|
|
514
|
+
if (!id || !owner) throw new Error("id and owner required");
|
|
515
|
+
if (_ccronPsV2.has(id)) throw new Error(`ccron profile ${id} already exists`);
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
const p = { id, owner, expr: expr || "0 0 * * *", status: CCRON_PROFILE_MATURITY_V2.PENDING, createdAt: now, updatedAt: now, lastTouchedAt: now, activatedAt: null, archivedAt: null, metadata: { ...(metadata || {}) } };
|
|
518
|
+
_ccronPsV2.set(id, p); return { ...p, metadata: { ...p.metadata } };
|
|
519
|
+
}
|
|
520
|
+
export function activateCcronProfileV2(id) {
|
|
521
|
+
const p = _ccronPsV2.get(id); if (!p) throw new Error(`ccron profile ${id} not found`);
|
|
522
|
+
const isInitial = p.status === CCRON_PROFILE_MATURITY_V2.PENDING;
|
|
523
|
+
_ccronCheckP(p.status, CCRON_PROFILE_MATURITY_V2.ACTIVE);
|
|
524
|
+
if (isInitial && _ccronCountActive(p.owner) >= _ccronMaxActive) throw new Error(`max active ccron profiles for owner ${p.owner} reached`);
|
|
525
|
+
const now = Date.now(); p.status = CCRON_PROFILE_MATURITY_V2.ACTIVE; p.updatedAt = now; p.lastTouchedAt = now;
|
|
526
|
+
if (!p.activatedAt) p.activatedAt = now;
|
|
527
|
+
return { ...p, metadata: { ...p.metadata } };
|
|
528
|
+
}
|
|
529
|
+
export function pauseCcronProfileV2(id) { const p = _ccronPsV2.get(id); if (!p) throw new Error(`ccron profile ${id} not found`); _ccronCheckP(p.status, CCRON_PROFILE_MATURITY_V2.PAUSED); p.status = CCRON_PROFILE_MATURITY_V2.PAUSED; p.updatedAt = Date.now(); return { ...p, metadata: { ...p.metadata } }; }
|
|
530
|
+
export function archiveCcronProfileV2(id) { const p = _ccronPsV2.get(id); if (!p) throw new Error(`ccron profile ${id} not found`); _ccronCheckP(p.status, CCRON_PROFILE_MATURITY_V2.ARCHIVED); const now = Date.now(); p.status = CCRON_PROFILE_MATURITY_V2.ARCHIVED; p.updatedAt = now; if (!p.archivedAt) p.archivedAt = now; return { ...p, metadata: { ...p.metadata } }; }
|
|
531
|
+
export function touchCcronProfileV2(id) { const p = _ccronPsV2.get(id); if (!p) throw new Error(`ccron profile ${id} not found`); if (_ccronPTerminal.has(p.status)) throw new Error(`cannot touch terminal ccron profile ${id}`); const now = Date.now(); p.lastTouchedAt = now; p.updatedAt = now; return { ...p, metadata: { ...p.metadata } }; }
|
|
532
|
+
export function getCcronProfileV2(id) { const p = _ccronPsV2.get(id); if (!p) return null; return { ...p, metadata: { ...p.metadata } }; }
|
|
533
|
+
export function listCcronProfilesV2() { return [..._ccronPsV2.values()].map((p) => ({ ...p, metadata: { ...p.metadata } })); }
|
|
534
|
+
export function createCcronTickV2({ id, profileId, tickAt, metadata } = {}) {
|
|
535
|
+
if (!id || !profileId) throw new Error("id and profileId required");
|
|
536
|
+
if (_ccronTsV2.has(id)) throw new Error(`ccron tick ${id} already exists`);
|
|
537
|
+
if (!_ccronPsV2.has(profileId)) throw new Error(`ccron profile ${profileId} not found`);
|
|
538
|
+
if (_ccronCountPending(profileId) >= _ccronMaxPending) throw new Error(`max pending ccron ticks for profile ${profileId} reached`);
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const t = { id, profileId, tickAt: tickAt || now, status: CCRON_TICK_LIFECYCLE_V2.QUEUED, createdAt: now, updatedAt: now, startedAt: null, settledAt: null, metadata: { ...(metadata || {}) } };
|
|
541
|
+
_ccronTsV2.set(id, t); return { ...t, metadata: { ...t.metadata } };
|
|
542
|
+
}
|
|
543
|
+
export function runningCcronTickV2(id) { const t = _ccronTsV2.get(id); if (!t) throw new Error(`ccron tick ${id} not found`); _ccronCheckT(t.status, CCRON_TICK_LIFECYCLE_V2.RUNNING); const now = Date.now(); t.status = CCRON_TICK_LIFECYCLE_V2.RUNNING; t.updatedAt = now; if (!t.startedAt) t.startedAt = now; return { ...t, metadata: { ...t.metadata } }; }
|
|
544
|
+
export function completeCcronTickV2(id) { const t = _ccronTsV2.get(id); if (!t) throw new Error(`ccron tick ${id} not found`); _ccronCheckT(t.status, CCRON_TICK_LIFECYCLE_V2.COMPLETED); const now = Date.now(); t.status = CCRON_TICK_LIFECYCLE_V2.COMPLETED; t.updatedAt = now; if (!t.settledAt) t.settledAt = now; return { ...t, metadata: { ...t.metadata } }; }
|
|
545
|
+
export function failCcronTickV2(id, reason) { const t = _ccronTsV2.get(id); if (!t) throw new Error(`ccron tick ${id} not found`); _ccronCheckT(t.status, CCRON_TICK_LIFECYCLE_V2.FAILED); const now = Date.now(); t.status = CCRON_TICK_LIFECYCLE_V2.FAILED; t.updatedAt = now; if (!t.settledAt) t.settledAt = now; if (reason) t.metadata.failReason = String(reason); return { ...t, metadata: { ...t.metadata } }; }
|
|
546
|
+
export function cancelCcronTickV2(id, reason) { const t = _ccronTsV2.get(id); if (!t) throw new Error(`ccron tick ${id} not found`); _ccronCheckT(t.status, CCRON_TICK_LIFECYCLE_V2.CANCELLED); const now = Date.now(); t.status = CCRON_TICK_LIFECYCLE_V2.CANCELLED; t.updatedAt = now; if (!t.settledAt) t.settledAt = now; if (reason) t.metadata.cancelReason = String(reason); return { ...t, metadata: { ...t.metadata } }; }
|
|
547
|
+
export function getCcronTickV2(id) { const t = _ccronTsV2.get(id); if (!t) return null; return { ...t, metadata: { ...t.metadata } }; }
|
|
548
|
+
export function listCcronTicksV2() { return [..._ccronTsV2.values()].map((t) => ({ ...t, metadata: { ...t.metadata } })); }
|
|
549
|
+
export function autoPauseIdleCcronProfilesV2({ now } = {}) { const t = now ?? Date.now(); const flipped = []; for (const p of _ccronPsV2.values()) if (p.status === CCRON_PROFILE_MATURITY_V2.ACTIVE && (t - p.lastTouchedAt) >= _ccronIdleMs) { p.status = CCRON_PROFILE_MATURITY_V2.PAUSED; p.updatedAt = t; flipped.push(p.id); } return { flipped, count: flipped.length }; }
|
|
550
|
+
export function autoFailStuckCcronTicksV2({ now } = {}) { const t = now ?? Date.now(); const flipped = []; for (const x of _ccronTsV2.values()) if (x.status === CCRON_TICK_LIFECYCLE_V2.RUNNING && x.startedAt != null && (t - x.startedAt) >= _ccronStuckMs) { x.status = CCRON_TICK_LIFECYCLE_V2.FAILED; x.updatedAt = t; if (!x.settledAt) x.settledAt = t; x.metadata.failReason = "auto-fail-stuck"; flipped.push(x.id); } return { flipped, count: flipped.length }; }
|
|
551
|
+
export function getCoworkCronGovStatsV2() {
|
|
552
|
+
const profilesByStatus = {}; for (const v of Object.values(CCRON_PROFILE_MATURITY_V2)) profilesByStatus[v] = 0; for (const p of _ccronPsV2.values()) profilesByStatus[p.status]++;
|
|
553
|
+
const ticksByStatus = {}; for (const v of Object.values(CCRON_TICK_LIFECYCLE_V2)) ticksByStatus[v] = 0; for (const t of _ccronTsV2.values()) ticksByStatus[t.status]++;
|
|
554
|
+
return { totalCcronProfilesV2: _ccronPsV2.size, totalCcronTicksV2: _ccronTsV2.size, maxActiveCcronProfilesPerOwner: _ccronMaxActive, maxPendingCcronTicksPerProfile: _ccronMaxPending, ccronProfileIdleMs: _ccronIdleMs, ccronTickStuckMs: _ccronStuckMs, profilesByStatus, ticksByStatus };
|
|
555
|
+
}
|