agentera 3.0.0-dev.22 → 3.0.0-dev.25

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 (274) hide show
  1. package/README.md +9 -5
  2. package/bundle/.cursor/agents/agentera.md +1 -1
  3. package/bundle/.opencode/agents/agentera.md +1 -1
  4. package/bundle/CHANGELOG.md +15 -5
  5. package/bundle/README.md +9 -8
  6. package/bundle/UPGRADE.md +10 -24
  7. package/bundle/agents/openai.yaml +1 -1
  8. package/bundle/references/adapters/package-manifest-interface-model.yaml +2 -0
  9. package/bundle/references/adapters/package-registry.yaml +9 -0
  10. package/bundle/references/artifacts/state-storage-authority.yaml +842 -15
  11. package/bundle/references/cli/agent-ready-state-contract.yaml +25 -17
  12. package/bundle/references/cli/prime-consumer-compatibility.yaml +676 -0
  13. package/bundle/references/cli/update-channels.yaml +4 -2
  14. package/bundle/references/cli/vocabulary-index.yaml +3 -3
  15. package/bundle/references/cli/vocabulary.md +14 -7
  16. package/bundle/skills/agentera/SKILL.md +35 -20
  17. package/bundle/skills/agentera/agents/audit.toml +1 -1
  18. package/bundle/skills/agentera/agents/build.toml +1 -1
  19. package/bundle/skills/agentera/agents/design.toml +1 -1
  20. package/bundle/skills/agentera/agents/discuss.toml +1 -1
  21. package/bundle/skills/agentera/agents/document.toml +1 -1
  22. package/bundle/skills/agentera/agents/optimize.toml +1 -1
  23. package/bundle/skills/agentera/agents/orchestrate.toml +1 -1
  24. package/bundle/skills/agentera/agents/plan.toml +1 -1
  25. package/bundle/skills/agentera/agents/profile.toml +1 -1
  26. package/bundle/skills/agentera/agents/research.toml +1 -1
  27. package/bundle/skills/agentera/agents/status.toml +1 -1
  28. package/bundle/skills/agentera/agents/vision.toml +1 -1
  29. package/bundle/skills/agentera/capabilities/audit/schemas/artifacts.yaml +12 -12
  30. package/bundle/skills/agentera/capabilities/audit/schemas/validation.yaml +2 -2
  31. package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +12 -12
  32. package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +8 -8
  33. package/bundle/skills/agentera/capabilities/design/schemas/artifacts.yaml +6 -6
  34. package/bundle/skills/agentera/capabilities/discuss/schemas/artifacts.yaml +7 -7
  35. package/bundle/skills/agentera/capabilities/document/schemas/artifacts.yaml +12 -12
  36. package/bundle/skills/agentera/capabilities/optimize/schemas/artifacts.yaml +9 -9
  37. package/bundle/skills/agentera/capabilities/orchestrate/schemas/artifacts.yaml +11 -11
  38. package/bundle/skills/agentera/capabilities/plan/schemas/artifacts.yaml +9 -9
  39. package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +2 -2
  40. package/bundle/skills/agentera/capabilities/research/schemas/artifacts.yaml +3 -3
  41. package/bundle/skills/agentera/capabilities/status/schemas/artifacts.yaml +11 -11
  42. package/bundle/skills/agentera/capabilities/vision/schemas/artifacts.yaml +8 -8
  43. package/bundle/skills/agentera/schemas/artifacts/decisions.yaml +15 -2
  44. package/bundle/skills/agentera/schemas/artifacts/docs.yaml +4 -1
  45. package/bundle/skills/agentera/schemas/artifacts/experiments.yaml +21 -21
  46. package/bundle/skills/agentera/schemas/artifacts/health.yaml +15 -2
  47. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +36 -15
  48. package/bundle/skills/agentera/schemas/artifacts/progress.yaml +15 -2
  49. package/bundle/skills/agentera/schemas/artifacts/todo.yaml +4 -4
  50. package/dist/capabilities/audit/instructions.js +13 -1
  51. package/dist/capabilities/audit/instructions.js.map +1 -1
  52. package/dist/capabilities/build/instructions.js +14 -1
  53. package/dist/capabilities/build/instructions.js.map +1 -1
  54. package/dist/capabilities/discuss/instructions.js +11 -1
  55. package/dist/capabilities/discuss/instructions.js.map +1 -1
  56. package/dist/capabilities/document/instructions.js +9 -1
  57. package/dist/capabilities/document/instructions.js.map +1 -1
  58. package/dist/capabilities/index.js +9 -8
  59. package/dist/capabilities/index.js.map +1 -1
  60. package/dist/capabilities/optimize/instructions.js +10 -1
  61. package/dist/capabilities/optimize/instructions.js.map +1 -1
  62. package/dist/capabilities/orchestrate/instructions.js +13 -2
  63. package/dist/capabilities/orchestrate/instructions.js.map +1 -1
  64. package/dist/capabilities/plan/instructions.js +10 -1
  65. package/dist/capabilities/plan/instructions.js.map +1 -1
  66. package/dist/capabilities/status/startupInstructions.js +35 -0
  67. package/dist/capabilities/status/startupInstructions.js.map +1 -0
  68. package/dist/cli/capabilityContext/build.js +3 -3
  69. package/dist/cli/capabilityContext/build.js.map +1 -1
  70. package/dist/cli/capabilityContext/closeout.js +5 -5
  71. package/dist/cli/capabilityContext/closeout.js.map +1 -1
  72. package/dist/cli/capabilityContext/contract.js +14 -19
  73. package/dist/cli/capabilityContext/contract.js.map +1 -1
  74. package/dist/cli/capabilityContext/evidence.js +7 -7
  75. package/dist/cli/capabilityContext/evidence.js.map +1 -1
  76. package/dist/cli/capabilityContext/planState.js +12 -11
  77. package/dist/cli/capabilityContext/planState.js.map +1 -1
  78. package/dist/cli/capabilityContext/progress.js +1 -1
  79. package/dist/cli/capabilityContext/progress.js.map +1 -1
  80. package/dist/cli/capabilityContext/shared.js +8 -2
  81. package/dist/cli/capabilityContext/shared.js.map +1 -1
  82. package/dist/cli/capabilityContext/slim.js +2 -1
  83. package/dist/cli/capabilityContext/slim.js.map +1 -1
  84. package/dist/cli/capabilityContext/startup.js +22 -11
  85. package/dist/cli/capabilityContext/startup.js.map +1 -1
  86. package/dist/cli/capabilityContext/types.js +4 -4
  87. package/dist/cli/capabilityContext/types.js.map +1 -1
  88. package/dist/cli/commands/backfill.js +39 -2
  89. package/dist/cli/commands/backfill.js.map +1 -1
  90. package/dist/cli/commands/durability.js +34 -1
  91. package/dist/cli/commands/durability.js.map +1 -1
  92. package/dist/cli/commands/entityMigrate.js +128 -0
  93. package/dist/cli/commands/entityMigrate.js.map +1 -0
  94. package/dist/cli/commands/migrate.js +4 -0
  95. package/dist/cli/commands/migrate.js.map +1 -1
  96. package/dist/cli/commands/prime/briefOrientation.js +584 -0
  97. package/dist/cli/commands/prime/briefOrientation.js.map +1 -0
  98. package/dist/cli/commands/prime/collectEntityOrientation.js +148 -0
  99. package/dist/cli/commands/prime/collectEntityOrientation.js.map +1 -0
  100. package/dist/cli/commands/prime/collectOrientationState.js +21 -27
  101. package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
  102. package/dist/cli/commands/prime/orientationOutput.js +118 -24
  103. package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
  104. package/dist/cli/commands/prime.js +32 -3
  105. package/dist/cli/commands/prime.js.map +1 -1
  106. package/dist/cli/commands/query.js +41 -15
  107. package/dist/cli/commands/query.js.map +1 -1
  108. package/dist/cli/commands/schema.js +10 -34
  109. package/dist/cli/commands/schema.js.map +1 -1
  110. package/dist/cli/commands/state/decisions.js +13 -0
  111. package/dist/cli/commands/state/decisions.js.map +1 -1
  112. package/dist/cli/commands/state/docs.js +38 -0
  113. package/dist/cli/commands/state/docs.js.map +1 -1
  114. package/dist/cli/commands/state/experimentRecords.js +82 -0
  115. package/dist/cli/commands/state/experimentRecords.js.map +1 -1
  116. package/dist/cli/commands/state/experiments.js +13 -0
  117. package/dist/cli/commands/state/experiments.js.map +1 -1
  118. package/dist/cli/commands/state/get.js +69 -2
  119. package/dist/cli/commands/state/get.js.map +1 -1
  120. package/dist/cli/commands/state/health.js +13 -0
  121. package/dist/cli/commands/state/health.js.map +1 -1
  122. package/dist/cli/commands/state/index.js +9 -0
  123. package/dist/cli/commands/state/index.js.map +1 -1
  124. package/dist/cli/commands/state/list.js +40 -7
  125. package/dist/cli/commands/state/list.js.map +1 -1
  126. package/dist/cli/commands/state/objective.js +24 -0
  127. package/dist/cli/commands/state/objective.js.map +1 -1
  128. package/dist/cli/commands/state/plan.js +17 -2
  129. package/dist/cli/commands/state/plan.js.map +1 -1
  130. package/dist/cli/commands/state/planTasks.js +38 -25
  131. package/dist/cli/commands/state/planTasks.js.map +1 -1
  132. package/dist/cli/commands/state/plans.js +32 -16
  133. package/dist/cli/commands/state/plans.js.map +1 -1
  134. package/dist/cli/commands/state/progress.js +12 -0
  135. package/dist/cli/commands/state/progress.js.map +1 -1
  136. package/dist/cli/commands/state/shared.js.map +1 -1
  137. package/dist/cli/commands/state/todo.js +30 -0
  138. package/dist/cli/commands/state/todo.js.map +1 -1
  139. package/dist/cli/commands/state/write.js +79 -4
  140. package/dist/cli/commands/state/write.js.map +1 -1
  141. package/dist/cli/commands/upgrade.js +65 -31
  142. package/dist/cli/commands/upgrade.js.map +1 -1
  143. package/dist/cli/commands/upgradeVerify.js +46 -14
  144. package/dist/cli/commands/upgradeVerify.js.map +1 -1
  145. package/dist/cli/commands/validate.js +65 -0
  146. package/dist/cli/commands/validate.js.map +1 -1
  147. package/dist/cli/dispatch/check.js +8 -1
  148. package/dist/cli/dispatch/check.js.map +1 -1
  149. package/dist/cli/dispatch/index.js +6 -0
  150. package/dist/cli/dispatch/index.js.map +1 -1
  151. package/dist/cli/dispatch/lifecycle.js +2 -5
  152. package/dist/cli/dispatch/lifecycle.js.map +1 -1
  153. package/dist/cli/dispatch/prime.js +15 -0
  154. package/dist/cli/dispatch/prime.js.map +1 -1
  155. package/dist/cli/dispatch/state.js +7 -1
  156. package/dist/cli/dispatch/state.js.map +1 -1
  157. package/dist/cli/help.js +79 -21
  158. package/dist/cli/help.js.map +1 -1
  159. package/dist/cli/migrationRequired.js +84 -0
  160. package/dist/cli/migrationRequired.js.map +1 -0
  161. package/dist/cli/orientation/attention.js +2 -2
  162. package/dist/cli/orientation/attention.js.map +1 -1
  163. package/dist/cli/orientation.js +13 -10
  164. package/dist/cli/orientation.js.map +1 -1
  165. package/dist/cli/planArtifacts.js +192 -42
  166. package/dist/cli/planArtifacts.js.map +1 -1
  167. package/dist/cli/prime-blob.js +5 -5
  168. package/dist/cli/prime-blob.js.map +1 -1
  169. package/dist/cli/startupCompletenessContract.js +3 -3
  170. package/dist/cli/startupCompletenessContract.js.map +1 -1
  171. package/dist/cli/stateQuery.js +42 -5
  172. package/dist/cli/stateQuery.js.map +1 -1
  173. package/dist/hooks/compaction/status.js +25 -1
  174. package/dist/hooks/compaction/status.js.map +1 -1
  175. package/dist/hooks/cursorSessionStart.js +6 -0
  176. package/dist/hooks/cursorSessionStart.js.map +1 -1
  177. package/dist/hooks/sessionStart.js +62 -0
  178. package/dist/hooks/sessionStart.js.map +1 -1
  179. package/dist/registries/artifactRegistry.js +23 -14
  180. package/dist/registries/artifactRegistry.js.map +1 -1
  181. package/dist/registries/packageRegistry.js +1 -1
  182. package/dist/registries/packageRegistry.js.map +1 -1
  183. package/dist/state/archivePublication.js +3 -2
  184. package/dist/state/archivePublication.js.map +1 -1
  185. package/dist/state/decisionEntities.js +279 -0
  186. package/dist/state/decisionEntities.js.map +1 -0
  187. package/dist/state/decisionLegacyValidation.js +185 -0
  188. package/dist/state/decisionLegacyValidation.js.map +1 -0
  189. package/dist/state/decisionOverlay.js +26 -3
  190. package/dist/state/decisionOverlay.js.map +1 -1
  191. package/dist/state/decisionRevision.js +632 -0
  192. package/dist/state/decisionRevision.js.map +1 -0
  193. package/dist/state/decisionRevisionPublication.js +532 -0
  194. package/dist/state/decisionRevisionPublication.js.map +1 -0
  195. package/dist/state/directRetrieval.js +55 -4
  196. package/dist/state/directRetrieval.js.map +1 -1
  197. package/dist/state/durability.js +92 -1
  198. package/dist/state/durability.js.map +1 -1
  199. package/dist/state/entityCutover.js +390 -0
  200. package/dist/state/entityCutover.js.map +1 -0
  201. package/dist/state/entityCutoverGit.js +128 -0
  202. package/dist/state/entityCutoverGit.js.map +1 -0
  203. package/dist/state/entityMigrationPreview.js +803 -0
  204. package/dist/state/entityMigrationPreview.js.map +1 -0
  205. package/dist/state/entityPublicationContext.js +612 -0
  206. package/dist/state/entityPublicationContext.js.map +1 -0
  207. package/dist/state/entityStorage.js +807 -0
  208. package/dist/state/entityStorage.js.map +1 -0
  209. package/dist/state/experimentIdentity.js +23 -11
  210. package/dist/state/experimentIdentity.js.map +1 -1
  211. package/dist/state/gitBackfill.js.map +1 -1
  212. package/dist/state/healthEntities.js +227 -0
  213. package/dist/state/healthEntities.js.map +1 -0
  214. package/dist/state/healthEntityValidation.js +6 -0
  215. package/dist/state/healthEntityValidation.js.map +1 -0
  216. package/dist/state/legacyEntityCutoverEvidence.js +66 -0
  217. package/dist/state/legacyEntityCutoverEvidence.js.map +1 -0
  218. package/dist/state/listRetrieval.js +42 -11
  219. package/dist/state/listRetrieval.js.map +1 -1
  220. package/dist/state/objectiveExperimentEntities.js +321 -0
  221. package/dist/state/objectiveExperimentEntities.js.map +1 -0
  222. package/dist/state/planEntities.js +358 -0
  223. package/dist/state/planEntities.js.map +1 -0
  224. package/dist/state/progressEntities.js +382 -0
  225. package/dist/state/progressEntities.js.map +1 -0
  226. package/dist/state/projectRoot.js +63 -0
  227. package/dist/state/projectRoot.js.map +1 -0
  228. package/dist/state/projectionRecovery.js +458 -0
  229. package/dist/state/projectionRecovery.js.map +1 -0
  230. package/dist/state/retrievalAuthority.js +13 -6
  231. package/dist/state/retrievalAuthority.js.map +1 -1
  232. package/dist/state/safeProjectFile.js +153 -0
  233. package/dist/state/safeProjectFile.js.map +1 -0
  234. package/dist/state/startupProjection.js +68 -1
  235. package/dist/state/startupProjection.js.map +1 -1
  236. package/dist/state/stateMode.js +76 -0
  237. package/dist/state/stateMode.js.map +1 -0
  238. package/dist/state/todoDocsEntities.js +223 -0
  239. package/dist/state/todoDocsEntities.js.map +1 -0
  240. package/dist/state/todoDocsEntityValidation.js +26 -0
  241. package/dist/state/todoDocsEntityValidation.js.map +1 -0
  242. package/dist/state/write/candidateMutation.js +281 -0
  243. package/dist/state/write/candidateMutation.js.map +1 -0
  244. package/dist/state/write/explain.js +156 -33
  245. package/dist/state/write/explain.js.map +1 -1
  246. package/dist/state/write/helpers.js +22 -0
  247. package/dist/state/write/helpers.js.map +1 -1
  248. package/dist/state/write/lock.js +794 -87
  249. package/dist/state/write/lock.js.map +1 -1
  250. package/dist/state/write/mutation.js +47 -2
  251. package/dist/state/write/mutation.js.map +1 -1
  252. package/dist/state/write/operations.js +99 -6
  253. package/dist/state/write/operations.js.map +1 -1
  254. package/dist/state/write/planEvaluation.js +107 -0
  255. package/dist/state/write/planEvaluation.js.map +1 -0
  256. package/dist/state/write/planPublication.js +11 -55
  257. package/dist/state/write/planPublication.js.map +1 -1
  258. package/dist/state/write/transaction.js +111 -358
  259. package/dist/state/write/transaction.js.map +1 -1
  260. package/dist/upgrade/compatibility.js +4 -4
  261. package/dist/upgrade/compatibility.js.map +1 -1
  262. package/dist/upgrade/migrateArtifactsV2ToV3.js +13 -14
  263. package/dist/upgrade/migrateArtifactsV2ToV3.js.map +1 -1
  264. package/dist/upgrade/upgradeOrchestrator.js +126 -23
  265. package/dist/upgrade/upgradeOrchestrator.js.map +1 -1
  266. package/dist/validate/crossCapability.js +2 -2
  267. package/dist/validate/crossCapability.js.map +1 -1
  268. package/dist/validate/lifecycleAdapters/lifecycleMain.js +154 -0
  269. package/dist/validate/lifecycleAdapters/lifecycleMain.js.map +1 -0
  270. package/dist/validate/lifecycleAdapters.js +5 -156
  271. package/dist/validate/lifecycleAdapters.js.map +1 -1
  272. package/package.json +3 -3
  273. package/dist/upgrade/upgradeSnapshot.js +0 -93
  274. package/dist/upgrade/upgradeSnapshot.js.map +0 -1
