From 1ed6c294c1d47fb1cc9a7961289d21aaa5d0024a Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 4 Sep 2026 01:06:40 -0400 Subject: [PATCH] fix(projects): admit nested projects in context packets --- .../src/projects-context.js | 2 +- .../test/projects-context.test.js | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/src/projects-context.js b/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/src/projects-context.js index 46270ae2..33f35282 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/src/projects-context.js +++ b/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/src/projects-context.js @@ -437,7 +437,7 @@ function validatePacketRecord(record) { if (!exactKeys(record, RECORD_PACKET_FIELDS) || !/^\w+_[0-9]{6,}$/.test(record.id) || !['project', 'outcome'].includes(record.kind)) return false; const allowedLifecycle = record.kind === 'project' ? ['active', 'paused', 'archived'] : ['planned', 'active', 'complete', 'archived']; if (requiredString(record.title, 'record.title') !== record.title || !Array.isArray(record.aliases) || record.aliases.some((alias) => requiredString(alias, 'record.alias') !== alias) || new Set(record.aliases).size !== record.aliases.length || !allowedLifecycle.includes(record.lifecycle)) return false; - if ((record.parentId !== null && (!/^prj_[0-9]{6,}$/.test(record.parentId) || record.parentId === record.id)) || (record.kind === 'project' && record.parentId !== null) || (record.kind === 'outcome' && record.parentId === null)) return false; + if ((record.parentId !== null && (!/^prj_[0-9]{6,}$/.test(record.parentId) || record.parentId === record.id)) || (record.kind === 'outcome' && record.parentId === null)) return false; if (!['high', 'medium', 'low', 'unset'].includes(record.declaredPriority) || !['high', 'medium', 'low', 'unset'].includes(record.effectivePriority)) return false; if (!exactKeys(record.priority, PRIORITY_FIELDS) || !['explicit', 'inherited', 'unset'].includes(record.priority.source) || !['high', 'medium', 'low', 'unset'].includes(record.priority.declared) || !['high', 'medium', 'low', 'unset'].includes(record.priority.effective) || (record.priority.sourceRecordId !== null && !/^\w+_[0-9]{6,}$/.test(record.priority.sourceRecordId)) || (record.priority.sourceKind !== null && !['project', 'outcome'].includes(record.priority.sourceKind))) return false; if (Number.isNaN(Date.parse(record.createdAt)) || Number.isNaN(Date.parse(record.updatedAt)) || (record.goal !== null && typeof record.goal !== 'string') || (record.definitionOfDone !== null && typeof record.definitionOfDone !== 'string') || !isPlainObject(record.links)) return false; diff --git a/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/test/projects-context.test.js b/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/test/projects-context.test.js index 8e9d9691..5ed7a8df 100644 --- a/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/test/projects-context.test.js +++ b/modules/jarvos-secondbrain/packages/jarvos-secondbrain-projects/test/projects-context.test.js @@ -147,6 +147,28 @@ test('canonical-only packet is useful and optional providers are visibly omitted assert.equal(validateContextPacket(result.packet).ok, true); }); +test('canonical packet accepts nested projects beneath a project parent', () => { + const { registry, root, outcome } = makeRegistry(); + const child = registry.create({ title: 'Active Assistant', parentId: root.id }).record; + const query = queryFor(root, outcome, { + scope: { projectIds: [root.id], outcomeIds: [], includeDescendants: true }, + }); + const result = buildContextPacket({ + registry, + query, + capability: issue(query), + capabilitySecret: SECRET, + subject: 'agent:test-session', + hostId: 'projects-host', + now: NOW, + providers: {}, + }); + + assert.equal(result.status, 'ok'); + assert.equal(result.packet.canonical.records.find((record) => record.id === child.id).parentId, root.id); + assert.equal(validateContextPacket(result.packet).ok, true); +}); + test('inference is versioned, provisional candidates are explicitly non-actionable, and coverage remains typed', () => { const { registry, root, outcome } = makeRegistry(); const query = queryFor(root, outcome);