@neurcode/action 0.2.1

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.
@@ -0,0 +1,84 @@
1
+ export type VerifyFallbackReason =
2
+ | 'missing_plan_context'
3
+ | 'verify_succeeded'
4
+ | 'already_policy_only'
5
+ | 'explicit_plan_id'
6
+ | 'not_missing_plan_failure';
7
+
8
+ export interface VerifyFallbackDecision {
9
+ shouldRetryPolicyOnly: boolean;
10
+ reason: VerifyFallbackReason;
11
+ }
12
+
13
+ export interface VerifyFallbackDecisionInput {
14
+ verifyExitCode: number;
15
+ policyOnlyRequested: boolean;
16
+ hasExplicitPlanId: boolean;
17
+ verifyOutput: string;
18
+ }
19
+
20
+ export interface VerifyArgsInput {
21
+ baseRef: string;
22
+ planId?: string;
23
+ projectId?: string;
24
+ policyOnly: boolean;
25
+ record: boolean;
26
+ compiledPolicyPath?: string;
27
+ changeContractPath?: string;
28
+ enforceChangeContract: boolean;
29
+ }
30
+
31
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
32
+
33
+ const MISSING_PLAN_VERIFY_PATTERNS = [
34
+ 'plan id is required in strict mode',
35
+ 'run "neurcode plan" first',
36
+ 'pass --plan-id',
37
+ '"mode": "plan_required"',
38
+ '"mode":"plan_required"',
39
+ 'missing plan context',
40
+ 'plan context missing',
41
+ 'no approved plan context',
42
+ ];
43
+
44
+ function stripAnsi(value: string): string {
45
+ return value.replace(ANSI_PATTERN, '');
46
+ }
47
+
48
+ export function isMissingPlanVerificationFailure(output: string): boolean {
49
+ const normalized = stripAnsi(output).toLowerCase();
50
+ return MISSING_PLAN_VERIFY_PATTERNS.some((pattern) => normalized.includes(pattern));
51
+ }
52
+
53
+ export function buildVerifyArgs(input: VerifyArgsInput): string[] {
54
+ const args = ['verify', '--json', '--base', input.baseRef];
55
+ if (input.policyOnly) {
56
+ args.push('--policy-only');
57
+ } else if (input.planId) {
58
+ args.push('--plan-id', input.planId);
59
+ }
60
+ if (input.projectId) args.push('--project-id', input.projectId);
61
+ if (input.compiledPolicyPath) args.push('--compiled-policy', input.compiledPolicyPath);
62
+ if (input.changeContractPath) args.push('--change-contract', input.changeContractPath);
63
+ if (input.enforceChangeContract) args.push('--enforce-change-contract');
64
+ if (input.record) args.push('--record');
65
+ return args;
66
+ }
67
+
68
+ export function getVerifyFallbackDecision(
69
+ input: VerifyFallbackDecisionInput
70
+ ): VerifyFallbackDecision {
71
+ if (input.verifyExitCode === 0) {
72
+ return { shouldRetryPolicyOnly: false, reason: 'verify_succeeded' };
73
+ }
74
+ if (input.policyOnlyRequested) {
75
+ return { shouldRetryPolicyOnly: false, reason: 'already_policy_only' };
76
+ }
77
+ if (input.hasExplicitPlanId) {
78
+ return { shouldRetryPolicyOnly: false, reason: 'explicit_plan_id' };
79
+ }
80
+ if (isMissingPlanVerificationFailure(input.verifyOutput)) {
81
+ return { shouldRetryPolicyOnly: true, reason: 'missing_plan_context' };
82
+ }
83
+ return { shouldRetryPolicyOnly: false, reason: 'not_missing_plan_failure' };
84
+ }
@@ -0,0 +1,78 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ buildVerifyArgs,
6
+ getVerifyFallbackDecision,
7
+ isMissingPlanVerificationFailure,
8
+ } from '../src/verify-mode';
9
+
10
+ test('verify args remain plan-aware when no plan_id is provided', () => {
11
+ const args = buildVerifyArgs({
12
+ baseRef: 'origin/main',
13
+ policyOnly: false,
14
+ record: true,
15
+ enforceChangeContract: false,
16
+ });
17
+
18
+ assert.deepEqual(args, ['verify', '--json', '--base', 'origin/main', '--record']);
19
+ assert.equal(args.includes('--policy-only'), false);
20
+ assert.equal(args.includes('--plan-id'), false);
21
+ });
22
+
23
+ test('verify args enforce explicit plan_id when provided', () => {
24
+ const args = buildVerifyArgs({
25
+ baseRef: 'origin/main',
26
+ policyOnly: false,
27
+ record: true,
28
+ planId: 'plan_123',
29
+ enforceChangeContract: false,
30
+ });
31
+
32
+ assert.deepEqual(args, [
33
+ 'verify',
34
+ '--json',
35
+ '--base',
36
+ 'origin/main',
37
+ '--plan-id',
38
+ 'plan_123',
39
+ '--record',
40
+ ]);
41
+ });
42
+
43
+ test('fallback decision retries policy-only when plan context is missing and no explicit plan_id exists', () => {
44
+ const decision = getVerifyFallbackDecision({
45
+ verifyExitCode: 2,
46
+ policyOnlyRequested: false,
47
+ hasExplicitPlanId: false,
48
+ verifyOutput:
49
+ 'verification failed: plan id is required in strict mode. run "neurcode plan" first.',
50
+ });
51
+
52
+ assert.equal(decision.shouldRetryPolicyOnly, true);
53
+ assert.equal(decision.reason, 'missing_plan_context');
54
+ });
55
+
56
+ test('fallback decision does not retry policy-only when explicit plan_id was supplied', () => {
57
+ const decision = getVerifyFallbackDecision({
58
+ verifyExitCode: 2,
59
+ policyOnlyRequested: false,
60
+ hasExplicitPlanId: true,
61
+ verifyOutput:
62
+ 'verification failed: plan id is required in strict mode. run "neurcode plan" first.',
63
+ });
64
+
65
+ assert.equal(decision.shouldRetryPolicyOnly, false);
66
+ assert.equal(decision.reason, 'explicit_plan_id');
67
+ });
68
+
69
+ test('missing-plan matcher recognizes canonical strict-mode errors', () => {
70
+ assert.equal(
71
+ isMissingPlanVerificationFailure('Error: plan context missing for current diff'),
72
+ true
73
+ );
74
+ assert.equal(
75
+ isMissingPlanVerificationFailure('policy violations found in changed files'),
76
+ false
77
+ );
78
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": false,
13
+ "sourceMap": true,
14
+ "resolveJsonModule": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
18
+ }
19
+