@codapult/guard 0.8.0 → 0.8.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.
- package/README.md +6 -37
- package/dist/core/audit/governance.js +1 -1
- package/dist/core/guard.d.ts +6 -0
- package/dist/core/guard.js +19 -0
- package/dist/core/history/runs.js +20 -3
- package/dist/core/policy/schemas.js +37 -4
- package/dist/core/verification/verify.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,43 +57,6 @@ The core is universal. The strongest first-class scenarios are Next.js SaaS and
|
|
|
57
57
|
development, including server/client boundaries, routes, persistence, authentication, billing,
|
|
58
58
|
background jobs, environment configuration, and AI integrations.
|
|
59
59
|
|
|
60
|
-
## Part of the Codapult ecosystem
|
|
61
|
-
|
|
62
|
-
Guard is an independent open-source project from [Codapult](https://codapult.dev). It does not
|
|
63
|
-
require Codapult and can be installed in any JavaScript or TypeScript repository.
|
|
64
|
-
|
|
65
|
-
The relationship is complementary:
|
|
66
|
-
|
|
67
|
-
| Project | Role |
|
|
68
|
-
| --------------------- | ---------------------------------------------------------------------------------------- |
|
|
69
|
-
| **`@codapult/guard`** | Universal architecture guardrails, project memory, contracts, MCP, and AI-agent context. |
|
|
70
|
-
| **`@codapult/cli`** | Codapult SaaS project CLI that includes Guard through a thin adapter. |
|
|
71
|
-
| **Codapult** | Full-source Next.js SaaS foundation with conventions Guard can discover and protect. |
|
|
72
|
-
|
|
73
|
-
Use standalone Guard for any compatible project. Use the Codapult CLI when working on a Codapult
|
|
74
|
-
SaaS project and you want project management, database, plugins, deployment, MCP, and Guard in one
|
|
75
|
-
CLI.
|
|
76
|
-
|
|
77
|
-
```text
|
|
78
|
-
Existing project
|
|
79
|
-
│
|
|
80
|
-
▼
|
|
81
|
-
deterministic discovery
|
|
82
|
-
AST · files · imports · Git
|
|
83
|
-
│
|
|
84
|
-
▼
|
|
85
|
-
project architecture
|
|
86
|
-
capabilities · graph · impact paths
|
|
87
|
-
│
|
|
88
|
-
▼
|
|
89
|
-
approved project policy
|
|
90
|
-
rules · contracts · conventions · baseline
|
|
91
|
-
│
|
|
92
|
-
▼
|
|
93
|
-
change verification
|
|
94
|
-
check · review packet · verify · CI
|
|
95
|
-
```
|
|
96
|
-
|
|
97
60
|
## Why Guard exists
|
|
98
61
|
|
|
99
62
|
AI agents can produce syntactically valid code that still violates the architecture of a real
|
|
@@ -519,6 +482,12 @@ pnpm release:check
|
|
|
519
482
|
- [Fixture matrix](docs/guard-fixtures.md)
|
|
520
483
|
- [Release process](docs/releasing.md)
|
|
521
484
|
|
|
485
|
+
## More from Codapult
|
|
486
|
+
|
|
487
|
+
Starting a SaaS from scratch? [Codapult](https://codapult.dev) is a production-ready Next.js SaaS
|
|
488
|
+
foundation built for AI-assisted development — with MCP context, built-in AI, modular
|
|
489
|
+
architecture, and the source code under your control.
|
|
490
|
+
|
|
522
491
|
## Project status
|
|
523
492
|
|
|
524
493
|
`@codapult/guard` starts at `0.1.0` as a public alpha. The deterministic core is usable, tested,
|
|
@@ -152,7 +152,7 @@ function auditGuardGovernanceUnlocked(root) {
|
|
|
152
152
|
reachability: {
|
|
153
153
|
approvedWithPolicyFingerprint,
|
|
154
154
|
approvedLinkedToRun,
|
|
155
|
-
approvedWithoutRun:
|
|
155
|
+
approvedWithoutRun: approved.length - approvedLinkedToRun,
|
|
156
156
|
runsWithPolicy: runs.filter((run) => run.policy !== undefined).length,
|
|
157
157
|
runsWithoutPolicy: runs.filter((run) => run.policy === undefined).length,
|
|
158
158
|
},
|
package/dist/core/guard.d.ts
CHANGED
|
@@ -247,6 +247,11 @@ export interface GuardPolicySnapshot {
|
|
|
247
247
|
source: 'working-tree' | 'git-ref';
|
|
248
248
|
ref?: string | undefined;
|
|
249
249
|
}
|
|
250
|
+
export interface GuardWorkingTreePolicyState {
|
|
251
|
+
config: GuardConfig;
|
|
252
|
+
snapshot: GuardPolicySnapshot;
|
|
253
|
+
proposals?: GuardProposalFile | undefined;
|
|
254
|
+
}
|
|
250
255
|
/**
|
|
251
256
|
* Fingerprints the effective policy used by a verification run.
|
|
252
257
|
*
|
|
@@ -278,6 +283,7 @@ export interface GuardWaiverUpdateOptions {
|
|
|
278
283
|
maxLifetimeDays?: number | undefined;
|
|
279
284
|
}
|
|
280
285
|
export declare function loadGuardWaivers(root: string): GuardWaiver[];
|
|
286
|
+
export declare function loadGuardWorkingTreePolicy(root: string): GuardWorkingTreePolicyState | undefined;
|
|
281
287
|
export declare function writeGuardWaivers(root: string): void;
|
|
282
288
|
export declare function updateGuardWaivers(root: string, options: GuardWaiverUpdateOptions): GuardWaiver[];
|
|
283
289
|
export declare function updateBaseline(root: string, options?: GuardBaselineUpdateOptions): Set<string>;
|
package/dist/core/guard.js
CHANGED
|
@@ -597,6 +597,25 @@ export function loadGuardWaivers(root) {
|
|
|
597
597
|
return readGuardWaiversUnlocked(root).waivers;
|
|
598
598
|
return withGuardStateLock(root, () => readGuardWaiversUnlocked(root).waivers);
|
|
599
599
|
}
|
|
600
|
+
function readGuardWorkingTreePolicyUnlocked(root) {
|
|
601
|
+
recoverGuardPolicyTransaction(root);
|
|
602
|
+
const guardConfig = readGuardConfigFilesUnlocked(root);
|
|
603
|
+
if (!guardConfig)
|
|
604
|
+
return undefined;
|
|
605
|
+
const baseline = loadBaseline(root);
|
|
606
|
+
const waivers = readGuardWaiversUnlocked(root).waivers;
|
|
607
|
+
const agentConfig = loadGuardAgentConfig(root);
|
|
608
|
+
return {
|
|
609
|
+
config: guardConfig,
|
|
610
|
+
snapshot: createGuardPolicySnapshot(guardConfig, baseline, waivers, 'working-tree', undefined, agentConfig),
|
|
611
|
+
proposals: loadGuardProposalsUnlocked(root),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
export function loadGuardWorkingTreePolicy(root) {
|
|
615
|
+
if (!existsSync(resolve(root, GUARD_DIR)))
|
|
616
|
+
return readGuardWorkingTreePolicyUnlocked(root);
|
|
617
|
+
return withGuardStateLock(root, () => readGuardWorkingTreePolicyUnlocked(root));
|
|
618
|
+
}
|
|
600
619
|
export function writeGuardWaivers(root) {
|
|
601
620
|
withGuardStateLock(root, () => {
|
|
602
621
|
const file = { version: 1, waivers: [], decisions: [] };
|
|
@@ -15,6 +15,22 @@ function isGuardRunManifest(value) {
|
|
|
15
15
|
if (value === null || typeof value !== 'object')
|
|
16
16
|
return false;
|
|
17
17
|
const manifest = value;
|
|
18
|
+
const stages = manifest.stages;
|
|
19
|
+
const validStages = stages !== null &&
|
|
20
|
+
typeof stages === 'object' &&
|
|
21
|
+
!Array.isArray(stages) &&
|
|
22
|
+
Object.values(stages).every((stage) => {
|
|
23
|
+
if (stage === null || typeof stage !== 'object')
|
|
24
|
+
return false;
|
|
25
|
+
const candidate = stage;
|
|
26
|
+
return (typeof candidate.durationMs === 'number' &&
|
|
27
|
+
Number.isFinite(candidate.durationMs) &&
|
|
28
|
+
candidate.durationMs >= 0 &&
|
|
29
|
+
(candidate.status === 'ok' ||
|
|
30
|
+
candidate.status === 'fail' ||
|
|
31
|
+
candidate.status === 'skipped') &&
|
|
32
|
+
(candidate.detail === undefined || typeof candidate.detail === 'string'));
|
|
33
|
+
});
|
|
18
34
|
return (manifest.version === 1 &&
|
|
19
35
|
typeof manifest.runId === 'string' &&
|
|
20
36
|
/^guard-[A-Za-z0-9-]+$/.test(manifest.runId) &&
|
|
@@ -23,6 +39,7 @@ function isGuardRunManifest(value) {
|
|
|
23
39
|
Number.isFinite(Date.parse(manifest.startedAt)) &&
|
|
24
40
|
typeof manifest.completedAt === 'string' &&
|
|
25
41
|
Number.isFinite(Date.parse(manifest.completedAt)) &&
|
|
42
|
+
Date.parse(manifest.completedAt) >= Date.parse(manifest.startedAt) &&
|
|
26
43
|
typeof manifest.durationMs === 'number' &&
|
|
27
44
|
Number.isFinite(manifest.durationMs) &&
|
|
28
45
|
manifest.durationMs >= 0 &&
|
|
@@ -32,8 +49,7 @@ function isGuardRunManifest(value) {
|
|
|
32
49
|
manifest.outcome === 'needs-review' ||
|
|
33
50
|
manifest.outcome === 'not-configured') &&
|
|
34
51
|
typeof manifest.gate === 'string' &&
|
|
35
|
-
|
|
36
|
-
typeof manifest.stages === 'object' &&
|
|
52
|
+
validStages &&
|
|
37
53
|
(manifest.policy === undefined ||
|
|
38
54
|
(manifest.policy !== null &&
|
|
39
55
|
typeof manifest.policy === 'object' &&
|
|
@@ -46,7 +62,8 @@ function isGuardRunManifest(value) {
|
|
|
46
62
|
(manifest.policy.changedFromBase === undefined ||
|
|
47
63
|
typeof manifest.policy.changedFromBase === 'boolean') &&
|
|
48
64
|
(manifest.policy.decisionIds === undefined ||
|
|
49
|
-
(
|
|
65
|
+
(manifest.policy.source === 'working-tree' &&
|
|
66
|
+
Array.isArray(manifest.policy.decisionIds) &&
|
|
50
67
|
new Set(manifest.policy.decisionIds).size === manifest.policy.decisionIds.length &&
|
|
51
68
|
manifest.policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0))))));
|
|
52
69
|
}
|
|
@@ -94,7 +94,8 @@ export const guardContractsFileSchema = z.object({
|
|
|
94
94
|
version: z.literal(1),
|
|
95
95
|
contracts: z.array(guardContractSchema),
|
|
96
96
|
});
|
|
97
|
-
export const guardProposalDecisionSchema = z
|
|
97
|
+
export const guardProposalDecisionSchema = z
|
|
98
|
+
.object({
|
|
98
99
|
decisionId: z.string().trim().min(1).optional(),
|
|
99
100
|
id: z.string().trim().min(1),
|
|
100
101
|
type: z.enum(['rule', 'contract']),
|
|
@@ -105,12 +106,29 @@ export const guardProposalDecisionSchema = z.object({
|
|
|
105
106
|
revision: z.number().optional(),
|
|
106
107
|
source: z.enum(['cli', 'mcp', 'external']).optional(),
|
|
107
108
|
proposalAuthor: z.string().trim().min(1).optional(),
|
|
108
|
-
actor: z.string().optional(),
|
|
109
|
-
commit: z.string().optional(),
|
|
109
|
+
actor: z.string().trim().min(1).optional(),
|
|
110
|
+
commit: z.string().trim().min(1).optional(),
|
|
110
111
|
policyFingerprint: z.string().trim().min(1).optional(),
|
|
111
112
|
policyRevision: z.number().int().nonnegative().optional(),
|
|
113
|
+
})
|
|
114
|
+
.superRefine((decision, context) => {
|
|
115
|
+
if (decision.policyFingerprint !== undefined && decision.decision !== 'approved') {
|
|
116
|
+
context.addIssue({
|
|
117
|
+
code: 'custom',
|
|
118
|
+
message: 'policyFingerprint is only valid for approved decisions.',
|
|
119
|
+
path: ['policyFingerprint'],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (decision.policyRevision !== undefined && decision.policyFingerprint === undefined) {
|
|
123
|
+
context.addIssue({
|
|
124
|
+
code: 'custom',
|
|
125
|
+
message: 'policyRevision requires policyFingerprint.',
|
|
126
|
+
path: ['policyRevision'],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
112
129
|
});
|
|
113
|
-
export const guardProposalSchema = z
|
|
130
|
+
export const guardProposalSchema = z
|
|
131
|
+
.object({
|
|
114
132
|
version: z.literal(1),
|
|
115
133
|
generatedAt: z.string(),
|
|
116
134
|
generatedBy: z.string().trim().min(1).optional(),
|
|
@@ -122,6 +140,21 @@ export const guardProposalSchema = z.object({
|
|
|
122
140
|
contracts: z.array(guardContractSchema),
|
|
123
141
|
questions: z.array(z.string()),
|
|
124
142
|
decisions: z.array(guardProposalDecisionSchema).optional(),
|
|
143
|
+
})
|
|
144
|
+
.superRefine((proposal, context) => {
|
|
145
|
+
const decisionIds = new Set();
|
|
146
|
+
for (const [index, decision] of (proposal.decisions ?? []).entries()) {
|
|
147
|
+
if (decision.decisionId === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
if (decisionIds.has(decision.decisionId)) {
|
|
150
|
+
context.addIssue({
|
|
151
|
+
code: 'custom',
|
|
152
|
+
message: 'decisionId values must be unique.',
|
|
153
|
+
path: ['decisions', index, 'decisionId'],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
decisionIds.add(decision.decisionId);
|
|
157
|
+
}
|
|
125
158
|
});
|
|
126
159
|
export const guardAgentConfigSchema = z.object({
|
|
127
160
|
version: z.literal(1),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {} from '../../adapters/command.js';
|
|
2
2
|
import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
|
|
3
|
-
import {
|
|
3
|
+
import { loadGuardPolicyAtRevision, loadGuardWorkingTreePolicy, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
|
|
4
4
|
import { analyzeProjectImpact } from '../analysis/impact.js';
|
|
5
5
|
import { discoverProject } from '../discovery/discovery.js';
|
|
6
6
|
import { finishGuardRun, recordGuardRunStage, startGuardRun, } from '../history/runs.js';
|
|
@@ -43,9 +43,9 @@ export function runGuardVerification(root, options = {}) {
|
|
|
43
43
|
run,
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
-
let
|
|
46
|
+
let workingTreeState;
|
|
47
47
|
try {
|
|
48
|
-
|
|
48
|
+
workingTreeState = loadGuardWorkingTreePolicy(root);
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
51
|
const run = finishGuardRun(root, runContext, 'fail', 'configuration');
|
|
@@ -64,7 +64,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
64
64
|
run,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
if (!
|
|
67
|
+
if (!workingTreeState) {
|
|
68
68
|
const run = finishGuardRun(root, runContext, 'not-configured', 'configuration');
|
|
69
69
|
return {
|
|
70
70
|
status: 'not-configured',
|
|
@@ -85,7 +85,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
85
85
|
let policyChanged;
|
|
86
86
|
let policyDecisionIds = [];
|
|
87
87
|
try {
|
|
88
|
-
workingTreeSnapshot =
|
|
88
|
+
workingTreeSnapshot = workingTreeState.snapshot;
|
|
89
89
|
const baseSnapshot = options.policyBase
|
|
90
90
|
? loadGuardPolicyAtRevision(root, options.policyBase)
|
|
91
91
|
: undefined;
|
|
@@ -110,7 +110,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
110
110
|
policyChanged =
|
|
111
111
|
baseSnapshot !== undefined && baseSnapshot.fingerprint !== workingTreeSnapshot.fingerprint;
|
|
112
112
|
if (policySnapshot.source === 'working-tree') {
|
|
113
|
-
policyDecisionIds = (
|
|
113
|
+
policyDecisionIds = (workingTreeState.proposals?.decisions ?? [])
|
|
114
114
|
.filter((decision) => decision.policyFingerprint === policySnapshot.fingerprint)
|
|
115
115
|
.map((decision) => decision.decisionId ??
|
|
116
116
|
`${decision.proposalId ?? 'unknown'}:${decision.type}:${decision.id}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codapult/guard",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"mcpName": "io.github.codapult/guard",
|
|
5
5
|
"description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
|
|
6
6
|
"license": "MIT",
|