@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 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: approvedWithPolicyFingerprint - approvedLinkedToRun,
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
  },
@@ -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>;
@@ -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
- manifest.stages !== null &&
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
- (Array.isArray(manifest.policy.decisionIds) &&
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.object({
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.object({
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 { loadBaseline, loadGuardPolicyAtRevision, createGuardPolicySnapshot, loadGuardWaivers, loadGuardAgentConfig, loadGuardConfig, loadGuardProposals, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
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 config;
46
+ let workingTreeState;
47
47
  try {
48
- config = loadGuardConfig(root);
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 (!config) {
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 = createGuardPolicySnapshot(config, loadBaseline(root), loadGuardWaivers(root), 'working-tree', undefined, loadGuardAgentConfig(root));
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 = (loadGuardProposals(root)?.decisions ?? [])
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.0",
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",