@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.
Files changed (62) hide show
  1. package/dist/sbom.cdx.json +31 -31
  2. package/install.ps1 +1 -1
  3. package/install.sh +1 -1
  4. package/package.json +14 -2
  5. package/packages/client/dist/commands/invoke.js +57 -56
  6. package/packages/client/dist/generated/command-registry.generated.d.ts +66 -23
  7. package/packages/client/dist/generated/command-registry.generated.js +1 -1
  8. package/packages/client/dist/generated/public-projections.generated.d.ts +100 -0
  9. package/packages/client/dist/generated/public-projections.generated.js +1 -0
  10. package/packages/client/dist/git/git-lifecycle-store.d.ts +17 -0
  11. package/packages/client/dist/git/git-lifecycle-store.js +4 -4
  12. package/packages/client/dist/index.d.ts +1 -0
  13. package/packages/client/dist/index.js +1 -1
  14. package/packages/client/dist/migrations/project-identity.js +7 -7
  15. package/packages/client/dist/migrations/state-json.d.ts +14 -0
  16. package/packages/client/dist/migrations/state-json.js +4 -0
  17. package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.d.ts +2 -0
  18. package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -1
  19. package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +8 -8
  20. package/packages/client/dist/runner/headless/run-yoloop.d.ts +7 -1
  21. package/packages/client/dist/runner/headless/run-yoloop.js +2 -2
  22. package/packages/client/dist/state/project-state-snapshot.js +1 -1
  23. package/packages/client/dist/state/state-json-repair.js +3 -3
  24. package/packages/client/dist/yoloop/invoke-hooks.d.ts +2 -3
  25. package/packages/client/dist/yoloop/invoke-hooks.js +9 -8
  26. package/packages/client/dist/yoloop/shared-discover-epics.d.ts +3 -3
  27. package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1
  28. package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +87 -1
  29. package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +27 -3
  30. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
  31. package/packages/shared/dist/command-registry-contract.js +38 -0
  32. package/packages/shared/dist/continuity/goal-operation-authority.js +48 -0
  33. package/packages/shared/dist/executable-graph-catalog/contracts.js +935 -0
  34. package/packages/shared/dist/generated/command-registry.generated.js +82 -23
  35. package/packages/shared/dist/generated/invocation-schema.generated.js +12 -4
  36. package/packages/shared/dist/generated/public-projections.generated.js +149 -0
  37. package/packages/shared/dist/invocation/executable-policy-recovery.d.ts +83 -0
  38. package/packages/shared/dist/invocation/executable-policy-recovery.js +272 -0
  39. package/packages/shared/dist/invocation/index.d.ts +3 -1
  40. package/packages/shared/dist/invocation/index.js +40 -1
  41. package/packages/shared/dist/invocation/recovery-route-compiler.d.ts +56 -0
  42. package/packages/shared/dist/invocation/recovery-route-compiler.js +214 -0
  43. package/packages/shared/dist/state/index.d.ts +1 -0
  44. package/packages/shared/dist/state/index.js +1 -0
  45. package/packages/shared/dist/state/state-migration.d.ts +58 -0
  46. package/packages/shared/dist/state/state-migration.js +404 -0
  47. package/packages/shared/dist/yoloop/canonical.js +15 -0
  48. package/packages/shared/dist/yoloop/operational-budget.js +164 -0
  49. package/packages/shared/dist/yoloop/recovery-types.js +39 -0
  50. package/targets-stubs/antigravity/gemini.md +1 -1
  51. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  52. package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
  53. package/targets-stubs/claude-code/neocortex-root.md +2 -2
  54. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  55. package/targets-stubs/claude-code/neocortex.md +2 -2
  56. package/targets-stubs/codex/AGENTS.md +11 -2
  57. package/targets-stubs/codex/README.md +7 -4
  58. package/targets-stubs/codex/neocortex.toml +53 -6
  59. package/targets-stubs/cursor/agent.md +2 -2
  60. package/targets-stubs/gemini-cli/agent.md +2 -2
  61. package/targets-stubs/opencode/neocortex-root.md +1 -1
  62. 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: "92a7a79243c60f0c53071b28dbdfbbf8587fb1cccde2e213aae89d4d2de6c329";
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';