agentera 3.0.0-dev.76 → 3.0.0-dev.77
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 +23 -0
- package/bundle/.agentera-build-source.json +5 -5
- package/bundle/CHANGELOG.md +10 -1
- package/bundle/UPGRADE.md +24 -5
- package/bundle/references/adapters/package-publication.json +4 -4
- package/bundle/references/adapters/package-registry.yaml +153 -151
- package/bundle/references/adapters/product-v1-reset.yaml +101 -0
- package/bundle/references/artifacts/glossary-entry-contract.yaml +163 -9
- package/bundle/references/artifacts/state-storage-authority.yaml +0 -5
- package/bundle/references/cli/update-channels.yaml +1 -1
- package/bundle/references/cli/vocabulary-index.yaml +2 -2
- package/bundle/references/cli/vocabulary.md +2 -3
- package/bundle/references/meta/retained-reference-authority.yaml +5 -0
- package/bundle/skills/agentera/schemas/artifacts/decisions.yaml +1 -20
- package/bundle/skills/agentera/schemas/artifacts/docs.yaml +0 -31
- package/bundle/skills/agentera/schemas/artifacts/experiments.yaml +0 -25
- package/bundle/skills/agentera/schemas/artifacts/glossary.yaml +1 -1
- package/bundle/skills/agentera/schemas/artifacts/health.yaml +1 -36
- package/bundle/skills/agentera/schemas/artifacts/objective.yaml +0 -31
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +1 -35
- package/bundle/skills/agentera/schemas/artifacts/progress.yaml +1 -25
- package/bundle/skills/agentera/schemas/artifacts/vision.yaml +0 -19
- package/dist/.agentera-build-source.json +5 -5
- package/dist/analytics/personalGlossaryAdmission.js +1 -1
- package/dist/analytics/personalGlossaryCandidateProjection.js +96 -0
- package/dist/analytics/personalGlossaryCandidateProjectionModel.js +1 -0
- package/dist/analytics/personalGlossaryRefreshProjection.js +351 -0
- package/dist/capabilities/status/instructions.js +1 -1
- package/dist/cli/commands/personalGlossaryCandidateReads.js +12 -1
- package/dist/cli/commands/prime/briefOrientation.js +1 -1
- package/dist/cli/commands/prime/collectOrientationState.js +0 -7
- package/dist/cli/commands/prime/orientationOutput.js +3 -9
- package/dist/cli/commands/report.js +98 -8
- package/dist/cli/commands/upgrade.js +30 -0
- package/dist/cli/dispatch/index.js +7 -0
- package/dist/cli/dispatch/lifecycle.js +20 -0
- package/dist/cli/help.js +10 -3
- package/dist/cli/orientation/attention.js +1 -4
- package/dist/cli/productV1Eol.js +55 -0
- package/dist/cli/startupCompletenessContract.js +0 -1
- package/dist/cli/stateQuery.js +1 -1
- package/dist/registries/activationTuples.js +11 -2
- package/dist/registries/glossaryCandidateProjectionAuthority.js +154 -6
- package/dist/registries/glossaryCandidateProjectionContract.js +4 -0
- package/dist/registries/glossaryEntryContract.js +3 -3
- package/dist/registries/packagePublication.js +2 -2
- package/dist/runtime/nativeResourceCleanup.js +1 -0
- package/dist/upgrade/legacyAgentCleanup.js +1 -1
- package/dist/upgrade/migrateArtifactsV2ToV3.js +1 -27
- package/dist/upgrade/nextMajorDoctor.js +0 -21
- package/dist/upgrade/productV1Reset.js +515 -0
- package/dist/upgrade/productV1ResetAuthority.js +139 -0
- package/dist/upgrade/projectIntegration.js +4 -6
- package/dist/upgrade/upgradeOrchestrator.js +4 -12
- package/dist/validate/{v1LegacyCruft.js → glossaryVariantGuard.js} +3 -38
- package/package.json +2 -2
- package/dist/cli/commands/prime/v1Migration.js +0 -38
|
@@ -7,40 +7,7 @@
|
|
|
7
7
|
# The active authority uses one plan or plan_task entity per writer-owned file.
|
|
8
8
|
# The aggregate model below is the complete atomic plan-create input. Its task
|
|
9
9
|
# numbers and numeric/string dependency references are create-local symbols,
|
|
10
|
-
# not persisted identities.
|
|
11
|
-
# compatibility input and are not ordinary post-cutover entity contracts.
|
|
12
|
-
#
|
|
13
|
-
# ── Legacy migration-input mapping: v1 PLAN.md → v2 plan.yaml ──
|
|
14
|
-
#
|
|
15
|
-
# The ordinal task fields in this mapping are create-local symbolic references.
|
|
16
|
-
# They are valid only inside one atomic `state plan create --input` document and
|
|
17
|
-
# are removed when the writer publishes plan and plan_task entities.
|
|
18
|
-
#
|
|
19
|
-
# v1 Field v2 Field v2 ID
|
|
20
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
21
|
-
# <!-- Level: light/full --> HEADER.level PL1
|
|
22
|
-
# <!-- Created: date --> HEADER.created PL2
|
|
23
|
-
# <!-- Status: active/completed --> HEADER.status PL3 (normalized)
|
|
24
|
-
# <!-- Reviewed: date --> HEADER.reviewed PL4
|
|
25
|
-
# <!-- Critic issues: N found... --> HEADER.critic_issues PL5
|
|
26
|
-
# <!-- Revised: description --> HEADER.revised PL6
|
|
27
|
-
# # Plan: <title> HEADER.title PL7
|
|
28
|
-
# ## What PLAN.what PL8
|
|
29
|
-
# ## Why PLAN.why PL9
|
|
30
|
-
# ## Constraints PLAN.constraints PL10
|
|
31
|
-
# ## Scope / **In**: <list> SCOPE.included PL11
|
|
32
|
-
# ## Scope / **Out**: <list> SCOPE.excluded PL12
|
|
33
|
-
# ## Scope / **Deferred**: <list> SCOPE.deferred PL13
|
|
34
|
-
# ## Design PLAN.design PL14
|
|
35
|
-
# ### Task N: <name> TASK.number PL15 (create-local only)
|
|
36
|
-
# (task name from heading) TASK.name PL16
|
|
37
|
-
# **Depends on**: <text> TASK.depends_on PL17
|
|
38
|
-
# **Status**: <status> TASK.status PL18
|
|
39
|
-
# **Acceptance**: <criteria list> TASK.acceptance PL19
|
|
40
|
-
# ## Overall Acceptance PLAN.overall_acceptance PL20
|
|
41
|
-
# ## Surprises PLAN.surprises PL21
|
|
42
|
-
# ## Unknowns UNKNOWN.entry PU1
|
|
43
|
-
# ## Rejected Review Findings REJECTED.entry PR1
|
|
10
|
+
# not persisted identities.
|
|
44
11
|
#
|
|
45
12
|
# Protocol references:
|
|
46
13
|
# status visual tokens: VT1-VT4 (protocol.yaml VISUAL_TOKENS)
|
|
@@ -726,7 +693,6 @@ LIFECYCLE_CONTRACT:
|
|
|
726
693
|
- packages/cli/src/upgrade/migrateArtifacts*.ts
|
|
727
694
|
- packages/cli/src/upgrade/upgradeOrchestrator.ts
|
|
728
695
|
- packages/cli/src/upgrade/doctor.ts
|
|
729
|
-
- packages/cli/src/cli/commands/prime/v1Migration.ts
|
|
730
696
|
- packages/cli/src/state/entityMigration*.ts
|
|
731
697
|
- packages/cli/src/state/canonicalMigrationRecord.ts
|
|
732
698
|
schemas:
|
|
@@ -10,31 +10,7 @@
|
|
|
10
10
|
# bounded current writer intent for Build and never delivers or persists it in
|
|
11
11
|
# plan state. Prime projects generic review attention without lifecycle details.
|
|
12
12
|
#
|
|
13
|
-
# The active authority is one progress_cycle entity per writer-owned file.
|
|
14
|
-
# numbered projection model below is retained only as an exact legacy migration
|
|
15
|
-
# input schema; it is not an ordinary read or write contract after cutover.
|
|
16
|
-
#
|
|
17
|
-
# ── Field-by-field mapping: v1 PROGRESS.md → v2 progress.yaml ──
|
|
18
|
-
#
|
|
19
|
-
# v1 Field v2 Field v2 ID
|
|
20
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
21
|
-
# ## Cycle N · date · type-prefix CYCLE.number PR1
|
|
22
|
-
# (date from heading) CYCLE.timestamp PR2
|
|
23
|
-
# (type-prefix from heading) CYCLE.type PR3
|
|
24
|
-
# **Phase**: <phase> CYCLE.phase PR4
|
|
25
|
-
# **What**: <text> CYCLE.what PR5
|
|
26
|
-
# **Inspiration**: <text> CYCLE.inspiration PR6
|
|
27
|
-
# **Discovered**: <text> CYCLE.discovered PR7
|
|
28
|
-
# **Verified**: <text> CYCLE.verified PR8
|
|
29
|
-
# **Next**: <text> CYCLE.next PR9
|
|
30
|
-
# **Context**: <text> CYCLE.context PR10
|
|
31
|
-
# (intent from Context) CONTEXT.intent PT1
|
|
32
|
-
# (constraints from Context) CONTEXT.constraints PT2
|
|
33
|
-
# (unknowns from Context) CONTEXT.unknowns PT3
|
|
34
|
-
# (scope from Context) CONTEXT.scope PT4
|
|
35
|
-
# ## Archived Cycles (implicit section) —
|
|
36
|
-
# - Cycle N (date): summary ARCHIVE.summary PA1
|
|
37
|
-
#
|
|
13
|
+
# The active authority is one progress_cycle entity per writer-owned file.
|
|
38
14
|
# Protocol references:
|
|
39
15
|
# phase: PH1-PH5 (protocol.yaml PHASES)
|
|
40
16
|
# type: conventional commit prefix (feat/fix/docs/refactor/chore/test)
|
|
@@ -7,25 +7,6 @@
|
|
|
7
7
|
# Structural pattern: UPPER_CASE groups with numbered entries and stable IDs,
|
|
8
8
|
# following capability_schema_contract.yaml and protocol.yaml conventions.
|
|
9
9
|
#
|
|
10
|
-
# ── Field-by-field mapping: v1 VISION.md → v2 vision.yaml ──
|
|
11
|
-
#
|
|
12
|
-
# v1 Field v2 Field v2 ID
|
|
13
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
14
|
-
# # <project name> HEADER.project_name VN1
|
|
15
|
-
# ## North Star NORTH_STAR.content VN2
|
|
16
|
-
# ## Who It's For / ### persona name PERSONA.entry VN3
|
|
17
|
-
# (persona heading) PERSONA.name VN4
|
|
18
|
-
# (persona prose) PERSONA.description VN5
|
|
19
|
-
# ## Principles / - principle PRINCIPLE.entry VN6
|
|
20
|
-
# (bold name + description) PRINCIPLE.name VN7
|
|
21
|
-
# (principle prose) PRINCIPLE.description VN8
|
|
22
|
-
# ## Direction DIRECTION.content VN9
|
|
23
|
-
# ## Identity / ### Personality IDENTITY.personality VN10
|
|
24
|
-
# ## Identity / ### Voice IDENTITY.voice VN11
|
|
25
|
-
# ## Identity / ### Emotional register IDENTITY.emotional_register VN12
|
|
26
|
-
# ## Identity / ### Naming IDENTITY.naming VN13
|
|
27
|
-
# ## The Tension TENSION.content VN14
|
|
28
|
-
#
|
|
29
10
|
# Protocol references:
|
|
30
11
|
# No protocol primitives directly. Vision defines project-level identity
|
|
31
12
|
# that capabilities reference for voice and personality consistency.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "agentera.generatedBuildSource.v1",
|
|
3
|
-
"commit": "
|
|
4
|
-
"tree": "
|
|
5
|
-
"files":
|
|
6
|
-
"workingTreeSha256": "
|
|
7
|
-
"identitySha256": "
|
|
3
|
+
"commit": "df1d7a16aa38028099462135eed9f3485e9b2976",
|
|
4
|
+
"tree": "92881acdddaeb9e0cda57eb37a66ef839437b418",
|
|
5
|
+
"files": 6815,
|
|
6
|
+
"workingTreeSha256": "aa5a547104def37dc4cad08cc57312e5b8ed1693db759ca573f436542a4bed77",
|
|
7
|
+
"identitySha256": "1d8e4406fbd4e18f39e2d32eaf8f9494a78e17a1b225e5d8cd5ba1fd5c652728"
|
|
8
8
|
}
|
|
@@ -108,6 +108,6 @@ export function admitPersonalGlossaryEvidence(input, resolveAnchor = resolveEvid
|
|
|
108
108
|
}
|
|
109
109
|
export { classifyExplicitGlossaryLanguage, discoverExplicitGlossaryCues, mineExplicitGlossaryCandidates, mineExplicitGlossaryEvidence, minePersonalExplicitGlossaryCandidates, EXPLICIT_GLOSSARY_REASONS, } from "./personalGlossaryExplicit.js";
|
|
110
110
|
export { mineRecurringGlossaryCandidates, mineRecurringGlossaryEvidence, PERSONAL_GLOSSARY_MINING_POLICY_VERSION, RECURRING_MEANING_PENDING_REVIEW, RECURRING_REASONS, } from "./personalGlossaryRecurrence.js";
|
|
111
|
-
export { maintainPersonalGlossaryCandidateProjection, personalGlossaryCandidateProjectionPath, personalGlossaryProjectionProjectIdentity, persistPersonalGlossaryCandidateProjection, projectPersonalGlossaryCandidates, readPersonalGlossaryCandidateProjection, } from "./personalGlossaryCandidateProjection.js";
|
|
111
|
+
export { maintainPersonalGlossaryCandidateProjection, personalGlossaryCandidateProjectionPath, personalGlossaryProjectionProjectIdentity, persistPersonalGlossaryCandidateProjection, persistPersonalGlossaryCandidateProjectionAfterRefresh, projectPersonalGlossaryCandidates, readPersonalGlossaryCandidateProjection, } from "./personalGlossaryCandidateProjection.js";
|
|
112
112
|
export { decidePersonalGlossaryCandidate, } from "./personalGlossaryDecision.js";
|
|
113
113
|
export { currentPersonalGlossaryReviewRecords, maintainPersonalGlossaryReviewRecords, personalGlossaryReviewRecordsPath, queuePersonalGlossaryReviewRecord, readPersonalGlossaryReviewRecords, } from "./personalGlossaryReviewRecords.js";
|
|
@@ -7,6 +7,7 @@ import { defaultProfileDir } from "./extractCorpus/core.js";
|
|
|
7
7
|
import { EXCERPT_OMISSION_REASONS, containsPersonalGlossarySensitiveContent, personalGlossaryCandidateProjectionExcerptExpiry, selectPersonalGlossarySafeExcerpt, validPersonalGlossarySafeExcerpt, } from "./personalGlossaryCandidateProjectionExcerpts.js";
|
|
8
8
|
const PROJECTION_SCHEMA_VERSION = "agentera.personalGlossaryCandidateProjection.v1";
|
|
9
9
|
const PROJECTION_REPORT_SCHEMA_VERSION = "agentera.personalGlossaryCandidateProjectionReport.v1";
|
|
10
|
+
const MINING_SUMMARY_SCHEMA_VERSION = "agentera.personalGlossaryMiningSummary.v1";
|
|
10
11
|
const PROJECT_IDENTITY_SCHEMA_VERSION = "agentera.personalGlossaryProjectionProjectIdentity.v1";
|
|
11
12
|
const PROJECTION_OWNER = "deterministic_discovery_projection";
|
|
12
13
|
const SHA256_RE = /^[a-f0-9]{64}$/u;
|
|
@@ -50,6 +51,9 @@ function projectionContract() {
|
|
|
50
51
|
contract.storageFile !== "candidate-projection.json" ||
|
|
51
52
|
contract.candidateSecretReason !== "secret_content" ||
|
|
52
53
|
contract.excerptSensitiveContentAction !== "omit_complete_excerpt_before_projection" ||
|
|
54
|
+
contract.miningSummarySchemaVersion !== MINING_SUMMARY_SCHEMA_VERSION ||
|
|
55
|
+
contract.explicitAbstentionKeys.length !== 24 ||
|
|
56
|
+
contract.recurringAbstentionKeys.length !== 12 ||
|
|
53
57
|
JSON.stringify(Object.keys(families)) !== JSON.stringify(["explicit", "recurring"]) ||
|
|
54
58
|
JSON.stringify(families.explicit) !== JSON.stringify(["personal_explicit_definition"]) ||
|
|
55
59
|
JSON.stringify(families.recurring) !==
|
|
@@ -58,6 +62,61 @@ function projectionContract() {
|
|
|
58
62
|
}
|
|
59
63
|
return contract;
|
|
60
64
|
}
|
|
65
|
+
function zeroReasons(keys) {
|
|
66
|
+
return Object.fromEntries(keys.map((key) => [key, 0]));
|
|
67
|
+
}
|
|
68
|
+
function projectionMiningSummary(input, contract) {
|
|
69
|
+
if (input.mining_summary)
|
|
70
|
+
return input.mining_summary;
|
|
71
|
+
let explicit = 0;
|
|
72
|
+
let recurring = 0;
|
|
73
|
+
for (const candidate of input.candidates) {
|
|
74
|
+
if (candidate.capsule.provenance_kind === "personal_explicit_definition")
|
|
75
|
+
explicit += 1;
|
|
76
|
+
else
|
|
77
|
+
recurring += 1;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
schema_version: MINING_SUMMARY_SCHEMA_VERSION,
|
|
81
|
+
explicit: {
|
|
82
|
+
candidate_count: explicit,
|
|
83
|
+
abstention_count: 0,
|
|
84
|
+
abstentions_by_reason: zeroReasons(contract.explicitAbstentionKeys),
|
|
85
|
+
},
|
|
86
|
+
recurring: {
|
|
87
|
+
candidate_count: recurring,
|
|
88
|
+
abstention_count: 0,
|
|
89
|
+
abstentions_by_reason: zeroReasons(contract.recurringAbstentionKeys),
|
|
90
|
+
},
|
|
91
|
+
total_candidate_count: explicit + recurring,
|
|
92
|
+
total_abstention_count: 0,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function validMiningFamily(value, keys) {
|
|
96
|
+
const family = mapping(value);
|
|
97
|
+
const reasons = mapping(family?.abstentions_by_reason);
|
|
98
|
+
return family !== null && reasons !== null &&
|
|
99
|
+
exactKeys(family, ["candidate_count", "abstention_count", "abstentions_by_reason"]) &&
|
|
100
|
+
nonNegativeInteger(family.candidate_count) && nonNegativeInteger(family.abstention_count) &&
|
|
101
|
+
exactKeys(reasons, keys) && Object.values(reasons).every(nonNegativeInteger) &&
|
|
102
|
+
family.abstention_count === Object.values(reasons).reduce((sum, count) => sum + Number(count), 0);
|
|
103
|
+
}
|
|
104
|
+
function validMiningSummary(value, inputCount, contract) {
|
|
105
|
+
const summary = mapping(value);
|
|
106
|
+
if (summary === null || !exactKeys(summary, [
|
|
107
|
+
"schema_version", "explicit", "recurring", "total_candidate_count", "total_abstention_count",
|
|
108
|
+
]) || summary.schema_version !== MINING_SUMMARY_SCHEMA_VERSION ||
|
|
109
|
+
!validMiningFamily(summary.explicit, contract.explicitAbstentionKeys) ||
|
|
110
|
+
!validMiningFamily(summary.recurring, contract.recurringAbstentionKeys) ||
|
|
111
|
+
!nonNegativeInteger(summary.total_candidate_count) ||
|
|
112
|
+
!nonNegativeInteger(summary.total_abstention_count))
|
|
113
|
+
return false;
|
|
114
|
+
const explicit = summary.explicit;
|
|
115
|
+
const recurring = summary.recurring;
|
|
116
|
+
return summary.total_candidate_count === explicit.candidate_count + recurring.candidate_count &&
|
|
117
|
+
summary.total_candidate_count === inputCount &&
|
|
118
|
+
summary.total_abstention_count === explicit.abstention_count + recurring.abstention_count;
|
|
119
|
+
}
|
|
61
120
|
function sourceFamily(capsule, contract) {
|
|
62
121
|
const matches = Object.entries(contract.sourceFamilies)
|
|
63
122
|
.filter(([, kinds]) => kinds.includes(capsule.provenance_kind))
|
|
@@ -218,6 +277,10 @@ export function projectPersonalGlossaryCandidates(input) {
|
|
|
218
277
|
if (!Array.isArray(input.candidates)) {
|
|
219
278
|
throw new TypeError("projection candidates must be a list");
|
|
220
279
|
}
|
|
280
|
+
const miningSummary = projectionMiningSummary(input, contract);
|
|
281
|
+
if (!validMiningSummary(miningSummary, input.candidates.length, contract)) {
|
|
282
|
+
throw new TypeError("candidate mining summary is invalid");
|
|
283
|
+
}
|
|
221
284
|
const merged = mergeCandidates(input, contract);
|
|
222
285
|
const selected = selectCandidates(merged.candidates, contract);
|
|
223
286
|
const availableByFamily = countByFamily(merged.candidates);
|
|
@@ -295,6 +358,7 @@ export function projectPersonalGlossaryCandidates(input) {
|
|
|
295
358
|
expired: 0,
|
|
296
359
|
omissions,
|
|
297
360
|
},
|
|
361
|
+
mining_summary: miningSummary,
|
|
298
362
|
};
|
|
299
363
|
const projection = {
|
|
300
364
|
schema_version: PROJECTION_SCHEMA_VERSION,
|
|
@@ -378,6 +442,7 @@ function validProjectionReport(value) {
|
|
|
378
442
|
"projects",
|
|
379
443
|
"coverage",
|
|
380
444
|
"excerpts",
|
|
445
|
+
"mining_summary",
|
|
381
446
|
]) ||
|
|
382
447
|
report.schema_version !== PROJECTION_REPORT_SCHEMA_VERSION ||
|
|
383
448
|
![
|
|
@@ -415,6 +480,7 @@ function validProjectionReport(value) {
|
|
|
415
480
|
!Array.isArray(coverage.uncovered_source_families) ||
|
|
416
481
|
coverage.uncovered_source_families.some((family) => family !== "explicit" && family !== "recurring") ||
|
|
417
482
|
!nonNegativeInteger(coverage.uncovered_projects) ||
|
|
483
|
+
!validMiningSummary(report.mining_summary, Number(report.input_count), projectionContract()) ||
|
|
418
484
|
excerpts === null ||
|
|
419
485
|
!exactKeys(excerpts, [
|
|
420
486
|
"provided",
|
|
@@ -645,6 +711,36 @@ export function persistPersonalGlossaryCandidateProjection(projection, options =
|
|
|
645
711
|
privateWrite(pathname, text);
|
|
646
712
|
return { status: "changed", path: pathname };
|
|
647
713
|
}
|
|
714
|
+
/** Persist after explicit refresh consent, replacing only a malformed owned regular file. */
|
|
715
|
+
export function persistPersonalGlossaryCandidateProjectionAfterRefresh(projection, options = {}) {
|
|
716
|
+
if (!validProjection(projection))
|
|
717
|
+
throw new TypeError("candidate projection is invalid");
|
|
718
|
+
const pathname = personalGlossaryCandidateProjectionPath(options);
|
|
719
|
+
const root = path.resolve(defaultProfileDir(options.env ?? process.env, options.platform ?? process.platform), "intermediate", "personal-glossary");
|
|
720
|
+
if (path.resolve(pathname) !== path.join(root, projectionContract().storageFile)) {
|
|
721
|
+
throw new TypeError("candidate projection path escapes its configured storage root");
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const metadata = fs.lstatSync(pathname);
|
|
725
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
726
|
+
throw new TypeError("candidate projection replacement requires the exact regular file");
|
|
727
|
+
}
|
|
728
|
+
const text = `${canonicalGlossaryJson(projection)}\n`;
|
|
729
|
+
const current = readPersonalGlossaryCandidateProjection(options);
|
|
730
|
+
if (current.status === "current" && fs.readFileSync(pathname, "utf8") === text) {
|
|
731
|
+
ensurePrivateProjectionMode(pathname);
|
|
732
|
+
return { status: "unchanged_replay", path: pathname };
|
|
733
|
+
}
|
|
734
|
+
privateWrite(pathname, text);
|
|
735
|
+
return { status: "changed", path: pathname };
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
if (error.code !== "ENOENT")
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
privateWrite(pathname, `${canonicalGlossaryJson(projection)}\n`);
|
|
742
|
+
return { status: "changed", path: pathname };
|
|
743
|
+
}
|
|
648
744
|
/**
|
|
649
745
|
* Apply only expiry or a local-host-authorized purge to the private projection.
|
|
650
746
|
* No CLI command exposes this primitive. The later review lifecycle owns the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { personalGlossaryCandidateProjectionContract } from "../registries/glossaryCandidateProjectionContract.js";
|
|
5
|
+
import { PERSONAL_GLOSSARY_MINING_POLICY_VERSION } from "../registries/glossaryMiningAuthority.js";
|
|
6
|
+
import { readCurrentGeneration } from "./extractCorpus/evidenceTiers.js";
|
|
7
|
+
import { mineExplicitGlossaryCandidates } from "./personalGlossaryExplicitMining.js";
|
|
8
|
+
import { mineRecurringGlossaryCandidates } from "./personalGlossaryRecurrence.js";
|
|
9
|
+
import { personalGlossaryCandidateProjectionPath, persistPersonalGlossaryCandidateProjectionAfterRefresh, projectPersonalGlossaryCandidates, } from "./personalGlossaryCandidateProjection.js";
|
|
10
|
+
const LOCK_SCHEMA_VERSION = "agentera.personalGlossaryRefreshLock.v1";
|
|
11
|
+
const MAX_LOCK_BYTES = 8 * 1024;
|
|
12
|
+
const FILE_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
13
|
+
const TOKEN_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
14
|
+
export class PersonalGlossaryRefreshCommitLockError extends Error {
|
|
15
|
+
recovery;
|
|
16
|
+
constructor(message, recovery) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.recovery = recovery;
|
|
19
|
+
this.name = "PersonalGlossaryRefreshCommitLockError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class PersonalGlossaryRefreshCommitBusyError extends PersonalGlossaryRefreshCommitLockError {
|
|
23
|
+
constructor(message = "another consented refresh is still publishing the current candidate projection") {
|
|
24
|
+
super(message, "npx -y agentera@next report refresh --consent local-history");
|
|
25
|
+
this.name = "PersonalGlossaryRefreshCommitBusyError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function unsafeLock(lockPath, detail) {
|
|
29
|
+
throw new PersonalGlossaryRefreshCommitLockError(`personal glossary refresh lock is unsafe: ${detail}`, `Inspect ${lockPath}; remove it only after verifying no refresh owns it, then rerun npx -y agentera@next report refresh --consent local-history`);
|
|
30
|
+
}
|
|
31
|
+
function identity(fd) {
|
|
32
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
33
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
34
|
+
}
|
|
35
|
+
function sameIdentity(left, right) {
|
|
36
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
37
|
+
}
|
|
38
|
+
function sameRecord(left, right) {
|
|
39
|
+
return left.schema_version === right.schema_version
|
|
40
|
+
&& left.pid === right.pid
|
|
41
|
+
&& left.token === right.token
|
|
42
|
+
&& left.created_at === right.created_at;
|
|
43
|
+
}
|
|
44
|
+
function parseRecord(value) {
|
|
45
|
+
if (value === null
|
|
46
|
+
|| typeof value !== "object"
|
|
47
|
+
|| value.schema_version !== LOCK_SCHEMA_VERSION
|
|
48
|
+
|| !Number.isSafeInteger(value.pid)
|
|
49
|
+
|| value.pid <= 0
|
|
50
|
+
|| typeof value.token !== "string"
|
|
51
|
+
|| !TOKEN_PATTERN.test(value.token)
|
|
52
|
+
|| typeof value.created_at !== "string"
|
|
53
|
+
|| !Number.isFinite(Date.parse(value.created_at)))
|
|
54
|
+
return null;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function readRecord(fd) {
|
|
58
|
+
try {
|
|
59
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
60
|
+
if (!stat.isFile() || stat.size === 0n || stat.size > BigInt(MAX_LOCK_BYTES))
|
|
61
|
+
return null;
|
|
62
|
+
const bytes = Buffer.alloc(Number(stat.size));
|
|
63
|
+
let offset = 0;
|
|
64
|
+
while (offset < bytes.length) {
|
|
65
|
+
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
66
|
+
if (count === 0)
|
|
67
|
+
return null;
|
|
68
|
+
offset += count;
|
|
69
|
+
}
|
|
70
|
+
return parseRecord(JSON.parse(bytes.toString("utf8")));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function openOwnedRecord(recordPath, label) {
|
|
77
|
+
let stat;
|
|
78
|
+
try {
|
|
79
|
+
stat = fs.lstatSync(recordPath);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code === "ENOENT")
|
|
83
|
+
return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (stat.isSymbolicLink())
|
|
87
|
+
unsafeLock(recordPath, `${label} is a symbolic link`);
|
|
88
|
+
if (!stat.isFile())
|
|
89
|
+
unsafeLock(recordPath, `${label} is not a regular file`);
|
|
90
|
+
let descriptor;
|
|
91
|
+
try {
|
|
92
|
+
descriptor = fs.openSync(recordPath, FILE_FLAGS);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error.code === "ENOENT")
|
|
96
|
+
unsafeLock(recordPath, `${label} changed during inspection`);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const opened = fs.fstatSync(descriptor);
|
|
100
|
+
if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino) {
|
|
101
|
+
fs.closeSync(descriptor);
|
|
102
|
+
unsafeLock(recordPath, `${label} changed identity during inspection`);
|
|
103
|
+
}
|
|
104
|
+
const record = readRecord(descriptor);
|
|
105
|
+
if (!record) {
|
|
106
|
+
fs.closeSync(descriptor);
|
|
107
|
+
unsafeLock(recordPath, `${label} has a malformed or foreign ownership record`);
|
|
108
|
+
}
|
|
109
|
+
return { descriptor, record };
|
|
110
|
+
}
|
|
111
|
+
function ownerState(pid) {
|
|
112
|
+
if (pid === process.pid)
|
|
113
|
+
return "live";
|
|
114
|
+
try {
|
|
115
|
+
process.kill(pid, 0);
|
|
116
|
+
return "live";
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return error.code === "ESRCH" ? "dead" : "indeterminate";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function pathMatches(pathname, descriptor) {
|
|
123
|
+
let observed;
|
|
124
|
+
try {
|
|
125
|
+
observed = fs.openSync(pathname, FILE_FLAGS);
|
|
126
|
+
return sameIdentity(identity(observed), identity(descriptor));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (observed !== undefined)
|
|
133
|
+
fs.closeSync(observed);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function unlinkOwned(pathname, descriptor) {
|
|
137
|
+
if (!pathMatches(pathname, descriptor))
|
|
138
|
+
return false;
|
|
139
|
+
try {
|
|
140
|
+
fs.unlinkSync(pathname);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error.code === "ENOENT")
|
|
145
|
+
return false;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function prepareRecord(directory, basename, record) {
|
|
150
|
+
const preparedPath = path.join(directory, `.${basename}.${record.token}.tmp`);
|
|
151
|
+
const descriptor = fs.openSync(preparedPath, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
152
|
+
try {
|
|
153
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`);
|
|
154
|
+
fs.fsyncSync(descriptor);
|
|
155
|
+
return { path: preparedPath, descriptor };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
fs.closeSync(descriptor);
|
|
159
|
+
fs.rmSync(preparedPath, { force: true });
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function publishPrepared(prepared, targetPath) {
|
|
164
|
+
try {
|
|
165
|
+
fs.linkSync(prepared.path, targetPath);
|
|
166
|
+
try {
|
|
167
|
+
fs.unlinkSync(prepared.path);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
unlinkOwned(targetPath, prepared.descriptor);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error.code === "EEXIST")
|
|
177
|
+
return false;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function removeDeadClaim(claimPath) {
|
|
182
|
+
const claim = openOwnedRecord(claimPath, "reclaim claim");
|
|
183
|
+
if (!claim)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
const state = ownerState(claim.record.pid);
|
|
187
|
+
if (state === "live")
|
|
188
|
+
throw new PersonalGlossaryRefreshCommitBusyError("another refresh is reclaiming an orphaned personal glossary refresh lock");
|
|
189
|
+
if (state === "indeterminate")
|
|
190
|
+
unsafeLock(claimPath, "reclaim claim owner liveness is indeterminate");
|
|
191
|
+
if (!unlinkOwned(claimPath, claim.descriptor))
|
|
192
|
+
unsafeLock(claimPath, "reclaim claim changed identity before recovery");
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
fs.closeSync(claim.descriptor);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function reclaimDeadOwner(lockPath, current, prepared, record) {
|
|
199
|
+
const claimPath = `${lockPath}.reclaim`;
|
|
200
|
+
const claim = prepareRecord(path.dirname(lockPath), path.basename(claimPath), record);
|
|
201
|
+
if (!publishPrepared(claim, claimPath)) {
|
|
202
|
+
fs.rmSync(claim.path, { force: true });
|
|
203
|
+
fs.closeSync(claim.descriptor);
|
|
204
|
+
throw new PersonalGlossaryRefreshCommitBusyError("another refresh is reclaiming an orphaned personal glossary refresh lock");
|
|
205
|
+
}
|
|
206
|
+
let adopted = false;
|
|
207
|
+
try {
|
|
208
|
+
const observed = readRecord(current.descriptor);
|
|
209
|
+
if (!pathMatches(lockPath, current.descriptor) || !observed || !sameRecord(observed, current.record)) {
|
|
210
|
+
unsafeLock(lockPath, "owner changed identity before orphan recovery");
|
|
211
|
+
}
|
|
212
|
+
const state = ownerState(current.record.pid);
|
|
213
|
+
if (state === "live")
|
|
214
|
+
throw new PersonalGlossaryRefreshCommitBusyError();
|
|
215
|
+
if (state === "indeterminate")
|
|
216
|
+
unsafeLock(lockPath, "owner liveness is indeterminate");
|
|
217
|
+
if (!unlinkOwned(lockPath, current.descriptor))
|
|
218
|
+
unsafeLock(lockPath, "owner changed identity during orphan recovery");
|
|
219
|
+
if (!publishPrepared(prepared, lockPath))
|
|
220
|
+
unsafeLock(lockPath, "a successor appeared during orphan recovery");
|
|
221
|
+
if (!unlinkOwned(claimPath, claim.descriptor)) {
|
|
222
|
+
unlinkOwned(lockPath, prepared.descriptor);
|
|
223
|
+
unsafeLock(claimPath, "reclaim claim changed identity before release");
|
|
224
|
+
}
|
|
225
|
+
adopted = true;
|
|
226
|
+
return { descriptor: prepared.descriptor, path: lockPath, record };
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
if (!adopted)
|
|
230
|
+
unlinkOwned(claimPath, claim.descriptor);
|
|
231
|
+
fs.closeSync(claim.descriptor);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Exclude another consented refresh until evidence and its projection are committed together. */
|
|
235
|
+
export function acquirePersonalGlossaryRefreshCommitLock(options = {}) {
|
|
236
|
+
const directory = path.dirname(personalGlossaryCandidateProjectionPath(options));
|
|
237
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
238
|
+
fs.chmodSync(directory, 0o700);
|
|
239
|
+
const lockPath = path.join(directory, ".refresh.lock");
|
|
240
|
+
removeDeadClaim(`${lockPath}.reclaim`);
|
|
241
|
+
const record = {
|
|
242
|
+
schema_version: LOCK_SCHEMA_VERSION,
|
|
243
|
+
pid: process.pid,
|
|
244
|
+
token: randomUUID(),
|
|
245
|
+
created_at: new Date().toISOString(),
|
|
246
|
+
};
|
|
247
|
+
const prepared = prepareRecord(directory, path.basename(lockPath), record);
|
|
248
|
+
try {
|
|
249
|
+
if (publishPrepared(prepared, lockPath))
|
|
250
|
+
return { descriptor: prepared.descriptor, path: lockPath, record };
|
|
251
|
+
const current = openOwnedRecord(lockPath, "owner");
|
|
252
|
+
if (!current)
|
|
253
|
+
unsafeLock(lockPath, "owner disappeared during inspection");
|
|
254
|
+
try {
|
|
255
|
+
const state = ownerState(current.record.pid);
|
|
256
|
+
if (state === "live")
|
|
257
|
+
throw new PersonalGlossaryRefreshCommitBusyError();
|
|
258
|
+
if (state === "indeterminate")
|
|
259
|
+
unsafeLock(lockPath, "owner liveness is indeterminate");
|
|
260
|
+
return reclaimDeadOwner(lockPath, current, prepared, record);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
fs.closeSync(current.descriptor);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (pathMatches(prepared.path, prepared.descriptor))
|
|
268
|
+
fs.rmSync(prepared.path, { force: true });
|
|
269
|
+
fs.closeSync(prepared.descriptor);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function releasePersonalGlossaryRefreshCommitLock(lock) {
|
|
274
|
+
try {
|
|
275
|
+
if (pathMatches(lock.path, lock.descriptor)) {
|
|
276
|
+
const observed = readRecord(lock.descriptor);
|
|
277
|
+
if (observed && sameRecord(observed, lock.record))
|
|
278
|
+
unlinkOwned(lock.path, lock.descriptor);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
fs.closeSync(lock.descriptor);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function familySummary(keys, candidateCount, abstentions) {
|
|
286
|
+
const counts = Object.fromEntries(keys.map((key) => [key, 0]));
|
|
287
|
+
for (const abstention of abstentions) {
|
|
288
|
+
if (!(abstention.reason in counts))
|
|
289
|
+
throw new TypeError("mining produced an unknown abstention reason");
|
|
290
|
+
counts[abstention.reason] += 1;
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
candidate_count: candidateCount,
|
|
294
|
+
abstention_count: abstentions.length,
|
|
295
|
+
abstentions_by_reason: counts,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Mine and publish the projection bound to the current successfully published evidence generation. */
|
|
299
|
+
export function produceCurrentPersonalGlossaryProjection(options) {
|
|
300
|
+
const before = readCurrentGeneration(options.tiersDir);
|
|
301
|
+
if (!before)
|
|
302
|
+
throw new TypeError("current evidence generation is unavailable");
|
|
303
|
+
const explicit = mineExplicitGlossaryCandidates({ tiersDir: options.tiersDir });
|
|
304
|
+
const recurring = mineRecurringGlossaryCandidates({ tiersDir: options.tiersDir });
|
|
305
|
+
const after = readCurrentGeneration(options.tiersDir);
|
|
306
|
+
if (!after || explicit.generation !== before.manifest.generation ||
|
|
307
|
+
recurring.generation !== before.manifest.generation ||
|
|
308
|
+
after.manifest.generation !== before.manifest.generation) {
|
|
309
|
+
throw new TypeError("current evidence generation changed during projection production");
|
|
310
|
+
}
|
|
311
|
+
const contract = personalGlossaryCandidateProjectionContract();
|
|
312
|
+
const explicitSummary = familySummary(contract.explicitAbstentionKeys, explicit.candidates.length, explicit.abstentions);
|
|
313
|
+
const recurringSummary = familySummary(contract.recurringAbstentionKeys, recurring.candidates.length, recurring.abstentions);
|
|
314
|
+
const miningSummary = {
|
|
315
|
+
schema_version: "agentera.personalGlossaryMiningSummary.v1",
|
|
316
|
+
explicit: explicitSummary,
|
|
317
|
+
recurring: recurringSummary,
|
|
318
|
+
total_candidate_count: explicitSummary.candidate_count + recurringSummary.candidate_count,
|
|
319
|
+
total_abstention_count: explicitSummary.abstention_count + recurringSummary.abstention_count,
|
|
320
|
+
};
|
|
321
|
+
const projection = projectPersonalGlossaryCandidates({
|
|
322
|
+
generation: before.manifest.generation,
|
|
323
|
+
policy_version: PERSONAL_GLOSSARY_MINING_POLICY_VERSION,
|
|
324
|
+
retained_at: before.manifest.published_at,
|
|
325
|
+
candidates: [
|
|
326
|
+
...explicit.candidates.map((candidate) => ({
|
|
327
|
+
capsule: candidate.capsule,
|
|
328
|
+
project_ids: candidate.project_ids,
|
|
329
|
+
})),
|
|
330
|
+
...recurring.candidates.map((candidate) => ({
|
|
331
|
+
capsule: candidate.capsule,
|
|
332
|
+
project_ids: candidate.project_ids,
|
|
333
|
+
})),
|
|
334
|
+
],
|
|
335
|
+
mining_summary: miningSummary,
|
|
336
|
+
});
|
|
337
|
+
const persisted = persistPersonalGlossaryCandidateProjectionAfterRefresh(projection, options);
|
|
338
|
+
const committed = readCurrentGeneration(options.tiersDir);
|
|
339
|
+
if (!committed || committed.manifest.generation !== projection.generation) {
|
|
340
|
+
throw new TypeError("current evidence generation changed before projection commit completed");
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
status: persisted.status,
|
|
344
|
+
generation: projection.generation,
|
|
345
|
+
policy_version: projection.policy_version,
|
|
346
|
+
candidate_projection_sha256: projection.projection_sha256,
|
|
347
|
+
candidate_count: projection.report.mining_summary.total_candidate_count,
|
|
348
|
+
abstention_count: projection.report.mining_summary.total_abstention_count,
|
|
349
|
+
path: persisted.path,
|
|
350
|
+
};
|
|
351
|
+
}
|