agentera 3.0.0-dev.72 → 3.0.0-dev.74

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.
Files changed (92) hide show
  1. package/README.md +111 -0
  2. package/bundle/.agentera-build-source.json +8 -0
  3. package/bundle/CHANGELOG.md +17 -17
  4. package/bundle/extract-corpus-parity.json +1 -1
  5. package/bundle/references/adapters/package-publication.json +49 -53
  6. package/bundle/references/adapters/package-registry.yaml +5 -1
  7. package/bundle/references/adapters/package-surface-characterization.md +6 -4
  8. package/bundle/references/analysis/evidence-tier-authority.yaml +45 -4
  9. package/bundle/references/analysis/personal-glossary-evaluation-authority.yaml +175 -0
  10. package/bundle/references/analysis/personal-glossary-evaluation-corpus.yaml +291 -0
  11. package/bundle/references/analysis/personal-glossary-holdout.yaml +192 -0
  12. package/bundle/references/artifacts/glossary-entry-contract.yaml +1533 -35
  13. package/bundle/references/cli/update-channels.yaml +2 -2
  14. package/bundle/references/cli/vocabulary.md +1 -1
  15. package/bundle/references/meta/retained-reference-authority.yaml +15 -0
  16. package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +11 -7
  17. package/bundle/skills/agentera/capabilities/profile/schemas/exit.yaml +6 -3
  18. package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +13 -7
  19. package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +5 -0
  20. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +7 -0
  21. package/dist/.agentera-build-source.json +8 -0
  22. package/dist/analytics/extractCorpus/copilotSessions.js +13 -4
  23. package/dist/analytics/extractCorpus/core.js +223 -3
  24. package/dist/analytics/extractCorpus/corpus.js +18 -1
  25. package/dist/analytics/extractCorpus/cursorSessions.js +22 -4
  26. package/dist/analytics/extractCorpus/evidenceTiers.js +123 -17
  27. package/dist/analytics/extractCorpus/filesystemSources.js +4 -0
  28. package/dist/analytics/extractCorpus/index.js +2 -2
  29. package/dist/analytics/extractCorpus/jsonlSessions.js +52 -3
  30. package/dist/analytics/extractCorpus/sqliteSessions.js +12 -3
  31. package/dist/analytics/personalGlossaryAdmission.js +26 -46
  32. package/dist/analytics/personalGlossaryCandidateProjection.js +692 -0
  33. package/dist/analytics/personalGlossaryCandidateProjectionExcerpts.js +144 -0
  34. package/dist/analytics/personalGlossaryCandidateReadView.js +23 -0
  35. package/dist/analytics/personalGlossaryCurrentGeneration.js +25 -0
  36. package/dist/analytics/personalGlossaryDecision.js +203 -0
  37. package/dist/analytics/personalGlossaryExplicit.js +788 -0
  38. package/dist/analytics/personalGlossaryExplicitMining.js +285 -0
  39. package/dist/analytics/personalGlossaryExplicitScope.js +41 -0
  40. package/dist/analytics/personalGlossaryExplicitSegments.js +418 -0
  41. package/dist/analytics/personalGlossaryExplicitTypes.js +26 -0
  42. package/dist/analytics/personalGlossaryLexicalClassifier.js +175 -0
  43. package/dist/analytics/personalGlossaryProfile.js +13 -3
  44. package/dist/analytics/personalGlossaryRecurrence.js +618 -0
  45. package/dist/analytics/personalGlossaryReviewRecordStorage.js +428 -0
  46. package/dist/analytics/personalGlossaryReviewRecords.js +332 -0
  47. package/dist/capabilities/index.js +5 -1
  48. package/dist/capabilities/profile/instructions.js +50 -15
  49. package/dist/cli/commands/personalGlossaryCandidateReads.js +744 -0
  50. package/dist/cli/commands/personalGlossaryDecision.js +349 -0
  51. package/dist/cli/commands/personalGlossaryPublish.js +484 -0
  52. package/dist/cli/commands/personalGlossaryReviewRecordReads.js +209 -0
  53. package/dist/cli/commands/personalGlossaryReviewRecords.js +623 -0
  54. package/dist/cli/commands/prime/briefOrientation.js +118 -19
  55. package/dist/cli/commands/report.js +18 -2
  56. package/dist/cli/commands/schema.js +179 -0
  57. package/dist/cli/commands/verify.js +51 -9
  58. package/dist/cli/dispatch/lifecycle.js +51 -6
  59. package/dist/cli/help.js +48 -2
  60. package/dist/cli/migrationRequired.js +4 -1
  61. package/dist/core/atomicWriter.js +4 -1
  62. package/dist/core/developmentInvocation.js +4 -1
  63. package/dist/core/yaml.js +53 -1
  64. package/dist/eval/glossaryEvaluation.js +751 -0
  65. package/dist/eval/glossaryEvaluationProcess.js +29 -0
  66. package/dist/eval/glossaryEvaluationRunner.js +571 -0
  67. package/dist/eval/glossaryEvaluationSuccessReport.js +297 -0
  68. package/dist/registries/activationTuples.js +31 -3
  69. package/dist/registries/evidenceTierContract.js +4 -0
  70. package/dist/registries/explicitSegmentGrammarContract.js +619 -0
  71. package/dist/registries/glossaryCandidateContractPaths.js +6 -0
  72. package/dist/registries/glossaryCandidateContracts.js +820 -0
  73. package/dist/registries/glossaryCandidateDecisionAuthority.js +119 -0
  74. package/dist/registries/glossaryCandidateDecisionContract.js +62 -0
  75. package/dist/registries/glossaryCandidateProjectionAuthority.js +197 -0
  76. package/dist/registries/glossaryCandidateProjectionContract.js +104 -0
  77. package/dist/registries/glossaryEntryContract.js +105 -103
  78. package/dist/registries/glossaryEntryTemporal.js +12 -0
  79. package/dist/registries/glossaryMiningAuthority.js +764 -0
  80. package/dist/registries/glossaryProfileFullContract.js +24 -0
  81. package/dist/registries/glossaryReviewRecordsAuthority.js +426 -0
  82. package/dist/registries/glossaryReviewRecordsContract.js +129 -0
  83. package/dist/registries/glossaryTermIdentity.js +122 -0
  84. package/dist/registries/packagePublication.js +3 -3
  85. package/dist/registries/personalGlossaryContracts.js +1 -0
  86. package/dist/release/releaseMetadata.js +14 -7
  87. package/dist/upgrade/npxPlatformStatus.js +2 -1
  88. package/dist/upgrade/projectIntegration.js +7 -1
  89. package/dist/validate/activationArtifactEvidence.js +18 -4
  90. package/dist/validate/activationEvidenceManifest.js +101 -22
  91. package/package.json +2 -4
  92. package/dist/cli/commands/personalGlossary.js +0 -176
