@ornexus/neocortex 4.60.41 → 4.60.43
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/sbom.cdx.json +31 -31
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +14 -2
- package/packages/client/dist/commands/invoke.js +57 -56
- package/packages/client/dist/generated/command-registry.generated.d.ts +66 -23
- package/packages/client/dist/generated/command-registry.generated.js +1 -1
- package/packages/client/dist/generated/public-projections.generated.d.ts +100 -0
- package/packages/client/dist/generated/public-projections.generated.js +1 -0
- package/packages/client/dist/git/git-lifecycle-store.d.ts +17 -0
- package/packages/client/dist/git/git-lifecycle-store.js +4 -4
- package/packages/client/dist/index.d.ts +1 -0
- package/packages/client/dist/index.js +1 -1
- package/packages/client/dist/migrations/project-identity.js +7 -7
- package/packages/client/dist/migrations/state-json.d.ts +14 -0
- package/packages/client/dist/migrations/state-json.js +4 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.d.ts +2 -0
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -1
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +8 -8
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +7 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2 -2
- package/packages/client/dist/state/project-state-snapshot.js +1 -1
- package/packages/client/dist/state/state-json-repair.js +3 -3
- package/packages/client/dist/yoloop/invoke-hooks.d.ts +2 -3
- package/packages/client/dist/yoloop/invoke-hooks.js +9 -8
- package/packages/client/dist/yoloop/shared-discover-epics.d.ts +3 -3
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +87 -1
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +27 -3
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
- package/packages/shared/dist/command-registry-contract.js +38 -0
- package/packages/shared/dist/continuity/goal-operation-authority.js +48 -0
- package/packages/shared/dist/executable-graph-catalog/contracts.js +935 -0
- package/packages/shared/dist/generated/command-registry.generated.js +82 -23
- package/packages/shared/dist/generated/invocation-schema.generated.js +12 -4
- package/packages/shared/dist/generated/public-projections.generated.js +149 -0
- package/packages/shared/dist/invocation/executable-policy-recovery.d.ts +83 -0
- package/packages/shared/dist/invocation/executable-policy-recovery.js +272 -0
- package/packages/shared/dist/invocation/index.d.ts +3 -1
- package/packages/shared/dist/invocation/index.js +40 -1
- package/packages/shared/dist/invocation/recovery-route-compiler.d.ts +56 -0
- package/packages/shared/dist/invocation/recovery-route-compiler.js +214 -0
- package/packages/shared/dist/state/index.d.ts +1 -0
- package/packages/shared/dist/state/index.js +1 -0
- package/packages/shared/dist/state/state-migration.d.ts +58 -0
- package/packages/shared/dist/state/state-migration.js +404 -0
- package/packages/shared/dist/yoloop/canonical.js +15 -0
- package/packages/shared/dist/yoloop/operational-budget.js +164 -0
- package/packages/shared/dist/yoloop/recovery-types.js +39 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +11 -2
- package/targets-stubs/codex/README.md +7 -4
- package/targets-stubs/codex/neocortex.toml +53 -6
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** P220.21 immutable policy, invocation, compatibility, recovery and budget closure. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { canonicalizeCommandRegistryJcs, commandRegistryChecksum, normalizeCommandRegistryDocument, } from '../command-registry-contract.js';
|
|
4
|
+
import { CATALOG_OPERATIONAL_BASELINE_V1, } from '../executable-graph-catalog/contracts.js';
|
|
5
|
+
import { YOLOOP_FAILURE_CATEGORIES } from '../yoloop/recovery-types.js';
|
|
6
|
+
export const EXECUTABLE_POLICY_RECOVERY_SCHEMA = 'ExecutablePolicyRecoveryProjection/1.0';
|
|
7
|
+
export const EXECUTABLE_POLICY_RECOVERY_EVIDENCE_ID = 'P220-EV-POLICY-CLOSURE';
|
|
8
|
+
export const EXECUTABLE_POLICY_RECOVERY_REASON_CODES = Object.freeze([
|
|
9
|
+
'POLICY_PROJECTION_INPUT_INVALID',
|
|
10
|
+
'POLICY_REGISTRY_MISMATCH',
|
|
11
|
+
'POLICY_INVOCATION_MISMATCH',
|
|
12
|
+
'POLICY_ROUTE_MISMATCH',
|
|
13
|
+
'POLICY_READ_ONLY_INVARIANT',
|
|
14
|
+
'POLICY_COMPATIBILITY_MISMATCH',
|
|
15
|
+
'POLICY_RECOVERY_INVALID',
|
|
16
|
+
'POLICY_OPERATIONAL_BUDGET_MISMATCH',
|
|
17
|
+
]);
|
|
18
|
+
export class ExecutablePolicyRecoveryError extends Error {
|
|
19
|
+
reasonCode;
|
|
20
|
+
constructor(reasonCode) {
|
|
21
|
+
super(reasonCode);
|
|
22
|
+
this.reasonCode = reasonCode;
|
|
23
|
+
this.name = 'ExecutablePolicyRecoveryError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const fail = (reasonCode) => {
|
|
27
|
+
throw new ExecutablePolicyRecoveryError(reasonCode);
|
|
28
|
+
};
|
|
29
|
+
const compareBytes = (left, right) => Buffer.from(left).compare(Buffer.from(right));
|
|
30
|
+
function deepFreeze(value) {
|
|
31
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
32
|
+
for (const child of Object.values(value))
|
|
33
|
+
deepFreeze(child);
|
|
34
|
+
Object.freeze(value);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function canonicalHash(domain, value) {
|
|
39
|
+
return createHash('sha256')
|
|
40
|
+
.update(`${domain}\0${canonicalizeCommandRegistryJcs(value)}`)
|
|
41
|
+
.digest('hex');
|
|
42
|
+
}
|
|
43
|
+
function canonicalSha256(value) {
|
|
44
|
+
return createHash('sha256')
|
|
45
|
+
.update(canonicalizeCommandRegistryJcs(value))
|
|
46
|
+
.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
function copyValue(value) {
|
|
49
|
+
return JSON.parse(canonicalizeCommandRegistryJcs(value));
|
|
50
|
+
}
|
|
51
|
+
function exactKeys(value, keys) {
|
|
52
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value)
|
|
53
|
+
&& Object.keys(value).sort(compareBytes).join('\0')
|
|
54
|
+
=== [...keys].sort(compareBytes).join('\0'));
|
|
55
|
+
}
|
|
56
|
+
function normalizedRegistry(input) {
|
|
57
|
+
try {
|
|
58
|
+
return normalizeCommandRegistryDocument(input);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return fail('POLICY_REGISTRY_MISMATCH');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function expectedInvocationCommands(commands) {
|
|
65
|
+
return commands
|
|
66
|
+
.filter(({ visibility }) => visibility === 'public')
|
|
67
|
+
.map((command) => ({
|
|
68
|
+
commandId: command.id,
|
|
69
|
+
inputShape: command.inputShape,
|
|
70
|
+
operation: {
|
|
71
|
+
required: command.invocation.operation.required,
|
|
72
|
+
unknownAsPositional: command.invocation.operation.unknownAsPositional,
|
|
73
|
+
operations: command.subcommands.map(({ id, aliases }) => ({ id, aliases: [...aliases] })),
|
|
74
|
+
},
|
|
75
|
+
options: command.invocation.options,
|
|
76
|
+
positionals: command.invocation.positionals,
|
|
77
|
+
terminator: true,
|
|
78
|
+
}))
|
|
79
|
+
.sort((left, right) => compareBytes(left.commandId, right.commandId));
|
|
80
|
+
}
|
|
81
|
+
function validateInvocationSchema(manifest, registry, commands, registryChecksum) {
|
|
82
|
+
const keys = [
|
|
83
|
+
'commandCount', 'commands', 'operationCount', 'optionCount', 'positionalCount',
|
|
84
|
+
'projectionSha256', 'registryChecksum', 'registryVersion', 'schemaChecksum', 'schemaVersion',
|
|
85
|
+
];
|
|
86
|
+
if (!exactKeys(manifest, keys)
|
|
87
|
+
|| manifest.schemaVersion !== 'p205-invocation-schema-v1'
|
|
88
|
+
|| manifest.registryVersion !== registry.registryVersion
|
|
89
|
+
|| manifest.registryChecksum !== registryChecksum
|
|
90
|
+
|| !Array.isArray(manifest.commands)) {
|
|
91
|
+
return fail('POLICY_INVOCATION_MISMATCH');
|
|
92
|
+
}
|
|
93
|
+
const expected = expectedInvocationCommands(commands);
|
|
94
|
+
const schemaProjection = {
|
|
95
|
+
schemaVersion: manifest.schemaVersion,
|
|
96
|
+
registryVersion: manifest.registryVersion,
|
|
97
|
+
registryChecksum: manifest.registryChecksum,
|
|
98
|
+
commands: manifest.commands,
|
|
99
|
+
};
|
|
100
|
+
const optionCount = manifest.commands.reduce((sum, command) => sum + (command.options?.length ?? 0), 0);
|
|
101
|
+
const positionalCount = manifest.commands.reduce((sum, command) => sum + (command.positionals?.length ?? 0), 0);
|
|
102
|
+
const operationCount = manifest.commands.reduce((sum, command) => sum + (command.operation?.operations?.length ?? 0), 0);
|
|
103
|
+
if (manifest.schemaChecksum !== canonicalSha256(schemaProjection)
|
|
104
|
+
|| manifest.projectionSha256 !== canonicalSha256(manifest.commands)
|
|
105
|
+
|| manifest.commandCount !== manifest.commands.length
|
|
106
|
+
|| manifest.optionCount !== optionCount
|
|
107
|
+
|| manifest.positionalCount !== positionalCount
|
|
108
|
+
|| manifest.operationCount !== operationCount
|
|
109
|
+
|| canonicalizeCommandRegistryJcs(manifest.commands) !== canonicalizeCommandRegistryJcs(expected)) {
|
|
110
|
+
return fail('POLICY_INVOCATION_MISMATCH');
|
|
111
|
+
}
|
|
112
|
+
const byCommand = new Map(manifest.commands.map((command) => [command.commandId, command]));
|
|
113
|
+
if (byCommand.size !== manifest.commands.length)
|
|
114
|
+
fail('POLICY_INVOCATION_MISMATCH');
|
|
115
|
+
return byCommand;
|
|
116
|
+
}
|
|
117
|
+
function validateOperationalBaseline(value) {
|
|
118
|
+
if (canonicalizeCommandRegistryJcs(value)
|
|
119
|
+
!== canonicalizeCommandRegistryJcs(CATALOG_OPERATIONAL_BASELINE_V1)) {
|
|
120
|
+
return fail('POLICY_OPERATIONAL_BUDGET_MISMATCH');
|
|
121
|
+
}
|
|
122
|
+
return canonicalHash('p220-operational-baseline-v1', value);
|
|
123
|
+
}
|
|
124
|
+
function normalizedRecovery(recovery) {
|
|
125
|
+
if (!recovery.ok
|
|
126
|
+
|| recovery.rules.length !== YOLOOP_FAILURE_CATEGORIES.length
|
|
127
|
+
|| recovery.receipt.outcome !== 'accepted'
|
|
128
|
+
|| recovery.receipt.evidenceId !== 'P220-EV-RECOVERY-CATALOG') {
|
|
129
|
+
return fail('POLICY_RECOVERY_INVALID');
|
|
130
|
+
}
|
|
131
|
+
const categorySet = new Set(recovery.rules.map(({ category }) => category));
|
|
132
|
+
if (categorySet.size !== YOLOOP_FAILURE_CATEGORIES.length
|
|
133
|
+
|| YOLOOP_FAILURE_CATEGORIES.some((category) => !categorySet.has(category))) {
|
|
134
|
+
return fail('POLICY_RECOVERY_INVALID');
|
|
135
|
+
}
|
|
136
|
+
const canonicalRules = recovery.rules.map(({ commandId: _commandId, ...rule }) => rule);
|
|
137
|
+
const catalogHash = canonicalHash('p220-recovery-catalog-v1', {
|
|
138
|
+
schema_version: 1,
|
|
139
|
+
rules: canonicalRules,
|
|
140
|
+
});
|
|
141
|
+
const candidates = recovery.rules.filter(({ commandId }) => commandId !== null).length;
|
|
142
|
+
const sentinels = recovery.rules.length - candidates;
|
|
143
|
+
if (recovery.catalogHash !== catalogHash
|
|
144
|
+
|| recovery.receipt.catalogDigestPrefix !== catalogHash.slice(0, 12)
|
|
145
|
+
|| recovery.receipt.ruleCount !== recovery.rules.length
|
|
146
|
+
|| recovery.receipt.candidateRuleCount !== candidates
|
|
147
|
+
|| recovery.receipt.sentinelRuleCount !== sentinels) {
|
|
148
|
+
return fail('POLICY_RECOVERY_INVALID');
|
|
149
|
+
}
|
|
150
|
+
return { rules: recovery.rules, catalogHash, candidates, sentinels };
|
|
151
|
+
}
|
|
152
|
+
function exactPolicy(value, isStatus) {
|
|
153
|
+
const policy = copyValue(value);
|
|
154
|
+
if (!exactKeys(policy, [
|
|
155
|
+
'billingClass', 'idempotencyClass', 'minimumTier', 'mutationClass',
|
|
156
|
+
'privilege', 'quotaClass', 'replayClass', 'strictReadOnly',
|
|
157
|
+
]))
|
|
158
|
+
return fail('POLICY_ROUTE_MISMATCH');
|
|
159
|
+
if (policy.strictReadOnly === 'allow'
|
|
160
|
+
&& (policy.mutationClass !== 'read-only'
|
|
161
|
+
|| policy.billingClass !== 'non-billable'
|
|
162
|
+
|| policy.quotaClass !== 'none'
|
|
163
|
+
|| policy.idempotencyClass !== 'none')) {
|
|
164
|
+
return fail('POLICY_READ_ONLY_INVARIANT');
|
|
165
|
+
}
|
|
166
|
+
if (isStatus && policy.strictReadOnly !== 'allow') {
|
|
167
|
+
return fail('POLICY_READ_ONLY_INVARIANT');
|
|
168
|
+
}
|
|
169
|
+
return policy;
|
|
170
|
+
}
|
|
171
|
+
function exactCompatibility(value) {
|
|
172
|
+
const compatibility = copyValue(value);
|
|
173
|
+
if (!exactKeys(compatibility, ['deprecatedIn', 'introducedIn', 'removedIn', 'replacement'])) {
|
|
174
|
+
return fail('POLICY_COMPATIBILITY_MISMATCH');
|
|
175
|
+
}
|
|
176
|
+
return compatibility;
|
|
177
|
+
}
|
|
178
|
+
function routeInvocation(command) {
|
|
179
|
+
return copyValue({
|
|
180
|
+
inputShape: command.inputShape,
|
|
181
|
+
operation: command.operation,
|
|
182
|
+
options: command.options,
|
|
183
|
+
positionals: command.positionals,
|
|
184
|
+
terminator: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export function compileExecutablePolicyRecoveryProjection(input) {
|
|
188
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
189
|
+
return fail('POLICY_PROJECTION_INPUT_INVALID');
|
|
190
|
+
}
|
|
191
|
+
const registry = normalizedRegistry(input.registry);
|
|
192
|
+
const commands = registry.commands;
|
|
193
|
+
const registryChecksum = commandRegistryChecksum(registry);
|
|
194
|
+
const invocationByCommand = validateInvocationSchema(input.invocationSchema, registry, commands, registryChecksum);
|
|
195
|
+
const recovery = normalizedRecovery(input.recovery);
|
|
196
|
+
const operationalBudgetHash = validateOperationalBaseline(input.operationalBaseline);
|
|
197
|
+
const recoveryByRoute = new Map();
|
|
198
|
+
for (const rule of recovery.rules) {
|
|
199
|
+
if (rule.commandId === null)
|
|
200
|
+
continue;
|
|
201
|
+
const routeKey = `${rule.commandId}\0`;
|
|
202
|
+
const command = commands.find(({ id }) => id === rule.commandId);
|
|
203
|
+
if (!command)
|
|
204
|
+
fail('POLICY_RECOVERY_INVALID');
|
|
205
|
+
const values = recoveryByRoute.get(routeKey) ?? [];
|
|
206
|
+
values.push(rule.category);
|
|
207
|
+
recoveryByRoute.set(routeKey, values);
|
|
208
|
+
}
|
|
209
|
+
const rows = [];
|
|
210
|
+
for (const command of commands) {
|
|
211
|
+
const invocation = invocationByCommand.get(command.id);
|
|
212
|
+
if ((command.visibility === 'public') !== Boolean(invocation)) {
|
|
213
|
+
fail('POLICY_INVOCATION_MISMATCH');
|
|
214
|
+
}
|
|
215
|
+
for (const route of [command, ...command.subcommands]) {
|
|
216
|
+
const operationId = route === command ? null : route.id;
|
|
217
|
+
const routeKey = `${command.id}\0${operationId ?? ''}`;
|
|
218
|
+
const isStatus = command.id === 'status' || operationId === 'status';
|
|
219
|
+
const base = deepFreeze({
|
|
220
|
+
routeKey,
|
|
221
|
+
commandId: command.id,
|
|
222
|
+
operationId,
|
|
223
|
+
aliases: Object.freeze([...route.aliases].sort(compareBytes)),
|
|
224
|
+
visibility: route.visibility,
|
|
225
|
+
invocation: invocation ? routeInvocation(invocation) : null,
|
|
226
|
+
policy: exactPolicy(route.policy, isStatus),
|
|
227
|
+
compatibility: exactCompatibility(route.compatibility),
|
|
228
|
+
recoveryCategories: Object.freeze([...(recoveryByRoute.get(routeKey) ?? [])].sort(compareBytes)),
|
|
229
|
+
statusCapability: isStatus ? 'read-only-observation' : 'not-status',
|
|
230
|
+
});
|
|
231
|
+
rows.push(deepFreeze({
|
|
232
|
+
...base,
|
|
233
|
+
rowHash: canonicalHash('p220-executable-policy-recovery-row-v1', base),
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
rows.sort((left, right) => compareBytes(left.routeKey, right.routeKey));
|
|
238
|
+
if (new Set(rows.map(({ routeKey }) => routeKey)).size !== rows.length) {
|
|
239
|
+
fail('POLICY_ROUTE_MISMATCH');
|
|
240
|
+
}
|
|
241
|
+
const publicRoutes = rows.filter(({ visibility }) => visibility === 'public').length;
|
|
242
|
+
const counts = deepFreeze({
|
|
243
|
+
routes: rows.length,
|
|
244
|
+
publicRoutes,
|
|
245
|
+
internalRoutes: rows.length - publicRoutes,
|
|
246
|
+
commands: commands.length,
|
|
247
|
+
subcommands: commands.reduce((sum, command) => sum + command.subcommands.length, 0),
|
|
248
|
+
aliases: rows.reduce((sum, row) => sum + row.aliases.length, 0),
|
|
249
|
+
options: input.invocationSchema.optionCount,
|
|
250
|
+
positionals: input.invocationSchema.positionalCount,
|
|
251
|
+
statusRoutes: rows.filter(({ statusCapability }) => statusCapability === 'read-only-observation').length,
|
|
252
|
+
recoveryCategories: recovery.rules.length,
|
|
253
|
+
recoveryCandidates: recovery.candidates,
|
|
254
|
+
recoverySentinels: recovery.sentinels,
|
|
255
|
+
});
|
|
256
|
+
const base = deepFreeze({
|
|
257
|
+
schemaVersion: EXECUTABLE_POLICY_RECOVERY_SCHEMA,
|
|
258
|
+
registryVersion: registry.registryVersion,
|
|
259
|
+
registryChecksum,
|
|
260
|
+
invocationSchemaChecksum: input.invocationSchema.schemaChecksum,
|
|
261
|
+
recoveryCatalogHash: recovery.catalogHash,
|
|
262
|
+
operationalBudgetHash,
|
|
263
|
+
rows: Object.freeze(rows),
|
|
264
|
+
counts,
|
|
265
|
+
evidenceId: EXECUTABLE_POLICY_RECOVERY_EVIDENCE_ID,
|
|
266
|
+
publicSafe: true,
|
|
267
|
+
});
|
|
268
|
+
return deepFreeze({
|
|
269
|
+
...base,
|
|
270
|
+
projectionHash: canonicalHash('p220-executable-policy-recovery-projection-v1', base),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -3,6 +3,8 @@ export * from './project-identity-migration.js';
|
|
|
3
3
|
export * from './state-projection.js';
|
|
4
4
|
export * from './strict-effect-receipt.js';
|
|
5
5
|
export * from './public-diagnostics.js';
|
|
6
|
+
export * from './recovery-route-compiler.js';
|
|
7
|
+
export * from './executable-policy-recovery.js';
|
|
6
8
|
export declare const EXACT_INVOCATION_POLICY: "p205-exact-invocation-v1";
|
|
7
9
|
export declare const INVOCATION_SCHEMA_VERSION: "p205-invocation-schema-v1";
|
|
8
10
|
export declare const INVOCATION_LIMITS: Readonly<{
|
|
@@ -120,7 +122,7 @@ export interface InvocationCommandSchemaV1 {
|
|
|
120
122
|
readonly positionals: readonly InvocationPositionalSchemaV1[];
|
|
121
123
|
readonly terminator: true;
|
|
122
124
|
}
|
|
123
|
-
export declare const INVOCATION_SCHEMA_CHECKSUM: "
|
|
125
|
+
export declare const INVOCATION_SCHEMA_CHECKSUM: "15e75fe8a4d8251414b661821c52eb0a9e92a3b7bca0f0d05fcb1827c4e24ce2";
|
|
124
126
|
export interface InvocationSchemaManifestV1 {
|
|
125
127
|
readonly schemaVersion: typeof INVOCATION_SCHEMA_VERSION;
|
|
126
128
|
readonly registryVersion: string;
|
|
@@ -9,11 +9,14 @@ import { createHash } from 'node:crypto';
|
|
|
9
9
|
import { COMMAND_REGISTRY_CHECKSUM, COMMAND_REGISTRY_VERSION, GENERATED_SHARED_COMMANDS, } from '../generated/command-registry.generated.js';
|
|
10
10
|
import { GENERATED_INVOCATION_SCHEMA_MANIFEST, GENERATED_INVOCATION_SCHEMAS, } from '../generated/invocation-schema.generated.js';
|
|
11
11
|
import { canonicalizeCommandRegistryJcs } from '../command-registry-contract.js';
|
|
12
|
+
import { CONTINUITY_GOAL_OPERATION_IDS } from '../continuity/goal-operation-authority.js';
|
|
12
13
|
export * from './input-envelope.js';
|
|
13
14
|
export * from './project-identity-migration.js';
|
|
14
15
|
export * from './state-projection.js';
|
|
15
16
|
export * from './strict-effect-receipt.js';
|
|
16
17
|
export * from './public-diagnostics.js';
|
|
18
|
+
export * from './recovery-route-compiler.js';
|
|
19
|
+
export * from './executable-policy-recovery.js';
|
|
17
20
|
export const EXACT_INVOCATION_POLICY = 'p205-exact-invocation-v1';
|
|
18
21
|
export const INVOCATION_SCHEMA_VERSION = 'p205-invocation-schema-v1';
|
|
19
22
|
export const INVOCATION_LIMITS = Object.freeze({
|
|
@@ -477,6 +480,39 @@ function admitArguments(schema, tokens) {
|
|
|
477
480
|
return positionalFailure;
|
|
478
481
|
return freeze({ options, positionals });
|
|
479
482
|
}
|
|
483
|
+
function boundedEditDistance(left, right, limit) {
|
|
484
|
+
if (Math.abs(left.length - right.length) > limit)
|
|
485
|
+
return limit + 1;
|
|
486
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
487
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
488
|
+
const current = [leftIndex];
|
|
489
|
+
let rowMinimum = current[0];
|
|
490
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
491
|
+
const substitution = previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
492
|
+
const value = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, substitution);
|
|
493
|
+
current.push(value);
|
|
494
|
+
rowMinimum = Math.min(rowMinimum, value);
|
|
495
|
+
}
|
|
496
|
+
if (rowMinimum > limit)
|
|
497
|
+
return limit + 1;
|
|
498
|
+
previous = current;
|
|
499
|
+
}
|
|
500
|
+
return previous[right.length];
|
|
501
|
+
}
|
|
502
|
+
function resemblesGoalOperation(candidate, tokens) {
|
|
503
|
+
const value = candidate.value;
|
|
504
|
+
const lower = value.toLowerCase();
|
|
505
|
+
if (CONTINUITY_GOAL_OPERATION_IDS.some((id) => id === lower
|
|
506
|
+
|| id === lower.replaceAll('_', '-')
|
|
507
|
+
|| id.startsWith(lower)
|
|
508
|
+
|| lower.startsWith(id)
|
|
509
|
+
|| boundedEditDistance(lower, id, 2) <= 2))
|
|
510
|
+
return true;
|
|
511
|
+
const joined = tokens.slice(0, 3).map((token) => token.value.toLowerCase()).join('-');
|
|
512
|
+
if (CONTINUITY_GOAL_OPERATION_IDS.some((id) => joined === id))
|
|
513
|
+
return true;
|
|
514
|
+
return tokens.slice(1).some((token) => /^@?goal-G\d{3,}$/i.test(token.value));
|
|
515
|
+
}
|
|
480
516
|
function admitOperation(schema, tokens) {
|
|
481
517
|
if (!schema.operation.operations.length)
|
|
482
518
|
return { rest: tokens };
|
|
@@ -484,11 +520,14 @@ function admitOperation(schema, tokens) {
|
|
|
484
520
|
return schema.operation.required ? 'operation_required' : { rest: tokens };
|
|
485
521
|
}
|
|
486
522
|
const candidate = tokens[0];
|
|
487
|
-
const normalized = candidate.value.toLowerCase();
|
|
523
|
+
const normalized = schema.commandId === 'goal' ? candidate.value : candidate.value.toLowerCase();
|
|
488
524
|
const operation = schema.operation.operations.find((entry) => entry.id === normalized || entry.aliases.includes(normalized));
|
|
489
525
|
if (operation && !candidate.whollyUnquoted)
|
|
490
526
|
return 'operation_quoted';
|
|
491
527
|
if (!operation) {
|
|
528
|
+
if (schema.commandId === 'goal' && resemblesGoalOperation(candidate, tokens)) {
|
|
529
|
+
return candidate.whollyUnquoted ? 'operation_unknown' : 'operation_quoted';
|
|
530
|
+
}
|
|
492
531
|
const resemblesOperation = schema.operation.operations.some((entry) => entry.id.startsWith(normalized) || normalized.startsWith(entry.id)
|
|
493
532
|
|| entry.aliases.some((alias) => alias.startsWith(normalized) || normalized.startsWith(alias)));
|
|
494
533
|
if (resemblesOperation)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** P220.10 registry-valid, root-owned yoloop recovery candidates. */
|
|
2
|
+
import type { SubscriptionTier } from '../types/api-contracts.js';
|
|
3
|
+
import { type FailureCategory } from '../yoloop/recovery-types.js';
|
|
4
|
+
import { type InvocationAdmissionReceiptV1 } from './index.js';
|
|
5
|
+
export declare const RECOVERY_CATALOG_EVIDENCE_ID: "P220-EV-RECOVERY-CATALOG";
|
|
6
|
+
export declare const RECOVERY_FAILURE_CATEGORIES: readonly FailureCategory[];
|
|
7
|
+
export type RecoveryCatalogReasonCode = 'recovery-catalog-accepted' | 'recovery-catalog-schema-invalid' | 'recovery-category-unknown' | 'recovery-category-duplicate' | 'recovery-category-missing' | 'recovery-sentinel-invalid' | 'recovery-target-unknown' | 'recovery-target-not-allowed' | 'recovery-target-cycle' | 'recovery-template-invalid' | 'recovery-budget-invalid' | 'recovery-tier-invalid' | 'recovery-tier-authority-expanded' | 'recovery-invocation-rejected' | 'recovery-catalog-fallback' | 'recovery-catalog-defaults-invalid';
|
|
8
|
+
export interface RecoveryRuleInput {
|
|
9
|
+
readonly category: FailureCategory;
|
|
10
|
+
readonly recovery_trigger: string;
|
|
11
|
+
readonly trigger_args_template: string;
|
|
12
|
+
readonly max_attempts_per_story: number;
|
|
13
|
+
readonly applicable_tiers: readonly SubscriptionTier[];
|
|
14
|
+
}
|
|
15
|
+
export interface CompiledRecoveryRule extends RecoveryRuleInput {
|
|
16
|
+
readonly commandId: 'refactor' | 'resolve-conflicts' | null;
|
|
17
|
+
}
|
|
18
|
+
export interface RecoveryCatalogReceiptV1 {
|
|
19
|
+
readonly schemaVersion: 1;
|
|
20
|
+
readonly evidenceId: typeof RECOVERY_CATALOG_EVIDENCE_ID;
|
|
21
|
+
readonly outcome: 'accepted' | 'rejected' | 'fallback';
|
|
22
|
+
readonly registryVersion: string;
|
|
23
|
+
readonly registryChecksumPrefix: string;
|
|
24
|
+
readonly schemaChecksumPrefix: string;
|
|
25
|
+
readonly catalogDigestPrefix?: string;
|
|
26
|
+
readonly ruleCount: number;
|
|
27
|
+
readonly candidateRuleCount: number;
|
|
28
|
+
readonly sentinelRuleCount: number;
|
|
29
|
+
readonly reasonCodes: readonly RecoveryCatalogReasonCode[];
|
|
30
|
+
readonly publicSafe: true;
|
|
31
|
+
}
|
|
32
|
+
export type RecoveryCatalogCompilation = {
|
|
33
|
+
readonly ok: true;
|
|
34
|
+
readonly rules: readonly CompiledRecoveryRule[];
|
|
35
|
+
readonly catalogHash: string;
|
|
36
|
+
readonly receipt: RecoveryCatalogReceiptV1;
|
|
37
|
+
} | {
|
|
38
|
+
readonly ok: false;
|
|
39
|
+
readonly reasonCodes: readonly RecoveryCatalogReasonCode[];
|
|
40
|
+
readonly receipt: RecoveryCatalogReceiptV1;
|
|
41
|
+
};
|
|
42
|
+
export type RecoveryCandidateResult = {
|
|
43
|
+
readonly kind: 'candidate';
|
|
44
|
+
readonly action: string;
|
|
45
|
+
readonly commandId: Exclude<CompiledRecoveryRule['commandId'], null>;
|
|
46
|
+
readonly admissionReceipt: InvocationAdmissionReceiptV1;
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'terminal';
|
|
49
|
+
readonly reasonCode: 'recovery-no-candidate' | 'recovery-story-id-invalid' | 'recovery-invocation-rejected';
|
|
50
|
+
};
|
|
51
|
+
export interface CompileRecoveryCatalogOptions {
|
|
52
|
+
readonly receiptOutcome?: 'accepted' | 'fallback';
|
|
53
|
+
readonly additionalReasonCodes?: readonly Extract<RecoveryCatalogReasonCode, 'recovery-catalog-fallback' | 'recovery-catalog-defaults-invalid'>[];
|
|
54
|
+
}
|
|
55
|
+
export declare function compileRecoveryCatalog(input: unknown, options?: CompileRecoveryCatalogOptions): RecoveryCatalogCompilation;
|
|
56
|
+
export declare function materializeRecoveryCandidate(rule: CompiledRecoveryRule, storyId: string): RecoveryCandidateResult;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** P220.10 registry-valid, root-owned yoloop recovery candidates. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { canonicalizeCommandRegistryJcs } from '../command-registry-contract.js';
|
|
4
|
+
import { COMMAND_REGISTRY_CHECKSUM, COMMAND_REGISTRY_VERSION, GENERATED_SHARED_COMMANDS, } from '../generated/command-registry.generated.js';
|
|
5
|
+
import { GENERATED_INVOCATION_SCHEMA_MANIFEST } from '../generated/invocation-schema.generated.js';
|
|
6
|
+
import { YOLOOP_FAILURE_CATEGORIES, } from '../yoloop/recovery-types.js';
|
|
7
|
+
import { admitInvocation, createInvocationAdmissionReceipt, } from './index.js';
|
|
8
|
+
export const RECOVERY_CATALOG_EVIDENCE_ID = 'P220-EV-RECOVERY-CATALOG';
|
|
9
|
+
export const RECOVERY_FAILURE_CATEGORIES = YOLOOP_FAILURE_CATEGORIES;
|
|
10
|
+
const KNOWN_CATEGORIES = new Set(RECOVERY_FAILURE_CATEGORIES);
|
|
11
|
+
const KNOWN_TIERS = new Set(['free', 'pro', 'enterprise']);
|
|
12
|
+
const TIER_RANK = Object.freeze({
|
|
13
|
+
free: 0,
|
|
14
|
+
pro: 1,
|
|
15
|
+
enterprise: 2,
|
|
16
|
+
});
|
|
17
|
+
const RULE_KEYS = Object.freeze([
|
|
18
|
+
'applicable_tiers',
|
|
19
|
+
'category',
|
|
20
|
+
'max_attempts_per_story',
|
|
21
|
+
'recovery_trigger',
|
|
22
|
+
'trigger_args_template',
|
|
23
|
+
]);
|
|
24
|
+
const STORY_TEMPLATE = '@docs/stories/{storyId}.story.md';
|
|
25
|
+
const SYNTHETIC_STORY_ID = 'P220.10';
|
|
26
|
+
const ROUTE_POLICY = Object.freeze({
|
|
27
|
+
lint_failure: 'refactor',
|
|
28
|
+
merge_conflict: 'resolve-conflicts',
|
|
29
|
+
runtime_local_reproducible_fixable: 'refactor',
|
|
30
|
+
});
|
|
31
|
+
function isObject(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function uniqueReasons(reasons) {
|
|
35
|
+
return Object.freeze([...new Set(reasons)].sort().slice(0, 16));
|
|
36
|
+
}
|
|
37
|
+
function receipt(outcome, ruleCount, candidateRuleCount, sentinelRuleCount, reasonCodes, catalogDigestPrefix) {
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
evidenceId: RECOVERY_CATALOG_EVIDENCE_ID,
|
|
41
|
+
outcome,
|
|
42
|
+
registryVersion: COMMAND_REGISTRY_VERSION,
|
|
43
|
+
registryChecksumPrefix: COMMAND_REGISTRY_CHECKSUM.slice(0, 12),
|
|
44
|
+
schemaChecksumPrefix: GENERATED_INVOCATION_SCHEMA_MANIFEST.schemaChecksum.slice(0, 12),
|
|
45
|
+
...(catalogDigestPrefix ? { catalogDigestPrefix } : {}),
|
|
46
|
+
ruleCount,
|
|
47
|
+
candidateRuleCount,
|
|
48
|
+
sentinelRuleCount,
|
|
49
|
+
reasonCodes: uniqueReasons(reasonCodes),
|
|
50
|
+
publicSafe: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function reject(ruleCount, reasons) {
|
|
54
|
+
const reasonCodes = uniqueReasons(reasons.length ? reasons : ['recovery-catalog-schema-invalid']);
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
ok: false,
|
|
57
|
+
reasonCodes,
|
|
58
|
+
receipt: receipt('rejected', ruleCount, 0, 0, reasonCodes),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function isSentinelShape(rule) {
|
|
62
|
+
return rule.recovery_trigger === ''
|
|
63
|
+
&& rule.trigger_args_template === ''
|
|
64
|
+
&& rule.max_attempts_per_story === 0
|
|
65
|
+
&& Array.isArray(rule.applicable_tiers)
|
|
66
|
+
&& rule.applicable_tiers.length === 0;
|
|
67
|
+
}
|
|
68
|
+
function hasSentinelField(rule) {
|
|
69
|
+
return rule.recovery_trigger === ''
|
|
70
|
+
|| rule.trigger_args_template === ''
|
|
71
|
+
|| rule.max_attempts_per_story === 0
|
|
72
|
+
|| (Array.isArray(rule.applicable_tiers) && rule.applicable_tiers.length === 0);
|
|
73
|
+
}
|
|
74
|
+
export function compileRecoveryCatalog(input, options = {}) {
|
|
75
|
+
if (!isObject(input) || input.schema_version !== 1 || !Array.isArray(input.rules)) {
|
|
76
|
+
return reject(0, ['recovery-catalog-schema-invalid']);
|
|
77
|
+
}
|
|
78
|
+
const reasons = [];
|
|
79
|
+
const compiled = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
for (const candidate of input.rules) {
|
|
82
|
+
if (!isObject(candidate) || Object.keys(candidate).sort().join('\0') !== RULE_KEYS.join('\0')) {
|
|
83
|
+
reasons.push('recovery-catalog-schema-invalid');
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (typeof candidate.category !== 'string' || !KNOWN_CATEGORIES.has(candidate.category)) {
|
|
87
|
+
reasons.push('recovery-category-unknown');
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const category = candidate.category;
|
|
91
|
+
if (seen.has(category)) {
|
|
92
|
+
reasons.push('recovery-category-duplicate');
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seen.add(category);
|
|
96
|
+
if (isSentinelShape(candidate)) {
|
|
97
|
+
compiled.push(Object.freeze({
|
|
98
|
+
category,
|
|
99
|
+
recovery_trigger: '',
|
|
100
|
+
trigger_args_template: '',
|
|
101
|
+
max_attempts_per_story: 0,
|
|
102
|
+
applicable_tiers: Object.freeze([]),
|
|
103
|
+
commandId: null,
|
|
104
|
+
}));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (hasSentinelField(candidate))
|
|
108
|
+
reasons.push('recovery-sentinel-invalid');
|
|
109
|
+
if (typeof candidate.recovery_trigger !== 'string'
|
|
110
|
+
|| typeof candidate.trigger_args_template !== 'string'
|
|
111
|
+
|| typeof candidate.max_attempts_per_story !== 'number'
|
|
112
|
+
|| !Array.isArray(candidate.applicable_tiers)) {
|
|
113
|
+
reasons.push('recovery-catalog-schema-invalid');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const match = /^\*([a-z0-9][a-z0-9-]*)$/.exec(candidate.recovery_trigger);
|
|
117
|
+
const commandId = match?.[1];
|
|
118
|
+
const command = GENERATED_SHARED_COMMANDS.find((entry) => entry.id === commandId);
|
|
119
|
+
if (!commandId || !command) {
|
|
120
|
+
reasons.push('recovery-target-unknown');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (commandId === 'yolo' || commandId === 'yoloop') {
|
|
124
|
+
reasons.push('recovery-target-cycle');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (command.category !== 'recovery' || ROUTE_POLICY[category] !== commandId) {
|
|
128
|
+
reasons.push('recovery-target-not-allowed');
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (candidate.recovery_trigger !== `*${command.id}`) {
|
|
132
|
+
reasons.push('recovery-target-not-allowed');
|
|
133
|
+
}
|
|
134
|
+
if (candidate.trigger_args_template !== STORY_TEMPLATE) {
|
|
135
|
+
reasons.push('recovery-template-invalid');
|
|
136
|
+
}
|
|
137
|
+
if (!Number.isInteger(candidate.max_attempts_per_story)
|
|
138
|
+
|| candidate.max_attempts_per_story < 1
|
|
139
|
+
|| candidate.max_attempts_per_story > 2) {
|
|
140
|
+
reasons.push('recovery-budget-invalid');
|
|
141
|
+
}
|
|
142
|
+
const tiers = candidate.applicable_tiers;
|
|
143
|
+
if (tiers.some((tier) => typeof tier !== 'string' || !KNOWN_TIERS.has(tier))
|
|
144
|
+
|| new Set(tiers).size !== tiers.length) {
|
|
145
|
+
reasons.push('recovery-tier-invalid');
|
|
146
|
+
}
|
|
147
|
+
else if (tiers.some((tier) => TIER_RANK[tier] < TIER_RANK[command.policy.minimumTier])) {
|
|
148
|
+
reasons.push('recovery-tier-authority-expanded');
|
|
149
|
+
}
|
|
150
|
+
const action = `${candidate.recovery_trigger} ${candidate.trigger_args_template.replace('{storyId}', SYNTHETIC_STORY_ID)}`;
|
|
151
|
+
const admission = admitInvocation(action);
|
|
152
|
+
if (admission.outcome !== 'resolved'
|
|
153
|
+
|| admission.selection.commandId !== command.id
|
|
154
|
+
|| Object.keys(admission.selection.options).length !== 0) {
|
|
155
|
+
reasons.push('recovery-invocation-rejected');
|
|
156
|
+
}
|
|
157
|
+
if (reasons.length === 0) {
|
|
158
|
+
compiled.push(Object.freeze({
|
|
159
|
+
category,
|
|
160
|
+
recovery_trigger: candidate.recovery_trigger,
|
|
161
|
+
trigger_args_template: candidate.trigger_args_template,
|
|
162
|
+
max_attempts_per_story: candidate.max_attempts_per_story,
|
|
163
|
+
applicable_tiers: Object.freeze([...tiers].sort((left, right) => TIER_RANK[left] - TIER_RANK[right])),
|
|
164
|
+
commandId: command.id,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (seen.size !== RECOVERY_FAILURE_CATEGORIES.length)
|
|
169
|
+
reasons.push('recovery-category-missing');
|
|
170
|
+
if (reasons.length || compiled.length !== RECOVERY_FAILURE_CATEGORIES.length) {
|
|
171
|
+
return reject(input.rules.length, reasons);
|
|
172
|
+
}
|
|
173
|
+
const rules = Object.freeze([...compiled].sort((left, right) => left.category.localeCompare(right.category)));
|
|
174
|
+
const canonical = canonicalizeCommandRegistryJcs({
|
|
175
|
+
schema_version: 1,
|
|
176
|
+
rules: rules.map(({ commandId: _commandId, ...rule }) => rule),
|
|
177
|
+
});
|
|
178
|
+
const catalogHash = createHash('sha256').update(`p220-recovery-catalog-v1\0${canonical}`).digest('hex');
|
|
179
|
+
const catalogDigestPrefix = catalogHash.slice(0, 12);
|
|
180
|
+
const candidateRuleCount = rules.filter((rule) => rule.commandId !== null).length;
|
|
181
|
+
const sentinelRuleCount = rules.length - candidateRuleCount;
|
|
182
|
+
const outcome = options.receiptOutcome ?? 'accepted';
|
|
183
|
+
const reasonCodes = uniqueReasons([
|
|
184
|
+
outcome === 'fallback' ? 'recovery-catalog-fallback' : 'recovery-catalog-accepted',
|
|
185
|
+
...(options.additionalReasonCodes ?? []),
|
|
186
|
+
]);
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
ok: true,
|
|
189
|
+
rules,
|
|
190
|
+
catalogHash,
|
|
191
|
+
receipt: receipt(outcome, rules.length, candidateRuleCount, sentinelRuleCount, reasonCodes, catalogDigestPrefix),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export function materializeRecoveryCandidate(rule, storyId) {
|
|
195
|
+
if (rule.commandId === null) {
|
|
196
|
+
return Object.freeze({ kind: 'terminal', reasonCode: 'recovery-no-candidate' });
|
|
197
|
+
}
|
|
198
|
+
if (!/^P\d+\.\d+$/.test(storyId)) {
|
|
199
|
+
return Object.freeze({ kind: 'terminal', reasonCode: 'recovery-story-id-invalid' });
|
|
200
|
+
}
|
|
201
|
+
const action = `${rule.recovery_trigger} ${rule.trigger_args_template.replace('{storyId}', storyId)}`;
|
|
202
|
+
const admission = admitInvocation(action);
|
|
203
|
+
if (admission.outcome !== 'resolved'
|
|
204
|
+
|| admission.selection.commandId !== rule.commandId
|
|
205
|
+
|| Object.keys(admission.selection.options).length !== 0) {
|
|
206
|
+
return Object.freeze({ kind: 'terminal', reasonCode: 'recovery-invocation-rejected' });
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
kind: 'candidate',
|
|
210
|
+
action,
|
|
211
|
+
commandId: rule.commandId,
|
|
212
|
+
admissionReceipt: createInvocationAdmissionReceipt(admission, 'shared'),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './state-migration.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './state-migration.js';
|