agentera 3.0.0-dev.72 → 3.0.0-dev.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/bundle/.agentera-build-source.json +8 -0
- package/bundle/CHANGELOG.md +17 -17
- package/bundle/extract-corpus-parity.json +1 -1
- package/bundle/references/adapters/package-publication.json +49 -53
- package/bundle/references/adapters/package-registry.yaml +5 -1
- package/bundle/references/adapters/package-surface-characterization.md +6 -4
- package/bundle/references/analysis/evidence-tier-authority.yaml +45 -4
- package/bundle/references/analysis/personal-glossary-evaluation-authority.yaml +175 -0
- package/bundle/references/analysis/personal-glossary-evaluation-corpus.yaml +291 -0
- package/bundle/references/analysis/personal-glossary-holdout.yaml +192 -0
- package/bundle/references/analysis/verification-policy.yaml +8 -13
- package/bundle/references/artifacts/glossary-entry-contract.yaml +1533 -35
- package/bundle/references/cli/update-channels.yaml +2 -2
- package/bundle/references/cli/vocabulary.md +1 -1
- package/bundle/references/meta/retained-reference-authority.yaml +16 -1
- package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +11 -7
- package/bundle/skills/agentera/capabilities/profile/schemas/exit.yaml +6 -3
- package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +13 -7
- package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +5 -0
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +7 -0
- package/dist/.agentera-build-source.json +8 -0
- package/dist/analytics/extractCorpus/copilotSessions.js +13 -4
- package/dist/analytics/extractCorpus/core.js +223 -3
- package/dist/analytics/extractCorpus/corpus.js +18 -1
- package/dist/analytics/extractCorpus/cursorSessions.js +22 -4
- package/dist/analytics/extractCorpus/evidenceTiers.js +123 -17
- package/dist/analytics/extractCorpus/filesystemSources.js +4 -0
- package/dist/analytics/extractCorpus/index.js +2 -2
- package/dist/analytics/extractCorpus/jsonlSessions.js +52 -3
- package/dist/analytics/extractCorpus/sqliteSessions.js +12 -3
- package/dist/analytics/personalGlossaryAdmission.js +26 -46
- package/dist/analytics/personalGlossaryCandidateProjection.js +692 -0
- package/dist/analytics/personalGlossaryCandidateProjectionExcerpts.js +144 -0
- package/dist/analytics/personalGlossaryCandidateReadView.js +23 -0
- package/dist/analytics/personalGlossaryCurrentGeneration.js +25 -0
- package/dist/analytics/personalGlossaryDecision.js +203 -0
- package/dist/analytics/personalGlossaryExplicit.js +788 -0
- package/dist/analytics/personalGlossaryExplicitMining.js +285 -0
- package/dist/analytics/personalGlossaryExplicitScope.js +41 -0
- package/dist/analytics/personalGlossaryExplicitSegments.js +418 -0
- package/dist/analytics/personalGlossaryExplicitTypes.js +26 -0
- package/dist/analytics/personalGlossaryLexicalClassifier.js +175 -0
- package/dist/analytics/personalGlossaryProfile.js +13 -3
- package/dist/analytics/personalGlossaryRecurrence.js +618 -0
- package/dist/analytics/personalGlossaryReviewRecordStorage.js +428 -0
- package/dist/analytics/personalGlossaryReviewRecords.js +332 -0
- package/dist/capabilities/index.js +5 -1
- package/dist/capabilities/profile/instructions.js +50 -15
- package/dist/cli/commands/personalGlossaryCandidateReads.js +744 -0
- package/dist/cli/commands/personalGlossaryDecision.js +349 -0
- package/dist/cli/commands/personalGlossaryPublish.js +484 -0
- package/dist/cli/commands/personalGlossaryReviewRecordReads.js +209 -0
- package/dist/cli/commands/personalGlossaryReviewRecords.js +623 -0
- package/dist/cli/commands/prime/briefOrientation.js +118 -19
- package/dist/cli/commands/report.js +18 -2
- package/dist/cli/commands/schema.js +179 -0
- package/dist/cli/commands/verify.js +51 -9
- package/dist/cli/dispatch/lifecycle.js +51 -6
- package/dist/cli/help.js +48 -2
- package/dist/cli/migrationRequired.js +4 -1
- package/dist/core/atomicWriter.js +4 -1
- package/dist/core/developmentInvocation.js +4 -1
- package/dist/core/yaml.js +53 -1
- package/dist/eval/glossaryEvaluation.js +751 -0
- package/dist/eval/glossaryEvaluationProcess.js +29 -0
- package/dist/eval/glossaryEvaluationRunner.js +571 -0
- package/dist/eval/glossaryEvaluationSuccessReport.js +297 -0
- package/dist/registries/activationTuples.js +31 -3
- package/dist/registries/evidenceTierContract.js +4 -0
- package/dist/registries/explicitSegmentGrammarContract.js +619 -0
- package/dist/registries/glossaryCandidateContractPaths.js +6 -0
- package/dist/registries/glossaryCandidateContracts.js +820 -0
- package/dist/registries/glossaryCandidateDecisionAuthority.js +119 -0
- package/dist/registries/glossaryCandidateDecisionContract.js +62 -0
- package/dist/registries/glossaryCandidateProjectionAuthority.js +197 -0
- package/dist/registries/glossaryCandidateProjectionContract.js +104 -0
- package/dist/registries/glossaryEntryContract.js +105 -103
- package/dist/registries/glossaryEntryTemporal.js +12 -0
- package/dist/registries/glossaryMiningAuthority.js +764 -0
- package/dist/registries/glossaryProfileFullContract.js +24 -0
- package/dist/registries/glossaryReviewRecordsAuthority.js +426 -0
- package/dist/registries/glossaryReviewRecordsContract.js +129 -0
- package/dist/registries/glossaryTermIdentity.js +122 -0
- package/dist/registries/packagePublication.js +3 -3
- package/dist/registries/personalGlossaryContracts.js +1 -0
- package/dist/release/releaseMetadata.js +15 -8
- package/dist/upgrade/npxPlatformStatus.js +2 -1
- package/dist/upgrade/projectIntegration.js +7 -1
- package/dist/validate/activationArtifactEvidence.js +18 -4
- package/dist/validate/activationEvidenceManifest.js +101 -22
- package/package.json +2 -4
- package/dist/cli/commands/personalGlossary.js +0 -176
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { containsGlossaryTerm } from "../registries/glossaryTermOccurrence.js";
|
|
2
|
+
import { compareGlossaryUnicodeStrings } from "../registries/glossaryTermIdentity.js";
|
|
3
|
+
const TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
4
|
+
const TOOL_ARGUMENTS_RE = /\b(?:tool[ _-]?arguments?|argv)\b/iu;
|
|
5
|
+
const CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/u;
|
|
6
|
+
const PRIVATE_KEY_RE = /-----BEGIN(?: [A-Z]+)? PRIVATE KEY-----/u;
|
|
7
|
+
const ASSIGNED_SENSITIVE_VALUE_RE = /(?:^|[^A-Za-z0-9_])["']?(?:api[_-]?key|access[_-]?token|password|passwd|cookie|private[_-]?key|authorization(?:[_-]?header)?|session(?:[_-]?id)?|email|phone|contact)["']?\s*[:=]\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\s,;}\]]+))/giu;
|
|
8
|
+
const BEARER_VALUE_RE = /\bbearer\s+([A-Za-z0-9._~+/=-]{6,})/giu;
|
|
9
|
+
const BARE_SESSION_VALUE_RE = /\bsession[-_]([A-Za-z0-9._~+/=-]{6,})\b/giu;
|
|
10
|
+
const API_KEY_RE = /\b(?:sk|pk)_(?:live|test)_?[A-Za-z0-9_-]{8,}\b/u;
|
|
11
|
+
const AWS_KEY_RE = /\bAKIA[0-9A-Z]{16}\b/u;
|
|
12
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/u;
|
|
13
|
+
const EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/iu;
|
|
14
|
+
const PHONE_RE = /(?:\+?\d[\d .()-]{7,}\d)/u;
|
|
15
|
+
const UNIX_PATH_RE = /(?:^|[\s("'`])(?:~\/|\/)(?:[^\s/]+\/)*[^\s/]+/u;
|
|
16
|
+
const WINDOWS_PATH_RE = /\b[A-Za-z]:\\(?:[^\\\s]+\\)*[^\\\s]+/u;
|
|
17
|
+
const SENSITIVE_CONTENT_PATTERNS = [
|
|
18
|
+
PRIVATE_KEY_RE,
|
|
19
|
+
API_KEY_RE,
|
|
20
|
+
AWS_KEY_RE,
|
|
21
|
+
JWT_RE,
|
|
22
|
+
EMAIL_RE,
|
|
23
|
+
PHONE_RE,
|
|
24
|
+
UNIX_PATH_RE,
|
|
25
|
+
WINDOWS_PATH_RE,
|
|
26
|
+
];
|
|
27
|
+
export const EXCERPT_OMISSION_REASONS = [
|
|
28
|
+
"no_excerpt",
|
|
29
|
+
"unrelated_context",
|
|
30
|
+
"source_bound_exceeded",
|
|
31
|
+
"unsafe_tool_arguments",
|
|
32
|
+
"unsafe_content",
|
|
33
|
+
];
|
|
34
|
+
function compareText(left, right) {
|
|
35
|
+
return compareGlossaryUnicodeStrings(left, right);
|
|
36
|
+
}
|
|
37
|
+
function validTimestamp(value) {
|
|
38
|
+
return typeof value === "string" && TIMESTAMP_RE.test(value) && !Number.isNaN(Date.parse(value));
|
|
39
|
+
}
|
|
40
|
+
export function personalGlossaryCandidateProjectionExcerptExpiry(retainedAt, pendingExcerptDays) {
|
|
41
|
+
return new Date(Date.parse(retainedAt) + pendingExcerptDays * 24 * 60 * 60 * 1000).toISOString();
|
|
42
|
+
}
|
|
43
|
+
function truncateUtf8(value, maximum) {
|
|
44
|
+
if (Buffer.byteLength(value, "utf8") <= maximum)
|
|
45
|
+
return { text: value, truncated: false };
|
|
46
|
+
let bytes = 0;
|
|
47
|
+
let text = "";
|
|
48
|
+
for (const scalar of value) {
|
|
49
|
+
const scalarBytes = Buffer.byteLength(scalar, "utf8");
|
|
50
|
+
if (bytes + scalarBytes > maximum)
|
|
51
|
+
break;
|
|
52
|
+
text += scalar;
|
|
53
|
+
bytes += scalarBytes;
|
|
54
|
+
}
|
|
55
|
+
return { text, truncated: true };
|
|
56
|
+
}
|
|
57
|
+
/** Classify secret or sensitive values without returning or transforming them. */
|
|
58
|
+
export function containsPersonalGlossarySensitiveContent(value) {
|
|
59
|
+
if (SENSITIVE_CONTENT_PATTERNS.some((pattern) => pattern.test(value)))
|
|
60
|
+
return true;
|
|
61
|
+
for (const match of value.matchAll(ASSIGNED_SENSITIVE_VALUE_RE)) {
|
|
62
|
+
if (credentialShapedValue(match[1] ?? match[2] ?? match[3] ?? ""))
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
for (const match of value.matchAll(BEARER_VALUE_RE)) {
|
|
66
|
+
if (credentialShapedValue(match[1] ?? ""))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
for (const match of value.matchAll(BARE_SESSION_VALUE_RE)) {
|
|
70
|
+
if (credentialShapedValue(match[1] ?? "", false))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
function credentialShapedValue(value, allowLongLetters = true) {
|
|
76
|
+
const candidate = value.trim();
|
|
77
|
+
if (candidate.length === 0 || candidate === "[REDACTED]")
|
|
78
|
+
return false;
|
|
79
|
+
const scheme = /^(?:bearer|basic)\s+(.+)$/iu.exec(candidate);
|
|
80
|
+
if (scheme)
|
|
81
|
+
return credentialShapedValue(scheme[1] ?? "");
|
|
82
|
+
if (/\s/u.test(candidate))
|
|
83
|
+
return false;
|
|
84
|
+
return ((candidate.length >= 6 && /[A-Za-z]/u.test(candidate) && /\d/u.test(candidate)) ||
|
|
85
|
+
(candidate.length >= 8 && /[A-Za-z]/u.test(candidate) && /[-_.=+/]/u.test(candidate)) ||
|
|
86
|
+
(allowLongLetters && candidate.length >= 16 && /^[A-Za-z]+$/u.test(candidate)));
|
|
87
|
+
}
|
|
88
|
+
export function validPersonalGlossarySafeExcerpt(value) {
|
|
89
|
+
const excerpt = value !== null && typeof value === "object" && !Array.isArray(value)
|
|
90
|
+
? value
|
|
91
|
+
: null;
|
|
92
|
+
return (excerpt !== null &&
|
|
93
|
+
typeof excerpt === "object" &&
|
|
94
|
+
Object.keys(excerpt).length === 3 &&
|
|
95
|
+
["expires_at", "redacted", "text"].every((key) => key in excerpt) &&
|
|
96
|
+
typeof excerpt.text === "string" &&
|
|
97
|
+
excerpt.text.length > 0 &&
|
|
98
|
+
Buffer.byteLength(excerpt.text, "utf8") <= 500 &&
|
|
99
|
+
!CONTROL_RE.test(excerpt.text) &&
|
|
100
|
+
!TOOL_ARGUMENTS_RE.test(excerpt.text) &&
|
|
101
|
+
!containsPersonalGlossarySensitiveContent(excerpt.text) &&
|
|
102
|
+
validTimestamp(excerpt.expires_at) &&
|
|
103
|
+
typeof excerpt.redacted === "boolean");
|
|
104
|
+
}
|
|
105
|
+
function safeExcerpt(source, term, retainedAt, contract) {
|
|
106
|
+
if (Buffer.byteLength(source, "utf8") > contract.sourceExcerptMaxUtf8Bytes) {
|
|
107
|
+
return { excerpt: null, omission: "source_bound_exceeded", provided: 1, truncated: false };
|
|
108
|
+
}
|
|
109
|
+
if (!containsGlossaryTerm(source, term)) {
|
|
110
|
+
return { excerpt: null, omission: "unrelated_context", provided: 1, truncated: false };
|
|
111
|
+
}
|
|
112
|
+
if (CONTROL_RE.test(source) || TOOL_ARGUMENTS_RE.test(source)) {
|
|
113
|
+
return { excerpt: null, omission: "unsafe_tool_arguments", provided: 1, truncated: false };
|
|
114
|
+
}
|
|
115
|
+
if (containsPersonalGlossarySensitiveContent(source)) {
|
|
116
|
+
return { excerpt: null, omission: "unsafe_content", provided: 1, truncated: false };
|
|
117
|
+
}
|
|
118
|
+
const bounded = truncateUtf8(source, 500);
|
|
119
|
+
if (bounded.text.length === 0) {
|
|
120
|
+
return { excerpt: null, omission: "unsafe_content", provided: 1, truncated: bounded.truncated };
|
|
121
|
+
}
|
|
122
|
+
const excerpt = {
|
|
123
|
+
text: bounded.text,
|
|
124
|
+
expires_at: personalGlossaryCandidateProjectionExcerptExpiry(retainedAt, contract.pendingExcerptDays),
|
|
125
|
+
redacted: false,
|
|
126
|
+
};
|
|
127
|
+
if (!validPersonalGlossarySafeExcerpt(excerpt)) {
|
|
128
|
+
return { excerpt: null, omission: "unsafe_content", provided: 1, truncated: bounded.truncated };
|
|
129
|
+
}
|
|
130
|
+
return { excerpt, omission: null, provided: 1, truncated: bounded.truncated };
|
|
131
|
+
}
|
|
132
|
+
export function selectPersonalGlossarySafeExcerpt(excerpts, term, retainedAt, contract) {
|
|
133
|
+
if (excerpts.size === 0) {
|
|
134
|
+
return { excerpt: null, omission: "no_excerpt", provided: 0, truncated: false };
|
|
135
|
+
}
|
|
136
|
+
let firstOmission = null;
|
|
137
|
+
for (const source of [...excerpts].sort(compareText)) {
|
|
138
|
+
const selected = safeExcerpt(source, term, retainedAt, contract);
|
|
139
|
+
if (selected.excerpt !== null)
|
|
140
|
+
return selected;
|
|
141
|
+
firstOmission ??= selected;
|
|
142
|
+
}
|
|
143
|
+
return firstOmission;
|
|
144
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { glossaryCanonicalSha256 } from "../registries/glossaryTermIdentity.js";
|
|
2
|
+
/** Derive the expiry-aware candidate view without mutating its private projection. */
|
|
3
|
+
export function currentPersonalGlossaryCandidateReadView(projection) {
|
|
4
|
+
const readTime = Date.now();
|
|
5
|
+
let expiredSafeContexts = 0;
|
|
6
|
+
const candidates = projection.candidates.map((candidate) => {
|
|
7
|
+
if (candidate.safe_excerpt !== null && Date.parse(candidate.safe_excerpt.expires_at) <= readTime) {
|
|
8
|
+
expiredSafeContexts += 1;
|
|
9
|
+
return { ...candidate, safe_excerpt: null };
|
|
10
|
+
}
|
|
11
|
+
return candidate;
|
|
12
|
+
});
|
|
13
|
+
const safeContextViewSha256 = glossaryCanonicalSha256({
|
|
14
|
+
schema_version: "agentera.personalGlossaryCandidateSafeContextView.v1",
|
|
15
|
+
candidates: candidates.map((candidate) => ({
|
|
16
|
+
candidate_id: candidate.capsule.candidate_id,
|
|
17
|
+
candidate_revision: candidate.capsule.candidate_revision,
|
|
18
|
+
capsule_sha256: candidate.capsule.capsule_sha256,
|
|
19
|
+
safe_context_available: candidate.safe_excerpt !== null,
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
return { projection, candidates, expiredSafeContexts, safeContextViewSha256 };
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defaultTiersDir, readCurrentGeneration, } from "./extractCorpus/evidenceTiers.js";
|
|
2
|
+
import { readPersonalGlossaryCandidateProjection, } from "./personalGlossaryCandidateProjection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read the private candidate projection only when it is bound to the readable
|
|
5
|
+
* current evidence-tier generation. A projection from a prior generation is
|
|
6
|
+
* never a current candidate source.
|
|
7
|
+
*/
|
|
8
|
+
export function readCurrentPersonalGlossaryCandidateProjection(options = {}) {
|
|
9
|
+
const generation = readCurrentGeneration(options.tiersDir ?? defaultTiersDir(options.env, options.platform));
|
|
10
|
+
if (generation === null ||
|
|
11
|
+
generation.pointer.generation !== generation.manifest.generation) {
|
|
12
|
+
return { status: "current_generation_unavailable", projection: null };
|
|
13
|
+
}
|
|
14
|
+
const projection = readPersonalGlossaryCandidateProjection(options);
|
|
15
|
+
if (projection.status === "missing") {
|
|
16
|
+
return { status: "projection_missing", projection: null };
|
|
17
|
+
}
|
|
18
|
+
if (projection.status !== "current" || projection.projection === null) {
|
|
19
|
+
return { status: "projection_corrupt", projection: null };
|
|
20
|
+
}
|
|
21
|
+
if (projection.projection.generation !== generation.pointer.generation) {
|
|
22
|
+
return { status: "projection_stale", projection: null };
|
|
23
|
+
}
|
|
24
|
+
return { status: "current", projection: projection.projection };
|
|
25
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveSourceRoot } from "../core/sourceRoot.js";
|
|
5
|
+
import { runGlossaryEvaluationProcess } from "../eval/glossaryEvaluationProcess.js";
|
|
6
|
+
import { validateGlossaryEvaluationSuccessReport } from "../eval/glossaryEvaluationSuccessReport.js";
|
|
7
|
+
import { createGlossaryAdmissionDecision, validateGlossaryHostClassificationReceipt, } from "../registries/glossaryCandidateContracts.js";
|
|
8
|
+
import { readCurrentPersonalGlossaryCandidateProjection, } from "./personalGlossaryCurrentGeneration.js";
|
|
9
|
+
import { defaultTiersDir } from "./extractCorpus/evidenceTiers.js";
|
|
10
|
+
import { mineExplicitGlossaryCandidates } from "./personalGlossaryExplicitMining.js";
|
|
11
|
+
const QUALITY_GATE_CACHE_LIMIT = 16;
|
|
12
|
+
const qualityGateCache = new Map();
|
|
13
|
+
function mapping(value) {
|
|
14
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
15
|
+
? value
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
function result(status, reason, decision = null) {
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: "agentera.personalGlossaryAdmissionResult.v1",
|
|
21
|
+
command: "report personal-glossary-decision",
|
|
22
|
+
status,
|
|
23
|
+
decision,
|
|
24
|
+
reason,
|
|
25
|
+
effects: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function exactCandidate(candidates, receipt) {
|
|
29
|
+
const candidateId = receipt.candidate_id;
|
|
30
|
+
if (typeof candidateId !== "string")
|
|
31
|
+
return null;
|
|
32
|
+
const sameIdentity = candidates.filter((candidate) => candidate.capsule.candidate_id === candidateId);
|
|
33
|
+
const exact = sameIdentity.filter((candidate) => candidate.capsule.candidate_revision === receipt.candidate_revision &&
|
|
34
|
+
candidate.capsule.capsule_sha256 === receipt.candidate_capsule_sha256 &&
|
|
35
|
+
candidate.capsule.generation === receipt.generation &&
|
|
36
|
+
candidate.capsule.policy_version === receipt.policy_version);
|
|
37
|
+
if (exact.length === 1)
|
|
38
|
+
return exact[0];
|
|
39
|
+
return sameIdentity.length === 1 ? sameIdentity[0] : null;
|
|
40
|
+
}
|
|
41
|
+
function hasEntryConflict(candidates, capsule) {
|
|
42
|
+
return candidates.some(({ capsule: other }) => other.candidate_id === capsule.candidate_id &&
|
|
43
|
+
other.candidate_revision !== capsule.candidate_revision &&
|
|
44
|
+
(other.meaning !== capsule.meaning ||
|
|
45
|
+
other.scope !== capsule.scope ||
|
|
46
|
+
other.provenance_kind !== capsule.provenance_kind));
|
|
47
|
+
}
|
|
48
|
+
function currentExplicitEvidence(capsule, tiersDir, precomputed) {
|
|
49
|
+
const mined = precomputed ?? mineExplicitGlossaryCandidates({ tiersDir });
|
|
50
|
+
if (mined.state !== "current")
|
|
51
|
+
return "unavailable";
|
|
52
|
+
if (mined.generation !== capsule.generation)
|
|
53
|
+
return "changed";
|
|
54
|
+
if (mined.candidates.some(({ capsule: current }) => current.candidate_revision === capsule.candidate_revision &&
|
|
55
|
+
current.capsule_sha256 === capsule.capsule_sha256)) {
|
|
56
|
+
return "current";
|
|
57
|
+
}
|
|
58
|
+
if (mined.abstentions.some((item) => item.candidate_id === capsule.candidate_id &&
|
|
59
|
+
["retracted_definition", "conflicting_meaning"].includes(item.reason))) {
|
|
60
|
+
return "retracted_or_conflicted";
|
|
61
|
+
}
|
|
62
|
+
return "changed";
|
|
63
|
+
}
|
|
64
|
+
function qualityGateFingerprint(sourceRoot) {
|
|
65
|
+
const digest = crypto.createHash("sha256");
|
|
66
|
+
try {
|
|
67
|
+
for (const relative of [
|
|
68
|
+
"references/analysis/personal-glossary-evaluation-authority.yaml",
|
|
69
|
+
"references/analysis/personal-glossary-holdout.yaml",
|
|
70
|
+
"references/analysis/personal-glossary-evaluation-corpus.yaml",
|
|
71
|
+
"references/analysis/evidence-tier-authority.yaml",
|
|
72
|
+
"references/artifacts/glossary-entry-contract.yaml",
|
|
73
|
+
]) {
|
|
74
|
+
digest.update(relative).update("\0").update(fs.readFileSync(path.join(sourceRoot, relative))).update("\0");
|
|
75
|
+
}
|
|
76
|
+
return digest.digest("hex");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function qualityGate(sourceRoot) {
|
|
83
|
+
try {
|
|
84
|
+
const fingerprint = qualityGateFingerprint(sourceRoot);
|
|
85
|
+
const cached = fingerprint === null ? undefined : qualityGateCache.get(sourceRoot);
|
|
86
|
+
if (cached?.fingerprint === fingerprint)
|
|
87
|
+
return cached.status;
|
|
88
|
+
const evaluation = runGlossaryEvaluationProcess(sourceRoot);
|
|
89
|
+
const status = validateGlossaryEvaluationSuccessReport(evaluation, sourceRoot) !== null
|
|
90
|
+
? "pass"
|
|
91
|
+
: "fail";
|
|
92
|
+
if (fingerprint !== null && status === "pass") {
|
|
93
|
+
qualityGateCache.set(sourceRoot, { fingerprint, status });
|
|
94
|
+
if (qualityGateCache.size > QUALITY_GATE_CACHE_LIMIT) {
|
|
95
|
+
qualityGateCache.delete(qualityGateCache.keys().next().value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return status;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return "unavailable";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function decision(capsule, receipt, outcome, reason) {
|
|
105
|
+
try {
|
|
106
|
+
const value = createGlossaryAdmissionDecision({
|
|
107
|
+
capsule,
|
|
108
|
+
receipt: receipt,
|
|
109
|
+
outcome,
|
|
110
|
+
reason,
|
|
111
|
+
});
|
|
112
|
+
return result(outcome, reason, value);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return result("abstain", "decision_validation_failed");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate one untrusted host classification against current private evidence and
|
|
120
|
+
* derive its only permitted admission outcome. This function never persists a
|
|
121
|
+
* review, profile entry, projection, or project artifact.
|
|
122
|
+
*/
|
|
123
|
+
function decidePersonalGlossaryCandidateInternal(receiptInput, options, evaluationQualityGate, precomputedExplicitMining, precomputedCurrentProjection) {
|
|
124
|
+
let read = precomputedCurrentProjection;
|
|
125
|
+
if (read === undefined) {
|
|
126
|
+
try {
|
|
127
|
+
read = readCurrentPersonalGlossaryCandidateProjection(options);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return result("abstain", "projection_unavailable");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (read.status !== "current" || read.projection === null) {
|
|
134
|
+
return result("abstain", read.status === "current_generation_unavailable"
|
|
135
|
+
? "current_generation_unavailable"
|
|
136
|
+
: read.status === "projection_stale"
|
|
137
|
+
? "projection_stale"
|
|
138
|
+
: "projection_unavailable");
|
|
139
|
+
}
|
|
140
|
+
const receipt = mapping(receiptInput);
|
|
141
|
+
if (!receipt)
|
|
142
|
+
return result("abstain", "receipt_invalid");
|
|
143
|
+
const selected = exactCandidate(read.projection.candidates, receipt);
|
|
144
|
+
if (!selected)
|
|
145
|
+
return result("abstain", "candidate_unavailable");
|
|
146
|
+
const capsule = selected.capsule;
|
|
147
|
+
const receiptErrors = validateGlossaryHostClassificationReceipt(receipt, capsule, {
|
|
148
|
+
candidateProjectionSha256: read.projection.projection_sha256,
|
|
149
|
+
});
|
|
150
|
+
if (receiptErrors.length > 0) {
|
|
151
|
+
return result("abstain", receiptErrors.includes("host_classification_receipt.candidate_projection_sha256 does not match the current projection")
|
|
152
|
+
? "projection_changed"
|
|
153
|
+
: "receipt_invalid");
|
|
154
|
+
}
|
|
155
|
+
const classification = mapping(receipt.classification);
|
|
156
|
+
if (capsule.scope === "project" || classification.scope === "project") {
|
|
157
|
+
return decision(capsule, receipt, "abstain", "scope_project");
|
|
158
|
+
}
|
|
159
|
+
if (capsule.scope !== "personal" || classification.scope !== "personal") {
|
|
160
|
+
return decision(capsule, receipt, "review_required", "scope_ambiguous");
|
|
161
|
+
}
|
|
162
|
+
if (classification.consistency === "inconsistent") {
|
|
163
|
+
return decision(capsule, receipt, "review_required", "classification_inconsistent");
|
|
164
|
+
}
|
|
165
|
+
if (classification.consistency !== "consistent" ||
|
|
166
|
+
classification.term !== capsule.term ||
|
|
167
|
+
classification.meaning !== capsule.meaning) {
|
|
168
|
+
return decision(capsule, receipt, "review_required", "classification_changed");
|
|
169
|
+
}
|
|
170
|
+
if (hasEntryConflict(read.projection.candidates, capsule)) {
|
|
171
|
+
return decision(capsule, receipt, "review_required", "entry_conflict");
|
|
172
|
+
}
|
|
173
|
+
if (capsule.provenance_kind !== "personal_explicit_definition") {
|
|
174
|
+
return decision(capsule, receipt, "review_required", "inferred_requires_review");
|
|
175
|
+
}
|
|
176
|
+
const evidence = currentExplicitEvidence(capsule, options.tiersDir ?? defaultTiersDir(options.env, options.platform), precomputedExplicitMining);
|
|
177
|
+
if (evidence === "unavailable") {
|
|
178
|
+
return decision(capsule, receipt, "abstain", "evidence_unavailable");
|
|
179
|
+
}
|
|
180
|
+
if (evidence === "changed") {
|
|
181
|
+
return decision(capsule, receipt, "abstain", "evidence_changed");
|
|
182
|
+
}
|
|
183
|
+
if (evidence === "retracted_or_conflicted") {
|
|
184
|
+
return decision(capsule, receipt, "review_required", "evidence_retracted_or_conflicted");
|
|
185
|
+
}
|
|
186
|
+
const gate = evaluationQualityGate ? "pass" : qualityGate(options.sourceRoot ?? resolveSourceRoot());
|
|
187
|
+
if (gate !== "pass") {
|
|
188
|
+
return decision(capsule, receipt, "review_required", "quality_gate_not_authorizing");
|
|
189
|
+
}
|
|
190
|
+
return decision(capsule, receipt, "automatic_admission", "explicit_current_authorized");
|
|
191
|
+
}
|
|
192
|
+
/** Apply the release-authorizing quality report before an explicit admission. */
|
|
193
|
+
export function decidePersonalGlossaryCandidate(receiptInput, options = {}) {
|
|
194
|
+
return decidePersonalGlossaryCandidateInternal(receiptInput, options, false);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Exercise the same bounded decision boundary while measuring the prerequisite
|
|
198
|
+
* explicit policy. This internal evaluator seam is never wired to CLI dispatch;
|
|
199
|
+
* inferred candidates still return review_required before the quality branch.
|
|
200
|
+
*/
|
|
201
|
+
export function evaluatePersonalGlossaryCandidate(receiptInput, options = {}, precomputedExplicitMining, precomputedCurrentProjection) {
|
|
202
|
+
return decidePersonalGlossaryCandidateInternal(receiptInput, options, true, precomputedExplicitMining, precomputedCurrentProjection);
|
|
203
|
+
}
|