@njeybe/nativ 2.0.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/bin/cli.js +31 -0
  5. package/dist/commands/bench.d.ts +8 -0
  6. package/dist/commands/bench.js +52 -0
  7. package/dist/commands/db.d.ts +22 -0
  8. package/dist/commands/db.js +415 -0
  9. package/dist/commands/doctor.d.ts +38 -0
  10. package/dist/commands/doctor.js +169 -0
  11. package/dist/commands/hook.d.ts +31 -0
  12. package/dist/commands/hook.js +105 -0
  13. package/dist/commands/init.d.ts +3 -0
  14. package/dist/commands/init.js +144 -0
  15. package/dist/commands/mcp.d.ts +5 -0
  16. package/dist/commands/mcp.js +16 -0
  17. package/dist/commands/setup.d.ts +17 -0
  18. package/dist/commands/setup.js +51 -0
  19. package/dist/commands/status.d.ts +5 -0
  20. package/dist/commands/status.js +75 -0
  21. package/dist/commands/task.d.ts +83 -0
  22. package/dist/commands/task.js +807 -0
  23. package/dist/commands/test-gen.d.ts +14 -0
  24. package/dist/commands/test-gen.js +52 -0
  25. package/dist/commands/triage.d.ts +70 -0
  26. package/dist/commands/triage.js +343 -0
  27. package/dist/commands/update.d.ts +14 -0
  28. package/dist/commands/update.js +112 -0
  29. package/dist/commands/validate.d.ts +1 -0
  30. package/dist/commands/validate.js +89 -0
  31. package/dist/commands/verify.d.ts +8 -0
  32. package/dist/commands/verify.js +65 -0
  33. package/dist/commands/worktree.d.ts +47 -0
  34. package/dist/commands/worktree.js +396 -0
  35. package/dist/core/benchmark.d.ts +57 -0
  36. package/dist/core/benchmark.js +772 -0
  37. package/dist/core/enforcement.d.ts +80 -0
  38. package/dist/core/enforcement.js +271 -0
  39. package/dist/core/legacy-templates.d.ts +24 -0
  40. package/dist/core/legacy-templates.js +60 -0
  41. package/dist/core/lock-manager.d.ts +54 -0
  42. package/dist/core/lock-manager.js +260 -0
  43. package/dist/core/root-resolver.d.ts +32 -0
  44. package/dist/core/root-resolver.js +149 -0
  45. package/dist/core/setup-assets.d.ts +123 -0
  46. package/dist/core/setup-assets.js +438 -0
  47. package/dist/core/telemetry.d.ts +156 -0
  48. package/dist/core/telemetry.js +416 -0
  49. package/dist/core/test-generator-types.d.ts +227 -0
  50. package/dist/core/test-generator-types.js +179 -0
  51. package/dist/core/test-generator.d.ts +34 -0
  52. package/dist/core/test-generator.js +1690 -0
  53. package/dist/core/tier1-liaison.d.ts +139 -0
  54. package/dist/core/tier1-liaison.js +320 -0
  55. package/dist/core/verifier.d.ts +52 -0
  56. package/dist/core/verifier.js +249 -0
  57. package/dist/core/version.d.ts +5 -0
  58. package/dist/core/version.js +14 -0
  59. package/dist/core/worktree-merge.d.ts +30 -0
  60. package/dist/core/worktree-merge.js +98 -0
  61. package/dist/db/data-engine.d.ts +46 -0
  62. package/dist/db/data-engine.js +802 -0
  63. package/dist/db/diff.d.ts +39 -0
  64. package/dist/db/diff.js +420 -0
  65. package/dist/db/env-parser.d.ts +86 -0
  66. package/dist/db/env-parser.js +549 -0
  67. package/dist/db/introspector.d.ts +21 -0
  68. package/dist/db/introspector.js +835 -0
  69. package/dist/db/types.d.ts +217 -0
  70. package/dist/db/types.js +9 -0
  71. package/dist/governor/circuit-breaker.d.ts +100 -0
  72. package/dist/governor/circuit-breaker.js +231 -0
  73. package/dist/governor/evaluator.d.ts +67 -0
  74. package/dist/governor/evaluator.js +851 -0
  75. package/dist/governor/index.d.ts +6 -0
  76. package/dist/governor/index.js +6 -0
  77. package/dist/governor/rules/api-rules.d.ts +2 -0
  78. package/dist/governor/rules/api-rules.js +140 -0
  79. package/dist/governor/rules/db-rules.d.ts +2 -0
  80. package/dist/governor/rules/db-rules.js +180 -0
  81. package/dist/governor/store.d.ts +33 -0
  82. package/dist/governor/store.js +84 -0
  83. package/dist/governor/types.d.ts +64 -0
  84. package/dist/governor/types.js +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.js +358 -0
  87. package/dist/mcp/server.d.ts +3 -0
  88. package/dist/mcp/server.js +309 -0
  89. package/dist/providers/claude-api.d.ts +5 -0
  90. package/dist/providers/claude-api.js +75 -0
  91. package/dist/providers/claude-cli.d.ts +13 -0
  92. package/dist/providers/claude-cli.js +134 -0
  93. package/dist/providers/gemini.d.ts +8 -0
  94. package/dist/providers/gemini.js +78 -0
  95. package/dist/providers/index.d.ts +5 -0
  96. package/dist/providers/index.js +5 -0
  97. package/dist/providers/registry.d.ts +38 -0
  98. package/dist/providers/registry.js +121 -0
  99. package/dist/providers/types.d.ts +74 -0
  100. package/dist/providers/types.js +25 -0
  101. package/dist/runner/agent-supervisor.d.ts +363 -0
  102. package/dist/runner/agent-supervisor.js +1672 -0
  103. package/dist/scanner/context-builder.d.ts +2 -0
  104. package/dist/scanner/context-builder.js +29 -0
  105. package/dist/scanner/detector.d.ts +2 -0
  106. package/dist/scanner/detector.js +760 -0
  107. package/dist/scanner/types.d.ts +80 -0
  108. package/dist/scanner/types.js +50 -0
  109. package/dist/server/studio-server.d.ts +39 -0
  110. package/dist/server/studio-server.js +1931 -0
  111. package/dist/server/studio-ui.d.ts +15 -0
  112. package/dist/server/studio-ui.js +5321 -0
  113. package/package.json +70 -0
  114. package/plugin/.claude-plugin/plugin.json +18 -0
  115. package/plugin/.mcp.json +10 -0
  116. package/plugin/agents/architect.md +28 -0
  117. package/plugin/agents/verifier.md +28 -0
  118. package/plugin/agents/worker.md +26 -0
  119. package/plugin/hooks/hooks.json +28 -0
  120. package/plugin/skills/nativ/SKILL.md +75 -0
  121. package/templates/.claudeignore +45 -0
  122. package/templates/AGENTS.md +67 -0
  123. package/templates/CLAUDE.md +32 -0
  124. package/templates/GEMINI.md +66 -0
  125. package/templates/claude-agents/architect.md +28 -0
  126. package/templates/claude-agents/verifier.md +28 -0
  127. package/templates/claude-agents/worker.md +26 -0
  128. package/templates/dot-ai/api_contracts.json +66 -0
  129. package/templates/dot-ai/context.md +38 -0
  130. package/templates/dot-ai/db_schema.json +39 -0
  131. package/templates/dot-ai/master_plan.json +28 -0
  132. package/templates/dot-ai/subagents/backend.md +11 -0
  133. package/templates/dot-ai/subagents/database.md +10 -0
  134. package/templates/dot-ai/subagents/db-migration.md +11 -0
  135. package/templates/dot-ai/subagents/devops-agent.md +11 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +11 -0
  137. package/templates/dot-ai/subagents/frontend.md +126 -0
  138. package/templates/dot-ai/subagents/qa-tester.md +13 -0
  139. package/templates/dot-ai/subagents/security-auditor.md +11 -0
  140. package/templates/dot-ai/ui_specs.md +78 -0
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './rules/db-rules.js';
3
+ export * from './rules/api-rules.js';
4
+ export * from './circuit-breaker.js';
5
+ export * from './evaluator.js';
6
+ export * from './store.js';
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './rules/db-rules.js';
3
+ export * from './rules/api-rules.js';
4
+ export * from './circuit-breaker.js';
5
+ export * from './evaluator.js';
6
+ export * from './store.js';
@@ -0,0 +1,2 @@
1
+ import { ContractPatch, RuleEvaluationResult } from '../types.js';
2
+ export declare function evaluateApiPatch(patch: ContractPatch, currentContracts: any): RuleEvaluationResult;
@@ -0,0 +1,140 @@
1
+ export function evaluateApiPatch(patch, currentContracts) {
2
+ const violations = [];
3
+ const { operation, path, value } = patch;
4
+ const normalizedPath = path.trim().toLowerCase();
5
+ const endpoints = currentContracts?.endpoints || [];
6
+ // Parse path: e.g. "endpoints.user-login" or "endpoints./users.get.responses.200.body.avatar"
7
+ const segments = normalizedPath.split('.').filter(Boolean);
8
+ const endpointIdOrPath = segments[0] === 'endpoints' ? segments[1] : segments[0];
9
+ const existingEndpoint = endpoints.find((ep) => ep.id?.toLowerCase() === endpointIdOrPath?.toLowerCase() ||
10
+ ep.path?.toLowerCase() === endpointIdOrPath?.toLowerCase() ||
11
+ `${ep.method?.toLowerCase()} ${ep.path?.toLowerCase()}` === endpointIdOrPath?.toLowerCase());
12
+ // Rule 1: Path Collision & Target Checks
13
+ if (operation === 'ADD') {
14
+ const isNewEndpoint = segments.length <= 2 && (!existingEndpoint || (value?.method && value?.path));
15
+ if (isNewEndpoint && existingEndpoint && value?.method?.toUpperCase() === existingEndpoint.method?.toUpperCase()) {
16
+ violations.push(`PATH_COLLISION: Endpoint '${existingEndpoint.method} ${existingEndpoint.path}' already exists. Use ALTER.`);
17
+ }
18
+ }
19
+ else if (operation === 'ALTER' || operation === 'DROP') {
20
+ if (!existingEndpoint && segments[0] === 'endpoints' && segments.length <= 2) {
21
+ violations.push(`TARGET_NOT_FOUND: Cannot ${operation.toLowerCase()} non-existent endpoint '${endpointIdOrPath}'.`);
22
+ }
23
+ }
24
+ if (violations.length > 0) {
25
+ return {
26
+ approved: false,
27
+ blastRadius: 'HIGH_DESTRUCTIVE',
28
+ ruleId: 'API_COLLISION_OR_TARGET_MISMATCH',
29
+ message: violations[0],
30
+ violations,
31
+ };
32
+ }
33
+ // Rule 2: DROP operations
34
+ if (operation === 'DROP') {
35
+ const isDroppingEndpoint = segments.length <= 2;
36
+ const msg = isDroppingEndpoint
37
+ ? `CANNOT_DROP_ENDPOINT: Dropping endpoint '${existingEndpoint?.method || ''} ${existingEndpoint?.path || endpointIdOrPath}' causes immediate 404 client breakage.`
38
+ : `CANNOT_DROP_RESPONSE_FIELD: Dropping field on '${path}' causes client runtime deserialization failures.`;
39
+ violations.push(msg);
40
+ return {
41
+ approved: false,
42
+ blastRadius: 'HIGH_DESTRUCTIVE',
43
+ ruleId: 'API_DROP_FORBIDDEN',
44
+ message: msg,
45
+ violations,
46
+ };
47
+ }
48
+ // Rule 3: RENAME operations
49
+ if (operation === 'RENAME') {
50
+ const msg = `CANNOT_RENAME_API_ROUTE: Renaming route/path '${endpointIdOrPath}' breaks consumer integration.`;
51
+ violations.push(msg);
52
+ return {
53
+ approved: false,
54
+ blastRadius: 'HIGH_DESTRUCTIVE',
55
+ ruleId: 'API_RENAME_FORBIDDEN',
56
+ message: msg,
57
+ violations,
58
+ };
59
+ }
60
+ // Rule 4: ALTER operations
61
+ if (operation === 'ALTER') {
62
+ // Auth policy mutation (unauthorized client break)
63
+ if (segments.includes('auth') && existingEndpoint) {
64
+ if (existingEndpoint.auth === false && (value === true || value?.auth === true)) {
65
+ violations.push(`CANNOT_RESTRICT_AUTH: Changing endpoint from public to authenticated breaks unauthenticated consumers.`);
66
+ }
67
+ }
68
+ // Changing HTTP method
69
+ if (segments.includes('method') && existingEndpoint && value && value.toUpperCase() !== existingEndpoint.method?.toUpperCase()) {
70
+ violations.push(`CANNOT_ALTER_HTTP_METHOD: Changing HTTP method from ${existingEndpoint.method} to ${value} breaks client calls.`);
71
+ }
72
+ if (violations.length > 0) {
73
+ return {
74
+ approved: false,
75
+ blastRadius: 'HIGH_DESTRUCTIVE',
76
+ ruleId: 'API_ALTER_DESTRUCTIVE',
77
+ message: violations[0],
78
+ violations,
79
+ };
80
+ }
81
+ }
82
+ // Rule 5: ADD operations
83
+ if (operation === 'ADD') {
84
+ // Adding a brand new endpoint
85
+ const isNewEndpoint = (segments.length <= 2 && !existingEndpoint) || (value?.path && value?.method);
86
+ if (isNewEndpoint) {
87
+ return {
88
+ approved: true,
89
+ blastRadius: 'LOW_ADDITIVE',
90
+ ruleId: 'API_ADD_ENDPOINT_APPROVED',
91
+ message: `Approved: Adding new endpoint '${value?.method || 'GET'} ${value?.path || endpointIdOrPath}' is backward-compatible.`,
92
+ violations: [],
93
+ };
94
+ }
95
+ // Adding request parameter/field
96
+ const isRequestMutation = segments.includes('request') || segments.includes('queryparams') || segments.includes('headers') || segments.includes('body');
97
+ if (isRequestMutation) {
98
+ // Check if required
99
+ const isRequired = value?.required === true ||
100
+ (typeof value === 'string' && value.toLowerCase().includes('required')) ||
101
+ (value?.nullable === false && value?.default === undefined);
102
+ if (isRequired) {
103
+ const msg = `CANNOT_ADD_REQUIRED_REQUEST_FIELD: Adding a required parameter or field to '${path}' causes existing client requests to fail with 400 Bad Request.`;
104
+ violations.push(msg);
105
+ return {
106
+ approved: false,
107
+ blastRadius: 'HIGH_DESTRUCTIVE',
108
+ ruleId: 'API_ADD_REQUIRED_FORBIDDEN',
109
+ message: msg,
110
+ violations,
111
+ };
112
+ }
113
+ return {
114
+ approved: true,
115
+ blastRadius: 'LOW_ADDITIVE',
116
+ ruleId: 'API_ADD_OPTIONAL_REQUEST_FIELD_APPROVED',
117
+ message: `Approved: Adding optional request field to '${path}'.`,
118
+ violations: [],
119
+ };
120
+ }
121
+ // Adding response field or response status code (Postel's Law)
122
+ const isResponseMutation = segments.includes('responses') || segments.includes('response') || segments.includes('body');
123
+ if (isResponseMutation) {
124
+ return {
125
+ approved: true,
126
+ blastRadius: 'LOW_ADDITIVE',
127
+ ruleId: 'API_ADD_RESPONSE_FIELD_APPROVED',
128
+ message: `Approved: Adding response property or status code to '${path}' (conforms to Postel's Law).`,
129
+ violations: [],
130
+ };
131
+ }
132
+ }
133
+ return {
134
+ approved: true,
135
+ blastRadius: 'LOW_ADDITIVE',
136
+ ruleId: 'API_DEFAULT_ADDITIVE',
137
+ message: `API operation '${operation}' on '${path}' approved as safe.`,
138
+ violations: [],
139
+ };
140
+ }
@@ -0,0 +1,2 @@
1
+ import { ContractPatch, RuleEvaluationResult } from '../types.js';
2
+ export declare function evaluateDbPatch(patch: ContractPatch, currentSchema: any): RuleEvaluationResult;
@@ -0,0 +1,180 @@
1
+ export function evaluateDbPatch(patch, currentSchema) {
2
+ const violations = [];
3
+ const { operation, path, value } = patch;
4
+ const normalizedPath = path.trim().toLowerCase();
5
+ // Helper: Find table by name
6
+ const tables = currentSchema?.tables || [];
7
+ // Parse path: e.g. "tables.users.columns.bio" or "users.columns.bio" or "users.columns"
8
+ const segments = normalizedPath.split('.').filter(Boolean);
9
+ const tableName = segments[0] === 'tables' ? segments[1] : segments[0];
10
+ const targetType = segments.includes('columns') ? 'column' : (segments.includes('indexes') ? 'index' : (segments.includes('foreignkeys') ? 'foreignKey' : 'table'));
11
+ const propertyName = segments.length > 2 ? segments[segments.length - 1] : undefined;
12
+ const existingTable = tables.find((t) => t.name?.toLowerCase() === tableName?.toLowerCase());
13
+ const existingColumn = existingTable?.columns?.find((c) => c.name?.toLowerCase() === propertyName?.toLowerCase());
14
+ // Rule 1: Path Collision & Existence Checks
15
+ if (operation === 'ADD') {
16
+ if (targetType === 'table' && existingTable) {
17
+ violations.push(`PATH_COLLISION: Table '${tableName}' already exists in .ai/db_schema.json. Use ALTER.`);
18
+ }
19
+ else if (targetType === 'column' && existingColumn) {
20
+ violations.push(`PATH_COLLISION: Column '${propertyName}' already exists on table '${tableName}'. Use ALTER.`);
21
+ }
22
+ }
23
+ else if (operation === 'ALTER' || operation === 'DROP') {
24
+ if (targetType === 'table' && !existingTable) {
25
+ violations.push(`TARGET_NOT_FOUND: Cannot ${operation.toLowerCase()} non-existent table '${tableName}'.`);
26
+ }
27
+ else if (targetType === 'column' && !existingColumn) {
28
+ violations.push(`TARGET_NOT_FOUND: Cannot ${operation.toLowerCase()} non-existent column '${propertyName}' on table '${tableName}'.`);
29
+ }
30
+ }
31
+ // If path collision or missing target detected, exit immediately with High Destructive
32
+ if (violations.length > 0) {
33
+ return {
34
+ approved: false,
35
+ blastRadius: 'HIGH_DESTRUCTIVE',
36
+ ruleId: 'DB_COLLISION_OR_TARGET_MISMATCH',
37
+ message: violations[0],
38
+ violations,
39
+ };
40
+ }
41
+ // Rule 2: DROP operations are always HIGH_DESTRUCTIVE
42
+ if (operation === 'DROP') {
43
+ const msg = targetType === 'table'
44
+ ? `CANNOT_DROP_TABLE: Dropping table '${tableName}' results in permanent data loss and catastrophic runtime failures.`
45
+ : `CANNOT_DROP_COLUMN: Dropping column '${propertyName}' on table '${tableName}' causes data loss and breaks existing queries.`;
46
+ violations.push(msg);
47
+ return {
48
+ approved: false,
49
+ blastRadius: 'HIGH_DESTRUCTIVE',
50
+ ruleId: 'DB_DROP_FORBIDDEN',
51
+ message: msg,
52
+ violations,
53
+ };
54
+ }
55
+ // Rule 3: RENAME operations are HIGH_DESTRUCTIVE
56
+ if (operation === 'RENAME') {
57
+ const msg = `CANNOT_RENAME_${targetType.toUpperCase()}: Renaming '${propertyName || tableName}' breaks existing queries and ORM mappings.`;
58
+ violations.push(msg);
59
+ return {
60
+ approved: false,
61
+ blastRadius: 'HIGH_DESTRUCTIVE',
62
+ ruleId: 'DB_RENAME_FORBIDDEN',
63
+ message: msg,
64
+ violations,
65
+ };
66
+ }
67
+ // Rule 4: ALTER operations
68
+ if (operation === 'ALTER') {
69
+ if (targetType === 'column') {
70
+ // Nullability tightening: nullable -> NOT NULL without default
71
+ if (existingColumn && existingColumn.nullable !== false && value?.nullable === false && value?.default === undefined && existingColumn.default === undefined) {
72
+ violations.push(`CANNOT_ENFORCE_NOT_NULL: Changing column '${propertyName}' to NOT NULL without a default value will cause runtime failures on existing rows.`);
73
+ }
74
+ // Column type change (narrowing or altering)
75
+ if (existingColumn && value?.type && value.type.toUpperCase() !== existingColumn.type?.toUpperCase()) {
76
+ violations.push(`CANNOT_MUTATE_COLUMN_TYPE: Altering column type from '${existingColumn.type}' to '${value.type}' carries data corruption or truncation risk.`);
77
+ }
78
+ if (violations.length > 0) {
79
+ return {
80
+ approved: false,
81
+ blastRadius: 'HIGH_DESTRUCTIVE',
82
+ ruleId: 'DB_ALTER_DESTRUCTIVE',
83
+ message: violations[0],
84
+ violations,
85
+ };
86
+ }
87
+ }
88
+ }
89
+ // Rule 5: ADD operations
90
+ if (operation === 'ADD') {
91
+ // ADD TABLE
92
+ if (targetType === 'table') {
93
+ return {
94
+ approved: true,
95
+ blastRadius: 'LOW_ADDITIVE',
96
+ ruleId: 'DB_ADD_TABLE_APPROVED',
97
+ message: `Approved: Creating new table '${value?.name || tableName}' is backward-compatible.`,
98
+ violations: [],
99
+ };
100
+ }
101
+ // ADD COLUMN
102
+ if (targetType === 'column') {
103
+ const isNullable = value?.nullable !== false;
104
+ const hasDefault = value?.default !== undefined && value?.default !== null;
105
+ if (!isNullable && !hasDefault) {
106
+ const msg = `CANNOT_ADD_REQUIRED_COLUMN_WITHOUT_DEFAULT: Column '${value?.name || propertyName}' cannot be NOT NULL without a default value.`;
107
+ violations.push(msg);
108
+ return {
109
+ approved: false,
110
+ blastRadius: 'HIGH_DESTRUCTIVE',
111
+ ruleId: 'DB_ADD_NOT_NULL_FORBIDDEN',
112
+ message: msg,
113
+ violations,
114
+ };
115
+ }
116
+ return {
117
+ approved: true,
118
+ blastRadius: 'LOW_ADDITIVE',
119
+ ruleId: 'DB_ADD_COLUMN_APPROVED',
120
+ message: `Approved: Adding additive column '${value?.name || propertyName}' to table '${tableName}'.`,
121
+ violations: [],
122
+ };
123
+ }
124
+ // ADD FOREIGN KEY (Referential integrity DAG check)
125
+ if (targetType === 'foreignKey') {
126
+ const targetTable = value?.targetTable || value?.referenceTable;
127
+ const targetCol = value?.targetColumn || value?.referenceColumn;
128
+ const refTableExists = tables.some((t) => t.name?.toLowerCase() === targetTable?.toLowerCase());
129
+ if (!refTableExists) {
130
+ const msg = `ORPHANED_FOREIGN_KEY: Target table '${targetTable}' does not exist in .ai/db_schema.json.`;
131
+ violations.push(msg);
132
+ return {
133
+ approved: false,
134
+ blastRadius: 'HIGH_DESTRUCTIVE',
135
+ ruleId: 'DB_ORPHANED_FK_FORBIDDEN',
136
+ message: msg,
137
+ violations,
138
+ };
139
+ }
140
+ // Check circular dependency: does targetTable have an existing FK back to this tableName?
141
+ const targetTableObj = tables.find((t) => t.name?.toLowerCase() === targetTable?.toLowerCase());
142
+ const hasCircularFk = targetTableObj?.foreignKeys?.some((fk) => (fk.targetTable?.toLowerCase() === tableName?.toLowerCase() || fk.referenceTable?.toLowerCase() === tableName?.toLowerCase()));
143
+ if (hasCircularFk && value?.nullable === false) {
144
+ const msg = `CIRCULAR_DEPENDENCY: Circular non-nullable foreign key between '${tableName}' and '${targetTable}'.`;
145
+ violations.push(msg);
146
+ return {
147
+ approved: false,
148
+ blastRadius: 'HIGH_DESTRUCTIVE',
149
+ ruleId: 'DB_CIRCULAR_FK_FORBIDDEN',
150
+ message: msg,
151
+ violations,
152
+ };
153
+ }
154
+ return {
155
+ approved: true,
156
+ blastRadius: 'LOW_ADDITIVE',
157
+ ruleId: 'DB_ADD_FOREIGN_KEY_APPROVED',
158
+ message: `Approved: Foreign key on '${tableName}' referencing '${targetTable}' verified.`,
159
+ violations: [],
160
+ };
161
+ }
162
+ // ADD INDEX
163
+ if (targetType === 'index') {
164
+ return {
165
+ approved: true,
166
+ blastRadius: 'LOW_ADDITIVE',
167
+ ruleId: 'DB_ADD_INDEX_APPROVED',
168
+ message: `Approved: Adding index to table '${tableName}'.`,
169
+ violations: [],
170
+ };
171
+ }
172
+ }
173
+ return {
174
+ approved: true,
175
+ blastRadius: 'LOW_ADDITIVE',
176
+ ruleId: 'DB_DEFAULT_ADDITIVE',
177
+ message: `Operation '${operation}' on '${path}' approved as safe.`,
178
+ violations: [],
179
+ };
180
+ }
@@ -0,0 +1,33 @@
1
+ import type { EscalationFile, EscalationRecord } from '../scanner/types.js';
2
+ /**
3
+ * Shared on-disk state of the governor: `.ai/.governor_ledger.json` and `.ai/escalation.json`.
4
+ * Parallel agents in separate worktrees share one `.ai/`, so every read-modify-write here holds
5
+ * a file lock and replaces the file atomically.
6
+ */
7
+ export interface LedgerEntry {
8
+ consecutiveFailures: number;
9
+ lastFailureTime: string;
10
+ lastRuleId?: string;
11
+ lastViolation?: string;
12
+ patchCount: number;
13
+ /** Commit recorded at `nativ task start`; the test-integrity guard diffs against it. */
14
+ baselineRef?: string;
15
+ baselineRecordedAt?: string;
16
+ }
17
+ export type TaskFailureLedger = Record<string, LedgerEntry>;
18
+ export declare function ledgerPath(targetDir: string): string;
19
+ export declare function escalationPath(targetDir: string): string;
20
+ export declare function loadLedger(targetDir: string): TaskFailureLedger;
21
+ /** Locked read-modify-write of the ledger; the mutator's return value is passed through. */
22
+ export declare function mutateLedger<T>(targetDir: string, mutator: (ledger: TaskFailureLedger) => T): T;
23
+ export declare function loadEscalationFile(targetDir: string, projectName: string): EscalationFile;
24
+ export type EscalationIdStyle = 'sequential' | 'unique';
25
+ /**
26
+ * Appends an escalation under the file lock. The id is assigned inside the lock, so concurrent
27
+ * writers can neither collide nor overwrite each other's records. Returns the id.
28
+ */
29
+ export declare function appendEscalation(targetDir: string, build: (id: string) => EscalationRecord, options: {
30
+ style: EscalationIdStyle;
31
+ position?: 'first' | 'last';
32
+ projectName?: string;
33
+ }): string;
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { withFileLockSync, writeJsonAtomicSync } from '../core/lock-manager.js';
5
+ export function ledgerPath(targetDir) {
6
+ return path.join(targetDir, '.ai', '.governor_ledger.json');
7
+ }
8
+ export function escalationPath(targetDir) {
9
+ return path.join(targetDir, '.ai', 'escalation.json');
10
+ }
11
+ function readJson(file, fallback) {
12
+ try {
13
+ const raw = fs.readFileSync(file, 'utf8');
14
+ // The lock helpers create an empty placeholder before the first write.
15
+ return raw.trim() ? JSON.parse(raw) : fallback;
16
+ }
17
+ catch {
18
+ return fallback;
19
+ }
20
+ }
21
+ export function loadLedger(targetDir) {
22
+ return readJson(ledgerPath(targetDir), {});
23
+ }
24
+ /** Locked read-modify-write of the ledger; the mutator's return value is passed through. */
25
+ export function mutateLedger(targetDir, mutator) {
26
+ const file = ledgerPath(targetDir);
27
+ return withFileLockSync(file, () => {
28
+ const ledger = readJson(file, {});
29
+ const result = mutator(ledger);
30
+ writeJsonAtomicSync(file, ledger);
31
+ return result;
32
+ });
33
+ }
34
+ export function loadEscalationFile(targetDir, projectName) {
35
+ const empty = {
36
+ $schema: 'http://json-schema.org/draft-07/schema#',
37
+ version: '1.0.0',
38
+ projectName,
39
+ lastUpdated: new Date().toISOString(),
40
+ escalations: [],
41
+ };
42
+ const file = readJson(escalationPath(targetDir), empty);
43
+ if (!Array.isArray(file.escalations))
44
+ file.escalations = [];
45
+ return file;
46
+ }
47
+ /**
48
+ * Picks an id no existing record uses. `sequential` keeps the human-friendly `esc-NN` of
49
+ * `nativ task escalate`; `unique` (circuit-breaker trips) adds random bits to a timestamp so two
50
+ * trips in the same millisecond still get different ids.
51
+ */
52
+ function nextEscalationId(existing, style) {
53
+ if (style === 'sequential') {
54
+ const numbers = [...existing].map((id) => /^esc-(\d+)$/.exec(id)?.[1]).filter(Boolean).map(Number);
55
+ let n = numbers.length ? Math.max(...numbers) + 1 : 1;
56
+ while (existing.has(`esc-${String(n).padStart(2, '0')}`))
57
+ n++;
58
+ return `esc-${String(n).padStart(2, '0')}`;
59
+ }
60
+ for (;;) {
61
+ const id = `esc-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
62
+ if (!existing.has(id))
63
+ return id;
64
+ }
65
+ }
66
+ /**
67
+ * Appends an escalation under the file lock. The id is assigned inside the lock, so concurrent
68
+ * writers can neither collide nor overwrite each other's records. Returns the id.
69
+ */
70
+ export function appendEscalation(targetDir, build, options) {
71
+ const file = escalationPath(targetDir);
72
+ return withFileLockSync(file, () => {
73
+ const data = loadEscalationFile(targetDir, options.projectName ?? path.basename(targetDir));
74
+ const id = nextEscalationId(new Set(data.escalations.map((e) => e?.id).filter(Boolean)), options.style);
75
+ const record = build(id);
76
+ if (options.position === 'last')
77
+ data.escalations.push(record);
78
+ else
79
+ data.escalations.unshift(record);
80
+ data.lastUpdated = new Date().toISOString();
81
+ writeJsonAtomicSync(file, data);
82
+ return id;
83
+ });
84
+ }
@@ -0,0 +1,64 @@
1
+ export type BlastRadius = 'LOW_ADDITIVE' | 'HIGH_DESTRUCTIVE';
2
+ export type PatchOperation = 'ADD' | 'ALTER' | 'DROP' | 'RENAME';
3
+ export type ContractTarget = 'db_schema' | 'api_contracts';
4
+ export interface ContractPatch {
5
+ taskId: string;
6
+ target: ContractTarget;
7
+ operation: PatchOperation;
8
+ path: string;
9
+ value?: any;
10
+ before?: any;
11
+ reason: string;
12
+ baseHash?: string;
13
+ }
14
+ export interface AuditLogEntry {
15
+ timestamp: string;
16
+ taskId: string;
17
+ target: ContractTarget;
18
+ operation: PatchOperation;
19
+ path: string;
20
+ blastRadius: BlastRadius;
21
+ ruleId: string;
22
+ reason: string;
23
+ applied: boolean;
24
+ }
25
+ export interface CircuitBreakerState {
26
+ active: boolean;
27
+ consecutiveFailures: number;
28
+ maxThreshold: number;
29
+ tripped: boolean;
30
+ }
31
+ export interface EscalationDiagnosticBundle {
32
+ escalationId: string;
33
+ taskId: string;
34
+ circuitBreakerTripped: boolean;
35
+ consecutiveFailures: number;
36
+ intent: string;
37
+ invariantCollision: string;
38
+ proposedPatch: {
39
+ target: ContractTarget;
40
+ operation: PatchOperation;
41
+ path: string;
42
+ value?: any;
43
+ reason: string;
44
+ };
45
+ recommendedActions: string[];
46
+ }
47
+ export interface GovernorVerdict {
48
+ approved: boolean;
49
+ blastRadius: BlastRadius;
50
+ ruleId: string;
51
+ message: string;
52
+ violations: string[];
53
+ patchApplied: boolean;
54
+ circuitBreaker: CircuitBreakerState;
55
+ auditEntry?: AuditLogEntry;
56
+ diagnosticBundle?: EscalationDiagnosticBundle;
57
+ }
58
+ export interface RuleEvaluationResult {
59
+ approved: boolean;
60
+ blastRadius: BlastRadius;
61
+ ruleId: string;
62
+ message: string;
63
+ violations: string[];
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createProgram(): Command;
3
+ export declare function run(): Promise<void>;