@cassiomc1/forgeloop 1.9.0 → 1.10.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/AGENT_COMPATIBILITY.md +15 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +4 -2
- package/LOOP_ENGINEERING.md +13 -0
- package/LOOP_SYSTEM_DESIGN.md +33 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +31 -0
- package/README.md +40 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +24 -0
- package/completions/_forgeloop +2 -1
- package/completions/forgeloop.bash +3 -1
- package/completions/forgeloop.fish +9 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +28 -2
- package/docs/ARTIFACT_REFERENCE.md +14 -0
- package/docs/CLI_REFERENCE.md +40 -0
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +7 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +10 -0
- package/docs/MCP.md +17 -1
- package/docs/RECIPES.md +80 -0
- package/docs/RELEASE_CHECKLIST.md +14 -0
- package/docs/TROUBLESHOOTING.md +54 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/package.json +2 -1
- package/schemas/handoff-envelope.schema.json +1 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +6 -0
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/cli-command-definitions.js +17 -0
- package/src/core/command-executors.js +12 -0
- package/src/core/command-input.js +11 -1
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +113 -0
- package/src/core/events.js +32 -0
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/filesystem.js +18 -2
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/integration-invocation-policy.js +19 -2
- package/src/core/integration-resources.js +21 -1
- package/src/core/portable-context.js +103 -0
- package/src/core/protocol-info.js +18 -2
- package/src/core/runtime-context.js +31 -0
- package/src/integration.d.ts +116 -0
- package/src/integration.js +22 -0
|
@@ -26,6 +26,26 @@ export function unreleasedSection(changelog) {
|
|
|
26
26
|
return nextHeading ? rest.slice(0, nextHeading.index) : rest;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function releaseSection(changelog) {
|
|
30
|
+
const heading = /^##[ \t]+(v?\d+\.\d+\.\d+)(?:[ \t]+-[^\r\n]*)?$/imu.exec(changelog);
|
|
31
|
+
if (!heading) return null;
|
|
32
|
+
const rest = changelog.slice(heading.index + heading[0].length);
|
|
33
|
+
const nextHeading = /^##\s/imu.exec(rest);
|
|
34
|
+
return {
|
|
35
|
+
version: heading[1].replace(/^v/u, ""),
|
|
36
|
+
section: nextHeading ? rest.slice(0, nextHeading.index) : rest,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareVersions(left, right) {
|
|
41
|
+
const leftParts = left.split(".").map(Number);
|
|
42
|
+
const rightParts = right.split(".").map(Number);
|
|
43
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
44
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
export function stripHtmlComments(value) {
|
|
30
50
|
const chunks = [];
|
|
31
51
|
let cursor = 0;
|
|
@@ -55,11 +75,15 @@ export function hasMeaningfulUnreleasedContent(section) {
|
|
|
55
75
|
|
|
56
76
|
export function checkChangelogFreshness({ changelog, tags = [], commitsSinceLatestTag = 0, allowEmpty = false } = {}) {
|
|
57
77
|
const latestTag = latestReleaseTag(tags);
|
|
58
|
-
const
|
|
78
|
+
const unreleased = unreleasedSection(changelog);
|
|
79
|
+
const pendingRelease = releaseSection(changelog);
|
|
80
|
+
const pendingReleaseIsNewer = pendingRelease &&
|
|
81
|
+
(!latestTag || compareVersions(pendingRelease.version, latestTag.replace(/^v/u, "")) > 0);
|
|
82
|
+
const section = unreleased || (pendingReleaseIsNewer ? pendingRelease.section : "");
|
|
59
83
|
const hasChanges = commitsSinceLatestTag > 0;
|
|
60
84
|
const populated = hasMeaningfulUnreleasedContent(section);
|
|
61
85
|
const ok = allowEmpty || !hasChanges || populated;
|
|
62
|
-
return { ok, latestTag, hasChanges, populated, section };
|
|
86
|
+
return { ok, latestTag, hasChanges, populated, section, pendingRelease };
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
async function run() {
|
|
@@ -71,7 +95,7 @@ async function run() {
|
|
|
71
95
|
: Number(git(["rev-list", "--count", "HEAD"]));
|
|
72
96
|
const result = checkChangelogFreshness({ changelog, tags, commitsSinceLatestTag, allowEmpty: process.argv.includes("--allow-empty") });
|
|
73
97
|
if (!result.ok) {
|
|
74
|
-
console.error(`CHANGELOG.md has changes since ${result.latestTag ?? "the initial commit"} but
|
|
98
|
+
console.error(`CHANGELOG.md has changes since ${result.latestTag ?? "the initial commit"} but no populated Unreleased or pending release section exists.`);
|
|
75
99
|
process.exitCode = 1;
|
|
76
100
|
return;
|
|
77
101
|
}
|
|
@@ -49,6 +49,22 @@ async function render() {
|
|
|
49
49
|
const features = Object.entries(info.features)
|
|
50
50
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
51
51
|
.map(([name, feature]) => [name, feature.version ?? "n/a", feature.supported === false ? "no" : "yes"]);
|
|
52
|
+
const handoffs = info.features.canonicalHandoffs;
|
|
53
|
+
const advisory = info.features.advisoryContextProviders;
|
|
54
|
+
const capabilityContracts = `## Capability contracts
|
|
55
|
+
|
|
56
|
+
- \`canonicalHandoffs\` v${handoffs.version}: immutable, supported, and
|
|
57
|
+
ledger-backed for exactly-once operational acceptance through
|
|
58
|
+
\`${handoffs.acceptanceCommand}\`. Acceptance statuses are
|
|
59
|
+
\`${handoffs.acceptanceStatuses.join("\` / \`")}\`; it creates no claims,
|
|
60
|
+
evidence, or lifecycle authority.
|
|
61
|
+
- \`advisoryContextProviders\` v${advisory.version}: provider-neutral,
|
|
62
|
+
Integration-API-only, lazy, and opt-in. Provider results are not persisted by
|
|
63
|
+
ForgeLoop and are never lifecycle state, evidence, authority, or executable
|
|
64
|
+
instructions.
|
|
65
|
+
|
|
66
|
+
Protocol v1, schema v1, and Integration API v1 remain independent of these
|
|
67
|
+
capability-family versions.`;
|
|
52
68
|
const artifacts = Object.values(ARTIFACT_REGISTRY)
|
|
53
69
|
.filter((artifact) => artifact.isPublic)
|
|
54
70
|
.sort((left, right) => left.key.localeCompare(right.key))
|
|
@@ -133,6 +149,8 @@ Phases: ${info.lifecycle.phases.join(", ")}
|
|
|
133
149
|
|
|
134
150
|
${table(["Feature", "Version", "Supported"], features)}
|
|
135
151
|
|
|
152
|
+
${capabilityContracts}
|
|
153
|
+
|
|
136
154
|
## Public artifact registry
|
|
137
155
|
|
|
138
156
|
${table(["Key", "Scope", "Path", "Schema", "Trust role"], artifacts)}
|
package/src/cli.js
CHANGED
|
@@ -77,6 +77,7 @@ import { formatWorkspaceStatusResult } from "./commands/workspace-status.js";
|
|
|
77
77
|
import { formatHandoffCreateResult } from "./commands/handoff-create.js";
|
|
78
78
|
import { formatHandoffListResult } from "./commands/handoff-list.js";
|
|
79
79
|
import { formatHandoffShowResult } from "./commands/handoff-show.js";
|
|
80
|
+
import { formatHandoffAcceptResult } from "./commands/handoff-accept.js";
|
|
80
81
|
import { formatResponsibilitySetResult } from "./commands/responsibility-set.js";
|
|
81
82
|
import { formatResponsibilityStatusResult } from "./commands/responsibility-status.js";
|
|
82
83
|
import { formatVerifyScopeResult } from "./commands/verify-scope.js";
|
|
@@ -463,6 +464,11 @@ export const COMMAND_HANDLERS = Object.freeze({
|
|
|
463
464
|
renderJsonOr(options, result, formatHandoffShowResult);
|
|
464
465
|
return exitCode;
|
|
465
466
|
},
|
|
467
|
+
"handoff-accept": async ({ target, packageRoot, options }) => {
|
|
468
|
+
const { result, exitCode } = await COMMAND_EXECUTORS["handoff-accept"]({ target, packageRoot, options });
|
|
469
|
+
renderJsonOr(options, result, formatHandoffAcceptResult);
|
|
470
|
+
return exitCode;
|
|
471
|
+
},
|
|
466
472
|
"responsibility-set": async ({ target, packageRoot, options }) => {
|
|
467
473
|
const { result, exitCode } = await COMMAND_EXECUTORS["responsibility-set"]({ target, packageRoot, options });
|
|
468
474
|
renderJsonOr(options, result, formatResponsibilitySetResult);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { acceptCanonicalHandoff } from "../core/handoff-acceptance.js";
|
|
2
|
+
|
|
3
|
+
export async function runHandoffAccept({
|
|
4
|
+
target,
|
|
5
|
+
packageRoot,
|
|
6
|
+
taskId,
|
|
7
|
+
handoffId,
|
|
8
|
+
consumerId,
|
|
9
|
+
harness,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const result = await acceptCanonicalHandoff(target, {
|
|
12
|
+
taskId,
|
|
13
|
+
handoffId,
|
|
14
|
+
consumerId,
|
|
15
|
+
harness,
|
|
16
|
+
packageRoot,
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
taskId,
|
|
20
|
+
...result,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatHandoffAcceptResult(result) {
|
|
25
|
+
return [
|
|
26
|
+
`FORGELOOP HANDOFF ACCEPTED: ${result.handoffId}`,
|
|
27
|
+
`consumer: ${result.consumerId}`,
|
|
28
|
+
`harness: ${result.harness ?? "none"}`,
|
|
29
|
+
`at: ${result.acceptedAt}`,
|
|
30
|
+
`idempotent: ${result.idempotent ? "yes" : "no"}`,
|
|
31
|
+
"authority: OPERATIONAL_RECEIPT_ONLY",
|
|
32
|
+
"evidence: NONE",
|
|
33
|
+
"claims transferred: NO",
|
|
34
|
+
"",
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { listCanonicalHandoffs } from "../core/handoff.js";
|
|
2
|
+
import {
|
|
3
|
+
readHandoffAcceptanceLedger,
|
|
4
|
+
resolveHandoffAcceptance,
|
|
5
|
+
} from "../core/handoff-acceptance.js";
|
|
2
6
|
|
|
3
7
|
export async function runHandoffList({ target, packageRoot, taskId } = {}) {
|
|
4
8
|
const handoffs = await listCanonicalHandoffs(target, { packageRoot, taskId });
|
|
5
|
-
|
|
9
|
+
const ledger = await readHandoffAcceptanceLedger(target, packageRoot, { taskId });
|
|
10
|
+
const handoffsWithAcceptance = handoffs.map((handoff) => {
|
|
11
|
+
const resolved = resolveHandoffAcceptance({
|
|
12
|
+
events: ledger.events,
|
|
13
|
+
handoff,
|
|
14
|
+
ledgerValid: ledger.valid,
|
|
15
|
+
ledgerErrors: ledger.errors,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
...handoff,
|
|
19
|
+
acceptance: {
|
|
20
|
+
status: resolved.status,
|
|
21
|
+
consumerId: resolved.consumerId ?? null,
|
|
22
|
+
harness: resolved.harness ?? null,
|
|
23
|
+
acceptedAt: resolved.acceptedAt ?? null,
|
|
24
|
+
...(resolved.reasonCodes ? { reasonCodes: [...resolved.reasonCodes] } : {}),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
return { taskId, count: handoffs.length, handoffs: handoffsWithAcceptance };
|
|
6
29
|
}
|
|
7
30
|
|
|
8
31
|
export function formatHandoffListResult(result) {
|
|
9
32
|
const lines = [`FORGELOOP HANDOFFS: ${result.count}`];
|
|
10
|
-
for (const handoff of result.handoffs)
|
|
33
|
+
for (const handoff of result.handoffs) {
|
|
34
|
+
const status = handoff.acceptance?.status ? ` [${handoff.acceptance.status}]` : "";
|
|
35
|
+
lines.push(`${handoff.handoffId} ${handoff.createdAt} ${handoff.artifactDigest}${status}`);
|
|
36
|
+
}
|
|
11
37
|
return `${lines.join("\n")}\n`;
|
|
12
38
|
}
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import { readCanonicalHandoff } from "../core/handoff.js";
|
|
2
|
+
import {
|
|
3
|
+
readHandoffAcceptanceLedger,
|
|
4
|
+
resolveHandoffAcceptance,
|
|
5
|
+
} from "../core/handoff-acceptance.js";
|
|
2
6
|
|
|
3
7
|
export async function runHandoffShow({ target, packageRoot, taskId, handoffId } = {}) {
|
|
4
8
|
const artifact = await readCanonicalHandoff(target, { packageRoot, taskId, handoffId });
|
|
5
|
-
|
|
9
|
+
const ledger = await readHandoffAcceptanceLedger(target, packageRoot, { taskId });
|
|
10
|
+
const resolved = resolveHandoffAcceptance({
|
|
11
|
+
events: ledger.events,
|
|
12
|
+
handoff: artifact.value,
|
|
13
|
+
ledgerValid: ledger.valid,
|
|
14
|
+
ledgerErrors: ledger.errors,
|
|
15
|
+
});
|
|
16
|
+
const acceptance = {
|
|
17
|
+
status: resolved.status,
|
|
18
|
+
consumerId: resolved.consumerId ?? null,
|
|
19
|
+
harness: resolved.harness ?? null,
|
|
20
|
+
acceptedAt: resolved.acceptedAt ?? null,
|
|
21
|
+
...(resolved.reasonCodes ? { reasonCodes: [...resolved.reasonCodes] } : {}),
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
taskId,
|
|
25
|
+
path: artifact.path,
|
|
26
|
+
fingerprint: artifact.fingerprint,
|
|
27
|
+
handoff: artifact.value,
|
|
28
|
+
acceptance,
|
|
29
|
+
};
|
|
6
30
|
}
|
|
7
31
|
|
|
8
32
|
export function formatHandoffShowResult(result) {
|
|
9
|
-
|
|
33
|
+
const acceptanceLine = result.acceptance ? `\nacceptance: ${result.acceptance.status}` : "";
|
|
34
|
+
return `FORGELOOP HANDOFF: VALID\nid: ${result.handoff.handoffId}\ntask: ${result.handoff.taskId}\ndigest: ${result.handoff.artifactDigest}\npath: ${result.path}${acceptanceLine}\n\n`;
|
|
10
35
|
}
|
|
@@ -20,6 +20,10 @@ export function formatReconcileContinuityResult(result) {
|
|
|
20
20
|
"Authority: OPERATIONAL_CONTEXT_ONLY",
|
|
21
21
|
"Evidence: NONE",
|
|
22
22
|
];
|
|
23
|
+
if (result.lint) {
|
|
24
|
+
const findingCount = result.lint.findings?.length ?? 0;
|
|
25
|
+
lines.push(`Lint: ${result.lint.status}${findingCount ? ` (${findingCount} finding(s))` : ""}`);
|
|
26
|
+
}
|
|
23
27
|
if (result.reasonCodes?.length) lines.push(`Reason codes: ${result.reasonCodes.join(", ")}`);
|
|
24
28
|
if (result.reasons?.length) lines.push(`Reasons: ${result.reasons.join(", ")}`);
|
|
25
29
|
return `${lines.join("\n")}\n`;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { E_ADVISORY_CONTEXT_REQUEST_INVALID } from "../error-codes.js";
|
|
2
|
+
|
|
3
|
+
export const ADVISORY_CONTEXT_LIMITS = Object.freeze({
|
|
4
|
+
defaultItems: 6,
|
|
5
|
+
maxItems: 20,
|
|
6
|
+
defaultMaxItemChars: 1200,
|
|
7
|
+
maxItemChars: 4000,
|
|
8
|
+
defaultMaxTotalChars: 6000,
|
|
9
|
+
maxTotalChars: 16000,
|
|
10
|
+
defaultTimeoutMs: 5000,
|
|
11
|
+
maxTimeoutMs: 30000,
|
|
12
|
+
maxProviderReturnedItems: 100,
|
|
13
|
+
maxQueryChars: 1000,
|
|
14
|
+
maxSourceRefChars: 2048,
|
|
15
|
+
maxTitleChars: 300,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function normalizeIntegerOption(value, {
|
|
19
|
+
name,
|
|
20
|
+
defaultValue,
|
|
21
|
+
minimum,
|
|
22
|
+
maximum,
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (value === undefined) return defaultValue;
|
|
25
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
26
|
+
const error = new Error(`${name} must be a finite integer greater than or equal to ${minimum}`);
|
|
27
|
+
error.name = "AdvisoryContextRequestError";
|
|
28
|
+
error.code = E_ADVISORY_CONTEXT_REQUEST_INVALID;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
return Math.min(value, maximum);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeAdvisoryRecallOptions({
|
|
35
|
+
limit,
|
|
36
|
+
maxItemChars,
|
|
37
|
+
maxTotalChars,
|
|
38
|
+
timeoutMs,
|
|
39
|
+
} = {}) {
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
limit: normalizeIntegerOption(limit, {
|
|
42
|
+
name: "limit",
|
|
43
|
+
defaultValue: ADVISORY_CONTEXT_LIMITS.defaultItems,
|
|
44
|
+
minimum: 1,
|
|
45
|
+
maximum: ADVISORY_CONTEXT_LIMITS.maxItems,
|
|
46
|
+
}),
|
|
47
|
+
maxItemChars: normalizeIntegerOption(maxItemChars, {
|
|
48
|
+
name: "maxItemChars",
|
|
49
|
+
defaultValue: ADVISORY_CONTEXT_LIMITS.defaultMaxItemChars,
|
|
50
|
+
minimum: 100,
|
|
51
|
+
maximum: ADVISORY_CONTEXT_LIMITS.maxItemChars,
|
|
52
|
+
}),
|
|
53
|
+
maxTotalChars: normalizeIntegerOption(maxTotalChars, {
|
|
54
|
+
name: "maxTotalChars",
|
|
55
|
+
defaultValue: ADVISORY_CONTEXT_LIMITS.defaultMaxTotalChars,
|
|
56
|
+
minimum: 500,
|
|
57
|
+
maximum: ADVISORY_CONTEXT_LIMITS.maxTotalChars,
|
|
58
|
+
}),
|
|
59
|
+
timeoutMs: normalizeIntegerOption(timeoutMs, {
|
|
60
|
+
name: "timeoutMs",
|
|
61
|
+
defaultValue: ADVISORY_CONTEXT_LIMITS.defaultTimeoutMs,
|
|
62
|
+
minimum: 1,
|
|
63
|
+
maximum: ADVISORY_CONTEXT_LIMITS.maxTimeoutMs,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ADVISORY_CONTEXT_TRUST = Object.freeze({
|
|
69
|
+
authority: "ADVISORY",
|
|
70
|
+
evidenceAuthority: "NONE",
|
|
71
|
+
actionability: "NON_EXECUTABLE",
|
|
72
|
+
trustRole: "NON_EVIDENCE_ADVISORY_CONTEXT",
|
|
73
|
+
persisted: false,
|
|
74
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import {
|
|
3
|
+
normalizePortableText,
|
|
4
|
+
assertPortableContextSafe,
|
|
5
|
+
deepFreeze,
|
|
6
|
+
} from "../portable-context.js";
|
|
7
|
+
import {
|
|
8
|
+
ADVISORY_CONTEXT_LIMITS,
|
|
9
|
+
ADVISORY_CONTEXT_TRUST,
|
|
10
|
+
normalizeAdvisoryRecallOptions,
|
|
11
|
+
} from "./constants.js";
|
|
12
|
+
import {
|
|
13
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
14
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
15
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
16
|
+
} from "../error-codes.js";
|
|
17
|
+
|
|
18
|
+
const PROVIDER_ID_REGEX = /^[a-z0-9][a-z0-9_-]*$/;
|
|
19
|
+
|
|
20
|
+
function providerError(code, message) {
|
|
21
|
+
const error = new Error(message);
|
|
22
|
+
error.name = "AdvisoryContextProviderError";
|
|
23
|
+
error.code = code;
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function assertAdvisoryContextProvider(provider) {
|
|
28
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
29
|
+
throw providerError(
|
|
30
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
31
|
+
"Advisory context provider must be an object",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof provider.id !== "string" || !PROVIDER_ID_REGEX.test(provider.id)) {
|
|
36
|
+
throw providerError(
|
|
37
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
38
|
+
`Provider id must be a lowercase identifier matching ${PROVIDER_ID_REGEX}: received "${provider.id}"`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (provider.version !== undefined && (typeof provider.version !== "string" || provider.version.trim() === "" || provider.version.length > 64)) {
|
|
43
|
+
throw providerError(
|
|
44
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
45
|
+
"Provider version must be a non-empty string under 64 characters when specified",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof provider.recall !== "function") {
|
|
50
|
+
throw providerError(
|
|
51
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
52
|
+
`Provider "${provider.id}" must implement recall(input) function`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return provider;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function assertAdvisoryContextProviderIdentity(provider, expectedId) {
|
|
60
|
+
assertAdvisoryContextProvider(provider);
|
|
61
|
+
if (provider.id !== expectedId) {
|
|
62
|
+
throw providerError(
|
|
63
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
64
|
+
`Provider id "${provider.id}" does not match requested registry key "${expectedId}"`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return provider;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createAdvisoryContextProviderRegistry({ providers = {} } = {}) {
|
|
71
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
|
|
72
|
+
throw providerError(
|
|
73
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
74
|
+
"Advisory context providers registry must be an object map",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const map = new Map();
|
|
79
|
+
for (const [key, entry] of Object.entries(providers)) {
|
|
80
|
+
if (!PROVIDER_ID_REGEX.test(key)) {
|
|
81
|
+
throw providerError(
|
|
82
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
83
|
+
`Invalid provider key "${key}" in registry`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (typeof entry === "function") {
|
|
87
|
+
map.set(key, entry);
|
|
88
|
+
} else {
|
|
89
|
+
assertAdvisoryContextProviderIdentity(entry, key);
|
|
90
|
+
map.set(key, entry);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
get(id) {
|
|
96
|
+
return map.get(id) ?? null;
|
|
97
|
+
},
|
|
98
|
+
has(id) {
|
|
99
|
+
return map.has(id);
|
|
100
|
+
},
|
|
101
|
+
list() {
|
|
102
|
+
return [...map.keys()].sort();
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function resolveAdvisoryContextProvider({ providers, providerName } = {}) {
|
|
108
|
+
if (!providers || typeof providerName !== "string") return null;
|
|
109
|
+
|
|
110
|
+
let entry = null;
|
|
111
|
+
if (providers instanceof Map) {
|
|
112
|
+
entry = providers.get(providerName) ?? null;
|
|
113
|
+
} else if (typeof providers.get === "function" && typeof providers.has === "function") {
|
|
114
|
+
entry = providers.get(providerName) ?? null;
|
|
115
|
+
} else if (typeof providers === "object" && !Array.isArray(providers)) {
|
|
116
|
+
entry = providers[providerName] ?? null;
|
|
117
|
+
}
|
|
118
|
+
if (!entry) return null;
|
|
119
|
+
|
|
120
|
+
const provider = typeof entry === "function" ? await entry() : entry;
|
|
121
|
+
return assertAdvisoryContextProviderIdentity(provider, providerName);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeAdvisoryContextResult(raw, {
|
|
125
|
+
provider,
|
|
126
|
+
taskId,
|
|
127
|
+
limit,
|
|
128
|
+
maxItemChars,
|
|
129
|
+
maxTotalChars,
|
|
130
|
+
timeoutMs,
|
|
131
|
+
} = {}) {
|
|
132
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
133
|
+
throw providerError(
|
|
134
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
135
|
+
"Advisory context result must be a JSON object",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!Array.isArray(raw.items)) {
|
|
140
|
+
throw providerError(
|
|
141
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
142
|
+
"Advisory context result items must be an array",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const effectiveOptions = normalizeAdvisoryRecallOptions({
|
|
147
|
+
limit,
|
|
148
|
+
maxItemChars,
|
|
149
|
+
maxTotalChars,
|
|
150
|
+
timeoutMs,
|
|
151
|
+
});
|
|
152
|
+
if (raw.items.length > ADVISORY_CONTEXT_LIMITS.maxProviderReturnedItems) {
|
|
153
|
+
throw providerError(
|
|
154
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
155
|
+
`Advisory context provider returned ${raw.items.length} items, exceeding the raw item ceiling of ${ADVISORY_CONTEXT_LIMITS.maxProviderReturnedItems}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const selectedItems = raw.items.slice(0, effectiveOptions.limit);
|
|
160
|
+
let totalChars = 0;
|
|
161
|
+
const normalizedItems = [];
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < selectedItems.length; i += 1) {
|
|
164
|
+
const item = selectedItems[i];
|
|
165
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
166
|
+
throw providerError(
|
|
167
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
168
|
+
`Item at index ${i} must be an object`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (item.summary === undefined || item.summary === null) {
|
|
173
|
+
throw providerError(
|
|
174
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
175
|
+
`Item at index ${i} requires a non-empty summary`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let summary;
|
|
180
|
+
try {
|
|
181
|
+
summary = normalizePortableText(item.summary, {
|
|
182
|
+
label: `items[${i}].summary`,
|
|
183
|
+
maxLength: effectiveOptions.maxItemChars,
|
|
184
|
+
});
|
|
185
|
+
} catch (err) {
|
|
186
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, err.message);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let title;
|
|
190
|
+
if (item.title !== undefined && item.title !== null) {
|
|
191
|
+
try {
|
|
192
|
+
title = normalizePortableText(item.title, {
|
|
193
|
+
label: `items[${i}].title`,
|
|
194
|
+
maxLength: ADVISORY_CONTEXT_LIMITS.maxTitleChars,
|
|
195
|
+
optional: true,
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, err.message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let sourceRef;
|
|
203
|
+
if (item.sourceRef !== undefined && item.sourceRef !== null) {
|
|
204
|
+
try {
|
|
205
|
+
sourceRef = normalizePortableText(item.sourceRef, {
|
|
206
|
+
label: `items[${i}].sourceRef`,
|
|
207
|
+
maxLength: ADVISORY_CONTEXT_LIMITS.maxSourceRefChars,
|
|
208
|
+
optional: true,
|
|
209
|
+
});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, err.message);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let observedAt;
|
|
216
|
+
if (item.observedAt !== undefined && item.observedAt !== null) {
|
|
217
|
+
try {
|
|
218
|
+
observedAt = normalizePortableText(item.observedAt, {
|
|
219
|
+
label: `items[${i}].observedAt`,
|
|
220
|
+
maxLength: 128,
|
|
221
|
+
optional: true,
|
|
222
|
+
});
|
|
223
|
+
} catch (err) {
|
|
224
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, err.message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let confidence;
|
|
229
|
+
if (item.confidence !== undefined && item.confidence !== null) {
|
|
230
|
+
if (
|
|
231
|
+
typeof item.confidence !== "number"
|
|
232
|
+
|| !Number.isFinite(item.confidence)
|
|
233
|
+
|| item.confidence < 0
|
|
234
|
+
|| item.confidence > 1
|
|
235
|
+
) {
|
|
236
|
+
throw providerError(
|
|
237
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
238
|
+
`Item at index ${i} confidence must be a finite number between 0 and 1`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
confidence = item.confidence;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const itemCharLength =
|
|
245
|
+
(title?.length ?? 0)
|
|
246
|
+
+ summary.length
|
|
247
|
+
+ (sourceRef?.length ?? 0)
|
|
248
|
+
+ (observedAt?.length ?? 0);
|
|
249
|
+
|
|
250
|
+
totalChars += itemCharLength;
|
|
251
|
+
if (totalChars > effectiveOptions.maxTotalChars) {
|
|
252
|
+
throw providerError(
|
|
253
|
+
E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
|
|
254
|
+
`Advisory context output exceeded the total character limit of ${effectiveOptions.maxTotalChars} (accumulated ${totalChars})`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const cleanItem = {
|
|
259
|
+
...(title ? { title } : {}),
|
|
260
|
+
summary,
|
|
261
|
+
...(sourceRef ? { sourceRef } : {}),
|
|
262
|
+
...(observedAt ? { observedAt } : {}),
|
|
263
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const itemFingerprint = canonicalFingerprint(cleanItem);
|
|
267
|
+
cleanItem.itemFingerprint = itemFingerprint;
|
|
268
|
+
normalizedItems.push(Object.freeze(cleanItem));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const normalized = {
|
|
272
|
+
provider: {
|
|
273
|
+
id: provider?.id ?? "unknown",
|
|
274
|
+
...(provider?.version ? { version: provider.version } : {}),
|
|
275
|
+
},
|
|
276
|
+
taskId: taskId ?? null,
|
|
277
|
+
authority: ADVISORY_CONTEXT_TRUST.authority,
|
|
278
|
+
evidenceAuthority: ADVISORY_CONTEXT_TRUST.evidenceAuthority,
|
|
279
|
+
actionability: ADVISORY_CONTEXT_TRUST.actionability,
|
|
280
|
+
trustRole: ADVISORY_CONTEXT_TRUST.trustRole,
|
|
281
|
+
persisted: ADVISORY_CONTEXT_TRUST.persisted,
|
|
282
|
+
items: Object.freeze(normalizedItems),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
assertPortableContextSafe(normalized, { label: "advisory context normalized result" });
|
|
286
|
+
return deepFreeze(normalized);
|
|
287
|
+
}
|