openplanr 1.18.0 → 1.20.0
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/dist/cli/commands/backlog.d.ts.map +1 -1
- package/dist/cli/commands/backlog.js +3 -0
- package/dist/cli/commands/backlog.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +3 -0
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/epic.d.ts.map +1 -1
- package/dist/cli/commands/epic.js +2 -0
- package/dist/cli/commands/epic.js.map +1 -1
- package/dist/cli/commands/feature.d.ts.map +1 -1
- package/dist/cli/commands/feature.js +2 -0
- package/dist/cli/commands/feature.js.map +1 -1
- package/dist/cli/commands/operate.d.ts +5 -2
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +10 -101
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/cli/commands/plan.d.ts.map +1 -1
- package/dist/cli/commands/plan.js +2 -0
- package/dist/cli/commands/plan.js.map +1 -1
- package/dist/cli/commands/quick.d.ts.map +1 -1
- package/dist/cli/commands/quick.js +2 -0
- package/dist/cli/commands/quick.js.map +1 -1
- package/dist/cli/commands/refine.d.ts.map +1 -1
- package/dist/cli/commands/refine.js +2 -0
- package/dist/cli/commands/refine.js.map +1 -1
- package/dist/cli/commands/revise.d.ts.map +1 -1
- package/dist/cli/commands/revise.js +2 -0
- package/dist/cli/commands/revise.js.map +1 -1
- package/dist/cli/commands/spec.d.ts.map +1 -1
- package/dist/cli/commands/spec.js +2 -0
- package/dist/cli/commands/spec.js.map +1 -1
- package/dist/cli/commands/sprint.d.ts.map +1 -1
- package/dist/cli/commands/sprint.js +2 -0
- package/dist/cli/commands/sprint.js.map +1 -1
- package/dist/cli/commands/story.d.ts.map +1 -1
- package/dist/cli/commands/story.js +3 -0
- package/dist/cli/commands/story.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/services/ai-service.d.ts +3 -5
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +7 -1
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/credentials-service.d.ts.map +1 -1
- package/dist/services/credentials-service.js +2 -0
- package/dist/services/credentials-service.js.map +1 -1
- package/dist/services/deprecation-notices.d.ts +5 -0
- package/dist/services/deprecation-notices.d.ts.map +1 -0
- package/dist/services/deprecation-notices.js +6 -0
- package/dist/services/deprecation-notices.js.map +1 -0
- package/dist/services/operate/advisors.d.ts +146 -108
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +178 -448
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/citation-resolution.d.ts.map +1 -1
- package/dist/services/operate/citation-resolution.js +50 -1
- package/dist/services/operate/citation-resolution.js.map +1 -1
- package/dist/services/operate/config.d.ts +28 -52
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +68 -203
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/doctor.d.ts +15 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +142 -32
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +22 -62
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +269 -191
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/evidence.d.ts +9 -78
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +0 -1194
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +15 -231
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/integrity.d.ts +81 -0
- package/dist/services/operate/integrity.d.ts.map +1 -0
- package/dist/services/operate/integrity.js +152 -0
- package/dist/services/operate/integrity.js.map +1 -0
- package/dist/services/operate/interaction/answer-service.d.ts +0 -10
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +63 -43
- package/dist/services/operate/interaction/answer-service.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +105 -21
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +16 -1
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +45 -39
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/session-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/session-service.js +16 -11
- package/dist/services/operate/interaction/session-service.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +16 -6
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/lifecycle.d.ts.map +1 -1
- package/dist/services/operate/lifecycle.js +17 -1
- package/dist/services/operate/lifecycle.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +95 -276
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/mission-dispatch.d.ts +73 -55
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
- package/dist/services/operate/mission-dispatch.js +91 -67
- package/dist/services/operate/mission-dispatch.js.map +1 -1
- package/dist/services/operate/projection-persistence.d.ts +35 -7
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +142 -53
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts +9 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +22 -12
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +1 -41
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js +7 -45
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/redaction.d.ts +3 -3
- package/dist/services/operate/redaction.js +4 -4
- package/dist/services/operate/redaction.js.map +1 -1
- package/dist/services/operate/reports.d.ts +20 -1
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +108 -4
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/routes.d.ts +84 -0
- package/dist/services/operate/routes.d.ts.map +1 -1
- package/dist/services/operate/routes.js +519 -10
- package/dist/services/operate/routes.js.map +1 -1
- package/dist/services/operate/types.d.ts +48 -133
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +0 -1
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +0 -2
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.js +2 -2
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +3 -3
- package/dist/services/operate/evidence-classifications.d.ts +0 -59
- package/dist/services/operate/evidence-classifications.d.ts.map +0 -1
- package/dist/services/operate/evidence-classifications.js +0 -132
- package/dist/services/operate/evidence-classifications.js.map +0 -1
- package/dist/services/operate/evidence-diagnostics.d.ts +0 -23
- package/dist/services/operate/evidence-diagnostics.d.ts.map +0 -1
- package/dist/services/operate/evidence-diagnostics.js +0 -152
- package/dist/services/operate/evidence-diagnostics.js.map +0 -1
- package/dist/services/operate/evidence-readiness.d.ts +0 -8
- package/dist/services/operate/evidence-readiness.d.ts.map +0 -1
- package/dist/services/operate/evidence-readiness.js +0 -84
- package/dist/services/operate/evidence-readiness.js.map +0 -1
|
@@ -3,15 +3,14 @@ import { mkdir, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { OPENPLANR_VERSION } from '../../utils/package-version.js';
|
|
5
5
|
import { loadConfig } from '../config-service.js';
|
|
6
|
-
import { advisorFailureGaps, buildAdvisorOperatingContext,
|
|
6
|
+
import { advisorFailureGaps, buildAdvisorOperatingContext, configuredAdvisorProviderPolicy, createConfiguredStructuredAdapter, createOfflineAdvisorAdapter, dispatchOperatingAdvisors, ensureOperatingProviderConsent, } from './advisors.js';
|
|
7
7
|
import { recordOperatingCadenceRun } from './cadence.js';
|
|
8
8
|
import { canonicalDigest, canonicalize } from './canonical.js';
|
|
9
|
-
import { operatingProjectKey,
|
|
9
|
+
import { operatingProjectKey, validateOperatingConfiguration } from './config.js';
|
|
10
10
|
import { consolidateOperatingResults } from './consolidation.js';
|
|
11
11
|
import { OperatingEventStore } from './event-store.js';
|
|
12
|
-
import {
|
|
12
|
+
import { evidenceProjectionSources } from './evidence.js';
|
|
13
13
|
import { OperatingEvidenceCache } from './evidence-cache.js';
|
|
14
|
-
import { evaluateEvidenceReadiness } from './evidence-readiness.js';
|
|
15
14
|
import { nextCycleId } from './ids.js';
|
|
16
15
|
import { enforceRecordedProposalCitations } from './interaction/action-service.js';
|
|
17
16
|
import { applyJournalTransaction, prepareJournalTransaction, readJournal, rollbackJournalTransaction, } from './journal.js';
|
|
@@ -34,110 +33,6 @@ function normalizeFocus(focus) {
|
|
|
34
33
|
function loadJson(target) {
|
|
35
34
|
return readFile(target, 'utf8').then((raw) => JSON.parse(raw));
|
|
36
35
|
}
|
|
37
|
-
const MISSION_REVISION_PATTERN = /^[a-f0-9]{7,64}$/;
|
|
38
|
-
const MISSION_CYCLE_PATTERN = /^CYCLE-[0-9]{3,}$/;
|
|
39
|
-
const MISSION_DECISION_PATTERN = /^DEC-[0-9]{3,}$/;
|
|
40
|
-
const MISSION_GAP_PATTERN = /^GAP-[A-Za-z0-9._-]+$/;
|
|
41
|
-
const MISSION_OUTCOME_PATTERN = /^OUT-[0-9]{3,}$/;
|
|
42
|
-
function boundedMissionText(value, maximum, fallback) {
|
|
43
|
-
const text = value.replace(/\s+/g, ' ').trim().slice(0, maximum);
|
|
44
|
-
return text || fallback;
|
|
45
|
-
}
|
|
46
|
-
function missionOpenItemIds(items, pattern) {
|
|
47
|
-
return [...new Set(items.map((item) => item.id).filter((id) => pattern.test(id)))].sort();
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Source a mission packet's non-evidence payload (FR1/E-001) from live cycle
|
|
51
|
-
* and workspace state at the point mission packets are constructed: the product
|
|
52
|
-
* charter and current goals, a prior-cycle summary with its open
|
|
53
|
-
* decisions/gaps/pending outcomes, planning and delivery status, the read roots
|
|
54
|
-
* declared for the index, and the revision pinned once at cycle start (recorded
|
|
55
|
-
* in the workspace manifest's control repository and threaded identically into
|
|
56
|
-
* every mission packet). Nothing here is invented: every value is read from the
|
|
57
|
-
* parsed charter context, the reduced state, the operating config, and the
|
|
58
|
-
* captured workspace manifest.
|
|
59
|
-
*/
|
|
60
|
-
export function sourceOperatingMissionPacketState(input) {
|
|
61
|
-
const { context } = input;
|
|
62
|
-
const charter = context.charter;
|
|
63
|
-
const charterLine = [
|
|
64
|
-
charter.purpose && `Purpose: ${charter.purpose}`,
|
|
65
|
-
charter.stage && `Stage: ${charter.stage}`,
|
|
66
|
-
charter.businessModel && `Business model: ${charter.businessModel}`,
|
|
67
|
-
charter.idealCustomer && `Ideal customer: ${charter.idealCustomer}`,
|
|
68
|
-
]
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.join('. ');
|
|
71
|
-
const currentGoals = [...new Set(charter.goals.map((goal) => goal.trim()).filter(Boolean))]
|
|
72
|
-
.map((goal) => goal.slice(0, 512))
|
|
73
|
-
.sort();
|
|
74
|
-
const prior = context.priorCycle;
|
|
75
|
-
const priorSummaryText = prior
|
|
76
|
-
? `Prior cycle ${prior.id} (${prior.state}, health ${prior.health ?? 'unknown'}) closed with ` +
|
|
77
|
-
`${prior.findings} finding(s), ${prior.decisions} decision(s), ${prior.gaps} gap(s), and ` +
|
|
78
|
-
`${prior.pendingOutcomes} pending outcome(s).`
|
|
79
|
-
: 'No prior operating cycle.';
|
|
80
|
-
const priorCycleSummary = {
|
|
81
|
-
...(prior && MISSION_CYCLE_PATTERN.test(prior.id) ? { cycleId: prior.id } : {}),
|
|
82
|
-
summary: boundedMissionText(priorSummaryText, 8192, 'No prior operating cycle.'),
|
|
83
|
-
openDecisions: missionOpenItemIds(context.openDecisions, MISSION_DECISION_PATTERN),
|
|
84
|
-
openGaps: missionOpenItemIds(context.openGaps, MISSION_GAP_PATTERN),
|
|
85
|
-
pendingOutcomes: missionOpenItemIds(context.pendingOutcomes, MISSION_OUTCOME_PATTERN),
|
|
86
|
-
};
|
|
87
|
-
const planning = boundedMissionText(`${context.openDecisions.length} open decision(s); ${context.openGaps.length} open gap(s); ` +
|
|
88
|
-
`${context.pendingOutcomes.length} pending outcome(s).`, 2048, 'No open planning items.');
|
|
89
|
-
const delivery = boundedMissionText(prior
|
|
90
|
-
? `Most recent cycle ${prior.id} is ${prior.state}.`
|
|
91
|
-
: 'No delivery cycle has completed yet.', 2048, 'No delivery status recorded.');
|
|
92
|
-
const pinnedRevision = input.workspace.controlRepository.pinnedRevision;
|
|
93
|
-
return {
|
|
94
|
-
cycleId: input.cycleId,
|
|
95
|
-
...(pinnedRevision && MISSION_REVISION_PATTERN.test(pinnedRevision) ? { pinnedRevision } : {}),
|
|
96
|
-
charter: {
|
|
97
|
-
productCharter: boundedMissionText(charterLine, 8192, 'No product charter recorded.'),
|
|
98
|
-
currentGoals,
|
|
99
|
-
},
|
|
100
|
-
priorCycleSummary,
|
|
101
|
-
planningStatus: {
|
|
102
|
-
planningEngine: input.config.planningEngine,
|
|
103
|
-
planning,
|
|
104
|
-
delivery,
|
|
105
|
-
},
|
|
106
|
-
declaredRoots: deriveOperatingDeclaredRoots(input.evidenceIndex),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Build the Protocol v1.3 mission packets (FR1/E-001) for a cycle from live
|
|
111
|
-
* state: derive the body-free evidence index from the collected snapshot,
|
|
112
|
-
* source the non-evidence payload once, and construct one digest-bound packet
|
|
113
|
-
* per requested role. Each packet is measured against its role's derived
|
|
114
|
-
* single-digit-KiB mission budget and fails closed with
|
|
115
|
-
* `E_OPERATE_MISSION_PACKET_BUDGET` naming the role before any dispatch. Pack
|
|
116
|
-
* mode (`createOperatingAdvisorPack`) is untouched.
|
|
117
|
-
*/
|
|
118
|
-
export async function buildOperatingMissionPackets(input) {
|
|
119
|
-
const evidenceIndex = buildOperatingEvidenceIndex(input.evidence, {
|
|
120
|
-
sensitivityCeiling: input.sensitivityCeiling,
|
|
121
|
-
});
|
|
122
|
-
const state = sourceOperatingMissionPacketState({
|
|
123
|
-
cycleId: input.cycleId,
|
|
124
|
-
config: input.config,
|
|
125
|
-
workspace: input.workspace,
|
|
126
|
-
context: input.context,
|
|
127
|
-
evidenceIndex,
|
|
128
|
-
});
|
|
129
|
-
const roleIds = input.roleIds ?? input.config.enabledRoles;
|
|
130
|
-
const packets = [];
|
|
131
|
-
for (const roleId of roleIds) {
|
|
132
|
-
packets.push(await buildOperatingMissionPacket({
|
|
133
|
-
roleId,
|
|
134
|
-
...state,
|
|
135
|
-
evidenceIndex,
|
|
136
|
-
maxEvidenceItems: input.maxEvidenceItems,
|
|
137
|
-
}));
|
|
138
|
-
}
|
|
139
|
-
return packets;
|
|
140
|
-
}
|
|
141
36
|
async function snapshotWorkspace(projectRoot, localRoot) {
|
|
142
37
|
return refreshOperatingWorkspaceManifest(projectRoot, { localRoot });
|
|
143
38
|
}
|
|
@@ -196,6 +91,10 @@ function readinessGaps(readiness, decisionOwner, now) {
|
|
|
196
91
|
updatedAt: now,
|
|
197
92
|
}));
|
|
198
93
|
}
|
|
94
|
+
// Retained callerless: this fed the collector's `requiredEvidenceRefs` input,
|
|
95
|
+
// which the pinned-revision seed no longer takes. The collector and its dedicated
|
|
96
|
+
// feeders are retired wholesale in T-009; nothing is deleted here.
|
|
97
|
+
// biome-ignore lint/correctness/noUnusedVariables: retained pending collector retirement (T-009)
|
|
199
98
|
function requiredOpenEvidenceRefs(state) {
|
|
200
99
|
const records = [
|
|
201
100
|
...state.findings.filter((finding) => !['done', 'rejected', 'superseded'].includes(String(finding.status))),
|
|
@@ -267,6 +166,121 @@ export function buildChairEvidence(evidence, results, now) {
|
|
|
267
166
|
}),
|
|
268
167
|
};
|
|
269
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* FR1/FR2 (isolation rebind): the cycle's pinned-revision evidence seed. Evidence
|
|
171
|
+
* is an OUTPUT now, so a fresh cycle no longer collects a body-bearing snapshot
|
|
172
|
+
* before anyone has investigated; it seeds an empty evidence-of-record whose
|
|
173
|
+
* fingerprint binds the cycle to its control-repository pinned revision. This
|
|
174
|
+
* seed is what the adapter isolation binds to (`persistedAdapterEvidence`'s
|
|
175
|
+
* fingerprint === the prepared session's `evidenceDigest`), so a tampered or
|
|
176
|
+
* revision-mismatched session still fails closed with `E_OPERATE_ADVISOR_ISOLATION`
|
|
177
|
+
* — the guarantee is preserved, only its input is the pinned revision rather than
|
|
178
|
+
* a collected-evidence digest. Nothing is guessed, budgeted, or packed.
|
|
179
|
+
*/
|
|
180
|
+
async function buildPinnedRevisionEvidenceSeed(input) {
|
|
181
|
+
return assertOperatingArtifact('operating-evidence', {
|
|
182
|
+
kind: 'operating-evidence',
|
|
183
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
184
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
185
|
+
cycleId: input.cycleId,
|
|
186
|
+
fingerprint: canonicalDigest({
|
|
187
|
+
cycleId: input.cycleId,
|
|
188
|
+
pinnedRevision: input.pinnedRevision,
|
|
189
|
+
}),
|
|
190
|
+
collectedAt: input.collectedAt,
|
|
191
|
+
truncated: false,
|
|
192
|
+
items: [],
|
|
193
|
+
sources: [],
|
|
194
|
+
warnings: [],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* FR2: dispatch is no longer pre-gated on collected evidence — every enabled role
|
|
199
|
+
* is dispatched with a mandate and post-gated on what its response actually cites.
|
|
200
|
+
* This builds the "all roles runnable" readiness the dispatch consumes, replacing
|
|
201
|
+
* the collector-era evidence-readiness evaluation.
|
|
202
|
+
*/
|
|
203
|
+
async function buildDispatchReadiness(input) {
|
|
204
|
+
const hasPinnedRevision = !/^0+$/.test(input.pinnedRevision);
|
|
205
|
+
return assertOperatingArtifact('operating-evidence-readiness', {
|
|
206
|
+
kind: 'operating-evidence-readiness',
|
|
207
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
208
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
209
|
+
cycleId: input.cycleId,
|
|
210
|
+
inputDigest: canonicalDigest({ cycleId: input.cycleId, roles: [...input.roles].sort() }),
|
|
211
|
+
evaluatedAt: input.now.toISOString(),
|
|
212
|
+
roles: input.roles.map((roleId, index) => ({
|
|
213
|
+
roleId,
|
|
214
|
+
readiness: 'ready',
|
|
215
|
+
requirements: [
|
|
216
|
+
{
|
|
217
|
+
source: 'mandate',
|
|
218
|
+
claimTypes: ['resolvable-citation-required'],
|
|
219
|
+
minimumItems: 1,
|
|
220
|
+
observedItems: hasPinnedRevision ? 1 : 0,
|
|
221
|
+
maxAgeHours: 8760,
|
|
222
|
+
oldestAgeHours: hasPinnedRevision ? 0 : null,
|
|
223
|
+
observationWindow: 'current-cycle',
|
|
224
|
+
sensitivityCeiling: input.sensitivityCeiling,
|
|
225
|
+
satisfied: hasPinnedRevision,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
missingEvidence: hasPinnedRevision
|
|
229
|
+
? []
|
|
230
|
+
: ['mandate:resolvable-citation-required (0/1; no pinned revision)'],
|
|
231
|
+
evidenceRefs: [],
|
|
232
|
+
modelCallAllowed: hasPinnedRevision,
|
|
233
|
+
gapId: hasPinnedRevision ? null : `GAP-${String(index + 1).padStart(3, '0')}`,
|
|
234
|
+
})),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* FR2 (evidence-as-output): assemble the cycle's evidence-of-record for
|
|
239
|
+
* consolidation from the resolved-citation snapshots the universal gate minted.
|
|
240
|
+
* Every evidenceRef a gated proposal carries was snapshotted to machine-local
|
|
241
|
+
* evidence (`putCitationSnapshot`); this reads each back (`getCitationSnapshot`)
|
|
242
|
+
* so downstream confidence/sensitivity/freshness scoring resolves against what
|
|
243
|
+
* was actually cited and verified rather than a pre-collected guess. A ref whose
|
|
244
|
+
* snapshot is unreadable still materializes a minimal item so consolidation never
|
|
245
|
+
* rejects a legitimately-minted reference.
|
|
246
|
+
*/
|
|
247
|
+
async function assembleCitationEvidence(input) {
|
|
248
|
+
const refs = [
|
|
249
|
+
...new Set(input.roleResults.flatMap((result) => result.proposals.flatMap((proposal) => proposal.evidenceRefs))),
|
|
250
|
+
].sort();
|
|
251
|
+
const items = [];
|
|
252
|
+
for (const ref of refs) {
|
|
253
|
+
const snapshot = await input.cache.getCitationSnapshot(ref);
|
|
254
|
+
items.push({
|
|
255
|
+
id: ref,
|
|
256
|
+
source: 'citation',
|
|
257
|
+
location: snapshot?.sourceLocation ?? ref,
|
|
258
|
+
digest: canonicalDigest(snapshot?.content ?? ref),
|
|
259
|
+
collectedAt: input.now,
|
|
260
|
+
observedFrom: null,
|
|
261
|
+
observedTo: null,
|
|
262
|
+
freshness: 'fresh',
|
|
263
|
+
sensitivity: snapshot?.sensitivity ?? 'internal',
|
|
264
|
+
claimTypes: ['verified-citation'],
|
|
265
|
+
...(snapshot ? { summary: snapshot.content.slice(0, 512) } : {}),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return assertOperatingArtifact('operating-evidence', {
|
|
269
|
+
kind: 'operating-evidence',
|
|
270
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
271
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
272
|
+
cycleId: input.cycleId,
|
|
273
|
+
fingerprint: canonicalDigest({
|
|
274
|
+
source: input.baseFingerprint,
|
|
275
|
+
citations: items.map((item) => ({ id: item.id, digest: item.digest })),
|
|
276
|
+
}),
|
|
277
|
+
collectedAt: input.now,
|
|
278
|
+
truncated: false,
|
|
279
|
+
items,
|
|
280
|
+
sources: [],
|
|
281
|
+
warnings: [],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
270
284
|
const SENSITIVE_ROUTE_LANGUAGE = /\b(?:payment|billing|security|privacy|legal|tenant|isolation|destructive|delete|purge|credential|secret)\b/i;
|
|
271
285
|
function containsSensitiveRouteProposal(results) {
|
|
272
286
|
return results.some((result) => result.proposals.some((proposal) => SENSITIVE_ROUTE_LANGUAGE.test(`${proposal.type} ${proposal.title} ${proposal.problem} ${proposal.proposal}`)));
|
|
@@ -430,50 +444,104 @@ async function persistedCycleRoleResults(store, events, cycleId) {
|
|
|
430
444
|
* dry-run never enter the mutating branch.
|
|
431
445
|
*/
|
|
432
446
|
/**
|
|
433
|
-
*
|
|
447
|
+
* The single governed gap opened for a role whose citation-bearing response
|
|
448
|
+
* grounded zero evidence (FR2). It names the role and its empty grounding so the
|
|
449
|
+
* integrity surface can render the role `not_evaluated` with a real reason rather
|
|
450
|
+
* than a silent absence or a "quiet" that pretends the lens evaluated.
|
|
451
|
+
*/
|
|
452
|
+
function roleEmptyGroundingGap(roleId, context, now) {
|
|
453
|
+
const createdAt = now.toISOString();
|
|
454
|
+
return {
|
|
455
|
+
kind: 'operating-data-gap',
|
|
456
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
457
|
+
protocolVersion: OPERATE_MISSION_PROTOCOL_VERSION,
|
|
458
|
+
id: `GAP-${canonicalDigest({
|
|
459
|
+
roleId,
|
|
460
|
+
cycleId: context.cycleId,
|
|
461
|
+
reason: 'empty-grounding',
|
|
462
|
+
}).slice('sha256:'.length)}`,
|
|
463
|
+
cycleId: context.cycleId,
|
|
464
|
+
category: 'missing-evidence',
|
|
465
|
+
question: `What evidence can ${roleId} cite? Its response resolved zero citations to evidence.`,
|
|
466
|
+
reason: `Every citation ${roleId} returned failed to resolve at the pinned revision, so the role ` +
|
|
467
|
+
'grounded no evidence and is recorded not_evaluated instead of dispatching a proposal on an ' +
|
|
468
|
+
'empty base.',
|
|
469
|
+
unblocks: [],
|
|
470
|
+
affectedRoles: [roleId],
|
|
471
|
+
status: 'open',
|
|
472
|
+
owner: context.owner && context.owner.length > 0 ? context.owner : 'chair',
|
|
473
|
+
evidenceRefs: [],
|
|
474
|
+
createdAt,
|
|
475
|
+
updatedAt: createdAt,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* FR2 universal citation gate — the PRIMARY, unconditional mechanism, run on
|
|
480
|
+
* every dispatch path and every evidence source (no `bearing.length === 0`
|
|
481
|
+
* bypass remains, so it is entered even for a citation-free result).
|
|
434
482
|
*
|
|
435
483
|
* Every recorded proposal that carries citations is resolved against the cycle's
|
|
436
|
-
* pinned revision through
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
484
|
+
* pinned revision through `enforceRecordedProposalCitations`. A proposal with ANY
|
|
485
|
+
* unresolvable citation is DROPPED — it never reaches `consolidateOperatingResults`
|
|
486
|
+
* — and exactly one `unresolvable-citation` gap is opened in its place. A proposal
|
|
487
|
+
* whose citations all resolve keeps its minted evidence IDs.
|
|
488
|
+
*
|
|
489
|
+
* On top of that per-proposal gate, the FR2 role-level rule: a role result whose
|
|
490
|
+
* proposals carried citations but whose accepted citations resolve to ZERO
|
|
491
|
+
* evidence IDs is `not_evaluated` — every one of its proposals is dropped, its id
|
|
492
|
+
* is returned in `notEvaluatedRoleIds`, and one governed `missing-evidence` gap
|
|
493
|
+
* naming the role and its empty grounding is opened. This replaces SPEC-003's
|
|
494
|
+
* mission-only, repository-only starvation gate with one rule that holds whatever
|
|
495
|
+
* the dispatch path or evidence source. A v1.2 pack result (evidenceRefs, no
|
|
496
|
+
* citations) carries no citation-bearing proposal, so it is never demoted — the
|
|
497
|
+
* pack path stays functional until T-003 removes it.
|
|
443
498
|
*/
|
|
444
499
|
export async function gateRecordedProposalCitations(input) {
|
|
445
500
|
const bearing = [];
|
|
501
|
+
const citationBearingProposalKeys = new Set();
|
|
446
502
|
for (const result of input.roleResults) {
|
|
447
503
|
for (const proposal of result.proposals) {
|
|
448
504
|
const citations = proposal.citations;
|
|
449
505
|
if (Array.isArray(citations) && citations.length > 0) {
|
|
450
506
|
bearing.push({ proposalKey: proposal.proposalKey, citations });
|
|
507
|
+
citationBearingProposalKeys.add(proposal.proposalKey);
|
|
451
508
|
}
|
|
452
509
|
}
|
|
453
510
|
}
|
|
454
|
-
if (bearing.length === 0)
|
|
455
|
-
return { roleResults: input.roleResults, gaps: [] };
|
|
456
511
|
const enforcement = await enforceRecordedProposalCitations(bearing, input.context);
|
|
457
512
|
const rejectedKeys = new Set(enforcement.rejected.map((entry) => entry.proposalKey));
|
|
458
513
|
const evidenceByKey = new Map(enforcement.accepted.map((entry) => [entry.proposal.proposalKey, entry.evidenceRefs]));
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
514
|
+
const gaps = [...enforcement.gaps];
|
|
515
|
+
const notEvaluatedRoleIds = [];
|
|
516
|
+
const now = input.context.now ?? new Date();
|
|
517
|
+
const roleResults = await Promise.all(input.roleResults.map(async (result) => {
|
|
518
|
+
const gated = {
|
|
519
|
+
...result,
|
|
520
|
+
proposals: result.proposals
|
|
521
|
+
.filter((proposal) => !rejectedKeys.has(proposal.proposalKey))
|
|
522
|
+
.map((proposal) => {
|
|
523
|
+
const minted = evidenceByKey.get(proposal.proposalKey);
|
|
524
|
+
return minted && minted.length > 0
|
|
525
|
+
? {
|
|
526
|
+
...proposal,
|
|
527
|
+
evidenceRefs: [...new Set([...proposal.evidenceRefs, ...minted])].sort(),
|
|
528
|
+
}
|
|
529
|
+
: proposal;
|
|
530
|
+
}),
|
|
531
|
+
};
|
|
532
|
+
// FR2 role-level rule: a role whose proposals were citation-bearing but
|
|
533
|
+
// grounded ZERO evidence after resolution is not_evaluated. Drop its
|
|
534
|
+
// surviving (evidence-free) proposals and open one governed gap.
|
|
535
|
+
const wasCitationBearing = result.proposals.some((proposal) => citationBearingProposalKeys.has(proposal.proposalKey));
|
|
536
|
+
const mintedAny = gated.proposals.some((proposal) => proposal.evidenceRefs.length > 0);
|
|
537
|
+
if (wasCitationBearing && !mintedAny) {
|
|
538
|
+
notEvaluatedRoleIds.push(result.roleId);
|
|
539
|
+
gaps.push(await assertOperatingArtifact('operating-data-gap', roleEmptyGroundingGap(result.roleId, input.context, now)));
|
|
540
|
+
return { ...gated, proposals: [] };
|
|
541
|
+
}
|
|
542
|
+
return gated;
|
|
472
543
|
}));
|
|
473
|
-
|
|
474
|
-
// against the v1.3 operating-data-gap schema, which admits `category`). They
|
|
475
|
-
// are persisted alongside the cycle's other gaps by the committed path.
|
|
476
|
-
return { roleResults, gaps: enforcement.gaps };
|
|
544
|
+
return { roleResults, gaps, notEvaluatedRoleIds };
|
|
477
545
|
}
|
|
478
546
|
export async function runOperatingCycle(input) {
|
|
479
547
|
const projectRoot = await assertOperatingProject(input.projectRoot);
|
|
@@ -482,12 +550,6 @@ export async function runOperatingCycle(input) {
|
|
|
482
550
|
const initializedWorkspace = await loadJson(paths.workspace);
|
|
483
551
|
await assertOperatingArtifact('operating-workspace-manifest', initializedWorkspace);
|
|
484
552
|
const preferences = await readPreferences(projectRoot, input.localRoot);
|
|
485
|
-
// FR4 / E-004 fold-in: the live dispatch call site reads the persisted
|
|
486
|
-
// per-project dispatch-mode overrides once and threads them into every advisor
|
|
487
|
-
// dispatch below, so a persisted override reaches dispatch provenance.
|
|
488
|
-
const dispatchModeOverrides = await readOperatingDispatchModeOverrides(projectRoot, {
|
|
489
|
-
localRoot: input.localRoot,
|
|
490
|
-
});
|
|
491
553
|
const store = new OperatingEventStore(projectRoot, { localRoot: input.localRoot });
|
|
492
554
|
const now = input.now ?? new Date();
|
|
493
555
|
const deadlineReconciliation = !input.preview && !input.dryRun
|
|
@@ -634,36 +696,36 @@ export async function runOperatingCycle(input) {
|
|
|
634
696
|
state: initialState,
|
|
635
697
|
cycleId: cycle.id,
|
|
636
698
|
});
|
|
699
|
+
const citationCache = new OperatingEvidenceCache(resolveOperatingPaths(projectRoot, { localRoot: input.localRoot }).evidence, preferences.sensitivityCeiling);
|
|
700
|
+
// FR2 universal gate, bound to the cycle's pinned revision. Injected into
|
|
701
|
+
// dispatch so advisors.ts never imports engine.ts, and reused by the
|
|
702
|
+
// consolidation-time gate below so both paths resolve citations identically.
|
|
703
|
+
const resolveCitations = (roleResults) => gateRecordedProposalCitations({
|
|
704
|
+
roleResults,
|
|
705
|
+
context: {
|
|
706
|
+
projectRoot,
|
|
707
|
+
cycleId: cycle.id,
|
|
708
|
+
descriptor: workspace.controlRepository,
|
|
709
|
+
cache: citationCache,
|
|
710
|
+
owner: config.decisionOwner,
|
|
711
|
+
now,
|
|
712
|
+
},
|
|
713
|
+
});
|
|
637
714
|
const evaluate = async (persist) => {
|
|
638
715
|
await assertCycleWorkspace();
|
|
639
716
|
const evidence = existingEvidence ??
|
|
640
|
-
(await
|
|
641
|
-
projectRoot,
|
|
717
|
+
(await buildPinnedRevisionEvidenceSeed({
|
|
642
718
|
cycleId: cycle.id,
|
|
643
|
-
workspace,
|
|
644
|
-
|
|
645
|
-
sensitivityCeiling: preferences.sensitivityCeiling,
|
|
646
|
-
budgets: {
|
|
647
|
-
...config.budgets,
|
|
648
|
-
maxItemBytes: Math.min(config.budgets.maxBytes, 256 * 1024),
|
|
649
|
-
},
|
|
650
|
-
localRoot: input.localRoot,
|
|
651
|
-
now,
|
|
652
|
-
incremental: depth === 'standard',
|
|
653
|
-
persistIncremental: persist && depth === 'standard',
|
|
654
|
-
requiredEvidenceRefs: requiredOpenEvidenceRefs(initialState),
|
|
719
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
720
|
+
collectedAt: timestamp,
|
|
655
721
|
}));
|
|
656
|
-
const evidenceGaps =
|
|
657
|
-
cycleId: cycle.id,
|
|
658
|
-
evidence,
|
|
659
|
-
owner: config.decisionOwner,
|
|
660
|
-
now: timestamp,
|
|
661
|
-
});
|
|
722
|
+
const evidenceGaps = [];
|
|
662
723
|
const nonChair = config.enabledRoles.filter((role) => role !== 'chair');
|
|
663
|
-
let readiness = await
|
|
724
|
+
let readiness = await buildDispatchReadiness({
|
|
664
725
|
cycleId: cycle.id,
|
|
665
|
-
|
|
666
|
-
|
|
726
|
+
roles: nonChair,
|
|
727
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
728
|
+
sensitivityCeiling: preferences.sensitivityCeiling,
|
|
667
729
|
now,
|
|
668
730
|
});
|
|
669
731
|
readiness = await remapReadinessGaps(readiness, initialState);
|
|
@@ -731,13 +793,14 @@ export async function runOperatingCycle(input) {
|
|
|
731
793
|
const first = runnableMissingNonChair.length > 0
|
|
732
794
|
? await dispatchOperatingAdvisors({
|
|
733
795
|
cycleId: cycle.id,
|
|
734
|
-
|
|
796
|
+
projectRoot,
|
|
797
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
735
798
|
readiness: { ...readiness, roles: missingNonChair },
|
|
736
799
|
context: advisorContext,
|
|
737
800
|
adapter: await resolveAdapter(),
|
|
738
801
|
depth,
|
|
739
802
|
runtime: resolvedRuntime,
|
|
740
|
-
|
|
803
|
+
resolveCitations,
|
|
741
804
|
})
|
|
742
805
|
: {
|
|
743
806
|
results: [],
|
|
@@ -746,6 +809,7 @@ export async function runOperatingCycle(input) {
|
|
|
746
809
|
blocked: false,
|
|
747
810
|
skipped: skippedMissingNonChair,
|
|
748
811
|
failed: [],
|
|
812
|
+
gaps: [],
|
|
749
813
|
};
|
|
750
814
|
let roleResults = [
|
|
751
815
|
...existingRoleResults.filter((result) => result.roleId !== 'chair'),
|
|
@@ -756,14 +820,19 @@ export async function runOperatingCycle(input) {
|
|
|
756
820
|
const dispatchProvenance = [...first.provenance];
|
|
757
821
|
const skipped = [...first.skipped];
|
|
758
822
|
const failed = [...first.failed];
|
|
759
|
-
|
|
823
|
+
// FR2: the governed gaps opened while resolving each dispatched role's
|
|
824
|
+
// citations (unresolvable-citation, empty-grounding) are recorded alongside
|
|
825
|
+
// the cycle's other gaps.
|
|
826
|
+
const dispatchGaps = [...first.gaps];
|
|
760
827
|
if (config.enabledRoles.includes('chair')) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
828
|
+
// FR1: the Chair dispatches its own mandate and investigates independently —
|
|
829
|
+
// it no longer receives a synthesized advisor-results evidence body. Its
|
|
830
|
+
// proposals cite resolvable content the same way every other lens does.
|
|
831
|
+
let chairReadiness = await buildDispatchReadiness({
|
|
764
832
|
cycleId: cycle.id,
|
|
765
|
-
|
|
766
|
-
|
|
833
|
+
roles: ['chair'],
|
|
834
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
835
|
+
sensitivityCeiling: preferences.sensitivityCeiling,
|
|
767
836
|
now,
|
|
768
837
|
});
|
|
769
838
|
chairReadiness = await remapReadinessGaps(chairReadiness, {
|
|
@@ -814,47 +883,52 @@ export async function runOperatingCycle(input) {
|
|
|
814
883
|
blocked: false,
|
|
815
884
|
skipped: [],
|
|
816
885
|
failed: [],
|
|
886
|
+
gaps: [],
|
|
817
887
|
}
|
|
818
888
|
: await dispatchOperatingAdvisors({
|
|
819
889
|
cycleId: cycle.id,
|
|
820
|
-
|
|
890
|
+
projectRoot,
|
|
891
|
+
pinnedRevision: workspace.controlRepository.pinnedRevision,
|
|
821
892
|
readiness: chairReadiness,
|
|
822
893
|
context: advisorContext,
|
|
823
894
|
adapter: await resolveAdapter(),
|
|
824
895
|
depth,
|
|
825
896
|
runtime: resolvedRuntime,
|
|
826
|
-
|
|
897
|
+
resolveCitations,
|
|
827
898
|
});
|
|
828
899
|
roleResults = [...roleResults, ...chair.results];
|
|
829
900
|
modelCalls += chair.modelCalls;
|
|
830
901
|
dispatchProvenance.push(...chair.provenance);
|
|
831
902
|
skipped.push(...chair.skipped);
|
|
832
903
|
failed.push(...chair.failed);
|
|
904
|
+
dispatchGaps.push(...chair.gaps);
|
|
833
905
|
blocked ||= chair.blocked || chairReadiness.roles.some((role) => !role.modelCallAllowed);
|
|
834
906
|
}
|
|
835
907
|
const technologyRiskReady = readiness.roles.some((role) => role.roleId === 'technology-risk' && role.modelCallAllowed);
|
|
836
908
|
const technologyRiskFailed = failed.some((entry) => entry.roleId === 'technology-risk');
|
|
837
909
|
const sensitiveRouteBlocked = containsSensitiveRouteProposal(roleResults) && (!technologyRiskReady || technologyRiskFailed);
|
|
838
910
|
blocked ||= sensitiveRouteBlocked;
|
|
839
|
-
// FR3 / E-003:
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
911
|
+
// FR2/FR3 / E-003: the universal citation gate. Every dispatched role already
|
|
912
|
+
// resolved its citations at dispatch time (the committed result is v1.2-valid
|
|
913
|
+
// with minted evidenceRefs, no raw citations), so re-running the gate over the
|
|
914
|
+
// full role set — including any natively-committed existing results — is a
|
|
915
|
+
// no-op that provides defence in depth and a single point where consolidation
|
|
916
|
+
// input is proven citation-clean.
|
|
917
|
+
const citationGate = await resolveCitations(roleResults);
|
|
918
|
+
// FR2 (evidence-as-output): the cycle's evidence-of-record is assembled from
|
|
919
|
+
// the resolved-citation snapshots, so confidence/sensitivity/freshness scoring
|
|
920
|
+
// resolves against what was actually cited and verified — never a pre-collected
|
|
921
|
+
// guess. This replaces the retired collected-evidence consolidation input.
|
|
922
|
+
const consolidationEvidence = await assembleCitationEvidence({
|
|
923
|
+
cycleId: cycle.id,
|
|
924
|
+
roleResults: citationGate.roleResults,
|
|
925
|
+
cache: citationCache,
|
|
926
|
+
baseFingerprint: evidence.fingerprint,
|
|
927
|
+
now: timestamp,
|
|
853
928
|
});
|
|
854
929
|
// Only the gated proposals feed consolidation, so a proposal with an
|
|
855
|
-
// unresolvable citation can never become a finding/route. The
|
|
856
|
-
//
|
|
857
|
-
// opened unresolvable-citation gap threaded into `gaps` below.
|
|
930
|
+
// unresolvable citation can never become a finding/route. The rejection is
|
|
931
|
+
// recorded as the opened unresolvable-citation gap threaded into `gaps` below.
|
|
858
932
|
const consolidated = await consolidateOperatingResults({
|
|
859
933
|
cycleId: cycle.id,
|
|
860
934
|
results: citationGate.roleResults,
|
|
@@ -925,9 +999,13 @@ export async function runOperatingCycle(input) {
|
|
|
925
999
|
decisions: remapped.decisions,
|
|
926
1000
|
// Citation gaps bypass remapConsolidation (their IDs are digest-derived and
|
|
927
1001
|
// already canonical) and are persisted alongside the cycle's other gaps.
|
|
1002
|
+
// `dispatchGaps` are the unresolvable-citation/empty-grounding gaps opened
|
|
1003
|
+
// while resolving each role's citations at dispatch time; `citationGate.gaps`
|
|
1004
|
+
// covers the no-op consolidation-time re-gate (empty in practice).
|
|
928
1005
|
gaps: [
|
|
929
1006
|
...readinessGaps(readiness, config.decisionOwner, timestamp),
|
|
930
1007
|
...remapped.gaps,
|
|
1008
|
+
...dispatchGaps,
|
|
931
1009
|
...citationGate.gaps,
|
|
932
1010
|
],
|
|
933
1011
|
routes: [],
|