@agenticprimitives/fabric 0.0.0-alpha.1 → 0.0.0-alpha.11
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/CLAUDE.md +26 -30
- package/README.md +16 -16
- package/dist/case-emissions.d.ts +95 -0
- package/dist/case-emissions.d.ts.map +1 -0
- package/dist/case-emissions.js +208 -0
- package/dist/case-emissions.js.map +1 -0
- package/dist/case-machine.d.ts +28 -2
- package/dist/case-machine.d.ts.map +1 -1
- package/dist/case-machine.js +34 -7
- package/dist/case-machine.js.map +1 -1
- package/dist/cloudflare/authority-verdict-cache.d.ts +28 -1
- package/dist/cloudflare/authority-verdict-cache.d.ts.map +1 -1
- package/dist/cloudflare/authority-verdict-cache.js +32 -1
- package/dist/cloudflare/authority-verdict-cache.js.map +1 -1
- package/dist/cloudflare/index.d.ts +4 -1
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +7 -1
- package/dist/cloudflare/index.js.map +1 -1
- package/dist/cloudflare/interaction-store.d.ts +55 -0
- package/dist/cloudflare/interaction-store.d.ts.map +1 -0
- package/dist/cloudflare/interaction-store.js +230 -0
- package/dist/cloudflare/interaction-store.js.map +1 -0
- package/dist/cloudflare/principal-gateway-do.d.ts +136 -2
- package/dist/cloudflare/principal-gateway-do.d.ts.map +1 -1
- package/dist/cloudflare/principal-gateway-do.js +190 -8
- package/dist/cloudflare/principal-gateway-do.js.map +1 -1
- package/dist/cloudflare/vault-exchange-store.d.ts +47 -0
- package/dist/cloudflare/vault-exchange-store.d.ts.map +1 -0
- package/dist/cloudflare/vault-exchange-store.js +118 -0
- package/dist/cloudflare/vault-exchange-store.js.map +1 -0
- package/dist/cloudflare/vault-interaction-store.d.ts +44 -0
- package/dist/cloudflare/vault-interaction-store.d.ts.map +1 -0
- package/dist/cloudflare/vault-interaction-store.js +245 -0
- package/dist/cloudflare/vault-interaction-store.js.map +1 -0
- package/dist/communicative-act.d.ts +24 -1
- package/dist/communicative-act.d.ts.map +1 -1
- package/dist/communicative-act.js +28 -0
- package/dist/communicative-act.js.map +1 -1
- package/dist/delivery-envelope.d.ts.map +1 -1
- package/dist/delivery-envelope.js +0 -1
- package/dist/delivery-envelope.js.map +1 -1
- package/dist/enum-retirement.d.ts +43 -0
- package/dist/enum-retirement.d.ts.map +1 -0
- package/dist/enum-retirement.js +93 -0
- package/dist/enum-retirement.js.map +1 -0
- package/dist/exchange-record.d.ts +35 -4
- package/dist/exchange-record.d.ts.map +1 -1
- package/dist/exchange-record.js.map +1 -1
- package/dist/exchange-reducer.d.ts +32 -4
- package/dist/exchange-reducer.d.ts.map +1 -1
- package/dist/exchange-reducer.js +75 -1
- package/dist/exchange-reducer.js.map +1 -1
- package/dist/exchange-state.d.ts +30 -8
- package/dist/exchange-state.d.ts.map +1 -1
- package/dist/exchange-state.js +12 -10
- package/dist/exchange-state.js.map +1 -1
- package/dist/inbox-doc.d.ts +2 -2
- package/dist/inbox-doc.d.ts.map +1 -1
- package/dist/inbox-doc.js.map +1 -1
- package/dist/inbox-projection.d.ts +2 -2
- package/dist/inbox-projection.d.ts.map +1 -1
- package/dist/inbox-projection.js.map +1 -1
- package/dist/inbox-revision.d.ts +42 -0
- package/dist/inbox-revision.d.ts.map +1 -0
- package/dist/inbox-revision.js +57 -0
- package/dist/inbox-revision.js.map +1 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/interaction-machines.d.ts +136 -0
- package/dist/interaction-machines.d.ts.map +1 -0
- package/dist/interaction-machines.js +174 -0
- package/dist/interaction-machines.js.map +1 -0
- package/dist/interaction-profile.d.ts +126 -0
- package/dist/interaction-profile.d.ts.map +1 -0
- package/dist/interaction-profile.js +192 -0
- package/dist/interaction-profile.js.map +1 -0
- package/dist/interaction-record.d.ts +168 -0
- package/dist/interaction-record.d.ts.map +1 -0
- package/dist/interaction-record.js +108 -0
- package/dist/interaction-record.js.map +1 -0
- package/dist/interactions/machine.d.ts +2 -2
- package/dist/interactions/machine.d.ts.map +1 -1
- package/dist/interactions/machine.js +2 -2
- package/dist/interactions/machine.js.map +1 -1
- package/dist/interactions/open-case.d.ts +60 -0
- package/dist/interactions/open-case.d.ts.map +1 -0
- package/dist/interactions/open-case.js +83 -0
- package/dist/interactions/open-case.js.map +1 -0
- package/dist/interactions/store.d.ts +9 -0
- package/dist/interactions/store.d.ts.map +1 -1
- package/dist/interactions/store.js +57 -2
- package/dist/interactions/store.js.map +1 -1
- package/dist/messaging/board.d.ts +86 -6
- package/dist/messaging/board.d.ts.map +1 -1
- package/dist/messaging/board.js +100 -9
- package/dist/messaging/board.js.map +1 -1
- package/dist/messaging/consult-routing.d.ts +289 -0
- package/dist/messaging/consult-routing.d.ts.map +1 -0
- package/dist/messaging/consult-routing.js +455 -0
- package/dist/messaging/consult-routing.js.map +1 -0
- package/dist/messaging/consult.d.ts +158 -0
- package/dist/messaging/consult.d.ts.map +1 -0
- package/dist/messaging/consult.js +141 -0
- package/dist/messaging/consult.js.map +1 -0
- package/dist/messaging/conversation-interaction.d.ts +66 -0
- package/dist/messaging/conversation-interaction.d.ts.map +1 -0
- package/dist/messaging/conversation-interaction.js +139 -0
- package/dist/messaging/conversation-interaction.js.map +1 -0
- package/dist/messaging/delivery.d.ts +2 -2
- package/dist/messaging/delivery.d.ts.map +1 -1
- package/dist/messaging/delivery.js +8 -1
- package/dist/messaging/delivery.js.map +1 -1
- package/dist/messaging/directory.d.ts +16 -0
- package/dist/messaging/directory.d.ts.map +1 -1
- package/dist/messaging/directory.js.map +1 -1
- package/dist/messaging/envelope.d.ts +7 -3
- package/dist/messaging/envelope.d.ts.map +1 -1
- package/dist/messaging/envelope.js +18 -1
- package/dist/messaging/envelope.js.map +1 -1
- package/dist/messaging/hash.d.ts +8 -2
- package/dist/messaging/hash.d.ts.map +1 -1
- package/dist/messaging/hash.js +6 -0
- package/dist/messaging/hash.js.map +1 -1
- package/dist/messaging/inbox-assistant.d.ts +80 -0
- package/dist/messaging/inbox-assistant.d.ts.map +1 -0
- package/dist/messaging/inbox-assistant.js +125 -0
- package/dist/messaging/inbox-assistant.js.map +1 -0
- package/dist/messaging/index.d.ts +10 -3
- package/dist/messaging/index.d.ts.map +1 -1
- package/dist/messaging/index.js +20 -2
- package/dist/messaging/index.js.map +1 -1
- package/dist/messaging/outbound-message.d.ts +42 -0
- package/dist/messaging/outbound-message.d.ts.map +1 -0
- package/dist/messaging/outbound-message.js +64 -0
- package/dist/messaging/outbound-message.js.map +1 -0
- package/dist/messaging/projector.d.ts +3 -3
- package/dist/messaging/projector.d.ts.map +1 -1
- package/dist/messaging/projector.js.map +1 -1
- package/dist/messaging/receipts.d.ts +3 -3
- package/dist/messaging/receipts.d.ts.map +1 -1
- package/dist/messaging/receipts.js.map +1 -1
- package/dist/messaging/rules.d.ts +10 -3
- package/dist/messaging/rules.d.ts.map +1 -1
- package/dist/messaging/rules.js +8 -1
- package/dist/messaging/rules.js.map +1 -1
- package/dist/messaging/store.d.ts +4 -4
- package/dist/messaging/store.d.ts.map +1 -1
- package/dist/messaging/store.js +5 -3
- package/dist/messaging/store.js.map +1 -1
- package/dist/messaging/types.d.ts +52 -0
- package/dist/messaging/types.d.ts.map +1 -1
- package/dist/messaging/types.js +18 -1
- package/dist/messaging/types.js.map +1 -1
- package/dist/messaging/vault-store.d.ts +35 -2
- package/dist/messaging/vault-store.d.ts.map +1 -1
- package/dist/messaging/vault-store.js +53 -5
- package/dist/messaging/vault-store.js.map +1 -1
- package/dist/protocol-binding.d.ts +71 -0
- package/dist/protocol-binding.d.ts.map +1 -0
- package/dist/protocol-binding.js +0 -0
- package/dist/protocol-binding.js.map +1 -0
- package/dist/record-catalog.d.ts.map +1 -1
- package/dist/record-catalog.js +24 -0
- package/dist/record-catalog.js.map +1 -1
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../src/interactions/machine.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,mBAAmB,EAEnB,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../../src/interactions/machine.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,mBAAmB,EAEnB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAEpG,eAAO,MAAM,eAAe,EAAE,SAAS,gBAAgB,EAAyB,CAAC;AACjF,eAAO,MAAM,eAAe,4BAAsB,CAAC;AAEnD,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC;AAChD,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,4BAA4B,EACnC,GAAG,GAAE,qBAA0B,GAC9B,gBAAgB,CAclB"}
|
|
@@ -13,7 +13,7 @@ export const isTerminalState = isTerminalCaseState;
|
|
|
13
13
|
* interactions `interactionId` onto the case `caseId` and delegates to `applyCaseTransition`. Pure,
|
|
14
14
|
* fail-closed, role-checked — unchanged semantics for the surviving transitions.
|
|
15
15
|
*/
|
|
16
|
-
export function applyInteractionTransition(current, event) {
|
|
16
|
+
export function applyInteractionTransition(current, event, ctx = {}) {
|
|
17
17
|
return applyCaseTransition(current, {
|
|
18
18
|
version: 'ap.case.event.v1',
|
|
19
19
|
caseId: event.interactionId,
|
|
@@ -26,6 +26,6 @@ export function applyInteractionTransition(current, event) {
|
|
|
26
26
|
evidenceRef: event.evidenceRef,
|
|
27
27
|
authorityRef: event.authorityRef,
|
|
28
28
|
signature: event.signature,
|
|
29
|
-
});
|
|
29
|
+
}, ctx);
|
|
30
30
|
}
|
|
31
31
|
//# sourceMappingURL=machine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machine.js","sourceRoot":"","sources":["../../src/interactions/machine.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,2GAA2G;AAC3G,2GAA2G;AAC3G,sGAAsG;AACtG,EAAE;AACF,iGAAiG;AACjG,6EAA6E;AAE7E,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"machine.js","sourceRoot":"","sources":["../../src/interactions/machine.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,2GAA2G;AAC3G,2GAA2G;AAC3G,sGAAsG;AACtG,EAAE;AACF,iGAAiG;AACjG,6EAA6E;AAE7E,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GAGrB,MAAM,oBAAoB,CAAC;AAG5B,MAAM,CAAC,MAAM,eAAe,GAAgC,oBAAoB,CAAC;AACjF,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAOnD;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACxC,OAA0B,EAC1B,KAAmC,EACnC,MAA6B,EAAE;IAE/B,OAAO,mBAAmB,CAAC,OAAO,EAAE;QAClC,OAAO,EAAE,kBAAkB;QAC3B,MAAM,EAAE,KAAK,CAAC,aAAa;QAC3B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,SAAS,EAAE,KAAK,CAAC,SAAS;KAC3B,EAAE,GAAG,CAAC,CAAC;AACV,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CanonicalAgentId } from '@agenticprimitives/types';
|
|
2
|
+
import type { ContextRefV1, MessageId } from '../messaging/index.js';
|
|
3
|
+
import type { InteractionCaseV1, InteractionId, InteractionKind } from './types.js';
|
|
4
|
+
export interface OpenCaseInput {
|
|
5
|
+
/** The Interaction this case belongs to. DERIVED by the caller (e.g. `ixn_work_<workItemId>`), never
|
|
6
|
+
* minted — see `derivedCaseId` for why. */
|
|
7
|
+
interactionId: string;
|
|
8
|
+
/** Who is asking. The party the decision is about, verified by the recipient's gate before this runs. */
|
|
9
|
+
requester: CanonicalAgentId;
|
|
10
|
+
/** Who must answer. */
|
|
11
|
+
responder: CanonicalAgentId;
|
|
12
|
+
kind: InteractionKind;
|
|
13
|
+
/** Human-facing one-liner. What the responder is being asked, in their words not the system's. */
|
|
14
|
+
subject: string;
|
|
15
|
+
/** The message that opened it — the case's root in the exchange stream. */
|
|
16
|
+
rootMessageId: MessageId;
|
|
17
|
+
/** What this case is ABOUT (the work item, the artifact). About-ness, never authority. */
|
|
18
|
+
contextRefs?: ContextRefV1[];
|
|
19
|
+
at: string;
|
|
20
|
+
expiresAt?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The case id, DERIVED from what makes it the same request.
|
|
24
|
+
*
|
|
25
|
+
* NOT a uuid, and this is the whole of §N.7.2. A transition can be retried — a delivery re-attempt, a
|
|
26
|
+
* user pressing twice, an at-least-once queue — and a minted id would make every retry a NEW obligation
|
|
27
|
+
* sitting in someone's inbox. Deriving it means a retry finds the case that already exists.
|
|
28
|
+
*
|
|
29
|
+
* The tuple is (interaction, requester, responder, kind): the same party asking the same party for the
|
|
30
|
+
* same thing about the same interaction IS the same request. A different KIND is a different ask, so it
|
|
31
|
+
* gets its own case — an access request and an approval request about one work item are two questions.
|
|
32
|
+
*
|
|
33
|
+
* Precedent: W11 derives `ixn_topic_<channelId>` so "the two cannot drift and no join table exists to go
|
|
34
|
+
* stale", and `org.applications` keeps one entry per applicant so re-applying updates in place rather
|
|
35
|
+
* than flooding the queue.
|
|
36
|
+
*/
|
|
37
|
+
export declare function derivedCaseId(input: {
|
|
38
|
+
interactionId: string;
|
|
39
|
+
requester: CanonicalAgentId;
|
|
40
|
+
responder: CanonicalAgentId;
|
|
41
|
+
kind: InteractionKind;
|
|
42
|
+
}): InteractionId;
|
|
43
|
+
export type OpenCaseResult = {
|
|
44
|
+
ok: true;
|
|
45
|
+
case: InteractionCaseV1;
|
|
46
|
+
created: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: 'self-addressed' | 'missing-subject';
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Open a case, or return the one already open for this request.
|
|
53
|
+
*
|
|
54
|
+
* `created: false` is a SUCCESS, not a conflict — a retried transition is supposed to be a no-op, and
|
|
55
|
+
* reporting it as an error would push every caller into distinguishing "already asked" from "failed to
|
|
56
|
+
* ask". The existing case is returned unchanged: re-asking does not reset a decision already in progress,
|
|
57
|
+
* and must never revive one already answered.
|
|
58
|
+
*/
|
|
59
|
+
export declare function openCase(input: OpenCaseInput, existing?: InteractionCaseV1 | null): OpenCaseResult;
|
|
60
|
+
//# sourceMappingURL=open-case.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-case.d.ts","sourceRoot":"","sources":["../../src/interactions/open-case.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,aAAa;IAC5B;gDAC4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB,yGAAyG;IACzG,SAAS,EAAE,gBAAgB,CAAC;IAC5B,uBAAuB;IACvB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,kGAAkG;IAClG,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,aAAa,EAAE,SAAS,CAAC;IACzB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,IAAI,EAAE,eAAe,CAAC;CACvB,GAAG,aAAa,CAGhB;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAA;CAAE,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,IAAI,GAAG,cAAc,CAoClG"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// `openCase` — the CASE PRODUCER (spec 340 §N).
|
|
2
|
+
//
|
|
3
|
+
// THE GAP THIS FILLS. Everything downstream of a case was built and wired — the store, the reducer,
|
|
4
|
+
// `applyApproveWithMandate` with its four gates, the server action, the UI button — and nothing ever
|
|
5
|
+
// created one. `putCase` was called in exactly one place: `hydrate`, reading a stored document back. So
|
|
6
|
+
// `cases`/`cards`/`mandates` were structurally always empty and the governed-situation half of the model
|
|
7
|
+
// had never executed against a live deployment.
|
|
8
|
+
//
|
|
9
|
+
// WHAT THIS DOES NOT KNOW. It has no idea what a work item is, and it must not learn (§N.7.1). The
|
|
10
|
+
// coordination profile decides WHICH transitions open a case; this decides what a case IS. The boundary
|
|
11
|
+
// has a test: if this function ever needs a work-item field, the boundary has moved and the design is
|
|
12
|
+
// wrong. Callers pass a `subject` and `contextRefs` — about-ness, never authority.
|
|
13
|
+
//
|
|
14
|
+
// OPENING A CASE GRANTS NOTHING (§N.7.3, ADR-0041). It creates an obligation to ANSWER, not permission to
|
|
15
|
+
// act. `authorityRefs` starts empty and stays empty until a mandate is issued; the case points at
|
|
16
|
+
// authority and never becomes it. That is why this function takes no grant, no delegation and no
|
|
17
|
+
// credential — there is nothing it could legitimately do with one.
|
|
18
|
+
/**
|
|
19
|
+
* The case id, DERIVED from what makes it the same request.
|
|
20
|
+
*
|
|
21
|
+
* NOT a uuid, and this is the whole of §N.7.2. A transition can be retried — a delivery re-attempt, a
|
|
22
|
+
* user pressing twice, an at-least-once queue — and a minted id would make every retry a NEW obligation
|
|
23
|
+
* sitting in someone's inbox. Deriving it means a retry finds the case that already exists.
|
|
24
|
+
*
|
|
25
|
+
* The tuple is (interaction, requester, responder, kind): the same party asking the same party for the
|
|
26
|
+
* same thing about the same interaction IS the same request. A different KIND is a different ask, so it
|
|
27
|
+
* gets its own case — an access request and an approval request about one work item are two questions.
|
|
28
|
+
*
|
|
29
|
+
* Precedent: W11 derives `ixn_topic_<channelId>` so "the two cannot drift and no join table exists to go
|
|
30
|
+
* stale", and `org.applications` keeps one entry per applicant so re-applying updates in place rather
|
|
31
|
+
* than flooding the queue.
|
|
32
|
+
*/
|
|
33
|
+
export function derivedCaseId(input) {
|
|
34
|
+
const part = (s) => String(s).toLowerCase().replace(/[^a-z0-9:_.-]/g, '');
|
|
35
|
+
return `${part(input.interactionId)}:${input.kind}:${part(input.requester)}:${part(input.responder)}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Open a case, or return the one already open for this request.
|
|
39
|
+
*
|
|
40
|
+
* `created: false` is a SUCCESS, not a conflict — a retried transition is supposed to be a no-op, and
|
|
41
|
+
* reporting it as an error would push every caller into distinguishing "already asked" from "failed to
|
|
42
|
+
* ask". The existing case is returned unchanged: re-asking does not reset a decision already in progress,
|
|
43
|
+
* and must never revive one already answered.
|
|
44
|
+
*/
|
|
45
|
+
export function openCase(input, existing) {
|
|
46
|
+
if (!input.subject.trim())
|
|
47
|
+
return { ok: false, reason: 'missing-subject' };
|
|
48
|
+
// A case is a question addressed to somebody else. Asking yourself for a decision would create an
|
|
49
|
+
// obligation with no counterparty, and it is the shape a mis-wired emitter produces — worth refusing
|
|
50
|
+
// loudly rather than filling an inbox with self-addressed requests.
|
|
51
|
+
if (input.requester.toLowerCase() === input.responder.toLowerCase())
|
|
52
|
+
return { ok: false, reason: 'self-addressed' };
|
|
53
|
+
if (existing)
|
|
54
|
+
return { ok: true, case: existing, created: false };
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
created: true,
|
|
58
|
+
case: {
|
|
59
|
+
version: 'ap.interaction.v1',
|
|
60
|
+
id: derivedCaseId(input),
|
|
61
|
+
kind: input.kind,
|
|
62
|
+
subject: input.subject.trim(),
|
|
63
|
+
requester: input.requester,
|
|
64
|
+
responder: input.responder,
|
|
65
|
+
// SUBMITTED, not 'draft' and not an invented 'requested'. W10b made `CaseState` execution-only
|
|
66
|
+
// (11 → 8), and a case that has been asked but not yet triaged is exactly `submitted` — 'draft'
|
|
67
|
+
// would mean the requester had not sent it. Reusing the existing vocabulary rather than widening
|
|
68
|
+
// it is the point: the enum retirement (§R) exists so states are not added per feature.
|
|
69
|
+
state: 'submitted',
|
|
70
|
+
createdAt: input.at,
|
|
71
|
+
updatedAt: input.at,
|
|
72
|
+
rootMessageId: input.rootMessageId,
|
|
73
|
+
latestMessageId: input.rootMessageId,
|
|
74
|
+
evidenceRefs: [],
|
|
75
|
+
// EMPTY, AND IT STAYS EMPTY UNTIL A MANDATE EXISTS. The case points at authority; it never carries
|
|
76
|
+
// it (ADR-0041). A producer that seeded this would make opening a case a way to acquire permission.
|
|
77
|
+
authorityRefs: [],
|
|
78
|
+
...(input.contextRefs?.length ? { contextRefs: input.contextRefs } : {}),
|
|
79
|
+
...(input.expiresAt ? { expiresAt: input.expiresAt } : {}),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=open-case.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-case.js","sourceRoot":"","sources":["../../src/interactions/open-case.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,wGAAwG;AACxG,yGAAyG;AACzG,gDAAgD;AAChD,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,sGAAsG;AACtG,mFAAmF;AACnF,EAAE;AACF,0GAA0G;AAC1G,kGAAkG;AAClG,iGAAiG;AACjG,mEAAmE;AAyBnE;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAAC,KAK7B;IACC,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IAC1F,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAmB,CAAC;AACzH,CAAC;AAMD;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAoB,EAAE,QAAmC;IAChF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC3E,kGAAkG;IAClG,qGAAqG;IACrG,oEAAoE;IACpE,IAAI,KAAK,CAAC,SAAS,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,SAAS,CAAC,WAAW,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAEpH,IAAI,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAElE,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO,EAAE,IAAI;QACb,IAAI,EAAE;YACJ,OAAO,EAAE,mBAAmB;YAC5B,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC;YACxB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE;YAC7B,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,+FAA+F;YAC/F,gGAAgG;YAChG,iGAAiG;YACjG,wFAAwF;YACxF,KAAK,EAAE,WAAW;YAClB,SAAS,EAAE,KAAK,CAAC,EAAE;YACnB,SAAS,EAAE,KAAK,CAAC,EAAE;YACnB,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,eAAe,EAAE,KAAK,CAAC,aAAa;YACpC,YAAY,EAAE,EAAE;YAChB,mGAAmG;YACnG,oGAAoG;YACpG,aAAa,EAAE,EAAE;YACjB,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { type TransitionResult } from './machine.js';
|
|
2
|
+
import type { CommitmentRecordV1, DecisionRecordV1 } from '../interaction-machines.js';
|
|
2
3
|
import type { InteractionCaseV1, InteractionId, InteractionState, InteractionTransitionEventV1 } from './types.js';
|
|
3
4
|
export interface InteractionStore {
|
|
4
5
|
/** Register a new case. Fail-closed: only `draft` cases enter; duplicate ids throw. */
|
|
5
6
|
putCase(c: InteractionCaseV1): void;
|
|
6
7
|
getCase(id: InteractionId): InteractionCaseV1 | null;
|
|
8
|
+
/**
|
|
9
|
+
* The determination this case produced, if any (spec 340 W10). `null` until a transition emits one —
|
|
10
|
+
* `approve`/`deny`/`expire` are the three that do. Separate from the case's own state on purpose:
|
|
11
|
+
* a decision has its own lifecycle, its own signer, and outlives the transition that recorded it.
|
|
12
|
+
*/
|
|
13
|
+
getDecision(id: InteractionId): DecisionRecordV1 | null;
|
|
14
|
+
/** The obligation this case gave rise to, if any. `null` until `issue-credential` creates one. */
|
|
15
|
+
getCommitment(id: InteractionId): CommitmentRecordV1 | null;
|
|
7
16
|
/**
|
|
8
17
|
* Apply a signed transition event. Idempotent: replaying an event whose
|
|
9
18
|
* `idempotencyKey` was already applied returns the current case unchanged
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/interactions/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/interactions/store.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAElG,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC;AAEnH,MAAM,WAAW,gBAAgB;IAC/B,uFAAuF;IACvF,OAAO,CAAC,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,EAAE,aAAa,GAAG,iBAAiB,GAAG,IAAI,CAAC;IACrD;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;IACxD,kGAAkG;IAClG,aAAa,CAAC,EAAE,EAAE,aAAa,GAAG,kBAAkB,GAAG,IAAI,CAAC;IAC5D;;;;OAIG;IACH,UAAU,CAAC,KAAK,EAAE,4BAA4B,GAAG,gBAAgB,CAAC;IAClE,UAAU,CAAC,EAAE,EAAE,aAAa,GAAG,4BAA4B,EAAE,CAAC;IAC9D,SAAS,CAAC,KAAK,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,EAAE,CAAC;IACzD;iEAC6D;IAC7D,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAAC;CACjD;AAED,wBAAgB,8BAA8B,IAAI,gBAAgB,CAyHjE"}
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
// In-memory InteractionStore: cases + transition log with idempotent replay
|
|
2
2
|
// and deterministic expiry sweeps (spec 309 §6.1, §8.10).
|
|
3
3
|
import { applyInteractionTransition, isTerminalState } from './machine.js';
|
|
4
|
+
import { applyCaseEmissions, caseTransitionEmissions } from '../case-emissions.js';
|
|
4
5
|
export function createInMemoryInteractionStore() {
|
|
5
6
|
const cases = new Map();
|
|
6
7
|
const events = new Map();
|
|
7
8
|
const appliedKeys = new Map();
|
|
9
|
+
const decisions = new Map();
|
|
10
|
+
const commitments = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* spec 340 W10 — record what the transition emitted into the other two machines.
|
|
13
|
+
*
|
|
14
|
+
* Called only after the case machine ACCEPTED, and derived from the PRE-transition case: the seed
|
|
15
|
+
* reads identity and party fields, none of which a transition changes.
|
|
16
|
+
*
|
|
17
|
+
* A rejected emission does not roll back the case. That is deliberate and it is the honest option:
|
|
18
|
+
* the case transition is already a fact, and the alternatives are worse — silently dropping it would
|
|
19
|
+
* make an unrecorded determination indistinguishable from a recorded one, and unwinding an accepted
|
|
20
|
+
* transition would let the decision machine veto a move the case machine's own rules permitted.
|
|
21
|
+
* `void` on a case that never had a commitment is the ordinary, expected case of this.
|
|
22
|
+
*/
|
|
23
|
+
function emit(before, event) {
|
|
24
|
+
const emissions = caseTransitionEmissions(before, {
|
|
25
|
+
version: 'ap.case.event.v1',
|
|
26
|
+
caseId: event.interactionId,
|
|
27
|
+
transition: event.transition,
|
|
28
|
+
actor: event.actor,
|
|
29
|
+
at: event.at,
|
|
30
|
+
});
|
|
31
|
+
if (!emissions.decision && !emissions.commitment)
|
|
32
|
+
return;
|
|
33
|
+
const out = applyCaseEmissions(emissions, {
|
|
34
|
+
decision: decisions.get(event.interactionId) ?? null,
|
|
35
|
+
commitment: commitments.get(event.interactionId) ?? null,
|
|
36
|
+
}, { actor: event.actor, at: event.at });
|
|
37
|
+
if (out.decision?.applied)
|
|
38
|
+
decisions.set(event.interactionId, out.decision.record);
|
|
39
|
+
if (out.commitment?.applied)
|
|
40
|
+
commitments.set(event.interactionId, out.commitment.record);
|
|
41
|
+
}
|
|
8
42
|
return {
|
|
9
43
|
putCase(c) {
|
|
10
44
|
if (c.state !== 'draft') {
|
|
@@ -27,15 +61,26 @@ export function createInMemoryInteractionStore() {
|
|
|
27
61
|
if (event.idempotencyKey !== undefined && keys.has(event.idempotencyKey)) {
|
|
28
62
|
return { ok: true, case: current };
|
|
29
63
|
}
|
|
30
|
-
|
|
64
|
+
// W10b-3: the case machine cannot see the decision machine, so the store — which holds both —
|
|
65
|
+
// supplies it. `issue-credential` refuses without a granted determination.
|
|
66
|
+
const result = applyInteractionTransition(current, event, {
|
|
67
|
+
decisionState: decisions.get(event.interactionId)?.state ?? null,
|
|
68
|
+
});
|
|
31
69
|
if (!result.ok)
|
|
32
70
|
return result;
|
|
33
71
|
cases.set(event.interactionId, result.case);
|
|
34
72
|
events.get(event.interactionId).push(event);
|
|
35
73
|
if (event.idempotencyKey !== undefined)
|
|
36
74
|
keys.add(event.idempotencyKey);
|
|
75
|
+
emit(current, event);
|
|
37
76
|
return result;
|
|
38
77
|
},
|
|
78
|
+
getDecision(id) {
|
|
79
|
+
return decisions.get(id) ?? null;
|
|
80
|
+
},
|
|
81
|
+
getCommitment(id) {
|
|
82
|
+
return commitments.get(id) ?? null;
|
|
83
|
+
},
|
|
39
84
|
listEvents(id) {
|
|
40
85
|
return [...(events.get(id) ?? [])];
|
|
41
86
|
},
|
|
@@ -55,10 +100,20 @@ export function createInMemoryInteractionStore() {
|
|
|
55
100
|
transition: 'expire',
|
|
56
101
|
actor: c.responder, // role 'any' — deterministic sweep, not a user action
|
|
57
102
|
at: nowIso,
|
|
58
|
-
});
|
|
103
|
+
}, { decisionState: decisions.get(c.id)?.state ?? null });
|
|
59
104
|
if (result.ok) {
|
|
60
105
|
cases.set(c.id, result.case);
|
|
61
106
|
expired.push(result.case);
|
|
107
|
+
// The sweep records the determination too. `expire` is the one decision transition the
|
|
108
|
+
// machine lets any actor make, and the emission attributes it to the creditor rather than
|
|
109
|
+
// to `c.responder` — the sweep's stand-in actor, who decided nothing.
|
|
110
|
+
emit(c, {
|
|
111
|
+
version: 'ap.interaction.event.v1',
|
|
112
|
+
interactionId: c.id,
|
|
113
|
+
transition: 'expire',
|
|
114
|
+
actor: c.responder,
|
|
115
|
+
at: nowIso,
|
|
116
|
+
});
|
|
62
117
|
}
|
|
63
118
|
}
|
|
64
119
|
return expired;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/interactions/store.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,0DAA0D;AAE1D,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAyB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/interactions/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,23 +1,95 @@
|
|
|
1
1
|
import type { CanonicalAgentId } from '@agenticprimitives/types';
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyMessageEnvelope, ContextRefV1, ConversationDescriptorV1 } from './types.js';
|
|
3
|
+
import { type ConsultProvenanceV1 } from './consult.js';
|
|
3
4
|
export interface ChannelMessageEntryV1 {
|
|
4
|
-
envelope:
|
|
5
|
+
envelope: AnyMessageEnvelope;
|
|
5
6
|
authorName: string;
|
|
7
|
+
/** spec 324 §9 acting-agent provenance (spec 327): WHO performed the authoring move when it was
|
|
8
|
+
* not a human session — for org-assistant posts, the org SA (CAIP-10). Absent ⇒ human-session
|
|
9
|
+
* post. Never erased; the UI renders an "agent" badge from it. */
|
|
10
|
+
actor?: CanonicalAgentId;
|
|
11
|
+
/** spec 329 §6 — PROV attribution on a turn-2 synthesis post (consulted agents + the contributing
|
|
12
|
+
* ConsultationIntent record ids). Absent on every non-synthesis post. */
|
|
13
|
+
prov?: ConsultProvenanceV1;
|
|
14
|
+
}
|
|
15
|
+
/** Steward-controlled org-assistant enablement for one topic (spec 327; 318 §8.1 slice — the org's
|
|
16
|
+
* OWN agent, not a participant bot). Handle + display name are captured AT ENABLE TIME so trigger
|
|
17
|
+
* matching and attribution need no per-post chain read (ADR-0012); re-enabling refreshes both
|
|
18
|
+
* after a name rotation. */
|
|
19
|
+
export interface TopicAssistantV1 {
|
|
20
|
+
/** 'mention' — reply only when a post @-mentions the org. 'all' — reply to every member post. */
|
|
21
|
+
trigger: 'mention' | 'all';
|
|
22
|
+
/** Lowercased mention label, e.g. 'grace' for grace.impact — matched as @grace / @grace.impact.
|
|
23
|
+
* Empty ONLY for trigger:'all' (a nameless org cannot be mentioned). */
|
|
24
|
+
mentionHandle: string;
|
|
25
|
+
/** Author display name stamped on assistant posts (org primary name / profile display name). */
|
|
26
|
+
displayName: string;
|
|
27
|
+
enabledBy: string;
|
|
28
|
+
enabledAt: string;
|
|
29
|
+
}
|
|
30
|
+
/** Steward-controlled member ROUTING enablement for one topic (spec 329 §7): the org's assistant
|
|
31
|
+
* may consult opted-in member agents in this topic. Requires the assistant ON (routing is a mode
|
|
32
|
+
* of the 327 assistant, never a second bot); the org-level consult wire (§3.1) is custodied
|
|
33
|
+
* separately at the org's execution point — flag without wire ⇒ routing stays inert. */
|
|
34
|
+
export interface TopicRoutingV1 {
|
|
35
|
+
/** Fan-out K per routed question (1..5, default 3 — spec 329 §4). */
|
|
36
|
+
maxFanout: number;
|
|
37
|
+
enabledBy: string;
|
|
38
|
+
enabledAt: string;
|
|
6
39
|
}
|
|
7
40
|
export interface ChannelV1 {
|
|
8
41
|
descriptor: ConversationDescriptorV1;
|
|
9
42
|
title: string;
|
|
10
43
|
createdBy: string;
|
|
11
44
|
messages: ChannelMessageEntryV1[];
|
|
45
|
+
/** apmsg:participationPolicy — `open` (every org MEMBER automatically participates; derived, never a
|
|
46
|
+
* stored list) or `restricted` (invite-only; `participants` is the projection of accepted
|
|
47
|
+
* DiscussionParticipation situations). Absent ⇒ resolved from legacy `visibility` (public→open,
|
|
48
|
+
* private→restricted); absent both ⇒ open. */
|
|
49
|
+
participationPolicy?: 'open' | 'restricted';
|
|
12
50
|
visibility?: 'public' | 'private';
|
|
13
|
-
/** For a
|
|
51
|
+
/** For a restricted topic: the PARTICIPANT SAs (lowercased 0x) allowed to see/post — a projection of
|
|
52
|
+
* accepted participations (the creator is always included). Legacy field name `members` kept on the
|
|
53
|
+
* wire; these are discussion PARTICIPANTS, not the org member roster. */
|
|
14
54
|
members?: string[];
|
|
55
|
+
/** Present ⇔ the org's own agent participates in this topic (spec 327). */
|
|
56
|
+
assistant?: TopicAssistantV1;
|
|
57
|
+
/** Present ⇔ member routing is steward-enabled on this topic (spec 329; requires `assistant`). */
|
|
58
|
+
routing?: TopicRoutingV1;
|
|
15
59
|
}
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
60
|
+
/** Resolve a topic's participation policy, mapping the legacy `visibility` wire field
|
|
61
|
+
* (public→open, private→restricted). Absent both ⇒ open (the default + all legacy topics). */
|
|
62
|
+
export declare function channelParticipationPolicy(channel: Pick<ChannelV1, 'participationPolicy' | 'visibility'>): 'open' | 'restricted';
|
|
63
|
+
/** Can `viewerSa` (0x) see this topic? OPEN ⇒ always (every org member; the caller already gated org
|
|
64
|
+
* membership). RESTRICTED ⇒ only listed participants. `isSteward` overrides (a steward/custodian is a
|
|
65
|
+
* facilitator of every topic). One mechanism — used by the store list + the UI. */
|
|
18
66
|
export declare function canSeeChannel(channel: ChannelV1, viewerSa: string, isSteward?: boolean): boolean;
|
|
19
67
|
/** Bounded per-channel history in the board doc (projection only — bodies live in the vault). */
|
|
20
68
|
export declare const BOARD_HISTORY_LIMIT = 200;
|
|
69
|
+
/** Should a just-committed post trigger the topic's org assistant (spec 327 §3)? Pure predicate —
|
|
70
|
+
* the caller supplies the posted body text (it never round-trips through the stored entry, whose
|
|
71
|
+
* body is hash-bound in the vault). False when the assistant is off, when the post is itself
|
|
72
|
+
* agent-authored (`actor` set), or when the author IS the org — the community contextRef, NOT
|
|
73
|
+
* `descriptor.owner` (that is the topic CREATOR) — so the assistant NEVER triggers itself, even
|
|
74
|
+
* in `trigger:'all'` topics. */
|
|
75
|
+
export declare function assistantTrigger(channel: Pick<ChannelV1, 'assistant' | 'descriptor'>, post: {
|
|
76
|
+
from: CanonicalAgentId;
|
|
77
|
+
actor?: CanonicalAgentId;
|
|
78
|
+
bodyText: string;
|
|
79
|
+
}): boolean;
|
|
80
|
+
/** Enable/refresh (`assistant` set) or disable (`assistant` undefined) the org assistant on a topic
|
|
81
|
+
* (mutates the channel on success). Steward gating + audit are the caller's, as with every board
|
|
82
|
+
* mutation. `trigger:'mention'` requires a mention handle — a nameless org cannot be mentioned. */
|
|
83
|
+
export declare function setTopicAssistant(board: ChannelV1[], channelId: string, assistant: TopicAssistantV1 | undefined): BoardResult<{
|
|
84
|
+
channel: ChannelV1;
|
|
85
|
+
}>;
|
|
86
|
+
/** Enable/refresh (`routing` set) or disable (`routing` undefined) member routing on a topic
|
|
87
|
+
* (spec 329 §7). Enabling REQUIRES the 327 assistant to be on (routing is a mode of the
|
|
88
|
+
* assistant's turn); fan-out is clamped fail-closed to the approved 1..5 window. Disabling the
|
|
89
|
+
* ASSISTANT also disables routing transitively (setTopicAssistant clears it below). */
|
|
90
|
+
export declare function setTopicRouting(board: ChannelV1[], channelId: string, routing: TopicRoutingV1 | undefined): BoardResult<{
|
|
91
|
+
channel: ChannelV1;
|
|
92
|
+
}>;
|
|
21
93
|
export type BoardResult<T> = {
|
|
22
94
|
ok: true;
|
|
23
95
|
} & T | {
|
|
@@ -33,6 +105,7 @@ export declare function createBoardChannel(board: ChannelV1[], opts: {
|
|
|
33
105
|
title: string;
|
|
34
106
|
createdBy: string;
|
|
35
107
|
now?: string;
|
|
108
|
+
participationPolicy?: 'open' | 'restricted';
|
|
36
109
|
visibility?: 'public' | 'private';
|
|
37
110
|
members?: string[];
|
|
38
111
|
creatorSa?: string;
|
|
@@ -48,7 +121,14 @@ export declare function appendBoardPost(board: ChannelV1[], opts: {
|
|
|
48
121
|
authorName: string;
|
|
49
122
|
bodyText: string;
|
|
50
123
|
now?: string;
|
|
124
|
+
actor?: CanonicalAgentId;
|
|
125
|
+
/** spec 329 §6 — EXTRA context refs beyond the channel's own (the `routed-consultation` chip on
|
|
126
|
+
* the turn-1 status + turn-2 synthesis). Appended after the descriptor refs, deduped by
|
|
127
|
+
* kind+id; the envelope validator's MAX_CONTEXT_REFS bound still applies (fail-closed). */
|
|
128
|
+
contextRefs?: ContextRefV1[];
|
|
129
|
+
/** spec 329 §6 — PROV attribution stored on the entry (synthesis posts only). */
|
|
130
|
+
prov?: ConsultProvenanceV1;
|
|
51
131
|
}): Promise<BoardResult<{
|
|
52
|
-
envelope:
|
|
132
|
+
envelope: AnyMessageEnvelope;
|
|
53
133
|
}>>;
|
|
54
134
|
//# sourceMappingURL=board.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/messaging/board.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKjE,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/messaging/board.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,wBAAwB,EAAqB,MAAM,YAAY,CAAC;AAChH,OAAO,EAAkE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB;;uEAEmE;IACnE,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;8EAC0E;IAC1E,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC5B;AAED;;;6BAG6B;AAC7B,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,OAAO,EAAE,SAAS,GAAG,KAAK,CAAC;IAC3B;6EACyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;yFAGyF;AACzF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,wBAAwB,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAClC;;;mDAG+C;IAC/C,mBAAmB,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAG5C,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC;;8EAE0E;IAC1E,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,kGAAkG;IAClG,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED;+FAC+F;AAC/F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,qBAAqB,GAAG,YAAY,CAAC,GAAG,MAAM,GAAG,YAAY,CAGhI;AAED;;oFAEoF;AACpF,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAK9F;AAED,iGAAiG;AACjG,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;iCAKiC;AACjC,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,GAAG,YAAY,CAAC,EACpD,IAAI,EAAE;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC3E,OAAO,CAmBT;AAID;;oGAEoG;AACpG,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAAE,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,gBAAgB,GAAG,SAAS,GACtC,WAAW,CAAC;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC,CAgBrC;AAED;;;wFAGwF;AACxF,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GAAG,SAAS,GAClC,WAAW,CAAC;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC,CAcrC;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,CAAC,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E;;6FAE6F;AAC7F,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAAC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3N,WAAW,CAAC;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC,CAkCrC;AAED;;uFAEuF;AACvF,wBAAsB,eAAe,CACnC,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE;IACJ,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACxH;;gGAE4F;IAC5F,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,iFAAiF;IACjF,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC5B,GACA,OAAO,CAAC,WAAW,CAAC;IAAE,QAAQ,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAC,CAmCxD"}
|
package/dist/messaging/board.js
CHANGED
|
@@ -2,10 +2,19 @@ import { generateConversationId, generateMessageId, validateMessageEnvelope } fr
|
|
|
2
2
|
import { sha256Hex32 } from './hash.js';
|
|
3
3
|
import { validateConversationDescriptor } from './conversation.js';
|
|
4
4
|
import { topicBodyResource } from './vault-store.js';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { ROUTING_FANOUT_DEFAULT, ROUTING_FANOUT_MAX, ROUTING_FANOUT_MIN } from './consult.js';
|
|
6
|
+
/** Resolve a topic's participation policy, mapping the legacy `visibility` wire field
|
|
7
|
+
* (public→open, private→restricted). Absent both ⇒ open (the default + all legacy topics). */
|
|
8
|
+
export function channelParticipationPolicy(channel) {
|
|
9
|
+
if (channel.participationPolicy)
|
|
10
|
+
return channel.participationPolicy;
|
|
11
|
+
return channel.visibility === 'private' ? 'restricted' : 'open';
|
|
12
|
+
}
|
|
13
|
+
/** Can `viewerSa` (0x) see this topic? OPEN ⇒ always (every org member; the caller already gated org
|
|
14
|
+
* membership). RESTRICTED ⇒ only listed participants. `isSteward` overrides (a steward/custodian is a
|
|
15
|
+
* facilitator of every topic). One mechanism — used by the store list + the UI. */
|
|
7
16
|
export function canSeeChannel(channel, viewerSa, isSteward = false) {
|
|
8
|
-
if (channel
|
|
17
|
+
if (channelParticipationPolicy(channel) === 'open')
|
|
9
18
|
return true;
|
|
10
19
|
if (isSteward)
|
|
11
20
|
return true;
|
|
@@ -14,6 +23,82 @@ export function canSeeChannel(channel, viewerSa, isSteward = false) {
|
|
|
14
23
|
}
|
|
15
24
|
/** Bounded per-channel history in the board doc (projection only — bodies live in the vault). */
|
|
16
25
|
export const BOARD_HISTORY_LIMIT = 200;
|
|
26
|
+
/** Should a just-committed post trigger the topic's org assistant (spec 327 §3)? Pure predicate —
|
|
27
|
+
* the caller supplies the posted body text (it never round-trips through the stored entry, whose
|
|
28
|
+
* body is hash-bound in the vault). False when the assistant is off, when the post is itself
|
|
29
|
+
* agent-authored (`actor` set), or when the author IS the org — the community contextRef, NOT
|
|
30
|
+
* `descriptor.owner` (that is the topic CREATOR) — so the assistant NEVER triggers itself, even
|
|
31
|
+
* in `trigger:'all'` topics. */
|
|
32
|
+
export function assistantTrigger(channel, post) {
|
|
33
|
+
const assistant = channel.assistant;
|
|
34
|
+
if (!assistant)
|
|
35
|
+
return false;
|
|
36
|
+
if (post.actor)
|
|
37
|
+
return false;
|
|
38
|
+
const org = channel.descriptor.contextRefs?.find((r) => r.kind === 'community')?.id?.toLowerCase();
|
|
39
|
+
if (org && String(post.from).toLowerCase().endsWith(org))
|
|
40
|
+
return false;
|
|
41
|
+
if (assistant.trigger === 'all')
|
|
42
|
+
return true;
|
|
43
|
+
const handle = assistant.mentionHandle.trim().toLowerCase();
|
|
44
|
+
if (!handle)
|
|
45
|
+
return false;
|
|
46
|
+
// @handle, optionally with its dotted parent name (@handle.<parent>…) — TLD-agnostic (the parent
|
|
47
|
+
// registry is app config, never package vocabulary — ADR-0021). Case-insensitive, word-bounded
|
|
48
|
+
// (name labels are [a-z0-9-]); a preceding name char (e.g. an email local part) is not a mention.
|
|
49
|
+
const esc = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
const mention = new RegExp(`(?:^|[^a-z0-9-])@${esc}(?:\\.[a-z0-9-]+)*(?![a-z0-9-])`, 'i');
|
|
51
|
+
if (mention.test(post.bodyText))
|
|
52
|
+
return true;
|
|
53
|
+
// `@ask` — the universal, org-agnostic assistant alias: members shouldn't have to remember the
|
|
54
|
+
// org's handle to talk to its assistant. Same word-bounding rules; only meaningful where an
|
|
55
|
+
// assistant is enabled, so it can never mis-route across orgs.
|
|
56
|
+
return ASK_ALIAS.test(post.bodyText);
|
|
57
|
+
}
|
|
58
|
+
const ASK_ALIAS = /(?:^|[^a-z0-9-])@ask(?![a-z0-9-])/i;
|
|
59
|
+
/** Enable/refresh (`assistant` set) or disable (`assistant` undefined) the org assistant on a topic
|
|
60
|
+
* (mutates the channel on success). Steward gating + audit are the caller's, as with every board
|
|
61
|
+
* mutation. `trigger:'mention'` requires a mention handle — a nameless org cannot be mentioned. */
|
|
62
|
+
export function setTopicAssistant(board, channelId, assistant) {
|
|
63
|
+
const channel = board.find((c) => c.descriptor.id === channelId);
|
|
64
|
+
if (!channel)
|
|
65
|
+
return { ok: false, error: 'unknown channel' };
|
|
66
|
+
if (!assistant) {
|
|
67
|
+
delete channel.assistant;
|
|
68
|
+
// spec 329 §8 — assistant off ⇒ routing off transitively (routing is a mode of the assistant).
|
|
69
|
+
delete channel.routing;
|
|
70
|
+
return { ok: true, channel };
|
|
71
|
+
}
|
|
72
|
+
const mentionHandle = assistant.mentionHandle.trim().toLowerCase();
|
|
73
|
+
if (assistant.trigger === 'mention' && !mentionHandle) {
|
|
74
|
+
return { ok: false, error: 'mention trigger requires the org to have a primary name' };
|
|
75
|
+
}
|
|
76
|
+
if (!assistant.displayName.trim())
|
|
77
|
+
return { ok: false, error: 'displayName required' };
|
|
78
|
+
channel.assistant = { ...assistant, mentionHandle };
|
|
79
|
+
return { ok: true, channel };
|
|
80
|
+
}
|
|
81
|
+
/** Enable/refresh (`routing` set) or disable (`routing` undefined) member routing on a topic
|
|
82
|
+
* (spec 329 §7). Enabling REQUIRES the 327 assistant to be on (routing is a mode of the
|
|
83
|
+
* assistant's turn); fan-out is clamped fail-closed to the approved 1..5 window. Disabling the
|
|
84
|
+
* ASSISTANT also disables routing transitively (setTopicAssistant clears it below). */
|
|
85
|
+
export function setTopicRouting(board, channelId, routing) {
|
|
86
|
+
const channel = board.find((c) => c.descriptor.id === channelId);
|
|
87
|
+
if (!channel)
|
|
88
|
+
return { ok: false, error: 'unknown channel' };
|
|
89
|
+
if (!routing) {
|
|
90
|
+
delete channel.routing;
|
|
91
|
+
return { ok: true, channel };
|
|
92
|
+
}
|
|
93
|
+
if (!channel.assistant)
|
|
94
|
+
return { ok: false, error: 'routing requires the topic assistant to be enabled first' };
|
|
95
|
+
const raw = Number(routing.maxFanout);
|
|
96
|
+
const maxFanout = Number.isFinite(raw)
|
|
97
|
+
? Math.min(Math.max(Math.round(raw), ROUTING_FANOUT_MIN), ROUTING_FANOUT_MAX)
|
|
98
|
+
: ROUTING_FANOUT_DEFAULT;
|
|
99
|
+
channel.routing = { ...routing, maxFanout };
|
|
100
|
+
return { ok: true, channel };
|
|
101
|
+
}
|
|
17
102
|
/** Create a channel on the board (mutates `board` on success). Title-unique per context.
|
|
18
103
|
* `contextId` is the org/context SA (substrate vocabulary — spec 322 §5; the wire literal
|
|
19
104
|
* `kind:'community'` is inside signed hashes and renames only at 316's envelope re-cut). */
|
|
@@ -24,7 +109,8 @@ export function createBoardChannel(board, opts) {
|
|
|
24
109
|
if (board.some((c) => c.title.toLowerCase() === title.toLowerCase())) {
|
|
25
110
|
return { ok: false, error: 'a channel with this title already exists' };
|
|
26
111
|
}
|
|
27
|
-
|
|
112
|
+
// New callers say participationPolicy; legacy callers say visibility. restricted ≡ private.
|
|
113
|
+
const isPrivate = opts.participationPolicy ? opts.participationPolicy === 'restricted' : opts.visibility === 'private';
|
|
28
114
|
const descriptor = {
|
|
29
115
|
version: 'ap.conversation.v1',
|
|
30
116
|
id: generateConversationId(),
|
|
@@ -40,12 +126,14 @@ export function createBoardChannel(board, opts) {
|
|
|
40
126
|
const errors = validateConversationDescriptor(descriptor);
|
|
41
127
|
if (errors.length > 0)
|
|
42
128
|
return { ok: false, error: `invalid channel: ${errors.join(', ')}` };
|
|
43
|
-
// The creator is always a
|
|
129
|
+
// The creator is always a participant of a restricted topic; dedupe + lowercase.
|
|
44
130
|
const members = isPrivate
|
|
45
131
|
? [...new Set([opts.creatorSa?.toLowerCase(), ...(opts.members ?? []).map((m) => m.toLowerCase())].filter((m) => !!m))]
|
|
46
132
|
: undefined;
|
|
47
133
|
const channel = {
|
|
48
134
|
descriptor, title, createdBy: opts.createdBy, messages: [],
|
|
135
|
+
// Dual-write: participationPolicy is the vocabulary; visibility stays for legacy readers.
|
|
136
|
+
participationPolicy: isPrivate ? 'restricted' : 'open',
|
|
49
137
|
...(isPrivate ? { visibility: 'private', members } : {}),
|
|
50
138
|
};
|
|
51
139
|
board.push(channel);
|
|
@@ -63,11 +151,14 @@ export async function appendBoardPost(board, opts) {
|
|
|
63
151
|
return { ok: false, error: 'unknown channel' };
|
|
64
152
|
const now = opts.now ?? new Date().toISOString();
|
|
65
153
|
const messageId = generateMessageId();
|
|
154
|
+
const baseRefs = channel.descriptor.contextRefs ?? [];
|
|
155
|
+
const extraRefs = (opts.contextRefs ?? []).filter((r) => !baseRefs.some((b) => b.kind === r.kind && b.id === r.id));
|
|
156
|
+
const contextRefs = [...baseRefs, ...extraRefs];
|
|
66
157
|
const envelope = {
|
|
67
|
-
version: 'ap.message.
|
|
158
|
+
version: 'ap.message.v2',
|
|
68
159
|
id: messageId,
|
|
69
160
|
conversationId: channel.descriptor.id,
|
|
70
|
-
|
|
161
|
+
performative: 'INFORM',
|
|
71
162
|
from: opts.from,
|
|
72
163
|
// Shared-board addressing: the descriptor's owner stands for the board (spec 313 §6).
|
|
73
164
|
to: [channel.descriptor.owner],
|
|
@@ -76,12 +167,12 @@ export async function appendBoardPost(board, opts) {
|
|
|
76
167
|
body: { resource: topicBodyResource(channel.descriptor.id, messageId), classification: 'internal', updatedAt: now },
|
|
77
168
|
bodyHash: await sha256Hex32(new TextEncoder().encode(bodyText)),
|
|
78
169
|
bodyContentType: 'text/plain',
|
|
79
|
-
contextRefs
|
|
170
|
+
contextRefs,
|
|
80
171
|
};
|
|
81
172
|
const errors = validateMessageEnvelope(envelope);
|
|
82
173
|
if (errors.length > 0)
|
|
83
174
|
return { ok: false, error: `invalid post: ${errors.join(', ')}` };
|
|
84
|
-
channel.messages.push({ envelope, authorName: opts.authorName });
|
|
175
|
+
channel.messages.push({ envelope, authorName: opts.authorName, ...(opts.actor ? { actor: opts.actor } : {}), ...(opts.prov ? { prov: opts.prov } : {}) });
|
|
85
176
|
if (channel.messages.length > BOARD_HISTORY_LIMIT) {
|
|
86
177
|
channel.messages.splice(0, channel.messages.length - BOARD_HISTORY_LIMIT);
|
|
87
178
|
}
|