@@ -3,7 +3,7 @@ status: active_authority
3
3
  purpose: >-
4
4
  Define dual update channels that select which published Agentera line upgrade,
5
5
  doctor, and prime resolve: stable tracks the supported 2.x line; development
6
- tracks 3.x alphas and release candidates.
6
+ tracks 3.x prereleases.
7
7
  sources:
8
8
  prose:
9
9
  - references/cli/vocabulary.md#update-channels
@@ -60,7 +60,7 @@ channels:
60
60
  development:
61
61
  concept: v3_pre_release_line
62
62
  definition: >-
63
- Tracks 3.x alphas, betas, and release candidates for early adopters and
63
+ Tracks 3.x prereleases for early adopters and
64
64
  maintainers testing the npm self-contained TypeScript distribution. The
65
65
  feat/v3 branch and v3 CLI are npm-only; v2→v3 is a one-way upgrade through
66
66
  npx @next, not through uvx or git checkout installs.
@@ -399,7 +399,7 @@ for upgrade, doctor, prime, docs, and tests.
399
399
 
400
400
  Use this prose as guidance only: **stable** tracks the supported 2.x npm line
401
401
  and, for maintainers, `uvx --from git+...@main` (Python
402
- CLI). **development** tracks 3.x alphas and release candidates on **npm only**
402
+ CLI). **development** tracks 3.x prereleases on **npm only**
403
403
  (`npx -y agentera@next`); feat/v3 is TypeScript-only and has no uv/git install
404
404
  path. v2→v3 is a one-way upgrade through the development npm channel. Default
405
405
  channel is stable. Override with `--channel`, `AGENTERA_UPDATE_CHANNEL`, or
@@ -19,6 +19,21 @@ inventory:
19
19
  production_owner: { module: packages/cli/src/registries/evidenceTierContract.ts, symbol: loadEvidenceTierContract }
20
20
  consumers:
21
21
  - { kind: runtime, module: packages/cli/src/registries/evidenceTierContract.ts, symbol: loadEvidenceTierContract, consumption: loads }
22
+ - path: references/analysis/personal-glossary-evaluation-authority.yaml
23
+ classification: current
24
+ production_owner: { module: packages/cli/src/eval/glossaryEvaluation.ts, symbol: loadGlossaryEvaluationAuthority }
25
+ consumers:
26
+ - { kind: runtime, module: packages/cli/src/eval/glossaryEvaluationSuccessReport.ts, symbol: validateGlossaryEvaluationSuccessReport, consumption: loads }
27
+ - path: references/analysis/personal-glossary-holdout.yaml
28
+ classification: current
29
+ production_owner: { module: packages/cli/src/eval/glossaryEvaluation.ts, symbol: loadGlossaryEvaluationHoldout }
30
+ consumers:
31
+ - { kind: runtime, module: packages/cli/src/eval/glossaryEvaluationSuccessReport.ts, symbol: validateGlossaryEvaluationSuccessReport, consumption: loads }
32
+ - path: references/analysis/personal-glossary-evaluation-corpus.yaml
33
+ classification: current
34
+ production_owner: { module: packages/cli/src/eval/glossaryEvaluation.ts, symbol: loadGlossaryEvaluationBehaviorFixture }
35
+ consumers:
36
+ - { kind: runtime, module: packages/cli/src/eval/glossaryEvaluationSuccessReport.ts, symbol: validateGlossaryEvaluationSuccessReport, consumption: loads }
22
37
  - path: references/analysis/verification-policy.yaml