@@ -0,0 +1,807 @@
1
+ import { randomInt } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveSourceRoot } from "../core/sourceRoot.js";
5
+ import { dumpYamlMapping, loadYamlMapping } from "../core/yaml.js";
6
+ import { canonicalRecordJson } from "./archiveDiscovery.js";
7
+ import { decisionRevisionContract, decisionRevisionEntityViolations } from "./decisionRevision.js";
8
+ import { healthEntityViolations } from "./healthEntityValidation.js";
9
+ import { detectStateModeBinding } from "./stateMode.js";
10
+ import { acquireWriterLock } from "./write/lock.js";
11
+ import { planTaskRecordViolations } from "./write/planEvaluation.js";
12
+ import { todoDocsRecordViolations } from "./todoDocsEntityValidation.js";
13
+ const MAX_DIAGNOSTICS = 100;
14
+ export function entityArtifactValues(sourceRoot) {
15
+ return [...authority(sourceRoot).artifacts];
16
+ }
17
+ export function entityBoundariesForArtifact(artifact, sourceRoot) {
18
+ return authority(sourceRoot).entities
19
+ .filter((definition) => definition.artifact === artifact)
20
+ .map((definition) => definition.boundary)
21
+ .sort();
22
+ }
23
+ /** Validate bytes independently of a working-tree path, for committed recovery evidence. */
24
+ export function canonicalEntityEnvelope(bytes, expected, sourceRoot) {
25
+ const model = authority(sourceRoot);
26
+ return canonicalEntityEnvelopeAgainstModel(bytes, expected, model, sourceRoot);
27
+ }
28
+ function canonicalEntityEnvelopeAgainstModel(bytes, expected, model, sourceRoot) {
29
+ const owner = model.byBoundary.get(expected.boundary);
30
+ if (!owner || owner.artifact !== expected.artifact || !model.pattern.test(expected.id)) {
31
+ throw new Error("the requested artifact, boundary, or ID is not authority-declared");
32
+ }
33
+ const document = loadYamlMapping(bytes);
34
+ if (Object.keys(document).some((key) => !["id", "artifact", "record"].includes(key))) {
35
+ throw new Error("entity envelope may contain only id, artifact, and record");
36
+ }
37
+ if (document.id !== expected.id || document.artifact !== expected.artifact || !mapping(document.record)) {
38
+ throw new Error("entity envelope does not match the requested artifact and ID");
39
+ }
40
+ const record = document.record;
41
+ const violations = canonicalEntityRecordViolationsAgainstModel(expected.boundary, record, model);
42
+ if (owner.record?.timestampFormat === "YYYY-MM-DD HH:MM" && !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(String(record.timestamp ?? "")))
43
+ violations.push("timestamp must use YYYY-MM-DD HH:MM");
44
+ if (expected.boundary === "decision_revision")
45
+ violations.push(...decisionRevisionEntityViolations(record, decisionRevisionContract(sourceRoot)));
46
+ if (expected.boundary === "health_audit")
47
+ violations.push(...healthEntityViolations(record));
48
+ if (expected.boundary === "plan") {
49
+ const header = mapping(record.header) ? record.header : {};
50
+ if (!mapping(record.header) || typeof header.title !== "string" || typeof header.created !== "string" || !["open", "complete", "archived"].includes(String(header.status)))
51
+ violations.push("invalid plan lifecycle fields");
52
+ }
53
+ if (expected.boundary === "plan_task")
54
+ violations.push(...planTaskRecordViolations(record));
55
+ if (expected.boundary === "experiment" && (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(String(record.date ?? "")) || !["baseline", "kept", "discarded"].includes(String(record.status))))
56
+ violations.push("invalid experiment date or status");
57
+ if (expected.boundary === "todo_item" || expected.boundary === "documentation_inventory_entry")
58
+ violations.push(...todoDocsRecordViolations(expected.boundary, record));
59
+ if (violations.length)
60
+ throw new Error(`entity record violates the '${expected.boundary}' boundary contract: ${violations.join("; ")}`);
61
+ return { id: expected.id, artifact: expected.artifact, record };
62
+ }
63
+ export function canonicalEntityEnvelopeBytes(target) {
64
+ return dumpYamlMapping({ id: target.id, artifact: target.artifact, record: target.record });
65
+ }
66
+ function mapping(value) {
67
+ return value !== null && typeof value === "object" && !Array.isArray(value);
68
+ }
69
+ function strings(value) {
70
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : [];
71
+ }
72
+ function fieldShapes(value, authorityPath) {
73
+ if (value === undefined)
74
+ return {};
75
+ if (!mapping(value))
76
+ throw new Error(`invalid entity field_shapes declaration in '${authorityPath}'`);
77
+ return Object.fromEntries(Object.entries(value).map(([field, raw]) => {
78
+ if (!mapping(raw) || raw.type !== "mapping" || !mapping(raw.required_fields) || !["allowed", "forbidden"].includes(String(raw.additional_fields))) {
79
+ throw new Error(`invalid entity field_shapes.${field} declaration in '${authorityPath}'`);
80
+ }
81
+ const declaredFields = (group) => {
82
+ const declaration = raw[group];
83
+ if (declaration === undefined && group === "optional_fields")
84
+ return {};
85
+ if (!mapping(declaration))
86
+ throw new Error(`invalid entity field_shapes.${field}.${group} declaration in '${authorityPath}'`);
87
+ return Object.fromEntries(Object.entries(declaration).map(([name, type]) => {
88
+ if (type !== "string_list")
89
+ throw new Error(`invalid entity field_shapes.${field}.${group}.${name} declaration in '${authorityPath}'`);
90
+ return [name, type];
91
+ }));
92
+ };
93
+ return [field, {
94
+ type: "mapping",
95
+ requiredFields: declaredFields("required_fields"),
96
+ optionalFields: declaredFields("optional_fields"),
97
+ additionalFields: raw.additional_fields,
98
+ }];
99
+ }));
100
+ }
101
+ function authority(sourceRoot = resolveSourceRoot()) {
102
+ const authorityPath = path.join(sourceRoot, "references", "artifacts", "state-storage-authority.yaml");
103
+ const document = loadYamlMapping(fs.readFileSync(authorityPath, "utf8"));
104
+ const target = document.entity_target;
105
+ if (!mapping(target) || !mapping(target.identity) || !mapping(target.storage_boundary) || !mapping(target.public_schema) || !Array.isArray(target.entities) || !mapping(target.relationships)) {
106
+ throw new Error(`invalid entity authority '${authorityPath}'`);
107
+ }
108
+ const entities = target.entities.map((value) => {
109
+ if (!mapping(value) || typeof value.boundary !== "string" || typeof value.artifact !== "string") {
110
+ throw new Error(`invalid entity declaration in '${authorityPath}'`);
111
+ }
112
+ const record = mapping(value.record) ? value.record : null;
113
+ const ownership = mapping(value.ownership) ? value.ownership : null;
114
+ const baseline = mapping(value.baseline) ? value.baseline : null;
115
+ return {
116
+ boundary: value.boundary,
117
+ artifact: value.artifact,
118
+ ...(record ? { record: {
119
+ requiredFields: strings(record.required_fields),
120
+ requiredPaths: strings(record.required_paths),
121
+ forbiddenFields: strings(record.forbidden_fields),
122
+ fieldShapes: fieldShapes(record.field_shapes, authorityPath),
123
+ ...(typeof record.timestamp_format === "string" ? { timestampFormat: record.timestamp_format } : {}),
124
+ } } : {}),
125
+ ...(ownership ? { ownership: {
126
+ fields: strings(ownership.fields),
127
+ cardinality: String(ownership.cardinality ?? ""),
128
+ } } : {}),
129
+ ...(baseline && typeof baseline.field === "string" && typeof baseline.value === "string" ? { baseline: {
130
+ field: baseline.field,
131
+ value: baseline.value,
132
+ cardinality: String(baseline.cardinality ?? ""),
133
+ } } : {}),
134
+ };
135
+ });
136
+ const declarations = target.relationships.declarations;
137
+ if (!Array.isArray(declarations))
138
+ throw new Error(`invalid relationship declarations in '${authorityPath}'`);
139
+ const relationships = declarations.map((value) => {
140
+ if (!mapping(value) || typeof value.source !== "string" || typeof value.field !== "string" || typeof value.target !== "string" || typeof value.cardinality !== "string") {
141
+ throw new Error(`invalid relationship declaration in '${authorityPath}'`);
142
+ }
143
+ return { source: value.source, field: value.field, target: value.target, cardinality: value.cardinality };
144
+ });
145
+ const alphabet = String(target.identity.alphabet);
146
+ const length = Number(target.identity.length);
147
+ const acceptedPattern = String(target.identity.accepted_pattern);
148
+ const sharedPrimitives = target.storage_boundary.shared_primitives;
149
+ if (!mapping(sharedPrimitives) || typeof sharedPrimitives.canonical_root !== "string") {
150
+ throw new Error(`invalid shared entity storage declaration in '${authorityPath}'`);
151
+ }
152
+ const entityRoot = sharedPrimitives.canonical_root;
153
+ if (path.isAbsolute(entityRoot) || entityRoot.split(/[\\/]/).some((segment) => segment === "..")) {
154
+ throw new Error(`unsafe shared entity storage root '${entityRoot}' in '${authorityPath}'`);
155
+ }
156
+ return {
157
+ entityRoot,
158
+ alphabet,
159
+ length,
160
+ pattern: new RegExp(acceptedPattern),
161
+ entities,
162
+ relationships,
163
+ artifacts: [...new Set(entities.map(({ artifact }) => artifact))].sort(),
164
+ byBoundary: new Map(entities.map((entity) => [entity.boundary, entity])),
165
+ forbiddenAliases: strings(target.public_schema.forbidden_canonical_aliases),
166
+ };
167
+ }
168
+ export function canonicalEntityRecordViolations(boundary, record, sourceRoot) {
169
+ const model = authority(sourceRoot);
170
+ return canonicalEntityRecordViolationsAgainstModel(boundary, record, model);
171
+ }
172
+ function canonicalEntityRecordViolationsAgainstModel(boundary, record, model) {
173
+ const definition = model.byBoundary.get(boundary);
174
+ if (!definition?.record)
175
+ throw new Error(`unknown entity record boundary '${boundary}'`);
176
+ const missing = definition.record.requiredFields.filter((field) => record[field] === undefined);
177
+ const missingPaths = definition.record.requiredPaths.filter((field) => {
178
+ let current = record;
179
+ for (const part of field.split("."))
180
+ current = mapping(current) ? current[part] : undefined;
181
+ return typeof current !== "string" || current.length === 0;
182
+ });
183
+ const forbidden = [...new Set([...definition.record.forbiddenFields, ...model.forbiddenAliases])]
184
+ .filter((field) => record[field] !== undefined);
185
+ const shapeViolations = Object.entries(definition.record.fieldShapes).flatMap(([field, shape]) => {
186
+ const value = record[field];
187
+ if (!mapping(value))
188
+ return [`${field} must be a mapping`];
189
+ const required = Object.entries(shape.requiredFields).flatMap(([name, type]) => {
190
+ if (!(name in value))
191
+ return [`${field}.${name} is required`];
192
+ if (type === "string_list" && (!Array.isArray(value[name]) || !value[name].every((item) => typeof item === "string")))
193
+ return [`${field}.${name} must be a list of strings`];
194
+ return [];
195
+ });
196
+ const optional = Object.entries(shape.optionalFields).flatMap(([name, type]) => {
197
+ if (!(name in value))
198
+ return [];
199
+ if (type === "string_list" && (!Array.isArray(value[name]) || !value[name].every((item) => typeof item === "string")))
200
+ return [`${field}.${name} must be a list of strings`];
201
+ return [];
202
+ });
203
+ if (shape.additionalFields === "allowed")
204
+ return [...required, ...optional];
205
+ const declared = new Set([...Object.keys(shape.requiredFields), ...Object.keys(shape.optionalFields)]);
206
+ const extras = Object.keys(value).filter((name) => !declared.has(name));
207
+ return [...required, ...optional, ...extras.map((name) => `${field}.${name} is not allowed`)];
208
+ });
209
+ return [
210
+ ...missing.map((field) => `${field} is required by the canonical ${boundary} record contract`),
211
+ ...missingPaths.map((field) => `${field} is required by the canonical ${boundary} record contract`),
212
+ ...forbidden.map((field) => `${field} is forbidden by the canonical entity authority`),
213
+ ...shapeViolations,
214
+ ];
215
+ }
216
+ function relative(projectRoot, candidate) {
217
+ return path.relative(path.resolve(projectRoot), candidate).split(path.sep).join("/") || ".";
218
+ }
219
+ function recovery(projectRoot, action) {
220
+ return `${action}; rerun agentera check validate state --cwd ${JSON.stringify(path.resolve(projectRoot))}`;
221
+ }
222
+ function diagnosticSort(left, right) {
223
+ return left.path.localeCompare(right.path) || left.code.localeCompare(right.code) || (left.relation ?? "").localeCompare(right.relation ?? "");
224
+ }
225
+ function noSymlinkPrefix(projectRoot, candidate) {
226
+ const root = path.resolve(projectRoot);
227
+ const absolute = path.resolve(candidate);
228
+ const rel = path.relative(root, absolute);
229
+ if (rel.startsWith("..") || path.isAbsolute(rel))
230
+ return candidate;
231
+ let cursor = root;
232
+ for (const segment of rel.split(path.sep).filter(Boolean)) {
233
+ cursor = path.join(cursor, segment);
234
+ try {
235
+ if (fs.lstatSync(cursor).isSymbolicLink())
236
+ return cursor;
237
+ }
238
+ catch (error) {
239
+ if (error.code !== "ENOENT")
240
+ throw error;
241
+ }
242
+ }
243
+ return null;
244
+ }
245
+ function unsafeIssue(projectRoot, candidate) {
246
+ const entityPath = relative(projectRoot, candidate);
247
+ return {
248
+ code: "unsafe_path",
249
+ path: entityPath,
250
+ message: `entity path '${entityPath}' is a symbolic link and was not traversed`,
251
+ recovery: recovery(projectRoot, `remove the symbolic link '${entityPath}' and restore a project-local directory or file`),
252
+ };
253
+ }
254
+ function unsafeEntity(projectRoot, candidate) {
255
+ return {
256
+ id: null,
257
+ artifact: null,
258
+ boundary: null,
259
+ record: null,
260
+ path: candidate,
261
+ relativePath: relative(projectRoot, candidate),
262
+ classification: "unsafe",
263
+ };
264
+ }
265
+ function listDirectories(directory) {
266
+ return fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
267
+ }
268
+ function discoverFile(projectRoot, file, pathArtifact, boundary, model, issues, sourceRoot) {
269
+ const relativePath = relative(projectRoot, file);
270
+ const filenameId = path.basename(file, path.extname(file));
271
+ let document = null;
272
+ try {
273
+ document = loadYamlMapping(fs.readFileSync(file, "utf8"));
274
+ }
275
+ catch (error) {
276
+ issues.push({
277
+ code: "malformed_entity",
278
+ path: relativePath,
279
+ message: `entity '${relativePath}' is not a YAML mapping: ${error.message}`,
280
+ recovery: recovery(projectRoot, `replace '${relativePath}' with a valid id/artifact/record entity envelope or remove it`),
281
+ });
282
+ }
283
+ const id = typeof document?.id === "string" ? document.id : filenameId;
284
+ const artifact = typeof document?.artifact === "string" ? document.artifact : pathArtifact;
285
+ const record = mapping(document?.record) ? document.record : null;
286
+ let malformed = document === null;
287
+ if (!model.pattern.test(filenameId)
288
+ || id !== filenameId
289
+ || typeof document?.id !== "string"
290
+ || typeof document?.artifact !== "string"
291
+ || record === null
292
+ || document === null
293
+ || Object.keys(document).some((key) => !["id", "artifact", "record"].includes(key))) {
294
+ malformed = true;
295
+ if (document !== null) {
296
+ issues.push({
297
+ code: "malformed_entity",
298
+ path: relativePath,
299
+ id,
300
+ artifact,
301
+ boundary,
302
+ message: `entity '${relativePath}' must contain only id, artifact, and record; id and filename must match ${model.pattern.source}`,
303
+ recovery: recovery(projectRoot, `rename and rewrite '${relativePath}' as <ten-lowercase-letter-id>.yaml with matching id, artifact, and mapping record fields`),
304
+ });
305
+ }
306
+ }
307
+ const owner = model.byBoundary.get(boundary);
308
+ if (!model.artifacts.includes(pathArtifact) || !model.artifacts.includes(artifact)) {
309
+ malformed = true;
310
+ issues.push({
311
+ code: "invalid_artifact",
312
+ path: relativePath,
313
+ id,
314
+ artifact,
315
+ boundary,
316
+ message: `entity '${relativePath}' declares unsupported artifact '${artifact}' under '${pathArtifact}'`,
317
+ recovery: recovery(projectRoot, `move or rewrite '${relativePath}'; valid artifact values: ${model.artifacts.join(", ")}`),
318
+ });
319
+ }
320
+ else {
321
+ if (!owner || owner.artifact !== pathArtifact || artifact !== pathArtifact) {
322
+ malformed = true;
323
+ issues.push({
324
+ code: "conflicting_ownership",
325
+ path: relativePath,
326
+ id,
327
+ artifact,
328
+ boundary,
329
+ message: `entity '${relativePath}' has conflicting ownership: path artifact '${pathArtifact}', boundary '${boundary}', envelope artifact '${artifact}'`,
330
+ recovery: recovery(projectRoot, owner
331
+ ? `move '${relativePath}' under .agentera/entities/${owner.artifact}/${boundary}/ and set artifact to '${owner.artifact}'`
332
+ : `move or rewrite '${relativePath}' using a boundary declared by the state storage authority`),
333
+ });
334
+ }
335
+ }
336
+ if (!malformed && owner?.record && record) {
337
+ const violations = canonicalEntityRecordViolationsAgainstModel(boundary, record, model);
338
+ const timestampInvalid = owner.record.timestampFormat === "YYYY-MM-DD HH:MM" && !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(String(record.timestamp ?? ""));
339
+ if (violations.length || timestampInvalid) {
340
+ malformed = true;
341
+ issues.push({
342
+ code: "malformed_entity",
343
+ path: relativePath,
344
+ id,
345
+ artifact,
346
+ boundary,
347
+ message: `entity '${relativePath}' violates the authority-declared '${boundary}' record contract: ${[...violations, ...(timestampInvalid ? ["timestamp must use YYYY-MM-DD HH:MM"] : [])].join("; ")}`,
348
+ recovery: recovery(projectRoot, `repair record fields in '${relativePath}' to match the state storage authority`),
349
+ });
350
+ }
351
+ }
352
+ if (!malformed && boundary === "decision_revision" && record) {
353
+ const violations = decisionRevisionEntityViolations(record, decisionRevisionContract(sourceRoot));
354
+ if (violations.length) {
355
+ malformed = true;
356
+ issues.push({
357
+ code: "malformed_entity",
358
+ path: relativePath,
359
+ id,
360
+ artifact,
361
+ boundary,
362
+ message: `entity '${relativePath}' has an invalid decision revision: ${violations.join("; ")}`,
363
+ recovery: recovery(projectRoot, `repair '${relativePath}' using the authority-declared decision revision contract`),
364
+ });
365
+ }
366
+ }
367
+ if (!malformed && boundary === "health_audit" && record) {
368
+ const violations = healthEntityViolations(record);
369
+ if (violations.length) {
370
+ malformed = true;
371
+ issues.push({
372
+ code: "malformed_entity",
373
+ path: relativePath,
374
+ id,
375
+ artifact,
376
+ boundary,
377
+ message: `entity '${relativePath}' has an invalid canonical health audit`,
378
+ recovery: recovery(projectRoot, `preserve the declared audit evidence and repair '${relativePath}' using the health schema`),
379
+ });
380
+ }
381
+ }
382
+ if (!malformed && boundary === "plan" && record) {
383
+ const header = mapping(record.header) ? record.header : {};
384
+ if (!mapping(record.header) || typeof header.title !== "string" || typeof header.created !== "string" || !["open", "complete", "archived"].includes(String(header.status))) {
385
+ malformed = true;
386
+ issues.push({ code: "malformed_entity", path: relativePath, id, artifact, boundary, message: `entity '${relativePath}' has invalid plan lifecycle fields`, recovery: recovery(projectRoot, `repair '${relativePath}' using the current plan header schema and open|complete|archived lifecycle`) });
387
+ }
388
+ }
389
+ if (!malformed && boundary === "plan_task" && record) {
390
+ if (planTaskRecordViolations(record).length) {
391
+ malformed = true;
392
+ issues.push({ code: "malformed_entity", path: relativePath, id, artifact, boundary, message: `entity '${relativePath}' has invalid plan task or evaluation fields`, recovery: recovery(projectRoot, `repair '${relativePath}' using the current plan task and evaluation schema`) });
393
+ }
394
+ }
395
+ if (!malformed && boundary === "experiment" && record) {
396
+ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(String(record.date ?? "")) || !["baseline", "kept", "discarded"].includes(String(record.status))) {
397
+ malformed = true;
398
+ issues.push({ code: "malformed_entity", path: relativePath, id, artifact, boundary, message: `entity '${relativePath}' has invalid experiment date or status`, recovery: recovery(projectRoot, `repair '${relativePath}' using YYYY-MM-DD HH:MM and baseline|kept|discarded`) });
399
+ }
400
+ }
401
+ if (!malformed && (boundary === "todo_item" || boundary === "documentation_inventory_entry") && record) {
402
+ const violations = todoDocsRecordViolations(boundary, record);
403
+ if (violations.length) {
404
+ malformed = true;
405
+ issues.push({ code: "malformed_entity", path: relativePath, id, artifact, boundary, message: `entity '${relativePath}' has an invalid ${boundary} record: ${violations.join("; ")}`, recovery: recovery(projectRoot, `repair '${relativePath}' using the authority-declared ${boundary} fields`) });
406
+ }
407
+ }
408
+ return { id, artifact, boundary, record, path: file, relativePath, classification: malformed ? "malformed" : "valid" };
409
+ }
410
+ export function discoverEntities(projectRoot, sourceRoot) {
411
+ const root = path.resolve(projectRoot);
412
+ const model = authority(sourceRoot);
413
+ const storageRoot = path.join(root, model.entityRoot);
414
+ const entities = [];
415
+ const issues = [];
416
+ const unsafePrefix = noSymlinkPrefix(root, storageRoot);
417
+ if (unsafePrefix)
418
+ return { entities: [unsafeEntity(root, unsafePrefix)], issues: [unsafeIssue(root, unsafePrefix)], validArtifactValues: model.artifacts };
419
+ if (!fs.existsSync(storageRoot))
420
+ return { entities, issues, validArtifactValues: model.artifacts };
421
+ if (!fs.statSync(storageRoot).isDirectory()) {
422
+ issues.push({ code: "malformed_entity", path: relative(root, storageRoot), message: `entity root '${relative(root, storageRoot)}' is not a directory`, recovery: recovery(root, `replace '${relative(root, storageRoot)}' with a directory`) });
423
+ return { entities, issues, validArtifactValues: model.artifacts };
424
+ }
425
+ for (const artifactEntry of listDirectories(storageRoot)) {
426
+ const artifactPath = path.join(storageRoot, artifactEntry.name);
427
+ if (artifactEntry.isSymbolicLink()) {
428
+ entities.push(unsafeEntity(root, artifactPath));
429
+ issues.push(unsafeIssue(root, artifactPath));
430
+ continue;
431
+ }
432
+ if (!artifactEntry.isDirectory()) {
433
+ issues.push({ code: "malformed_entity", path: relative(root, artifactPath), message: `unexpected file '${relative(root, artifactPath)}' in entity root`, recovery: recovery(root, `remove '${relative(root, artifactPath)}' or place it under an artifact/boundary directory`) });
434
+ continue;
435
+ }
436
+ for (const boundaryEntry of listDirectories(artifactPath)) {
437
+ const boundaryPath = path.join(artifactPath, boundaryEntry.name);
438
+ if (boundaryEntry.isSymbolicLink()) {
439
+ entities.push(unsafeEntity(root, boundaryPath));
440
+ issues.push(unsafeIssue(root, boundaryPath));
441
+ continue;
442
+ }
443
+ if (!boundaryEntry.isDirectory()) {
444
+ issues.push({ code: "malformed_entity", path: relative(root, boundaryPath), message: `unexpected file '${relative(root, boundaryPath)}' in artifact entity root`, recovery: recovery(root, `remove '${relative(root, boundaryPath)}' or place it under a declared boundary directory`) });
445
+ continue;
446
+ }
447
+ for (const fileEntry of listDirectories(boundaryPath)) {
448
+ const file = path.join(boundaryPath, fileEntry.name);
449
+ if (fileEntry.isSymbolicLink()) {
450
+ entities.push(unsafeEntity(root, file));
451
+ issues.push(unsafeIssue(root, file));
452
+ continue;
453
+ }
454
+ if (!fileEntry.isFile() || path.extname(fileEntry.name) !== ".yaml") {
455
+ issues.push({ code: "malformed_entity", path: relative(root, file), message: `entity path '${relative(root, file)}' is not a canonical YAML file`, recovery: recovery(root, `remove '${relative(root, file)}' or replace it with <ten-lowercase-letter-id>.yaml`) });
456
+ continue;
457
+ }
458
+ entities.push(discoverFile(root, file, artifactEntry.name, boundaryEntry.name, model, issues, sourceRoot));
459
+ }
460
+ }
461
+ }
462
+ const byId = new Map();
463
+ for (const entity of entities) {
464
+ if (entity.id && model.pattern.test(entity.id))
465
+ byId.set(entity.id, [...(byId.get(entity.id) ?? []), entity]);
466
+ }
467
+ for (const [id, matches] of byId) {
468
+ if (matches.length < 2)
469
+ continue;
470
+ for (const entity of matches) {
471
+ entity.classification = "duplicate";
472
+ issues.push({
473
+ code: "duplicate_id",
474
+ path: entity.relativePath,
475
+ id,
476
+ artifact: entity.artifact ?? undefined,
477
+ boundary: entity.boundary ?? undefined,
478
+ message: `entity ID '${id}' appears ${matches.length} times across project state`,
479
+ recovery: recovery(root, `assign a new generated ID to all but one '${id}' entity and rewrite every declared relationship to those entities`),
480
+ });
481
+ }
482
+ }
483
+ const rank = { duplicate: 0, malformed: 1, unsafe: 2, valid: 3 };
484
+ entities.sort((left, right) => rank[left.classification] - rank[right.classification] || left.relativePath.localeCompare(right.relativePath));
485
+ issues.sort(diagnosticSort);
486
+ return { entities, issues, validArtifactValues: model.artifacts };
487
+ }
488
+ function relationTargets(entity, relation) {
489
+ const value = entity.record?.[relation.field];
490
+ if (relation.cardinality === "exactly_one")
491
+ return typeof value === "string" ? [value] : null;
492
+ if (value === undefined || value === null)
493
+ return [];
494
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
495
+ }
496
+ function validateEntityDiscovery(projectRoot, sourceRoot, discovery, boundIssues = true) {
497
+ const model = authority(sourceRoot);
498
+ const issues = [...discovery.issues];
499
+ const byId = new Map();
500
+ for (const entity of discovery.entities) {
501
+ if (entity.id)
502
+ byId.set(entity.id, [...(byId.get(entity.id) ?? []), entity]);
503
+ }
504
+ for (const entity of discovery.entities) {
505
+ if (!entity.boundary || !entity.record)
506
+ continue;
507
+ for (const relation of model.relationships.filter(({ source }) => source === entity.boundary)) {
508
+ const targets = relationTargets(entity, relation);
509
+ const invalidTargets = targets === null ? [String(entity.record[relation.field] ?? "<missing>")] : targets.filter((targetId) => {
510
+ const matches = byId.get(targetId) ?? [];
511
+ if (matches.length !== 1 || matches[0].boundary !== relation.target)
512
+ return true;
513
+ if (relation.cardinality === "zero_or_many_same_plan")
514
+ return matches[0].record?.plan !== entity.record?.plan;
515
+ return false;
516
+ });
517
+ for (const targetId of invalidTargets) {
518
+ issues.push({
519
+ code: "unresolved_relation",
520
+ path: entity.relativePath,
521
+ id: entity.id ?? undefined,
522
+ artifact: entity.artifact ?? undefined,
523
+ boundary: entity.boundary,
524
+ relation: relation.field,
525
+ targetId,
526
+ message: `entity '${entity.id}' relation '${relation.field}' target '${targetId}' does not resolve to exactly one '${relation.target}' entity${relation.cardinality === "zero_or_many_same_plan" ? " in the same plan" : ""}`,
527
+ recovery: recovery(projectRoot, `set record.${relation.field} in '${entity.relativePath}' to ${relation.cardinality === "exactly_one" ? "one existing" : "only existing same-plan"} '${relation.target}' ID`),
528
+ });
529
+ }
530
+ }
531
+ }
532
+ const planTasks = discovery.entities.filter((entity) => entity.boundary === "plan_task" && entity.classification === "valid" && entity.id && entity.record);
533
+ const tasksByPlan = new Map();
534
+ for (const task of planTasks) {
535
+ const planId = typeof task.record?.plan === "string" ? task.record.plan : "";
536
+ tasksByPlan.set(planId, [...(tasksByPlan.get(planId) ?? []), task]);
537
+ }
538
+ for (const [planId, tasks] of tasksByPlan) {
539
+ const byTaskId = new Map(tasks.map((task) => [task.id, task]));
540
+ const visiting = new Set();
541
+ const visited = new Set();
542
+ const visit = (task) => {
543
+ if (visiting.has(task.id))
544
+ return true;
545
+ if (visited.has(task.id))
546
+ return false;
547
+ visiting.add(task.id);
548
+ const cyclic = (Array.isArray(task.record?.depends_on) ? task.record.depends_on : [])
549
+ .some((id) => typeof id === "string" && byTaskId.has(id) && visit(byTaskId.get(id)));
550
+ visiting.delete(task.id);
551
+ visited.add(task.id);
552
+ return cyclic;
553
+ };
554
+ for (const task of tasks)
555
+ if (visit(task))
556
+ issues.push({
557
+ code: "unresolved_relation", path: task.relativePath, id: task.id, artifact: task.artifact ?? undefined, boundary: task.boundary ?? undefined,
558
+ relation: "depends_on", message: `plan '${planId}' task dependency graph contains a cycle involving '${task.id}'`,
559
+ recovery: recovery(projectRoot, `remove one record.depends_on edge in plan '${planId}' so the task graph is acyclic`),
560
+ });
561
+ }
562
+ for (const plan of discovery.entities.filter((entity) => entity.boundary === "plan" && entity.classification === "valid" && entity.id && entity.record)) {
563
+ const header = mapping(plan.record.header) ? plan.record.header : {};
564
+ if (header.status === "complete") {
565
+ const incomplete = (tasksByPlan.get(plan.id) ?? []).filter((task) => task.record?.status !== "complete");
566
+ if (incomplete.length)
567
+ issues.push({
568
+ code: "conflicting_ownership", path: plan.relativePath, id: plan.id, artifact: plan.artifact ?? undefined, boundary: plan.boundary ?? undefined,
569
+ message: `complete plan '${plan.id}' owns ${incomplete.length} incomplete task entities`,
570
+ recovery: recovery(projectRoot, `complete every task related to plan '${plan.id}' or restore the plan lifecycle to open`),
571
+ });
572
+ }
573
+ }
574
+ for (const definition of model.entities) {
575
+ if (!definition.ownership || definition.ownership.cardinality !== "zero_or_one")
576
+ continue;
577
+ const claims = new Map();
578
+ for (const entity of discovery.entities.filter(({ boundary, classification }) => boundary === definition.boundary && classification === "valid")) {
579
+ const values = definition.ownership.fields.map((field) => entity.record?.[field]);
580
+ if (values.some((value) => value === undefined))
581
+ continue;
582
+ const key = canonicalRecordJson(values);
583
+ claims.set(key, [...(claims.get(key) ?? []), entity]);
584
+ }
585
+ for (const claimants of claims.values()) {
586
+ if (claimants.length < 2)
587
+ continue;
588
+ for (const entity of claimants)
589
+ issues.push({
590
+ code: "conflicting_ownership",
591
+ path: entity.relativePath,
592
+ id: entity.id ?? undefined,
593
+ artifact: entity.artifact ?? undefined,
594
+ boundary: entity.boundary ?? undefined,
595
+ message: `entity '${entity.relativePath}' shares the authority-owned ${definition.ownership.fields.join("+")} claim with ${claimants.length - 1} other '${definition.boundary}' entity`,
596
+ recovery: recovery(projectRoot, `preserve every claimant and resolve the divergent '${definition.boundary}' ownership explicitly`),
597
+ });
598
+ }
599
+ }
600
+ for (const definition of model.entities) {
601
+ if (!definition.baseline || definition.baseline.cardinality !== "exactly_one_when_experiments_exist")
602
+ continue;
603
+ const byOwner = new Map();
604
+ for (const entity of discovery.entities.filter(({ boundary, classification, record }) => boundary === definition.boundary && classification === "valid" && record)) {
605
+ const owner = String(entity.record.objective ?? "");
606
+ byOwner.set(owner, [...(byOwner.get(owner) ?? []), entity]);
607
+ }
608
+ for (const [owner, entities] of byOwner) {
609
+ const baselines = entities.filter((entity) => entity.record?.[definition.baseline.field] === definition.baseline.value);
610
+ if (baselines.length === 1)
611
+ continue;
612
+ for (const entity of entities)
613
+ issues.push({
614
+ code: "conflicting_ownership", path: entity.relativePath, id: entity.id ?? undefined, artifact: entity.artifact ?? undefined, boundary: entity.boundary ?? undefined,
615
+ message: `objective '${owner}' owns ${baselines.length} '${definition.baseline.value}' experiments; exactly one is required`,
616
+ recovery: recovery(projectRoot, baselines.length ? `preserve one immutable baseline for objective '${owner}' and resolve competing ownership` : `restore the missing immutable baseline for objective '${owner}'`),
617
+ });
618
+ }
619
+ }
620
+ issues.sort(diagnosticSort);
621
+ const omittedIssueCount = Math.max(0, issues.length - MAX_DIAGNOSTICS);
622
+ const boundedIssues = boundIssues ? issues.slice(0, MAX_DIAGNOSTICS) : issues;
623
+ return { ...discovery, issues: boundedIssues, valid: issues.length === 0, entityCount: discovery.entities.length, omittedIssueCount };
624
+ }
625
+ export function validateEntityState(projectRoot, sourceRoot) {
626
+ return validateEntityDiscovery(projectRoot, sourceRoot, discoverEntities(projectRoot, sourceRoot));
627
+ }
628
+ /** Validate the exact canonical envelopes and graph proposed by a migration without publishing them. */
629
+ export function validateCanonicalEntityTargets(projectRoot, targets, sourceRoot) {
630
+ const model = authority(sourceRoot);
631
+ const entities = [];
632
+ const issues = [];
633
+ const sourceByPath = new Map(targets.map((target) => [target.path, target.sourceIdentity]));
634
+ const targetsById = new Map();
635
+ for (const target of targets)
636
+ targetsById.set(target.id, [...(targetsById.get(target.id) ?? []), target]);
637
+ for (const target of targets) {
638
+ const expectedPath = path.posix.join(model.entityRoot, target.artifact, target.boundary, `${target.id}.yaml`);
639
+ let record = null;
640
+ let message = target.path === expectedPath ? null : `canonical target path '${target.path}' must be '${expectedPath}'`;
641
+ if (!message) {
642
+ try {
643
+ record = canonicalEntityEnvelopeAgainstModel(canonicalEntityEnvelopeBytes(target), target, model, sourceRoot).record;
644
+ }
645
+ catch (error) {
646
+ message = error.message;
647
+ }
648
+ }
649
+ if (message)
650
+ issues.push({ code: "malformed_entity", path: target.path, id: target.id, artifact: target.artifact, boundary: target.boundary, message, recovery: recovery(projectRoot, `repair legacy source '${target.sourceIdentity}' so its canonical target satisfies the authority-backed boundary validator`) });
651
+ entities.push({ id: target.id, artifact: target.artifact, boundary: target.boundary, record, path: path.join(projectRoot, target.path), relativePath: target.path, classification: message ? "malformed" : "valid" });
652
+ }
653
+ for (const duplicates of targetsById.values()) {
654
+ if (duplicates.length < 2)
655
+ continue;
656
+ for (const target of duplicates)
657
+ issues.push({ code: "duplicate_id", path: target.path, id: target.id, artifact: target.artifact, boundary: target.boundary, message: `entity ID '${target.id}' appears ${duplicates.length} times across proposed canonical targets`, recovery: recovery(projectRoot, `repair migration identity allocation for '${target.sourceIdentity}'`) });
658
+ }
659
+ const validation = validateEntityDiscovery(projectRoot, sourceRoot, { entities, issues, validArtifactValues: model.artifacts }, false);
660
+ return validation.issues.map((issue) => ({ sourceIdentity: sourceByPath.get(issue.path) ?? issue.id ?? issue.path, path: issue.path, message: issue.message }));
661
+ }
662
+ function generatedId(model) {
663
+ let id = "";
664
+ for (let index = 0; index < model.length; index += 1)
665
+ id += model.alphabet[randomInt(model.alphabet.length)];
666
+ return id;
667
+ }
668
+ export function allocateEntityId(projectRoot, candidate, sourceRoot) {
669
+ const model = authority(sourceRoot);
670
+ const existing = new Set(discoverEntities(projectRoot, sourceRoot).entities.map(({ id }) => id).filter((id) => id !== null));
671
+ for (let attempt = 0; attempt < 1024; attempt += 1) {
672
+ const id = candidate ? candidate() : generatedId(model);
673
+ if (!model.pattern.test(id))
674
+ throw new Error(`entity ID candidate '${id}' must match ${model.pattern.source}`);
675
+ if (!existing.has(id))
676
+ return id;
677
+ }
678
+ throw new Error("could not allocate a unique entity ID after 1024 attempts; run agentera check validate state and retry");
679
+ }
680
+ function publishEntityLocked(request, model, context) {
681
+ context.assertValid();
682
+ if (!model.pattern.test(request.id))
683
+ throw new Error(`entity ID '${request.id}' must match ${model.pattern.source}`);
684
+ const owner = model.byBoundary.get(request.boundary);
685
+ if (!owner)
686
+ throw new Error(`unknown entity boundary '${request.boundary}'`);
687
+ if (owner.artifact !== request.artifact)
688
+ throw new Error(`boundary '${request.boundary}' is owned by artifact '${owner.artifact}', not '${request.artifact}'`);
689
+ if (!mapping(request.record))
690
+ throw new Error("entity record must be a mapping");
691
+ const root = context.pinnedPath();
692
+ const relativeTarget = path.join(model.entityRoot, request.artifact, request.boundary, `${request.id}.yaml`);
693
+ const target = path.join(root, relativeTarget);
694
+ const publicTarget = path.join(path.resolve(request.projectRoot), relativeTarget);
695
+ const symlink = noSymlinkPrefix(root, target);
696
+ if (symlink)
697
+ throw new Error(`entity path contains symbolic link '${relative(root, symlink)}'; remove the symbolic link and retry`);
698
+ const envelope = { id: request.id, artifact: request.artifact, record: request.record };
699
+ const logicalContent = canonicalRecordJson(envelope);
700
+ const bytes = dumpYamlMapping(envelope);
701
+ const matches = discoverEntities(root, request.sourceRoot).entities.filter(({ id }) => id === request.id);
702
+ const exact = matches.find(({ path: existing }) => existing === target);
703
+ if (exact && matches.length === 1) {
704
+ if (exact.record !== null && canonicalRecordJson({ id: exact.id, artifact: exact.artifact, record: exact.record }) === logicalContent) {
705
+ context.assertValid();
706
+ return { id: request.id, artifact: request.artifact, boundary: request.boundary, path: publicTarget, replay: true };
707
+ }
708
+ throw new Error(`divergent content for existing entity ID '${request.id}' at '${exact.relativePath}'; keep the existing ID unchanged or allocate a new ID`);
709
+ }
710
+ if (matches.length > 0)
711
+ throw new Error(`entity ID '${request.id}' already exists at '${matches[0].relativePath}' owned by boundary '${matches[0].boundary}'; allocate a new project-wide ID`);
712
+ const publishedIdentity = context.publishImmutable(relativeTarget, bytes);
713
+ if (!publishedIdentity) {
714
+ const existing = loadYamlMapping(fs.readFileSync(target, "utf8"));
715
+ context.assertValid();
716
+ if (canonicalRecordJson(existing) === logicalContent) {
717
+ return { id: request.id, artifact: request.artifact, boundary: request.boundary, path: publicTarget, replay: true };
718
+ }
719
+ throw new Error(`divergent content for existing entity ID '${request.id}' at '${relative(root, target)}'; keep the existing ID unchanged or allocate a new ID`);
720
+ }
721
+ return { id: request.id, artifact: request.artifact, boundary: request.boundary, path: publicTarget, replay: false, publishedIdentity };
722
+ }
723
+ export function withEntityWriterLock(context, run) {
724
+ context.assertValid();
725
+ const lock = acquireWriterLock(context.pinnedPath(), 2000);
726
+ try {
727
+ context.assertValid();
728
+ return run();
729
+ }
730
+ finally {
731
+ lock.release();
732
+ }
733
+ }
734
+ export function publishEntityUnderLock(request) {
735
+ if (!request.publicationContext)
736
+ throw new Error("locked entity publication requires a publication context");
737
+ return publishEntityLocked(request, authority(request.sourceRoot), request.publicationContext);
738
+ }
739
+ export function replaceEntityUnderLock(request) {
740
+ const context = request.publicationContext;
741
+ if (!context)
742
+ throw new Error("locked entity replacement requires a publication context");
743
+ const model = authority(request.sourceRoot);
744
+ context.assertValid();
745
+ const owner = model.byBoundary.get(request.boundary);
746
+ if (!owner || owner.artifact !== request.artifact)
747
+ throw new Error(`unknown '${request.artifact}' entity boundary '${request.boundary}'`);
748
+ const relativeTarget = path.join(model.entityRoot, request.artifact, request.boundary, `${request.id}.yaml`);
749
+ if (canonicalRecordJson(request.expectedRecord) === canonicalRecordJson(request.record))
750
+ return { id: request.id, artifact: request.artifact, boundary: request.boundary, path: path.join(path.resolve(request.projectRoot), relativeTarget), replay: true };
751
+ context.replaceExisting(relativeTarget, dumpYamlMapping({ id: request.id, artifact: request.artifact, record: request.expectedRecord }), dumpYamlMapping({ id: request.id, artifact: request.artifact, record: request.record }));
752
+ return { id: request.id, artifact: request.artifact, boundary: request.boundary, path: path.join(path.resolve(request.projectRoot), relativeTarget), replay: false };
753
+ }
754
+ function withPublicationContext(request, run) {
755
+ if (request.publicationContext) {
756
+ if (request.publicationContext.projectRoot !== path.resolve(request.projectRoot)) {
757
+ throw new Error("entity publication context belongs to a different project root");
758
+ }
759
+ return run(request.publicationContext);
760
+ }
761
+ const binding = detectStateModeBinding(request.projectRoot, request.sourceRoot);
762
+ if (binding.mode !== "entities") {
763
+ throw new Error("entity publication requires the durable entity-mode marker; legacy mode remains authoritative");
764
+ }
765
+ try {
766
+ return run(binding.publicationContext);
767
+ }
768
+ finally {
769
+ binding.publicationContext.close();
770
+ }
771
+ }
772
+ function publishWithContext(request, model, context) {
773
+ return withEntityWriterLock(context, () => publishEntityLocked(request, model, context));
774
+ }
775
+ export function publishEntity(request) {
776
+ const model = authority(request.sourceRoot);
777
+ return withPublicationContext(request, (context) => publishWithContext(request, model, context));
778
+ }
779
+ export function replaceEntity(request) {
780
+ return withPublicationContext(request, (context) => {
781
+ return withEntityWriterLock(context, () => replaceEntityUnderLock({ ...request, publicationContext: context }));
782
+ });
783
+ }
784
+ export function allocateAndPublishEntity(request, candidate) {
785
+ const model = authority(request.sourceRoot);
786
+ return withPublicationContext(request, (context) => {
787
+ context.assertValid();
788
+ const lock = acquireWriterLock(context.pinnedPath(), 2000);
789
+ try {
790
+ context.assertValid();
791
+ const existing = new Set(discoverEntities(context.pinnedPath(), request.sourceRoot).entities.map(({ id }) => id).filter((id) => id !== null));
792
+ for (let attempt = 0; attempt < 1024; attempt += 1) {
793
+ const id = candidate ? candidate() : generatedId(model);
794
+ if (!model.pattern.test(id))
795
+ throw new Error(`entity ID candidate '${id}' must match ${model.pattern.source}`);
796
+ if (existing.has(id))
797
+ continue;
798
+ return publishEntityLocked({ ...request, id }, model, context);
799
+ }
800
+ throw new Error("could not allocate a unique entity ID after 1024 attempts; run agentera check validate state and retry");
801
+ }
802
+ finally {
803
+ lock.release();
804
+ }
805
+ });
806
+ }
807
+ //# sourceMappingURL=entityStorage.js.map