@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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +334 -0
- package/bin/cli.js +31 -0
- package/dist/commands/bench.d.ts +8 -0
- package/dist/commands/bench.js +52 -0
- package/dist/commands/db.d.ts +22 -0
- package/dist/commands/db.js +415 -0
- package/dist/commands/doctor.d.ts +38 -0
- package/dist/commands/doctor.js +169 -0
- package/dist/commands/hook.d.ts +31 -0
- package/dist/commands/hook.js +105 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +144 -0
- package/dist/commands/mcp.d.ts +5 -0
- package/dist/commands/mcp.js +16 -0
- package/dist/commands/setup.d.ts +17 -0
- package/dist/commands/setup.js +51 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +75 -0
- package/dist/commands/task.d.ts +83 -0
- package/dist/commands/task.js +807 -0
- package/dist/commands/test-gen.d.ts +14 -0
- package/dist/commands/test-gen.js +52 -0
- package/dist/commands/triage.d.ts +70 -0
- package/dist/commands/triage.js +343 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +112 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +89 -0
- package/dist/commands/verify.d.ts +8 -0
- package/dist/commands/verify.js +65 -0
- package/dist/commands/worktree.d.ts +47 -0
- package/dist/commands/worktree.js +396 -0
- package/dist/core/benchmark.d.ts +57 -0
- package/dist/core/benchmark.js +772 -0
- package/dist/core/enforcement.d.ts +80 -0
- package/dist/core/enforcement.js +271 -0
- package/dist/core/legacy-templates.d.ts +24 -0
- package/dist/core/legacy-templates.js +60 -0
- package/dist/core/lock-manager.d.ts +54 -0
- package/dist/core/lock-manager.js +260 -0
- package/dist/core/root-resolver.d.ts +32 -0
- package/dist/core/root-resolver.js +149 -0
- package/dist/core/setup-assets.d.ts +123 -0
- package/dist/core/setup-assets.js +438 -0
- package/dist/core/telemetry.d.ts +156 -0
- package/dist/core/telemetry.js +416 -0
- package/dist/core/test-generator-types.d.ts +227 -0
- package/dist/core/test-generator-types.js +179 -0
- package/dist/core/test-generator.d.ts +34 -0
- package/dist/core/test-generator.js +1690 -0
- package/dist/core/tier1-liaison.d.ts +139 -0
- package/dist/core/tier1-liaison.js +320 -0
- package/dist/core/verifier.d.ts +52 -0
- package/dist/core/verifier.js +249 -0
- package/dist/core/version.d.ts +5 -0
- package/dist/core/version.js +14 -0
- package/dist/core/worktree-merge.d.ts +30 -0
- package/dist/core/worktree-merge.js +98 -0
- package/dist/db/data-engine.d.ts +46 -0
- package/dist/db/data-engine.js +802 -0
- package/dist/db/diff.d.ts +39 -0
- package/dist/db/diff.js +420 -0
- package/dist/db/env-parser.d.ts +86 -0
- package/dist/db/env-parser.js +549 -0
- package/dist/db/introspector.d.ts +21 -0
- package/dist/db/introspector.js +835 -0
- package/dist/db/types.d.ts +217 -0
- package/dist/db/types.js +9 -0
- package/dist/governor/circuit-breaker.d.ts +100 -0
- package/dist/governor/circuit-breaker.js +231 -0
- package/dist/governor/evaluator.d.ts +67 -0
- package/dist/governor/evaluator.js +851 -0
- package/dist/governor/index.d.ts +6 -0
- package/dist/governor/index.js +6 -0
- package/dist/governor/rules/api-rules.d.ts +2 -0
- package/dist/governor/rules/api-rules.js +140 -0
- package/dist/governor/rules/db-rules.d.ts +2 -0
- package/dist/governor/rules/db-rules.js +180 -0
- package/dist/governor/store.d.ts +33 -0
- package/dist/governor/store.js +84 -0
- package/dist/governor/types.d.ts +64 -0
- package/dist/governor/types.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +358 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +309 -0
- package/dist/providers/claude-api.d.ts +5 -0
- package/dist/providers/claude-api.js +75 -0
- package/dist/providers/claude-cli.d.ts +13 -0
- package/dist/providers/claude-cli.js +134 -0
- package/dist/providers/gemini.d.ts +8 -0
- package/dist/providers/gemini.js +78 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +5 -0
- package/dist/providers/registry.d.ts +38 -0
- package/dist/providers/registry.js +121 -0
- package/dist/providers/types.d.ts +74 -0
- package/dist/providers/types.js +25 -0
- package/dist/runner/agent-supervisor.d.ts +363 -0
- package/dist/runner/agent-supervisor.js +1672 -0
- package/dist/scanner/context-builder.d.ts +2 -0
- package/dist/scanner/context-builder.js +29 -0
- package/dist/scanner/detector.d.ts +2 -0
- package/dist/scanner/detector.js +760 -0
- package/dist/scanner/types.d.ts +80 -0
- package/dist/scanner/types.js +50 -0
- package/dist/server/studio-server.d.ts +39 -0
- package/dist/server/studio-server.js +1931 -0
- package/dist/server/studio-ui.d.ts +15 -0
- package/dist/server/studio-ui.js +5321 -0
- package/package.json +70 -0
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/.mcp.json +10 -0
- package/plugin/agents/architect.md +28 -0
- package/plugin/agents/verifier.md +28 -0
- package/plugin/agents/worker.md +26 -0
- package/plugin/hooks/hooks.json +28 -0
- package/plugin/skills/nativ/SKILL.md +75 -0
- package/templates/.claudeignore +45 -0
- package/templates/AGENTS.md +67 -0
- package/templates/CLAUDE.md +32 -0
- package/templates/GEMINI.md +66 -0
- package/templates/claude-agents/architect.md +28 -0
- package/templates/claude-agents/verifier.md +28 -0
- package/templates/claude-agents/worker.md +26 -0
- package/templates/dot-ai/api_contracts.json +66 -0
- package/templates/dot-ai/context.md +38 -0
- package/templates/dot-ai/db_schema.json +39 -0
- package/templates/dot-ai/master_plan.json +28 -0
- package/templates/dot-ai/subagents/backend.md +11 -0
- package/templates/dot-ai/subagents/database.md +10 -0
- package/templates/dot-ai/subagents/db-migration.md +11 -0
- package/templates/dot-ai/subagents/devops-agent.md +11 -0
- package/templates/dot-ai/subagents/flutter-developer.md +11 -0
- package/templates/dot-ai/subagents/frontend.md +126 -0
- package/templates/dot-ai/subagents/qa-tester.md +13 -0
- package/templates/dot-ai/subagents/security-auditor.md +11 -0
- package/templates/dot-ai/ui_specs.md +78 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database telemetry & schema types.
|
|
3
|
+
* Shapes conform to .ai/api_contracts.json (status/schema/diff/connect/export-contract)
|
|
4
|
+
* and the table model in .ai/db_schema.json.
|
|
5
|
+
*/
|
|
6
|
+
export type SqlEngine = 'postgresql' | 'mysql' | 'sqlite';
|
|
7
|
+
export type NoSqlEngine = 'mongodb' | 'firestore';
|
|
8
|
+
export type DatabaseEngine = SqlEngine | NoSqlEngine;
|
|
9
|
+
export declare const NOSQL_ENGINES: readonly NoSqlEngine[];
|
|
10
|
+
export declare function isNoSqlEngine(engine: string): engine is NoSqlEngine;
|
|
11
|
+
/** SQL engines expose tables; document stores expose collections. */
|
|
12
|
+
export type EntityType = 'table' | 'collection';
|
|
13
|
+
export type DatabaseEnv = 'dev' | 'prod';
|
|
14
|
+
/** Per-environment telemetry returned by GET /api/status. */
|
|
15
|
+
export interface DatabaseStatus {
|
|
16
|
+
connected: boolean;
|
|
17
|
+
engine: DatabaseEngine;
|
|
18
|
+
database: string;
|
|
19
|
+
maskedUrl: string;
|
|
20
|
+
pingMs: number;
|
|
21
|
+
/** Number of tables or collections (contract field). */
|
|
22
|
+
entityCount?: number;
|
|
23
|
+
entityType?: EntityType;
|
|
24
|
+
/** @deprecated Legacy alias of entityCount, kept for existing CLI/studio consumers. */
|
|
25
|
+
tableCount: number;
|
|
26
|
+
error: string | null;
|
|
27
|
+
sourceKey?: string;
|
|
28
|
+
detectedFromExample?: boolean;
|
|
29
|
+
exampleFile?: string | null;
|
|
30
|
+
suggestion?: string | null;
|
|
31
|
+
/** True when the URL was assembled from fragmented DB_HOST/DB_DATABASE-style keys. */
|
|
32
|
+
synthesized?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Engines that can be assembled from fragmented host/port/user/password/database keys. */
|
|
35
|
+
export type ComponentEngine = Extract<SqlEngine, 'mysql' | 'postgresql'>;
|
|
36
|
+
/**
|
|
37
|
+
* Discrete connection parameters (POST /api/connect `components`, XAMPP/Laravel-style .env keys).
|
|
38
|
+
* Contains the raw password: never log, print, or serialize this object.
|
|
39
|
+
*/
|
|
40
|
+
export interface ConnectionComponents {
|
|
41
|
+
engine: ComponentEngine;
|
|
42
|
+
host: string;
|
|
43
|
+
port?: number;
|
|
44
|
+
user?: string;
|
|
45
|
+
password?: string;
|
|
46
|
+
database?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Display-safe description of a fragmented config (GET /api/env-info `fragmentedConfig`). No secrets. */
|
|
49
|
+
export interface FragmentedConfig {
|
|
50
|
+
env: DatabaseEnv;
|
|
51
|
+
engine: ComponentEngine;
|
|
52
|
+
host: string;
|
|
53
|
+
port: number;
|
|
54
|
+
user: string | null;
|
|
55
|
+
database: string | null;
|
|
56
|
+
hasPassword: boolean;
|
|
57
|
+
/** Env keys the components were read from, e.g. ['DB_CONNECTION', 'DB_HOST', 'DB_DATABASE']. */
|
|
58
|
+
sourceKeys: string[];
|
|
59
|
+
maskedUrl: string;
|
|
60
|
+
}
|
|
61
|
+
export interface DetectedTemplateKey {
|
|
62
|
+
key: string;
|
|
63
|
+
targetEnv: DatabaseEnv;
|
|
64
|
+
engine: DatabaseEngine | null;
|
|
65
|
+
exampleValue?: string;
|
|
66
|
+
isConfiguredInEnv: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface TemplateDetectionResult {
|
|
69
|
+
templateFound: boolean;
|
|
70
|
+
templateFile: string | null;
|
|
71
|
+
detectedKeys: DetectedTemplateKey[];
|
|
72
|
+
missingKeys: DetectedTemplateKey[];
|
|
73
|
+
/** True when DB_HOST/DB_DATABASE-style component keys are configured and can be synthesized into a URL. */
|
|
74
|
+
fragmentedDetected?: boolean;
|
|
75
|
+
fragmentedConfig?: FragmentedConfig | null;
|
|
76
|
+
}
|
|
77
|
+
export interface StatusResponse {
|
|
78
|
+
dev: DatabaseStatus;
|
|
79
|
+
prod: DatabaseStatus;
|
|
80
|
+
}
|
|
81
|
+
/** Where a detected image field's value lives. */
|
|
82
|
+
export type MediaSource = 'url' | 'gcs' | 'base64' | 'bytes';
|
|
83
|
+
/**
|
|
84
|
+
* Metadata about a detected image/media field, taken from sampled documents.
|
|
85
|
+
* Raw binary never appears here: bytes/base64 fields only report MIME type, size,
|
|
86
|
+
* dimensions and a short truncated prefix. URLs have credential-like query params redacted.
|
|
87
|
+
*/
|
|
88
|
+
export interface MediaFieldInfo {
|
|
89
|
+
source: MediaSource;
|
|
90
|
+
mimeType: string | null;
|
|
91
|
+
sizeBytes: number | null;
|
|
92
|
+
width: number | null;
|
|
93
|
+
height: number | null;
|
|
94
|
+
/** Redacted http(s) URL or gs:// URI of one sampled value, for thumbnail preview. */
|
|
95
|
+
sampleUrl: string | null;
|
|
96
|
+
/** First few characters of a base64 value (display only, never decodable into the image). */
|
|
97
|
+
base64Head: string | null;
|
|
98
|
+
}
|
|
99
|
+
export interface ColumnSchema {
|
|
100
|
+
name: string;
|
|
101
|
+
/** SQL type (VARCHAR(255), UUID…) or NoSQL type (STRING, NUMBER, MAP, ARRAY, OBJECTID, DOCUMENT_ID, BYTES, IMAGE_URL…). */
|
|
102
|
+
type: string;
|
|
103
|
+
primaryKey: boolean;
|
|
104
|
+
nullable: boolean;
|
|
105
|
+
default: string | null;
|
|
106
|
+
/** True when the field holds images (URL, gs:// URI, Base64 data URI, or Bytes). */
|
|
107
|
+
isMedia?: boolean;
|
|
108
|
+
/** True when this entry is a Firestore nested subcollection rather than a document field. */
|
|
109
|
+
isSubcollection?: boolean;
|
|
110
|
+
media?: MediaFieldInfo;
|
|
111
|
+
/** NoSQL only: fraction (0–1) of sampled documents that contain this field. */
|
|
112
|
+
presence?: number;
|
|
113
|
+
/** NoSQL only: all distinct types observed across samples when a field is polymorphic. */
|
|
114
|
+
observedTypes?: string[];
|
|
115
|
+
}
|
|
116
|
+
export interface IndexSchema {
|
|
117
|
+
name: string;
|
|
118
|
+
columns: string[];
|
|
119
|
+
unique: boolean;
|
|
120
|
+
}
|
|
121
|
+
export interface ForeignKeySchema {
|
|
122
|
+
name: string;
|
|
123
|
+
column: string;
|
|
124
|
+
referencedTable: string;
|
|
125
|
+
referencedColumn: string;
|
|
126
|
+
onDelete?: string;
|
|
127
|
+
}
|
|
128
|
+
export interface TableSchema {
|
|
129
|
+
name: string;
|
|
130
|
+
/** 'collection' for MongoDB/Firestore; absent or 'table' for SQL engines. */
|
|
131
|
+
entityType?: EntityType;
|
|
132
|
+
description?: string;
|
|
133
|
+
/** For collections, `columns` are the document fields inferred from sampled documents. */
|
|
134
|
+
columns: ColumnSchema[];
|
|
135
|
+
indexes: IndexSchema[];
|
|
136
|
+
foreignKeys: ForeignKeySchema[];
|
|
137
|
+
/** NoSQL only: estimated number of documents in the collection. */
|
|
138
|
+
documentCount?: number;
|
|
139
|
+
/** NoSQL only: number of documents sampled (≤ 50) to infer fields. */
|
|
140
|
+
sampledDocuments?: number;
|
|
141
|
+
}
|
|
142
|
+
/** GET /api/schema response. */
|
|
143
|
+
export interface SchemaResponse {
|
|
144
|
+
devTables: TableSchema[];
|
|
145
|
+
prodTables: TableSchema[];
|
|
146
|
+
}
|
|
147
|
+
/** A column present on both sides whose definition differs. */
|
|
148
|
+
export interface ColumnChange {
|
|
149
|
+
name: string;
|
|
150
|
+
before: ColumnSchema;
|
|
151
|
+
after: ColumnSchema;
|
|
152
|
+
changedFields: Array<keyof Omit<ColumnSchema, 'name'>>;
|
|
153
|
+
}
|
|
154
|
+
export interface TableDiff {
|
|
155
|
+
name: string;
|
|
156
|
+
addedColumns: ColumnSchema[];
|
|
157
|
+
droppedColumns: ColumnSchema[];
|
|
158
|
+
alteredColumns: ColumnChange[];
|
|
159
|
+
addedIndexes: IndexSchema[];
|
|
160
|
+
droppedIndexes: IndexSchema[];
|
|
161
|
+
addedForeignKeys: ForeignKeySchema[];
|
|
162
|
+
droppedForeignKeys: ForeignKeySchema[];
|
|
163
|
+
isDestructive: boolean;
|
|
164
|
+
}
|
|
165
|
+
export interface SchemaDiffSummary {
|
|
166
|
+
addedTablesCount: number;
|
|
167
|
+
alteredTablesCount: number;
|
|
168
|
+
droppedTablesCount: number;
|
|
169
|
+
unchangedTablesCount: number;
|
|
170
|
+
hasDestructiveChanges: boolean;
|
|
171
|
+
}
|
|
172
|
+
/** GET /api/diff response. */
|
|
173
|
+
export interface SchemaDiff {
|
|
174
|
+
summary: SchemaDiffSummary;
|
|
175
|
+
addedTables: TableSchema[];
|
|
176
|
+
droppedTables: TableSchema[];
|
|
177
|
+
alteredTables: TableDiff[];
|
|
178
|
+
unchangedTables: string[];
|
|
179
|
+
}
|
|
180
|
+
export type DiffTarget = 'prod' | 'contract';
|
|
181
|
+
/** Firestore connection settings (credentials come from Application Default Credentials, never from here). */
|
|
182
|
+
export interface FirestoreConfig {
|
|
183
|
+
projectId: string;
|
|
184
|
+
/** host:port of a local Firestore emulator, e.g. "localhost:8080". */
|
|
185
|
+
emulatorHost?: string;
|
|
186
|
+
/** Firestore database id for multi-database projects (defaults to "(default)"). */
|
|
187
|
+
databaseId?: string;
|
|
188
|
+
}
|
|
189
|
+
/** POST /api/connect */
|
|
190
|
+
export interface ConnectRequest {
|
|
191
|
+
env: DatabaseEnv;
|
|
192
|
+
engine?: DatabaseEngine;
|
|
193
|
+
/** Required for SQL engines and MongoDB; optional for Firestore when firestoreConfig is given. */
|
|
194
|
+
connectionUrl?: string;
|
|
195
|
+
firestoreConfig?: FirestoreConfig;
|
|
196
|
+
}
|
|
197
|
+
export interface ConnectResponse {
|
|
198
|
+
success: boolean;
|
|
199
|
+
pingMs: number;
|
|
200
|
+
engine: string;
|
|
201
|
+
entityCount?: number;
|
|
202
|
+
/** @deprecated Legacy alias of entityCount. */
|
|
203
|
+
tableCount: number;
|
|
204
|
+
maskedUrl: string;
|
|
205
|
+
error: string | null;
|
|
206
|
+
}
|
|
207
|
+
/** POST /api/export-contract */
|
|
208
|
+
export interface ExportContractRequest {
|
|
209
|
+
sourceEnv: DatabaseEnv;
|
|
210
|
+
}
|
|
211
|
+
export interface ExportContractResponse {
|
|
212
|
+
success: boolean;
|
|
213
|
+
filePath: string;
|
|
214
|
+
exportedEntitiesCount?: number;
|
|
215
|
+
/** @deprecated Legacy alias of exportedEntitiesCount. */
|
|
216
|
+
exportedTablesCount: number;
|
|
217
|
+
}
|
package/dist/db/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database telemetry & schema types.
|
|
3
|
+
* Shapes conform to .ai/api_contracts.json (status/schema/diff/connect/export-contract)
|
|
4
|
+
* and the table model in .ai/db_schema.json.
|
|
5
|
+
*/
|
|
6
|
+
export const NOSQL_ENGINES = ['mongodb', 'firestore'];
|
|
7
|
+
export function isNoSqlEngine(engine) {
|
|
8
|
+
return NOSQL_ENGINES.includes(engine);
|
|
9
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { BlastRadius, CircuitBreakerState, ContractPatch, EscalationDiagnosticBundle, RuleEvaluationResult } from './types.js';
|
|
2
|
+
import { EscalationRecord, EscalationType } from '../scanner/types.js';
|
|
3
|
+
/** A contract patch as proposed, without the task binding. */
|
|
4
|
+
export type CandidatePatch = Omit<ContractPatch, 'taskId'>;
|
|
5
|
+
export interface ProofCheck {
|
|
6
|
+
name: string;
|
|
7
|
+
passed: boolean;
|
|
8
|
+
detail: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Evidence that a proposal was exercised away from the live `.ai/` contracts
|
|
12
|
+
* before a human is asked to approve it.
|
|
13
|
+
*/
|
|
14
|
+
export interface VerificationProof {
|
|
15
|
+
isolated: true;
|
|
16
|
+
method: 'sandbox_governor_replay' | 'baseline_blob_check';
|
|
17
|
+
passed: boolean;
|
|
18
|
+
checks: ProofCheck[];
|
|
19
|
+
/** Hash of the live contract the candidate was replayed against. */
|
|
20
|
+
baseContractHash?: string | null;
|
|
21
|
+
/** Hash of the sandbox contract after the candidate was applied. */
|
|
22
|
+
candidateContractHash?: string | null;
|
|
23
|
+
verdict?: {
|
|
24
|
+
approved: boolean;
|
|
25
|
+
blastRadius: BlastRadius;
|
|
26
|
+
ruleId: string;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
verifiedAt: string;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
}
|
|
32
|
+
export type SelfHealingStrategy = 'relax_to_nullable' | 'relax_to_optional' | 'deprecate_instead_of_drop' | 'expand_contract_rename' | 'shadow_column_for_type_change' | 'alter_existing_instead_of_add' | 'add_missing_target' | 'rebase_on_current_contract' | 'budget_override' | 'restore_test_suite';
|
|
33
|
+
interface ProposalBase {
|
|
34
|
+
proposalId: string;
|
|
35
|
+
strategy: SelfHealingStrategy;
|
|
36
|
+
rationale: string;
|
|
37
|
+
/** Proposals are never applied automatically; Tier 1 or the operator approves them. */
|
|
38
|
+
requiresHumanApproval: true;
|
|
39
|
+
candidatesEvaluated: number;
|
|
40
|
+
verificationProof: VerificationProof;
|
|
41
|
+
generatedAt: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ContractPatchProposal extends ProposalBase {
|
|
44
|
+
kind: 'contract_patch';
|
|
45
|
+
candidate: CandidatePatch;
|
|
46
|
+
original: CandidatePatch;
|
|
47
|
+
}
|
|
48
|
+
export interface TestRestorationProposal extends ProposalBase {
|
|
49
|
+
kind: 'restore_tests';
|
|
50
|
+
baselineRef: string;
|
|
51
|
+
files: string[];
|
|
52
|
+
commands: string[];
|
|
53
|
+
}
|
|
54
|
+
export type SelfHealingProposal = ContractPatchProposal | TestRestorationProposal;
|
|
55
|
+
/** `.ai/escalation.json` record carrying the optional self-healing proposal. */
|
|
56
|
+
export interface SelfHealingEscalationRecord extends EscalationRecord {
|
|
57
|
+
proposedPatch?: SelfHealingProposal;
|
|
58
|
+
}
|
|
59
|
+
export type ProposalFactory = () => SelfHealingProposal | null;
|
|
60
|
+
export interface GuardViolationContext {
|
|
61
|
+
escalationType: EscalationType;
|
|
62
|
+
/** Files or contracts the violation touches, listed on the escalation record. */
|
|
63
|
+
affected: string[];
|
|
64
|
+
intent: string;
|
|
65
|
+
}
|
|
66
|
+
export declare class CircuitBreaker {
|
|
67
|
+
static getStatus(targetDir: string, taskId: string): CircuitBreakerState;
|
|
68
|
+
static getPatchCount(targetDir: string, taskId: string): number;
|
|
69
|
+
static incrementPatchCount(targetDir: string, taskId: string): number;
|
|
70
|
+
static recordSuccess(targetDir: string, taskId: string): void;
|
|
71
|
+
static reset(targetDir: string, taskId: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Pins the commit a task started from. Restarting a blocked task keeps the
|
|
74
|
+
* original baseline so deletions made before the block still count.
|
|
75
|
+
*/
|
|
76
|
+
static recordBaseline(targetDir: string, taskId: string, ref: string): void;
|
|
77
|
+
static getBaseline(targetDir: string, taskId: string): string | null;
|
|
78
|
+
static clearBaseline(targetDir: string, taskId: string): void;
|
|
79
|
+
static recordFailure(targetDir: string, taskId: string, patch: ContractPatch, ruleResult: RuleEvaluationResult, proposalFactory?: ProposalFactory): {
|
|
80
|
+
state: CircuitBreakerState;
|
|
81
|
+
diagnosticBundle?: EscalationDiagnosticBundle & {
|
|
82
|
+
selfHealingProposal?: SelfHealingProposal | null;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Counts a non-contract guard violation (e.g. test integrity) toward the same
|
|
87
|
+
* 3-strike budget. On the trip the task is blocked and escalated with the
|
|
88
|
+
* factory's proposal attached.
|
|
89
|
+
*/
|
|
90
|
+
static recordGuardViolation(targetDir: string, taskId: string, ruleResult: RuleEvaluationResult, context: GuardViolationContext, proposalFactory?: ProposalFactory): {
|
|
91
|
+
state: CircuitBreakerState;
|
|
92
|
+
escalationId?: string;
|
|
93
|
+
proposal?: SelfHealingProposal | null;
|
|
94
|
+
};
|
|
95
|
+
/** Atomic increment under the ledger lock: concurrent failures are all counted. */
|
|
96
|
+
private static bumpFailures;
|
|
97
|
+
private static tripAndEscalate;
|
|
98
|
+
private static blockTask;
|
|
99
|
+
}
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { withPlanLockSync } from '../core/lock-manager.js';
|
|
3
|
+
import { recordCircuitBreakerTrip } from '../core/telemetry.js';
|
|
4
|
+
import { appendEscalation, loadLedger, mutateLedger } from './store.js';
|
|
5
|
+
const MAX_FAILURE_THRESHOLD = 3;
|
|
6
|
+
const newEntry = () => ({ consecutiveFailures: 0, lastFailureTime: '', patchCount: 0 });
|
|
7
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
8
|
+
/** Proposal generation must never turn a trip into a crash; a failed factory means "no proposal". */
|
|
9
|
+
function buildProposal(factory) {
|
|
10
|
+
if (!factory)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
return factory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function describeProposal(proposal) {
|
|
20
|
+
const verified = proposal.verificationProof.passed ? 'verified in an isolated sandbox' : 'NOT verified';
|
|
21
|
+
return `Review self-healing proposal ${proposal.proposalId} (${proposal.strategy}, ${verified}): ${proposal.rationale}`;
|
|
22
|
+
}
|
|
23
|
+
/** Escalation writes are advisory: a failure to record one must not mask the verdict itself. */
|
|
24
|
+
function fileEscalation(targetDir, build) {
|
|
25
|
+
try {
|
|
26
|
+
return appendEscalation(targetDir, build, { style: 'unique' });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Ledger writes are advisory in the same way: a locked or read-only ledger must not crash a verdict. */
|
|
33
|
+
function tryMutateLedger(targetDir, mutator) {
|
|
34
|
+
try {
|
|
35
|
+
mutateLedger(targetDir, mutator);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Non-fatal
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class CircuitBreaker {
|
|
42
|
+
static getStatus(targetDir, taskId) {
|
|
43
|
+
const ledger = loadLedger(targetDir);
|
|
44
|
+
const entry = ledger[taskId];
|
|
45
|
+
const failures = entry?.consecutiveFailures || 0;
|
|
46
|
+
return {
|
|
47
|
+
active: failures > 0,
|
|
48
|
+
consecutiveFailures: failures,
|
|
49
|
+
maxThreshold: MAX_FAILURE_THRESHOLD,
|
|
50
|
+
tripped: failures >= MAX_FAILURE_THRESHOLD,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
static getPatchCount(targetDir, taskId) {
|
|
54
|
+
const ledger = loadLedger(targetDir);
|
|
55
|
+
return ledger[taskId]?.patchCount || 0;
|
|
56
|
+
}
|
|
57
|
+
static incrementPatchCount(targetDir, taskId) {
|
|
58
|
+
let count = 0;
|
|
59
|
+
tryMutateLedger(targetDir, (ledger) => {
|
|
60
|
+
const entry = ledger[taskId] ?? { ...newEntry(), lastFailureTime: new Date().toISOString() };
|
|
61
|
+
entry.patchCount = (entry.patchCount || 0) + 1;
|
|
62
|
+
ledger[taskId] = entry;
|
|
63
|
+
count = entry.patchCount;
|
|
64
|
+
});
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
static recordSuccess(targetDir, taskId) {
|
|
68
|
+
tryMutateLedger(targetDir, (ledger) => {
|
|
69
|
+
if (ledger[taskId])
|
|
70
|
+
ledger[taskId].consecutiveFailures = 0;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
static reset(targetDir, taskId) {
|
|
74
|
+
tryMutateLedger(targetDir, (ledger) => {
|
|
75
|
+
delete ledger[taskId];
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pins the commit a task started from. Restarting a blocked task keeps the
|
|
80
|
+
* original baseline so deletions made before the block still count.
|
|
81
|
+
*/
|
|
82
|
+
static recordBaseline(targetDir, taskId, ref) {
|
|
83
|
+
tryMutateLedger(targetDir, (ledger) => {
|
|
84
|
+
const entry = ledger[taskId] ?? newEntry();
|
|
85
|
+
if (entry.baselineRef)
|
|
86
|
+
return;
|
|
87
|
+
entry.baselineRef = ref;
|
|
88
|
+
entry.baselineRecordedAt = new Date().toISOString();
|
|
89
|
+
ledger[taskId] = entry;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
static getBaseline(targetDir, taskId) {
|
|
93
|
+
return loadLedger(targetDir)[taskId]?.baselineRef ?? null;
|
|
94
|
+
}
|
|
95
|
+
static clearBaseline(targetDir, taskId) {
|
|
96
|
+
tryMutateLedger(targetDir, (ledger) => {
|
|
97
|
+
if (!ledger[taskId]?.baselineRef)
|
|
98
|
+
return;
|
|
99
|
+
delete ledger[taskId].baselineRef;
|
|
100
|
+
delete ledger[taskId].baselineRecordedAt;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
static recordFailure(targetDir, taskId, patch, ruleResult, proposalFactory) {
|
|
104
|
+
const state = this.bumpFailures(targetDir, taskId, ruleResult);
|
|
105
|
+
// Exactly one of any number of concurrent failures sees the threshold crossed, so a trip
|
|
106
|
+
// escalates once even when parallel agents fail at the same moment.
|
|
107
|
+
if (state.consecutiveFailures === MAX_FAILURE_THRESHOLD) {
|
|
108
|
+
const diagnosticBundle = this.tripAndEscalate(targetDir, taskId, patch, ruleResult, state.consecutiveFailures, proposalFactory);
|
|
109
|
+
return { state, diagnosticBundle };
|
|
110
|
+
}
|
|
111
|
+
return { state };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Counts a non-contract guard violation (e.g. test integrity) toward the same
|
|
115
|
+
* 3-strike budget. On the trip the task is blocked and escalated with the
|
|
116
|
+
* factory's proposal attached.
|
|
117
|
+
*/
|
|
118
|
+
static recordGuardViolation(targetDir, taskId, ruleResult, context, proposalFactory) {
|
|
119
|
+
const state = this.bumpFailures(targetDir, taskId, ruleResult);
|
|
120
|
+
if (state.consecutiveFailures !== MAX_FAILURE_THRESHOLD)
|
|
121
|
+
return { state };
|
|
122
|
+
const proposal = buildProposal(proposalFactory);
|
|
123
|
+
const recommendedAction = proposal
|
|
124
|
+
? describeProposal(proposal)
|
|
125
|
+
: `Review ${context.affected.join(', ')} and decide whether the change is intended; if so, have the architect update the task and unblock ${taskId}.`;
|
|
126
|
+
const escalationId = fileEscalation(targetDir, (id) => ({
|
|
127
|
+
id,
|
|
128
|
+
taskId,
|
|
129
|
+
type: context.escalationType,
|
|
130
|
+
reportedBy: 'backend',
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
summary: `Circuit Breaker Tripped (${state.consecutiveFailures}/${MAX_FAILURE_THRESHOLD}): ${ruleResult.ruleId}`,
|
|
133
|
+
details: `${context.intent}: ${ruleResult.message}`,
|
|
134
|
+
affectedContracts: context.affected,
|
|
135
|
+
recommendedAction,
|
|
136
|
+
status: 'pending_review',
|
|
137
|
+
...(proposal ? { proposedPatch: proposal } : {}),
|
|
138
|
+
}));
|
|
139
|
+
this.blockTask(targetDir, taskId, `Circuit breaker tripped by Governor: ${ruleResult.ruleId} (${escalationId ?? 'escalation not recorded'})`);
|
|
140
|
+
recordCircuitBreakerTrip(targetDir, taskId).catch(() => { });
|
|
141
|
+
return { state, escalationId: escalationId ?? undefined, proposal };
|
|
142
|
+
}
|
|
143
|
+
/** Atomic increment under the ledger lock: concurrent failures are all counted. */
|
|
144
|
+
static bumpFailures(targetDir, taskId, ruleResult) {
|
|
145
|
+
let failures = 0;
|
|
146
|
+
try {
|
|
147
|
+
failures = mutateLedger(targetDir, (ledger) => {
|
|
148
|
+
const entry = ledger[taskId] ?? newEntry();
|
|
149
|
+
entry.consecutiveFailures += 1;
|
|
150
|
+
entry.lastFailureTime = new Date().toISOString();
|
|
151
|
+
entry.lastRuleId = ruleResult.ruleId;
|
|
152
|
+
entry.lastViolation = ruleResult.message;
|
|
153
|
+
ledger[taskId] = entry;
|
|
154
|
+
return entry.consecutiveFailures;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Unwritable ledger: report this failure alone rather than crashing the verdict.
|
|
159
|
+
failures = (loadLedger(targetDir)[taskId]?.consecutiveFailures ?? 0) + 1;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
active: true,
|
|
163
|
+
consecutiveFailures: failures,
|
|
164
|
+
maxThreshold: MAX_FAILURE_THRESHOLD,
|
|
165
|
+
tripped: failures >= MAX_FAILURE_THRESHOLD,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
static tripAndEscalate(targetDir, taskId, patch, ruleResult, failures, proposalFactory) {
|
|
169
|
+
const contractFileName = patch.target === 'db_schema' ? 'db_schema.json' : 'api_contracts.json';
|
|
170
|
+
const proposal = buildProposal(proposalFactory);
|
|
171
|
+
const recommendedActions = [
|
|
172
|
+
`Review contract at .ai/${contractFileName} and either adjust schema or approve migration.`,
|
|
173
|
+
`If the operation is required, have the architect apply the change and unblock ${taskId}.`,
|
|
174
|
+
`If the operation is invalid, instruct the agent to use an alternative backward-compatible approach.`,
|
|
175
|
+
];
|
|
176
|
+
if (proposal)
|
|
177
|
+
recommendedActions.unshift(describeProposal(proposal));
|
|
178
|
+
// 1. Write to .ai/escalation.json (the id is assigned under the file lock)
|
|
179
|
+
const escalationId = fileEscalation(targetDir, (id) => ({
|
|
180
|
+
id,
|
|
181
|
+
taskId,
|
|
182
|
+
type: patch.target === 'db_schema' ? 'schema_flaw' : 'contract_drift',
|
|
183
|
+
reportedBy: 'backend',
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
summary: `Circuit Breaker Tripped (${failures}/${MAX_FAILURE_THRESHOLD}): ${ruleResult.ruleId} on ${patch.path}`,
|
|
186
|
+
details: `Autonomous agent failed ${failures} times attempting ${patch.operation} on ${patch.path}: ${ruleResult.message}`,
|
|
187
|
+
affectedContracts: [`.ai/${contractFileName}`],
|
|
188
|
+
recommendedAction: recommendedActions[0],
|
|
189
|
+
status: 'pending_review',
|
|
190
|
+
...(proposal ? { proposedPatch: proposal } : {}),
|
|
191
|
+
})) ?? 'unrecorded';
|
|
192
|
+
// 2. Mark task as blocked in .ai/master_plan.json
|
|
193
|
+
this.blockTask(targetDir, taskId, `Circuit breaker tripped by Governor: ${ruleResult.ruleId} (${escalationId})`);
|
|
194
|
+
recordCircuitBreakerTrip(targetDir, taskId).catch(() => { });
|
|
195
|
+
return {
|
|
196
|
+
escalationId,
|
|
197
|
+
taskId,
|
|
198
|
+
circuitBreakerTripped: true,
|
|
199
|
+
consecutiveFailures: failures,
|
|
200
|
+
intent: patch.reason || `Apply ${patch.operation} on ${patch.path}`,
|
|
201
|
+
invariantCollision: ruleResult.message,
|
|
202
|
+
proposedPatch: {
|
|
203
|
+
target: patch.target,
|
|
204
|
+
operation: patch.operation,
|
|
205
|
+
path: patch.path,
|
|
206
|
+
value: patch.value,
|
|
207
|
+
reason: patch.reason,
|
|
208
|
+
},
|
|
209
|
+
recommendedActions,
|
|
210
|
+
selfHealingProposal: proposal,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
static blockTask(targetDir, taskId, note) {
|
|
214
|
+
const planPath = path.join(targetDir, '.ai', 'master_plan.json');
|
|
215
|
+
try {
|
|
216
|
+
withPlanLockSync(planPath, (plan) => {
|
|
217
|
+
for (const milestone of plan.milestones) {
|
|
218
|
+
const task = milestone.tasks.find((t) => t.id === taskId);
|
|
219
|
+
if (task) {
|
|
220
|
+
task.status = 'blocked';
|
|
221
|
+
task.notes = note;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Non-fatal
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ContractPatch, GovernorVerdict, RuleEvaluationResult } from './types.js';
|
|
2
|
+
import { type CandidatePatch, type ContractPatchProposal, type SelfHealingProposal, type TestRestorationProposal, type VerificationProof } from './circuit-breaker.js';
|
|
3
|
+
export declare function getContractHash(filePath: string): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* Replays a candidate against a throwaway copy of the contract in the OS temp
|
|
6
|
+
* directory: governor rules, apply, re-parse, and proof that it changes
|
|
7
|
+
* something while the live `.ai/` file stays byte-identical.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyCandidateInSandbox(targetDir: string, taskId: string, candidate: CandidatePatch): VerificationProof;
|
|
10
|
+
/**
|
|
11
|
+
* Builds the first backward-compatible candidate that survives an isolated
|
|
12
|
+
* replay. Returns null when no candidate can be proven (a human decides).
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateSelfHealingProposal(targetDir: string, patch: ContractPatch, rule: RuleEvaluationResult): ContractPatchProposal | null;
|
|
15
|
+
export interface ProposalApplication {
|
|
16
|
+
applied: boolean;
|
|
17
|
+
ruleId: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Applies an operator-approved proposal. Contract patches are re-checked
|
|
22
|
+
* against the live contract and refused if it moved since the proof was taken.
|
|
23
|
+
* Human-approved patches do not count against the task's patch budget.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applySelfHealingProposal(targetDir: string, taskId: string, proposal: SelfHealingProposal): ProposalApplication;
|
|
26
|
+
export type TestIntegrityRule = 'TEST_SUITE_DELETED' | 'TEST_ASSERTIONS_REMOVED' | 'TEST_CASES_REMOVED' | 'TESTS_SKIPPED' | 'TESTS_FOCUSED' | 'TEST_SCRIPT_WEAKENED';
|
|
27
|
+
export interface TestIntegrityFinding {
|
|
28
|
+
rule: TestIntegrityRule;
|
|
29
|
+
/** Path relative to the project root. */
|
|
30
|
+
path: string;
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
export interface TestIntegrityReport extends RuleEvaluationResult {
|
|
34
|
+
/** Resolved commit the working tree was compared against; null when the check was skipped. */
|
|
35
|
+
baselineRef: string | null;
|
|
36
|
+
/** True when the baseline came from `nativ task start` rather than falling back to HEAD. */
|
|
37
|
+
baselineRecorded: boolean;
|
|
38
|
+
findings: TestIntegrityFinding[];
|
|
39
|
+
skippedReason?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface TestSuiteMetrics {
|
|
42
|
+
assertions: number;
|
|
43
|
+
testCases: number;
|
|
44
|
+
skipped: number;
|
|
45
|
+
focused: number;
|
|
46
|
+
}
|
|
47
|
+
export declare function isTestFile(file: string): boolean;
|
|
48
|
+
/** Heuristic, language-agnostic counts; only deltas against the baseline matter. */
|
|
49
|
+
export declare function measureTestSuite(source: string): TestSuiteMetrics;
|
|
50
|
+
/** Current commit of the checkout at `targetDir`, or null outside git / before the first commit. */
|
|
51
|
+
export declare function resolveGitHead(targetDir: string): string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Enforces that a task never deletes or weakens the test suites that existed
|
|
54
|
+
* when it started: no deleted test files, no fewer assertions or cases, no new
|
|
55
|
+
* skip/only markers, and no test files dropped from package.json test scripts.
|
|
56
|
+
*/
|
|
57
|
+
export declare class TestIntegrityGuard {
|
|
58
|
+
static evaluate(targetDir: string, taskId: string): TestIntegrityReport;
|
|
59
|
+
/**
|
|
60
|
+
* Proposal to restore every flagged file from the baseline, with proof that
|
|
61
|
+
* each baseline blob exists and what the restored suite measures.
|
|
62
|
+
*/
|
|
63
|
+
static proposeRestoration(targetDir: string, report: TestIntegrityReport): TestRestorationProposal | null;
|
|
64
|
+
}
|
|
65
|
+
export declare class ContractGovernor {
|
|
66
|
+
static evaluate(targetDir: string, patch: ContractPatch): GovernorVerdict;
|
|
67
|
+
}
|