23
38
  classification: current
24
39
  production_owner: { module: packages/cli/scripts/verify-lane.mjs, symbol: loadContract }
@@ -5,13 +5,17 @@ ARTIFACTS:
5
5
  local_role: produces_and_consumes
6
6
  description: >-
7
7
  Decision profile with per-entry confidence scoring, permanence
8
- classification, and temporal metadata. Profile Full also owns one
9
- deterministic personal Glossary section containing shared glossary entries
10
- and lifecycle metadata; rendering and persistence are active while lookup
11
- remains deferred. Profile writes this in both Full and Validate modes,
12
- then reads it for validation and update context. Prime exposes its
13
- canonical resolved path only inside `prime --context profile`; no other
14
- Prime context or profile-grounding failure may serialize that path.
8
+ classification, and temporal metadata. After writing the base profile,
9
+ Profile Full consumes one existing consent-bound user-local personal
10
+ glossary generation. Only complete current coverage reaches bounded
11
+ classification and current explicit CLI decisions can replace its
12
+ deterministic owned Glossary section; inferred or ambiguous candidates
13
+ use the bounded review lifecycle. Unavailable, stale, or degraded coverage
14
+ preserves the base and owned section and requires explicit refresh. Lookup remains deferred. Profile writes
15
+ this in both Full and Validate modes, then reads it for validation and
16
+ update context. Prime exposes its canonical resolved path only inside
17
+ `prime --context profile`; no other Prime context or profile-grounding
18
+ failure may serialize that path.
15
19
  2:
16
20
  id: A2
17
21
  artifact: decisions
@@ -4,8 +4,10 @@ EXIT_CONDITIONS:
4
4
  condition: complete
5
5
  description: >-
6
6
  PROFILE.md was written (Full mode) or updated (Validate mode).
7
- In Full mode its owned personal Glossary section was rendered and persisted,
8
- metadata changes were applied, prediction accuracy was assessed, and
7
+ In Full mode the base profile was written first, then any existing personal
8
+ glossary generation produced only current authorized explicit publication;
9
+ remaining review work was queued or bounded for a question channel.
10
+ Metadata changes were applied, prediction accuracy was assessed, and
9
11
  changes were summarized.
10
12
  exit_signal: complete
11
13
  2:
@@ -16,7 +18,8 @@ EXIT_CONDITIONS:
16
18
  issues: available runtime history was skipped without explicit
17
19
  acceptance, extraction failed for one or more sources, prediction
18
20
  accuracy was below 3/5, or significant unresolved tensions were
19
- found.
21
+ found. Full mode is also flagged when its existing consent-bound personal
22
+ glossary generation is unavailable, stale, degraded, or review-only.
20
23
  exit_signal: flagged
21
24
  3:
22
25
  id: E3
@@ -87,17 +87,22 @@ VALIDATION:
87
87
  id: V7
88
88
  rule: personal_glossary_output
89
89
  description: >-
90
- Profile Full MUST render and persist exactly one owned Glossary section
91
- through `npx -y agentera@next report profile-glossary`. Entries use only the shared
92
- primitive and personal provenance variants. Replacement preserves every
93
- non-glossary byte, malformed or ambiguous boundaries fail before effects,
94
- and personal production never reads a project glossary.
90
+ After writing the base profile, Profile Full MUST consume only one existing
91
+ consent-bound user-local generation. The canonical publication operation
92
+ may render and persist exactly one owned Glossary section only after current
93
+ decision revalidation, and only for an explicit automatic admission.
94
+ Entries use only the shared primitive and personal provenance variants.
95
+ Replacement preserves every non-glossary byte, malformed or ambiguous
96
+ boundaries fail before effects, and personal production never reads a
97
+ project glossary.
95
98
  severity: critical
96
99
  checks:
97
- - "Explicit entries retain exactly one evidence anchor"
98
- - "Inferred entries retain exactly two pairwise-distinct evidence anchors"
100
+ - "Explicit publication retains exactly one current user-authored evidence anchor"
101
+ - "Inferred entries cannot receive automatic publication"
99
102
  - "Non-glossary PROFILE.md bytes and decision-pattern confidence are unchanged"
100
103
  - "No project glossary identity, path, parameter, import, or read is used"
104
+ - "Missing, failed, empty, degraded, or review-only mining preserves the base and owned section"
105
+ - "Degraded coverage performs no candidate classification, decision, review queue, or publication"
101
106
  8:
102
107
  id: V8
103
108
  rule: personal_glossary_lifecycle
@@ -113,3 +118,4 @@ VALIDATION:
113
118
  - "Stable, durable, and situational classes use their existing lambdas and floor"
114
119
  - "Fresh evidence refreshes only matching meaning and provenance kind"
115
120
  - "Glossary confidence never mutates decision-pattern confidence"
