agentera 3.0.0-dev.47 → 3.0.0-dev.51
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 +15 -4
- package/bundle/CHANGELOG.md +4 -4
- package/bundle/references/adapters/package-publication.json +4 -4
- package/bundle/references/adapters/package-registry.yaml +2 -1
- package/bundle/references/artifacts/state-storage-authority.yaml +51 -11
- package/bundle/skills/agentera/SKILL.md +11 -2
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +38 -4
- package/dist/capabilities/orchestrate/instructions.js +2 -0
- package/dist/capabilities/plan/instructions.js +1 -0
- package/dist/capabilities/status/instructions.js +5 -1
- package/dist/cli/commands/compact.js +125 -3
- package/dist/cli/commands/doctor.js +8 -1
- package/dist/cli/commands/prime/collectEntityOrientation.js +22 -7
- package/dist/cli/commands/prime/collectOrientationState.js +3 -3
- package/dist/cli/commands/prime/orientationOutput.js +2 -0
- package/dist/cli/commands/state/write.js +1 -5
- package/dist/cli/commands/validate.js +1 -1
- package/dist/cli/help.js +4 -0
- package/dist/cli/orientation/attention.js +4 -2
- package/dist/core/developmentInvocation.js +1 -1
- package/dist/registries/activationTuples.js +3 -2
- package/dist/registries/packagePublication.js +2 -2
- package/dist/state/entityStorage.js +9 -6
- package/dist/state/planEntities.js +485 -33
- package/dist/state/planLineageValidation.js +52 -0
- package/dist/state/planReplacementTransaction.js +475 -0
- package/dist/state/todoActivationSafety.js +58 -0
- package/dist/state/todoDocsEntities.js +22 -17
- package/dist/state/todoReconciliationActivation.js +12 -4
- package/dist/state/todoReconciliationInspection.js +8 -12
- package/dist/state/write/explain.js +32 -1
- package/dist/state/write/grammar.js +5 -0
- package/dist/state/write/operations.js +1 -0
- package/dist/state/write/runtimeOperations.js +6 -4
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { listProgressEntities } from "../../../state/progressEntities.js";
|
|
3
3
|
import { listDecisionEntities } from "../../../state/decisionEntities.js";
|
|
4
4
|
import { listHealthEntities } from "../../../state/healthEntities.js";
|
|
5
|
-
import { listPlanEntities, listPlanTaskEntities } from "../../../state/planEntities.js";
|
|
5
|
+
import { listPlanEntities, listPlanTaskEntities, openPlanConflictDiagnostic } from "../../../state/planEntities.js";
|
|
6
6
|
import { listObjectiveEntities, listExperimentEntities } from "../../../state/objectiveExperimentEntities.js";
|
|
7
7
|
import { assertTodoReconciliationReadable, listTodoDocsEntities, projectTodoReadEntities } from "../../../state/todoDocsEntities.js";
|
|
8
8
|
import { inspectTodoReconciliationState } from "../../../state/todoReconciliationInspection.js";
|
|
@@ -105,22 +105,31 @@ function projectedHistory(list, artifact, fullCount, summaryCount, degraded) {
|
|
|
105
105
|
source_contract: list.source_contract,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
108
|
+
function entityPlanStatus(entry) {
|
|
109
|
+
const plan = entry.record ?? {};
|
|
110
|
+
const planHeader = plan.header && typeof plan.header === "object" && !Array.isArray(plan.header)
|
|
111
|
+
? plan.header
|
|
112
|
+
: {};
|
|
113
|
+
return String(planHeader.status ?? plan.status ?? "");
|
|
114
|
+
}
|
|
115
|
+
function selected(entries, artifact, candidateIds = entries.map((entry) => String(entry.id)), sourceRoot) {
|
|
116
|
+
if (candidateIds.length < 2)
|
|
110
117
|
return entries[0];
|
|
111
|
-
const ids =
|
|
118
|
+
const ids = candidateIds.slice().sort();
|
|
112
119
|
const noun = artifact === "plan" ? "open plans" : "active objectives";
|
|
113
120
|
const list = preCutoverCommand(`state ${artifact} list --format json`);
|
|
121
|
+
const planConflict = artifact === "plan" ? openPlanConflictDiagnostic(ids, sourceRoot) : undefined;
|
|
114
122
|
throw new StateRetrievalFailure({
|
|
115
123
|
schemaVersion: "agentera.stateFailure.v1",
|
|
116
124
|
status: "fail",
|
|
117
125
|
error: {
|
|
118
126
|
class: "ambiguous",
|
|
119
127
|
artifact,
|
|
120
|
-
message: `multiple ${noun} require explicit selection: ${ids}`,
|
|
128
|
+
message: planConflict?.message ?? `multiple ${noun} require explicit selection: ${ids.join(", ")}`,
|
|
121
129
|
syntax: preCutoverCommand(`state ${artifact} get --id ID --format json`),
|
|
122
130
|
example: preCutoverCommand(`state ${artifact} get --id ${String(entries[0]?.id)} --format json`),
|
|
123
|
-
recovery: `Run ${list}, resolve the competing ${noun}, and retry prime.`,
|
|
131
|
+
recovery: planConflict?.recovery ?? `Run ${list}, resolve the competing ${noun}, and retry prime.`,
|
|
132
|
+
...(planConflict ? { details: planConflict.details } : {}),
|
|
124
133
|
},
|
|
125
134
|
}, 1);
|
|
126
135
|
}
|
|
@@ -163,7 +172,13 @@ export function collectEntityOrientation(projectRoot, sourceRoot) {
|
|
|
163
172
|
const objectiveEntries = entries(objectiveList);
|
|
164
173
|
const todoEntries = entries(todoList);
|
|
165
174
|
const docsEntries = entries(docsList);
|
|
166
|
-
|
|
175
|
+
// Startup detail is bounded to two plans, but ambiguous diagnostics must use
|
|
176
|
+
// the complete canonical candidate set so their ID sample and omission count
|
|
177
|
+
// match direct plan selection without creating another selector.
|
|
178
|
+
const openPlanCandidateIds = discovery.entities
|
|
179
|
+
.filter((entry) => entry.boundary === "plan" && entry.classification === "valid" && entry.id && ["open", "active"].includes(entityPlanStatus(entry)))
|
|
180
|
+
.map((entry) => entry.id);
|
|
181
|
+
const selectedPlan = selected(planEntries, "plan", openPlanCandidateIds, sourceRoot);
|
|
167
182
|
const taskPage = selectedPlan
|
|
168
183
|
? listPlanTaskEntities(projectRoot, String(selectedPlan.id), 100, undefined, { sourceRoot, format: "json", discovery })
|
|
169
184
|
: null;
|
|
@@ -99,10 +99,10 @@ export function collectOrientationState(opts) {
|
|
|
99
99
|
const readiness = selectStatusReadiness(plan, health, objective, todoItems, decision, savedContext, entity.todoReadiness);
|
|
100
100
|
const reconciliationReadiness = entity.todoReconciliation?.status === "action_required"
|
|
101
101
|
? withRecommended(readiness, {
|
|
102
|
-
object: entity.todoReconciliation.state === "inactive" ? "Activate TODO reconciliation" : "Repair TODO reconciliation",
|
|
103
|
-
capability: "build",
|
|
102
|
+
object: entity.todoReconciliation.state === "unsafe_inactive" ? "Replan unsafe TODO activation correction" : entity.todoReconciliation.state === "inactive" ? "Activate TODO reconciliation" : "Repair TODO reconciliation",
|
|
103
|
+
capability: entity.todoReconciliation.state === "unsafe_inactive" ? "plan" : "build",
|
|
104
104
|
reason: entity.todoReconciliation.recovery_command,
|
|
105
|
-
phase: entity.todoReconciliation.state === "invalid_lifecycle" ? "audit" : "build",
|
|
105
|
+
phase: entity.todoReconciliation.state === "unsafe_inactive" ? "plan" : entity.todoReconciliation.state === "invalid_lifecycle" ? "audit" : "build",
|
|
106
106
|
})
|
|
107
107
|
: readiness;
|
|
108
108
|
const nextAction = cutover.status !== "complete"
|
|
@@ -214,6 +214,8 @@ export function buildStatusContextState(state, _command = "prime", _options = {}
|
|
|
214
214
|
plan: {
|
|
215
215
|
exists: plan.exists,
|
|
216
216
|
active: plan.active ?? false,
|
|
217
|
+
id: plan.id ?? null,
|
|
218
|
+
artifact: plan.exists ? plan.artifact ?? "plan" : null,
|
|
217
219
|
status: plan.status,
|
|
218
220
|
title: plan.title ?? null,
|
|
219
221
|
complete: plan.complete ?? 0,
|
|
@@ -272,7 +272,7 @@ function parseWrite(artifactRaw, argv) {
|
|
|
272
272
|
message: `${artifact} ${verb} accepts field flags, not --input`,
|
|
273
273
|
example: exampleFor(artifact, verb),
|
|
274
274
|
});
|
|
275
|
-
if (spec.inputRoot && !inputSource)
|
|
275
|
+
if (spec.inputRoot && !inputSource && !spec.inputOptional)
|
|
276
276
|
invalid({
|
|
277
277
|
class: "missing_argument",
|
|
278
278
|
message: `--input is required for ${artifact} ${verb}`,
|
|
@@ -311,11 +311,7 @@ function parseWrite(artifactRaw, argv) {
|
|
|
311
311
|
const id = mappingPath(values, "id");
|
|
312
312
|
if (taskVerb && id === undefined)
|
|
313
313
|
invalid({ class: "missing_argument", message: `--id is required for plan ${verb} in entity mode` });
|
|
314
|
-
if (force)
|
|
315
|
-
invalid({ class: "unrecognized_argument", message: "--force is unavailable for entity plans; incomplete plans remain canonical history" });
|
|
316
314
|
}
|
|
317
|
-
if (artifact === "plan" && verb === "create" && force)
|
|
318
|
-
invalid({ class: "unrecognized_argument", message: "--force is unavailable for entity plan create because multiple open plans coexist" });
|
|
319
315
|
return { artifact, spec, format, projectRoot, dryRun, force, values, callerPayload, inputSource };
|
|
320
316
|
}
|
|
321
317
|
function mappingPath(entry, field) {
|
|
@@ -457,7 +457,7 @@ export function cmdValidateState(args, io) {
|
|
|
457
457
|
return payload.valid ? 0 : 1;
|
|
458
458
|
}
|
|
459
459
|
function todoReconciliationValidationIssue(diagnosis) {
|
|
460
|
-
const label = diagnosis.state === "inactive" ? "inactive" : diagnosis.state === "unsafe_active" ? "unsafe active" : "invalid lifecycle";
|
|
460
|
+
const label = diagnosis.state === "inactive" ? "inactive" : diagnosis.state === "unsafe_inactive" ? "unsafe inactive" : diagnosis.state === "unsafe_active" ? "unsafe active" : "invalid lifecycle";
|
|
461
461
|
return {
|
|
462
462
|
code: `todo_reconciliation_${diagnosis.state}`,
|
|
463
463
|
path: "TODO.md",
|
package/dist/cli/help.js
CHANGED
|
@@ -146,6 +146,10 @@ export function printStateHelp(sub) {
|
|
|
146
146
|
"Invalid historical archives remain non-fatal compatibility diagnostics unless selected.",
|
|
147
147
|
"Task list accepts an optional bare plan ID and otherwise defaults to the sole open plan; task get requires --id.",
|
|
148
148
|
"Only the displayed bare-ID selectors are accepted.",
|
|
149
|
+
"Plan create rejects an open predecessor unless --force can archive exactly one unchanged; create --force records that predecessor's bare ID in successor.previous_plan_archived.",
|
|
150
|
+
"Targeted replacement is explicit: state plan replace --predecessor ID --successor ID, or --predecessor ID --input PLAN.yaml to create the successor. It changes only the named predecessor lifecycle and derived successor lineage.",
|
|
151
|
+
"Competing open-plan diagnostics retain bounded bare IDs and require explicit roles: state plan replace --predecessor PREDECESSOR_ID --successor SUCCESSOR_ID --format json. They never infer a role from list order.",
|
|
152
|
+
"Archive completed plans normally. Archive an unfinished selected plan with --force; an implicit archive with multiple open plans rejects without effects.",
|
|
149
153
|
"List limits are 1 through 100; structured pages are at most 32,768 UTF-8 bytes and omit whole entries only.",
|
|
150
154
|
"Legacy plan identity collisions return a structured ambiguous error.",
|
|
151
155
|
"",
|
|
@@ -6,8 +6,10 @@ export function buildOrientationAttention(state) {
|
|
|
6
6
|
const { v1_migration: v1Migration, project_integration: projectIntegration, health, plan, decision_attention: decisionAttention, glossary_caveat_attention: glossaryCaveatAttention, corpus_coverage: corpusCoverage, todo_items: todoItems, todo_reconciliation: todoReconciliation, } = state;
|
|
7
7
|
const attention = [];
|
|
8
8
|
if (todoReconciliation?.status === "action_required") {
|
|
9
|
-
const label = todoReconciliation.state === "inactive" ? "inactive" : todoReconciliation.state === "unsafe_active" ? "unsafe active" : "invalid lifecycle";
|
|
10
|
-
attention.push(
|
|
9
|
+
const label = todoReconciliation.state === "inactive" ? "inactive" : todoReconciliation.state === "unsafe_inactive" ? "unsafe inactive" : todoReconciliation.state === "unsafe_active" ? "unsafe active" : "invalid lifecycle";
|
|
10
|
+
attention.push(todoReconciliation.state === "unsafe_inactive"
|
|
11
|
+
? `action-required: TODO reconciliation is ${label}; ${todoReconciliation.recovery_command}`
|
|
12
|
+
: `action-required: TODO reconciliation is ${label}; preview \`${todoReconciliation.preview_command ?? "n/a"}\`; apply \`${todoReconciliation.apply_command ?? "n/a"}\``);
|
|
11
13
|
}
|
|
12
14
|
const skillDivergenceSignals = (state.app.signals ?? []).filter((s) => s.kind === "skill_root_divergence");
|
|
13
15
|
for (const signal of skillDivergenceSignals) {
|
|
@@ -284,7 +284,7 @@ function validateOperationExample(spec, template) {
|
|
|
284
284
|
}
|
|
285
285
|
if (!format)
|
|
286
286
|
invalid(`${spec.artifact}.${spec.verb} example omits --format`);
|
|
287
|
-
if (spec.inputMode === "structured" && !input)
|
|
287
|
+
if (spec.inputMode === "structured" && !spec.inputOptional && !input)
|
|
288
288
|
invalid(`${spec.artifact}.${spec.verb} example omits --input`);
|
|
289
289
|
for (const field of spec.fields) {
|
|
290
290
|
if (field.required && !seen.has(field.flag))
|
|
@@ -74,6 +74,7 @@ const decodedCatalog = gunzipSync(Buffer.from(CATALOG, "base64")).toString("utf8
|
|
|
74
74
|
const emittedReasons = JSON.parse(gunzipSync(Buffer.from(EMITTED_REASON_CATALOG, "base64")).toString("utf8"));
|
|
75
75
|
const rawTuples = JSON.parse(decodedCatalog.slice(decodedCatalog.indexOf("[", 2)).replace(/,\s*]$/, "]"));
|
|
76
76
|
const addedTuples = [
|
|
77
|
+
{ class: "state", surface_id: "write:plan.replace", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "plan.replace", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
77
78
|
{ class: "state", surface_id: "write:todo.activate", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.activate", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
78
79
|
{ class: "state", surface_id: "write:todo.repair", owner_path: "packages/cli/src/state/write/runtimeOperations.ts", owner_symbol_or_selector: "runtimeOperationSpecs", owner_selector: "todo.repair", semantic_selector_if_any: null, canonical_correction: "node packages/cli/dist/bin/agentera.js check validate state --format json" },
|
|
79
80
|
{ class: "package", surface_id: "emitted:packages/cli/src/cli/commands/doctor.ts", owner_path: "packages/cli/src/registries/packageRegistry.ts", owner_symbol_or_selector: "loadRegistry", owner_selector: "packages/cli/src/cli/commands/doctor.ts", semantic_selector_if_any: JSON.stringify({ path: "packages/cli/src/cli/commands/doctor.ts", selector: null, format: null, classification: null, reason: "Doctor project-state signals publish bounded reconciliation preview and apply guidance." }), canonical_correction: "pnpm -C packages/cli run verify:package" },
|
|
@@ -94,11 +95,11 @@ export const ACTIVATION_TUPLE_AUTHORITY = Object.freeze({
|
|
|
94
95
|
capability: { count: 12, sha256: "892e6e5e2a57b41064bc44fa2946453225f1b1195aff77aad05365fd0a1071c2" },
|
|
95
96
|
runtime: { count: 81, sha256: "99b2abff3ebff889b54b1781c563ab4b32609a479c4e90d6aad854f48fba7edc" },
|
|
96
97
|
reference: { count: 22, sha256: "243fd3c317553f8924c56a52c61af90ccf2793b7b0018af1373b73c69b6c3afb" },
|
|
97
|
-
state: { count:
|
|
98
|
+
state: { count: 37, sha256: "210bd831c5d62beac149efd888ee4a99af49a9469cf373031f8db9893904d0c1" },
|
|
98
99
|
package: { count: 66, sha256: "83e6971af7f7564e42369aaccc445b32a0793bfffade843d149b6abd4fd3dbbc" },
|
|
99
100
|
bootstrap: { count: 34, sha256: "9a7dd7e27110d85cf5c08835fdd8f08119e75579858e63bc6d396c733961d0bc" },
|
|
100
101
|
},
|
|
101
|
-
total: { count:
|
|
102
|
+
total: { count: 279, sha256: "c2d7a66bc9af04a5511348825995317a5968357c6cacbfd87eb2cc4fbe77156c" },
|
|
102
103
|
});
|
|
103
104
|
export function canonicalTupleJson(value) { return JSON.stringify(value); }
|
|
104
105
|
export function digestCanonicalTuples(values) {
|
|
@@ -53,11 +53,11 @@ export const ACTIVATION_CENSUS_AUTHORITY = deepFreeze({
|
|
|
53
53
|
capability: { count: 12, sha256: "007e1157a892fec54182b1803aeaa442cf8e2e332e6a055c3bd020e2b0731867" },
|
|
54
54
|
runtime: { count: 81, sha256: "03fe600a3a27f05daf0d1b59c16b00bedc354a13c70ea60be0b457175fdf743c" },
|
|
55
55
|
reference: { count: 22, sha256: "2b48c82bb7ee10388d9ebb4fa7ea3743ea82a7e9aaedfcd5a858eeedb9d891ae" },
|
|
56
|
-
state: { count:
|
|
56
|
+
state: { count: 37, sha256: "ca87acdb0fda04ba46a52163fc30ec6578c077e61fcb64597f861a36fdce1dc4" },
|
|
57
57
|
package: { count: 66, sha256: "3548af7e84151c90690a6eb3d1cb6c7847f39b290161ecab599d8cb1bf0d2cb0" },
|
|
58
58
|
bootstrap: { count: 34, sha256: "71c2038744e2518a6adb722acbb5f9352bddfb5d1c92eeb0e347297ef2ca2f1e" },
|
|
59
59
|
},
|
|
60
|
-
total: { count:
|
|
60
|
+
total: { count: 279, sha256: "b92cd469f858e9584682d05b4d9bb03c2fc4b905f3485475723786c0a978c138" },
|
|
61
61
|
});
|
|
62
62
|
/** Each dimension names the production contract it observes independently. */
|
|
63
63
|
export const ACTIVATION_EVIDENCE_SOURCES = deepFreeze({
|
|
@@ -13,6 +13,7 @@ import { summaryMigrationProvenanceDeclaration, summaryMigrationProvenanceViolat
|
|
|
13
13
|
import { validateCompactedSummarySourceRowAuthority } from "./summarySourceRowAuthority.js";
|
|
14
14
|
import { acquireWriterLock } from "./write/lock.js";
|
|
15
15
|
import { planTaskRecordViolations } from "./write/planEvaluation.js";
|
|
16
|
+
import { planLineageIssues } from "./planLineageValidation.js";
|
|
16
17
|
import { todoDocsRecordViolations } from "./todoDocsEntityValidation.js";
|
|
17
18
|
import { applyGlossaryCaveatLifecycleValidation, validateProgressGlossaryCaveat } from "./progressGlossaryCaveat.js";
|
|
18
19
|
import { progressPublicationOrderViolations } from "./progressPublicationOrder.js";
|
|
@@ -63,8 +64,8 @@ function canonicalEntityEnvelopeAgainstModel(bytes, expected, model, sourceRoot,
|
|
|
63
64
|
violations.push(...healthEntityViolations(record));
|
|
64
65
|
if (expected.boundary === "plan") {
|
|
65
66
|
const header = mapping(record.header) ? record.header : {};
|
|
66
|
-
if (!mapping(record.header) || typeof header.title !== "string" || typeof header.created !== "string" || !["open", "complete", "archived"].includes(String(header.status)))
|
|
67
|
-
violations.push("invalid plan lifecycle fields");
|
|
67
|
+
if (!mapping(record.header) || typeof header.title !== "string" || typeof header.created !== "string" || !["open", "complete", "archived"].includes(String(header.status)) || (record.replacement_input_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(String(record.replacement_input_sha256))))
|
|
68
|
+
violations.push("invalid plan lifecycle or replacement identity fields");
|
|
68
69
|
}
|
|
69
70
|
if (expected.boundary === "plan_task")
|
|
70
71
|
violations.push(...planTaskRecordViolations(record));
|
|
@@ -430,9 +431,9 @@ function discoverFile(projectRoot, file, pathArtifact, boundary, model, issues,
|
|
|
430
431
|
}
|
|
431
432
|
if (!malformed && boundary === "plan" && record) {
|
|
432
433
|
const header = mapping(record.header) ? record.header : {};
|
|
433
|
-
if (!mapping(record.header) || typeof header.title !== "string" || typeof header.created !== "string" || !["open", "complete", "archived"].includes(String(header.status))) {
|
|
434
|
+
if (!mapping(record.header) || typeof header.title !== "string" || typeof header.created !== "string" || !["open", "complete", "archived"].includes(String(header.status)) || (record.replacement_input_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(String(record.replacement_input_sha256)))) {
|
|
434
435
|
malformed = true;
|
|
435
|
-
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
|
|
436
|
+
issues.push({ code: "malformed_entity", path: relativePath, id, artifact, boundary, message: `entity '${relativePath}' has invalid plan lifecycle or replacement identity fields`, recovery: recovery(projectRoot, `repair '${relativePath}' using the current plan header schema, open|complete|archived lifecycle, and lowercase SHA-256 replacement identity`) });
|
|
436
437
|
}
|
|
437
438
|
}
|
|
438
439
|
if (!malformed && boundary === "plan_task" && record) {
|
|
@@ -550,8 +551,8 @@ export function assertEntityDiscoveryOrigin(projectRoot, sourceRoot, discovery)
|
|
|
550
551
|
}
|
|
551
552
|
function relationTargets(entity, relation) {
|
|
552
553
|
const value = entity.record?.[relation.field];
|
|
553
|
-
if (relation.cardinality === "exactly_one")
|
|
554
|
-
return typeof value === "string" ? [value] : null;
|
|
554
|
+
if (relation.cardinality === "exactly_one" || relation.cardinality === "zero_or_one")
|
|
555
|
+
return typeof value === "string" ? [value] : relation.cardinality === "zero_or_one" && (value === undefined || value === null) ? [] : null;
|
|
555
556
|
if (value === undefined || value === null)
|
|
556
557
|
return [];
|
|
557
558
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
@@ -649,6 +650,8 @@ export function validateEntityDiscovery(projectRoot, sourceRoot, discovery, boun
|
|
|
649
650
|
});
|
|
650
651
|
}
|
|
651
652
|
}
|
|
653
|
+
const canonicalPlans = discovery.entities.filter((entity) => entity.boundary === "plan" && entity.classification === "valid" && entity.id && entity.record);
|
|
654
|
+
issues.push(...planLineageIssues(canonicalPlans, (action) => recovery(projectRoot, action)));
|
|
652
655
|
for (const definition of model.entities) {
|
|
653
656
|
if (!definition.ownership || definition.ownership.cardinality !== "zero_or_one")
|
|
654
657
|
continue;
|