121
+ - "Question-capable hosts present only a bounded queued review set; other hosts use the durable queue"
@@ -19,6 +19,11 @@ CONFORMANCE:
19
19
  ownership_contract: project
20
20
  provenance_variant: project_file
21
21
  term_identity_runtime: packages/cli/src/registries/glossaryTermIdentity.ts#unicodeCaselessExact
22
+ personal_mining_authority: references/artifacts/glossary-entry-contract.yaml#personal_mining_authority
23
+ stable_term_identity_runtime: packages/cli/src/registries/glossaryTermIdentity.ts#stableGlossaryTermIdentity
24
+ candidate_revision_runtime: packages/cli/src/registries/glossaryTermIdentity.ts#glossaryCandidateRevision
25
+ candidate_contracts_authority: references/artifacts/glossary-entry-contract.yaml#candidate_contracts
26
+ candidate_contracts_runtime: packages/cli/src/registries/glossaryCandidateContracts.ts#validatePersonalCandidateContracts
22
27
  term_occurrence_runtime: packages/cli/src/registries/glossaryTermOccurrence.ts#containsGlossaryTerm
23
28
 
24
29
  DOCUMENT:
@@ -623,6 +623,13 @@ LIFECYCLE_CONTRACT:
623
623
  roots:
624
624
  - .
625
625
  symlinks: Excluded because their targets may leave the repository boundary.
626
+ generated_output:
627
+ owner: packages/cli/scripts/generated-output.mjs#generatedOutputRoot
628
+ package_roots: [packages/cli]
629
+ treatment: exclude_exact_generated_root_subtree
630
+ reason: >-
631
+ Generated verification and build evidence is retained for bounded
632
+ downstream readers, but is not a tracked lifecycle-bearing source surface.
626
633
  excluded_directory_names:
627
634
  names:
628
635
  - .git
@@ -0,0 +1,8 @@
1
+ {
2
+ "schemaVersion": "agentera.generatedBuildSource.v1",
3
+ "commit": "15c2b8d04ef350ff9e09bc5f40a0d079aeedc399",
4
+ "tree": "0e0c77c6fc7da5b3da9b6b92eaa6117e669658ef",
5
+ "files": 6724,
6
+ "workingTreeSha256": "634b717f526f20689aa5fa08cf3c268675617f59e56d77e1555b9e49cab07606",
7
+ "identitySha256": "b94b49cdad8c10a7f99ee0189b1981a8d8f3217b345b7e18e29e1f9a8de3c7ed"
8
+ }
@@ -1,9 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import { resolvePath } from "../../core/paths.js";
3
- import { isoFromMtime, record, signalType, textFromContent, } from "./core.js";
3
+ import { authorClassForRole, isoFromMtime, record, signalType, textFromContent, transportProvenance, } from "./core.js";
4
4
  import { isPlainObject, rglob, isFilePath } from "./core.js";
5
5
  import { resolveSqliteCaps, } from "./sqliteCaps.js";
6
- import { PermissionDeniedError, openSqlite, sqliteTimestamp, tableColumns, firstColumn, qualified, } from "./sqliteSessions.js";
6
+ import { PermissionDeniedError, openSqlite, jsonDict, sqliteTimestamp, tableColumns, firstColumn, qualified, } from "./sqliteSessions.js";
7
7
  function copilotDbCandidates(storePath) {
8
8
  if (isFilePath(storePath))
9
9
  return [storePath];
@@ -208,15 +208,18 @@ export function extractCopilotSessions(storePath, errors, ctx) {
208
208
  let index = 0;
209
209
  for (const row of rows) {
210
210
  index += 1;
211
- const role = String(row.role).toLowerCase();
212
- if (role !== "user" && role !== "assistant")
211
+ if (typeof row.role !== "string" || row.role.length === 0)
213
212
  continue;
213
+ const role = row.role.toLowerCase();
214
214
  const content = textFromContent(row.turn_text);
215
215
  const toolItems = copilotRowTools(row, errors);
216
216
  if (!content && toolItems.length === 0)
217
217
  continue;
218
218
  const projectPath = row.project_path ? String(row.project_path) : null;
219
219
  const timestamp = sqliteTimestamp(row.turn_time || row.session_time, fallbackTimestamp);
220
+ const transport = transportProvenance(row, jsonDict(row.turn_data));
221
+ const originId = transport.originId;
222
+ const authorClass = authorClassForRole(role, transport.authorClass, transport.originId);
220
223
  if (content) {
221
224
  const data = { actor: role, content };
222
225
  if (role === "user") {
@@ -237,6 +240,9 @@ export function extractCopilotSessions(storePath, errors, ctx) {
237
240
  sourceProduct: "github-copilot",
238
241
  sourceParts: [resolvePath(dbPath), index, role, content.slice(0, 80)],
239
242
  sessionId: String(row.session_id),
243
+ originId,
244
+ authorClass,
245
+ content,
240
246
  data,
241
247
  }));
242
248
  }
@@ -265,6 +271,9 @@ export function extractCopilotSessions(storePath, errors, ctx) {
265
271
  sourceProduct: "github-copilot",
266
272
  sourceParts: [resolvePath(dbPath), index, "history", content.slice(0, 120)],
267
273
  sessionId: String(row.session_id),
274
+ originId,
275
+ authorClass,
276
+ content,
268
277
  data: { prompt: content, signal_type: sig },
269
278
  }));
270
279
  }
@@ -5,7 +5,7 @@ import { resolveProfileDirOverride, resolveXdgDataHome } from "../../core/envPat
5
5
  import { expanduser } from "../../core/paths.js";
6
6
  import crypto from "node:crypto";
7
7
  import { resolvePath } from "../../core/paths.js";
8
- export const ADAPTER_VERSION = "agentera-v3-corpus-2";
8
+ export const ADAPTER_VERSION = "agentera-v3-corpus-3";
9
9
  export const FAMILIES = [
10
10
  "instruction_document",
11
11
  "history_prompt",
@@ -27,6 +27,19 @@ export const MAX_TOOL_ARG_TEXT = 500;
27
27
  export const MAX_SQLITE_ROWS = 100_000;
28
28
  export const MAX_SQLITE_SESSIONS = 60;
29
29
  export const COPILOT_SPARSE_REMEDIATION = "/chronicle reindex";
30
+ const ORIGIN_ID_RE = /^[a-f0-9]{64}$/;
31
+ const CONTENT_FINGERPRINT_RE = /^[a-f0-9]{64}$/;
32
+ const CONVERSATION_SOURCE_KINDS = new Set(["conversation_turn", "history_prompt"]);
33
+ const PROVENANCE_NESTED_KEYS = ["payload", "item", "metadata", "meta", "provenance", "source"];
34
+ const PROVENANCE_ORIGIN_KEYS = [
35
+ "origin_id",
36
+ "original_origin_id",
37
+ "original_origin",
38
+ "origin",
39
+ "source_origin",
40
+ "instruction_origin",
41
+ ];
42
+ const PROVENANCE_AUTHOR_KEYS = ["author_class", "authorClass", "author"];
30
43
  const DECISION_RE = /\b(decide|decision|prefer|preference|instead|avoid|don't|do not|should|trade[- ]?off|scope|plan|commit|review|fix|why|question|blocked|stuck|approve|reject|change|keep|remove)\b/i;
31
44
  const CORRECTION_RE = /\b(no|not quite|actually|rather|instead|wrong|correction|that's not|that is not|don't|do not)\b/i;
32
45
  const QUESTION_RE = /\?|^\s*(why|what|how|should|can|could|would)\b/i;
@@ -44,6 +57,195 @@ export function stableId(...parts) {
44
57
  const raw = parts.map((p) => pyStr(p)).join("\0");
45
58
  return crypto.createHash("sha256").update(raw, "utf-8").digest("hex").slice(0, 24);
46
59
  }
60
+ /** A bounded, deterministic identity for the source that originally supplied a record. */
61
+ export function originIdentity(origin) {
62
+ if (typeof origin !== "string" || origin.length === 0) {
63
+ throw new TypeError("origin must be a non-empty string");
64
+ }
65
+ return crypto
66
+ .createHash("sha256")
67
+ .update("agentera.corpus.origin.v1\0", "utf-8")
68
+ .update(origin, "utf-8")
69
+ .digest("hex");
70
+ }
71
+ /** A lowercase SHA-256 of the exact source text, without normalization or truncation. */
72
+ export function contentFingerprint(content) {
73
+ if (typeof content !== "string")
74
+ throw new TypeError("content must be a string");
75
+ return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
76
+ }
77
+ function nonEmptyString(value) {
78
+ return typeof value === "string" && value.length > 0;
79
+ }
80
+ function provenanceObjects(sources) {
81
+ const out = [];
82
+ const pending = sources.slice();
83
+ const seen = new Set();
84
+ while (pending.length > 0 && out.length < 24) {
85
+ const value = pending.shift();
86
+ if (!isPlainObject(value) || seen.has(value))
87
+ continue;
88
+ seen.add(value);
89
+ out.push(value);
90
+ for (const key of PROVENANCE_NESTED_KEYS) {
91
+ const nested = value[key];
92
+ if (isPlainObject(nested))
93
+ pending.push(nested);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ function inspectStringField(sources, keys) {
99
+ for (const source of sources) {
100
+ for (const key of keys) {
101
+ if (!Object.prototype.hasOwnProperty.call(source, key))
102
+ continue;
103
+ const value = source[key];
104
+ return nonEmptyString(value)
105
+ ? { state: "valid", key, value }
106
+ : { state: "invalid", key, value: null };
107
+ }
108
+ }
109
+ return { state: "absent", key: null, value: null };
110
+ }
111
+ function transportOriginId(key, value) {
112
+ return ORIGIN_ID_RE.test(value) && key.endsWith("_id") ? value : originIdentity(value);
113
+ }
114
+ /** Inspect the first explicit transport fields without collapsing malformed values into absence. */
115
+ export function inspectTransportProvenance(...sources) {
116
+ const objects = provenanceObjects(sources);
117
+ const origin = inspectStringField(objects, PROVENANCE_ORIGIN_KEYS);
118
+ const author = inspectStringField(objects, PROVENANCE_AUTHOR_KEYS);
119
+ return {
120
+ originId: origin.state === "valid" ? transportOriginId(origin.key, origin.value) : null,
121
+ authorClass: author.state === "valid" ? author.value : null,
122
+ originState: origin.state,
123
+ authorState: author.state,
124
+ };
125
+ }
126
+ /**
127
+ * Read only explicit transport metadata. Content is never inspected to infer
128
+ * either ownership or origin. An adapter may provide an origin digest directly,
129
+ * or a bounded source label that is converted to one.
130
+ */
131
+ export function transportProvenance(...sources) {
132
+ let origin = null;
133
+ let authorClass = null;
134
+ for (const source of provenanceObjects(sources)) {
135
+ if (origin === null) {
136
+ for (const key of PROVENANCE_ORIGIN_KEYS) {
137
+ const value = source[key];
138
+ if (nonEmptyString(value)) {
139
+ origin = transportOriginId(key, value);
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ if (authorClass === null) {
145
+ for (const key of PROVENANCE_AUTHOR_KEYS) {
146
+ const value = source[key];
147
+ if (nonEmptyString(value)) {
148
+ authorClass = value;
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ if (origin !== null && authorClass !== null)
154
+ break;
155
+ }
156
+ return { originId: origin, authorClass };
157
+ }
158
+ /**
159
+ * Map a transport role to a source-author class without examining its text.
160
+ * A claimed transported origin requires the original author to be explicit;
161
+ * the envelope role is not evidence for the source that was transported.
162
+ */
163
+ export function authorClassForRole(role, explicitAuthorClass = null, transportedOriginId = null) {
164
+ const explicit = nonEmptyString(explicitAuthorClass);
165
+ if (!explicit && nonEmptyString(transportedOriginId))
166
+ return null;
167
+ const value = explicit ? explicitAuthorClass : nonEmptyString(role) ? role : null;
168
+ if (value === null)
169
+ return null;
170
+ switch (value.toLowerCase()) {
171
+ case "user":
172
+ return "user";
173
+ case "assistant":
174
+ case "model":
175
+ case "tool":
176
+ return "agent";
177
+ case "system":
178
+ case "developer":
179
+ case "instruction":
180
+ return "injected_instruction";
181
+ default:
182
+ return explicit ? value : null;
183
+ }
184
+ }
185
+ /** Return the bounded provenance fields missing from one retained record. */
186
+ export function missingProvenanceFields(item) {
187
+ const missing = [];
188
+ const required = [
189
+ ["source_id", nonEmptyString(item.source_id)],
190
+ ["source_kind", nonEmptyString(item.source_kind)],
191
+ ["timestamp", nonEmptyString(item.timestamp)],
192
+ ["project_id", nonEmptyString(item.project_id)],
193
+ ["source_class", nonEmptyString(item.source_class)],
194
+ ["source_product", nonEmptyString(item.source_product)],
195
+ ["adapter_version", nonEmptyString(item.adapter_version)],
196
+ ["data", isPlainObject(item.data)],
197
+ ];
198
+ for (const [field, present] of required)
199
+ if (!present)
200
+ missing.push(field);
201
+ if (typeof item.runtime !== "string" && item.runtime !== null)
202
+ missing.push("runtime");
203
+ if (typeof item.active_runtime !== "boolean")
204
+ missing.push("active_runtime");
205
+ if (CONVERSATION_SOURCE_KINDS.has(String(item.source_kind))) {
206
+ if (!ORIGIN_ID_RE.test(String(item.origin_id ?? "")))
207
+ missing.push("origin_id");
208
+ if (!nonEmptyString(item.author_class))
209
+ missing.push("author_class");
210
+ if (!CONTENT_FINGERPRINT_RE.test(String(item.content_fingerprint ?? ""))) {
211
+ missing.push("content_fingerprint");
212
+ }
213
+ if (!nonEmptyString(item.session_id))
214
+ missing.push("session_id");
215
+ }
216
+ return [...new Set(missing)];
217
+ }
218
+ export function assessProvenance(records) {
219
+ const fields = new Set();
220
+ let missingRecords = 0;
221
+ for (const record of records) {
222
+ const missing = missingProvenanceFields(record);
223
+ if (missing.length === 0)
224
+ continue;
225
+ missingRecords += 1;
226
+ for (const field of missing)
227
+ fields.add(field);
228
+ }
229
+ return { complete: missingRecords === 0, missingFields: [...fields].sort(), missingRecords };
230
+ }
231
+ /**
232
+ * Count independent conversation origins without using record IDs or copy
233
+ * counts. Repeated transport of one origin and fingerprint contributes once;
234
+ * a different origin remains independent even when its text is identical.
235
+ */
236
+ export function countIndependentOrigins(records) {
237
+ const identities = new Set();
238
+ for (const record of records) {
239
+ if (!CONVERSATION_SOURCE_KINDS.has(String(record.source_kind)))
240
+ continue;
241
+ const originId = record.origin_id;
242
+ const fingerprint = record.content_fingerprint;
243
+ if (!ORIGIN_ID_RE.test(String(originId ?? "")) || !CONTENT_FINGERPRINT_RE.test(String(fingerprint ?? "")))
244
+ continue;
245
+ identities.add(`${originId}\0${fingerprint}`);
246
+ }
247
+ return identities.size;
248
+ }
47
249
  /** Python str() for the scalar/None values stable_id receives. */
48
250
  function pyStr(value) {
49
251
  if (value === null || value === undefined)
@@ -112,6 +314,12 @@ export function runtimeStatus(runtime, opts) {
112
314
  item.truncation_cap = opts.truncationCap;
113
315
  if (opts.truncationLimit !== null && opts.truncationLimit !== undefined)
114
316
  item.truncation_limit = opts.truncationLimit;
317
+ if (opts.provenanceMissingFields && opts.provenanceMissingFields.length > 0) {
318
+ item.provenance_missing_fields = opts.provenanceMissingFields;
319
+ }
320
+ if (opts.provenanceMissingRecords !== null && opts.provenanceMissingRecords !== undefined) {
321
+ item.provenance_missing_records = opts.provenanceMissingRecords;
322
+ }
115
323
  return item;
116
324
  }
117
325
  function isDir(p) {
@@ -184,7 +392,7 @@ export function discoverRuntimeStore(runtime, storePath, opts = {}) {
184
392
  });
185
393
  }
186
394
  if (!isDir(storePath)) {
187
- return runtimeStatus(runtime, { status: "degraded", reason: "store_not_directory", storePath });
395
+ return runtimeStatus(runtime, { status: "degraded", reason: "store_not_directory", storePath, ...statusOpts });
188
396
  }
189
397
  let candidates;
190
398
  try {
@@ -194,7 +402,7 @@ export function discoverRuntimeStore(runtime, storePath, opts = {}) {
194
402
  candidates = rglob(storePath, pattern);
195
403
  }
196
404
  catch {
197
- return runtimeStatus(runtime, { status: "degraded", reason: "store_unreadable", storePath });
405
+ return runtimeStatus(runtime, { status: "degraded", reason: "store_unreadable", storePath, ...statusOpts });
198
406
  }
199
407
  if (candidates.length === 0) {
200
408
  return runtimeStatus(runtime, {
@@ -241,6 +449,16 @@ export function record(opts) {
241
449
  item.session_id = opts.sessionId;
242
450
  item.conversation_key = opts.sessionId;
243
451
  }
452
+ if (opts.originId)
453
+ item.origin_id = opts.originId;
454
+ else if (opts.origin)
455
+ item.origin_id = originIdentity(opts.origin);
456
+ else if (opts.sessionId)
457
+ item.origin_id = originIdentity(`session:${opts.sessionId}`);
458
+ if (opts.authorClass)
459
+ item.author_class = opts.authorClass;
460
+ if (typeof opts.content === "string")
461
+ item.content_fingerprint = contentFingerprint(opts.content);
244
462
  return item;
245
463
  }
246
464
  function isPlainObject(v) {
@@ -383,6 +601,8 @@ export function toolCallRecordFromItem(args) {
383
601
  activeRuntime: args.activeRuntime,
384
602
  sourceParts: [resolvePath(args.sourcePath), args.index, "tool", toolName],
385
603
  sessionId: args.sessionId,
604
+ originId: transportProvenance(args.event, args.item).originId,
605
+ authorClass: "agent",
386
606
  data: { tool_name: toolName, arguments: argumentsVal }, // cast: parsed JSON IO boundary
387
607
  });
388
608
  }
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { ADAPTER_VERSION, FAMILIES, isoNow, runtimeStatus, discoverRuntimeStore, COPILOT_SPARSE_REMEDIATION, } from "./core.js";
2
+ import { ADAPTER_VERSION, FAMILIES, isoNow, runtimeStatus, discoverRuntimeStore, COPILOT_SPARSE_REMEDIATION, assessProvenance, } from "./core.js";
3
3
  import { isPlainObject } from "./core.js";
4
4
  import { extractInstructionDocuments, extractProjectConfigSignals } from "./filesystemSources.js";
5
5
  import { extractCodexSessions, extractClaudeProjectSessions } from "./jsonlSessions.js";
@@ -36,6 +36,23 @@ function extractRuntimeStore(runtime, storePath, errors, extractor, sqliteCaps,
36
36
  }
37
37
  const fc = (discovery.file_count ?? null); // cast: discovery payload IO boundary
38
38
  const errorCount = errors.length - errorStart;
39
+ const recordProvenance = assessProvenance(records);
40
+ if (!recordProvenance.complete) {
41
+ return [
42
+ records,
43
+ runtimeStatus(runtime, {
44
+ status: "degraded",
45
+ reason: "provenance_missing",
46
+ storePath,
47
+ fileCount: fc,
48
+ recordCount: records.length,
49
+ errorCount,
50
+ provenanceMissingFields: recordProvenance.missingFields,
51
+ provenanceMissingRecords: recordProvenance.missingRecords,
52
+ ...provenance,
53
+ }),
54
+ ];
55
+ }
39
56
  if (errorCount) {
40
57
  return [records, runtimeStatus(runtime, { status: "degraded", reason: "schema_divergent", storePath, fileCount: fc, recordCount: records.length, errorCount, ...provenance })];
41
58
  }
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import crypto from "node:crypto";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { expanduser, resolvePath } from "../../core/paths.js";
7
- import { MAX_TOOL_ARG_TEXT, eventTimestamp, isFilePath, isoFromMtime, iterJsonl, record, signalType, textFromContent, toolCallRecordFromItem, } from "./core.js";
7
+ import { authorClassForRole, MAX_TOOL_ARG_TEXT, eventTimestamp, isFilePath, isoFromMtime, iterJsonl, record, signalType, textFromContent, transportProvenance, toolCallRecordFromItem, } from "./core.js";
8
8
  import { isPlainObject, rglob, isDir } from "./core.js";
9
9
  import { openSqlite, } from "./sqliteSessions.js";
10
10
  function pathStem(p) {
@@ -153,7 +153,7 @@ export function extractCursorSessions(projectsDir, errors, projectRoots = null)
153
153
  for (const event of iterJsonl(p, errors)) {
154
154
  index += 1;
155
155
  const role = event.role;
156
- if (role !== "user" && role !== "assistant")
156
+ if (typeof role !== "string" || role.length === 0)
157
157
  continue;
158
158
  let contentIndex = 0;
159
159
  for (const item of cursorContentItems(event)) {
@@ -180,6 +180,9 @@ export function extractCursorSessions(projectsDir, errors, projectRoots = null)
180
180
  if (!content)
181
181
  continue;
182
182
  const timestamp = eventTimestamp(event, fallbackTimestamp);
183
+ const transport = transportProvenance(event, ...items);
184
+ const originId = transport.originId;
185
+ const authorClass = authorClassForRole(role, transport.authorClass, transport.originId);
183
186
  const data = { actor: role, content };
184
187
  if (role === "user") {
185
188
  if (previousAssistant)
@@ -198,6 +201,9 @@ export function extractCursorSessions(projectsDir, errors, projectRoots = null)
198
201
  runtime: "cursor",
199
202
  sourceParts: [resolvePath(p), index, role, content.slice(0, 80)],
200
203
  sessionId,
204
+ originId,
205
+ authorClass,
206
+ content,
201
207
  data,
202
208
  }));
203
209
  if (role === "user") {
@@ -210,6 +216,9 @@ export function extractCursorSessions(projectsDir, errors, projectRoots = null)
210
216
  runtime: "cursor",
211
217
  sourceParts: [resolvePath(p), index, "history", content.slice(0, 120)],
212
218
  sessionId,
219
+ originId,
220
+ authorClass,
221
+ content,
213
222
  data: { prompt: content, signal_type: sig },
214
223
  }));
215
224
  }
@@ -356,6 +365,8 @@ export function extractCursorAgentSessions(chatsDir, errors, projectRoots = null
356
365
  for (const [blobId, message] of cursorAgentBlobMessages(storeDb, errors)) {
357
366
  index += 1;
358
367
  const role = message.role;
368
+ if (typeof role !== "string" || role.length === 0)
369
+ continue;
359
370
  let toolIndex = 0;
360
371
  for (const toolItem of cursorAgentToolItems(message)) {
361
372
  toolIndex += 1;
@@ -377,12 +388,13 @@ export function extractCursorAgentSessions(chatsDir, errors, projectRoots = null
377
388
  records.push(tr);
378
389
  }
379
390
  }
380
- if (role !== "user" && role !== "assistant")
381
- continue;
382
391
  const content = cursorAgentMessageText(message);
383
392
  if (!content)
384
393
  continue;
385
394
  const timestamp = eventTimestamp(message, fallbackTimestamp);
395
+ const transport = transportProvenance(message);
396
+ const originId = transport.originId;
397
+ const authorClass = authorClassForRole(role, transport.authorClass, transport.originId);
386
398
  const data = { actor: role, content };
387
399
  if (role === "user") {
388
400
  if (previousAssistant)
@@ -402,6 +414,9 @@ export function extractCursorAgentSessions(chatsDir, errors, projectRoots = null
402
414
  sourceProduct: "cursor-agent",
403
415
  sourceParts: [resolvePath(storeDb), blobId, role, content.slice(0, 80)],
404
416
  sessionId,
417
+ originId,
418
+ authorClass,
419
+ content,
405
420
  data,
406
421
  }));
407
422
  if (role === "user") {
@@ -415,6 +430,9 @@ export function extractCursorAgentSessions(chatsDir, errors, projectRoots = null
415
430
  sourceProduct: "cursor-agent",
416
431
  sourceParts: [resolvePath(storeDb), blobId, "history", content.slice(0, 120)],
417
432
  sessionId,
433
+ originId,
434
+ authorClass,
435
+ content,
418
436
  data: { prompt: content, signal_type: sig },
419
437
  }));
420
438
  }