@geraldmaron/construct 1.0.12 → 1.0.14

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 (88) hide show
  1. package/README.md +8 -2
  2. package/bin/construct +259 -11
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/lib/auto-docs.mjs +11 -7
  5. package/lib/dashboard-static.mjs +7 -4
  6. package/lib/doc-stamp.mjs +16 -0
  7. package/lib/docs-verify.mjs +1 -8
  8. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  9. package/lib/embed/daemon.mjs +20 -0
  10. package/lib/embed/docs-lifecycle.mjs +19 -0
  11. package/lib/embed/inbox.mjs +85 -2
  12. package/lib/embed/recommendation-store.mjs +29 -0
  13. package/lib/gates-audit.mjs +18 -12
  14. package/lib/hooks/_lib/input.mjs +52 -0
  15. package/lib/hooks/adaptive-lint.mjs +4 -0
  16. package/lib/hooks/agent-tracker.mjs +59 -15
  17. package/lib/hooks/audit-reads.mjs +83 -42
  18. package/lib/hooks/audit-trail.mjs +28 -18
  19. package/lib/hooks/bash-output-logger.mjs +8 -2
  20. package/lib/hooks/block-no-verify.mjs +4 -0
  21. package/lib/hooks/ci-status-check.mjs +4 -0
  22. package/lib/hooks/comment-lint.mjs +6 -4
  23. package/lib/hooks/config-protection.mjs +4 -0
  24. package/lib/hooks/context-watch.mjs +4 -0
  25. package/lib/hooks/context-window-recovery.mjs +4 -0
  26. package/lib/hooks/dep-audit.mjs +12 -5
  27. package/lib/hooks/doc-coupling-check.mjs +5 -1
  28. package/lib/hooks/edit-accumulator.mjs +25 -10
  29. package/lib/hooks/edit-error-recovery.mjs +4 -0
  30. package/lib/hooks/edit-guard.mjs +4 -0
  31. package/lib/hooks/guard-bash.mjs +4 -0
  32. package/lib/hooks/mcp-audit.mjs +4 -0
  33. package/lib/hooks/mcp-health-check.mjs +4 -0
  34. package/lib/hooks/model-fallback.mjs +7 -11
  35. package/lib/hooks/policy-engine.mjs +4 -0
  36. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  37. package/lib/hooks/post-merge-tracking.mjs +82 -0
  38. package/lib/hooks/pre-compact.mjs +4 -0
  39. package/lib/hooks/pre-push-gate.mjs +84 -231
  40. package/lib/hooks/proactive-activation.mjs +5 -2
  41. package/lib/hooks/readme-age-check.mjs +4 -0
  42. package/lib/hooks/registry-sync.mjs +35 -20
  43. package/lib/hooks/rule-verifier.mjs +3 -0
  44. package/lib/hooks/scan-secrets.mjs +4 -0
  45. package/lib/hooks/session-optimize.mjs +4 -0
  46. package/lib/hooks/session-reflect.mjs +4 -0
  47. package/lib/hooks/session-start.mjs +48 -1
  48. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  49. package/lib/hooks/stop-notify.mjs +13 -2
  50. package/lib/hooks/stop-typecheck.mjs +4 -0
  51. package/lib/hooks/test-watch.mjs +8 -4
  52. package/lib/init-unified.mjs +90 -23
  53. package/lib/intake/attribution.mjs +77 -0
  54. package/lib/intake/intake-config.mjs +3 -0
  55. package/lib/intake/manifest.mjs +107 -0
  56. package/lib/intake/poll-lock.mjs +136 -0
  57. package/lib/intake/prepare.mjs +42 -4
  58. package/lib/logging/rotate.mjs +394 -0
  59. package/lib/mcp/tools/project.mjs +2 -2
  60. package/lib/mcp/tools/telemetry.mjs +1 -1
  61. package/lib/opencode-config.mjs +10 -3
  62. package/lib/orchestration/routing-tables.mjs +176 -0
  63. package/lib/orchestration-policy.mjs +18 -106
  64. package/lib/parity.mjs +48 -7
  65. package/lib/profiles/lifecycle.mjs +19 -1
  66. package/lib/project-init-shared.mjs +11 -5
  67. package/lib/project-root.mjs +104 -0
  68. package/lib/roles/catalog.mjs +1 -1
  69. package/lib/roles/event-bus.mjs +29 -9
  70. package/lib/roles/router.mjs +8 -7
  71. package/lib/server/index.mjs +31 -9
  72. package/lib/server/static/index.html +1 -15
  73. package/lib/specialist-contracts-enforce.mjs +24 -10
  74. package/lib/status.mjs +1 -1
  75. package/lib/storage/backup.mjs +2 -2
  76. package/lib/telemetry/intent-verifications.mjs +16 -3
  77. package/lib/telemetry/skill-calls.mjs +12 -3
  78. package/lib/tracking-surfaces.mjs +375 -0
  79. package/lib/worker/trace.mjs +19 -2
  80. package/package.json +6 -2
  81. package/platforms/claude/settings.template.json +28 -27
  82. package/scripts/sync-specialists.mjs +269 -75
  83. package/specialists/registry.json +158 -13
  84. package/lib/hooks/env-check.mjs +0 -83
  85. package/lib/hooks/read-tracker.mjs +0 -61
  86. package/lib/policy/unified-gates.mjs +0 -96
  87. package/lib/server/static/assets/index-ab25c707.js +0 -70
  88. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
@@ -98,7 +98,10 @@
98
98
  "openingQuestion": "What are we trying to learn, and how will we know when we've learned it?",
99
99
  "failureMode": "If you can't write a falsifiable hypothesis, this is a planning task, not R&D."
100
100
  },
101
- "canEdit": false
101
+ "canEdit": false,
102
+ "subscriptions": [
103
+ "research.gate.required"
104
+ ]
102
105
  },
103
106
  {
104
107
  "internal": true,
@@ -147,6 +150,26 @@
147
150
  "canEdit": false,
148
151
  "collaborators": [
149
152
  "ux-researcher"
153
+ ],
154
+ "subscriptions": [
155
+ "backlog.stale",
156
+ "prd.requested"
157
+ ],
158
+ "docArtifacts": [
159
+ "backlog-proposal",
160
+ "customer-profile",
161
+ "meta-prd",
162
+ "one-pager",
163
+ "prd",
164
+ "prd-business",
165
+ "prd-platform",
166
+ "prfaq"
167
+ ],
168
+ "watchConditions": [
169
+ {
170
+ "watcher": "high-ambiguity-deep-work",
171
+ "reason": "clarify acceptance criteria (high ambiguity)"
172
+ }
150
173
  ]
151
174
  },
152
175
  {
@@ -234,7 +257,10 @@
234
257
  "openingQuestion": "What must be done first, what blocks what, and who owns each deliverable?",
235
258
  "failureMode": "If every task can run in parallel, the dependency graph wasn't drawn."
236
259
  },
237
- "canEdit": false
260
+ "canEdit": false,
261
+ "subscriptions": [
262
+ "plan.requested"
263
+ ]
238
264
  },
239
265
  {
240
266
  "internal": true,
@@ -273,7 +299,17 @@
273
299
  "openingQuestion": "What is the version, the publication date, and the primary source?",
274
300
  "failureMode": "If all sources are secondhand or undated, the research isn't done."
275
301
  },
276
- "canEdit": false
302
+ "canEdit": false,
303
+ "subscriptions": [
304
+ "evidence.requested",
305
+ "research.requested"
306
+ ],
307
+ "docArtifacts": [
308
+ "evidence-brief",
309
+ "product-intelligence-report",
310
+ "research-brief",
311
+ "signal-brief"
312
+ ]
277
313
  },
278
314
  {
279
315
  "internal": true,
@@ -319,6 +355,9 @@
319
355
  "data-analyst",
320
356
  "product-manager",
321
357
  "researcher"
358
+ ],
359
+ "subscriptions": [
360
+ "strategy.required"
322
361
  ]
323
362
  },
324
363
  {
@@ -409,6 +448,9 @@
409
448
  "product-manager",
410
449
  "qa",
411
450
  "rd-lead"
451
+ ],
452
+ "subscriptions": [
453
+ "eval.regression"
412
454
  ]
413
455
  },
414
456
  {
@@ -506,6 +548,17 @@
506
548
  "ai-engineer",
507
549
  "designer",
508
550
  "engineer"
551
+ ],
552
+ "subscriptions": [
553
+ "adr.requested",
554
+ "arch.boundary.violated"
555
+ ],
556
+ "docArtifacts": [
557
+ "adr",
558
+ "architecture-overview",
559
+ "rfc",
560
+ "rfc-platform",
561
+ "system-design"
509
562
  ]
510
563
  },
511
564
  {
@@ -600,7 +653,13 @@
600
653
  "openingQuestion": "What's the simplest reason this fails?",
601
654
  "failureMode": "If you find no CRITICAL challenges, you looked at the happy path. Dig into the error paths."
602
655
  },
603
- "canEdit": false
656
+ "canEdit": false,
657
+ "watchConditions": [
658
+ {
659
+ "watcher": "architecture-without-metric",
660
+ "reason": "architecture change without stated success metric"
661
+ }
662
+ ]
604
663
  },
605
664
  {
606
665
  "internal": true,
@@ -639,7 +698,11 @@
639
698
  "openingQuestion": "Does this do what it's supposed to do under the conditions it wasn't designed for?",
640
699
  "failureMode": "If your review only covered the happy path, you haven't reviewed."
641
700
  },
642
- "canEdit": true
701
+ "canEdit": true,
702
+ "subscriptions": [
703
+ "pr.opened",
704
+ "pr.ready-for-review"
705
+ ]
643
706
  },
644
707
  {
645
708
  "internal": true,
@@ -684,7 +747,22 @@
684
747
  "openingQuestion": "What does an attacker see when they look at this?",
685
748
  "failureMode": "If the only finding is 'no hardcoded secrets,' you checked one category out of eight."
686
749
  },
687
- "canEdit": true
750
+ "canEdit": true,
751
+ "subscriptions": [
752
+ "config.protection.violation",
753
+ "dep.cve",
754
+ "secrets.detected"
755
+ ],
756
+ "docArtifacts": [
757
+ "security-review",
758
+ "threat-model"
759
+ ],
760
+ "watchConditions": [
761
+ {
762
+ "watcher": "auth-payments-non-narrow",
763
+ "reason": "pre-dispatch threat model (auth/payments/PII + non-narrow blast radius)"
764
+ }
765
+ ]
688
766
  },
689
767
  {
690
768
  "internal": true,
@@ -723,7 +801,16 @@
723
801
  "openingQuestion": "For each acceptance criterion — how does the test fail when the criterion is violated?",
724
802
  "failureMode": "If every test passes on the first run with no debugging, the tests weren't hard enough."
725
803
  },
726
- "canEdit": true
804
+ "canEdit": true,
805
+ "subscriptions": [
806
+ "coverage.drop",
807
+ "test.fail",
808
+ "test.flake"
809
+ ],
810
+ "docArtifacts": [
811
+ "qa-strategy",
812
+ "test-plan"
813
+ ]
727
814
  },
728
815
  {
729
816
  "internal": true,
@@ -762,7 +849,11 @@
762
849
  "openingQuestion": "Can I reproduce this deterministically, and what is the exact state at the point of failure?",
763
850
  "failureMode": "If you can't state the invariant that was violated, you haven't found root cause."
764
851
  },
765
- "canEdit": true
852
+ "canEdit": true,
853
+ "subscriptions": [
854
+ "hang.detected",
855
+ "regression.detected"
856
+ ]
766
857
  },
767
858
  {
768
859
  "internal": true,
@@ -811,6 +902,23 @@
811
902
  "engineer",
812
903
  "qa",
813
904
  "security"
905
+ ],
906
+ "subscriptions": [
907
+ "edit_loop.stuck",
908
+ "mcp.unhealthy.persistent",
909
+ "push_gate.fail",
910
+ "service.down"
911
+ ],
912
+ "docArtifacts": [
913
+ "incident-report",
914
+ "postmortem",
915
+ "runbook"
916
+ ],
917
+ "watchConditions": [
918
+ {
919
+ "watcher": "wide-blast-radius",
920
+ "reason": "wide blast radius — operational readiness review"
921
+ }
814
922
  ]
815
923
  },
816
924
  {
@@ -861,6 +969,10 @@
861
969
  "qa",
862
970
  "security",
863
971
  "sre"
972
+ ],
973
+ "subscriptions": [
974
+ "infra.change.requested",
975
+ "service.scale.event"
864
976
  ]
865
977
  },
866
978
  {
@@ -902,7 +1014,11 @@
902
1014
  "openingQuestion": "What data is being collected, stored, or processed, and do we have documented legal basis for each?",
903
1015
  "failureMode": "If the risk list is empty, you didn't check AI processing obligations or dependency licenses past layer one."
904
1016
  },
905
- "canEdit": false
1017
+ "canEdit": false,
1018
+ "subscriptions": [
1019
+ "dep.license",
1020
+ "privacy-policy.review"
1021
+ ]
906
1022
  },
907
1023
  {
908
1024
  "internal": true,
@@ -941,7 +1057,11 @@
941
1057
  "openingQuestion": "If this goes wrong 30 minutes after full rollout, what exactly do we do?",
942
1058
  "failureMode": "If the rollback procedure isn't tested, it doesn't exist. You'll find out during an incident."
943
1059
  },
944
- "canEdit": true
1060
+ "canEdit": true,
1061
+ "subscriptions": [
1062
+ "release.candidate",
1063
+ "version.bump.needed"
1064
+ ]
945
1065
  },
946
1066
  {
947
1067
  "internal": true,
@@ -982,7 +1102,17 @@
982
1102
  "openingQuestion": "What did we decide, why did we decide it, and where will the next person find it?",
983
1103
  "failureMode": "If the project context file hasn't been updated since the work started, something important wasn't captured."
984
1104
  },
985
- "canEdit": true
1105
+ "canEdit": true,
1106
+ "subscriptions": [
1107
+ "changelog.missing",
1108
+ "document.stale",
1109
+ "pr.merged.no-docs",
1110
+ "readme.stale"
1111
+ ],
1112
+ "docArtifacts": [
1113
+ "changelog",
1114
+ "memo"
1115
+ ]
986
1116
  },
987
1117
  {
988
1118
  "internal": true,
@@ -1025,7 +1155,16 @@
1025
1155
  "openingQuestion": "What is the user doing, what are they feeling, and what should the interface show them?",
1026
1156
  "failureMode": "If you don't have designed error and empty states, you have an incomplete design."
1027
1157
  },
1028
- "canEdit": true
1158
+ "canEdit": true,
1159
+ "subscriptions": [
1160
+ "design.requested"
1161
+ ],
1162
+ "watchConditions": [
1163
+ {
1164
+ "watcher": "visual-or-ui-risk",
1165
+ "reason": "visual deliverable / UI risk surface"
1166
+ }
1167
+ ]
1029
1168
  },
1030
1169
  {
1031
1170
  "internal": true,
@@ -1069,6 +1208,9 @@
1069
1208
  "legal-compliance",
1070
1209
  "qa",
1071
1210
  "ux-researcher"
1211
+ ],
1212
+ "subscriptions": [
1213
+ "a11y.violation"
1072
1214
  ]
1073
1215
  },
1074
1216
  {
@@ -1146,7 +1288,10 @@
1146
1288
  "openingQuestion": "Which agents have degraded since the last cycle, and what does the trace evidence say about why?",
1147
1289
  "failureMode": "If all agents look stable, you haven't checked variance and trend. Median hides deterioration."
1148
1290
  },
1149
- "canEdit": true
1291
+ "canEdit": true,
1292
+ "subscriptions": [
1293
+ "trace.anomaly"
1294
+ ]
1150
1295
  },
1151
1296
  {
1152
1297
  "internal": true,
@@ -1,83 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * env-check.mjs — SessionStart
4
- *
5
- * Reads .env.example from the project root, compares against .env and
6
- * process.env, and writes missing required vars to stdout so Claude sees
7
- * them at session open. Silent when everything is in order.
8
- *
9
- * @p95ms 20
10
- * @maxBlockingScope SessionStart
11
- */
12
- import { readFileSync, existsSync, realpathSync } from 'fs';
13
- import { fileURLToPath } from 'url';
14
-
15
- // Resolve symlinks immediately to avoid CJS/ESM loader edge cases during
16
- // rapid concurrent session-start hook invocations.
17
- try { realpathSync(fileURLToPath(import.meta.url)); } catch { /* non-critical */ }
18
- import { join } from 'path';
19
-
20
- let input = {};
21
- try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { /* no stdin */ }
22
-
23
- const cwd = input?.cwd || process.cwd();
24
-
25
- // Find .env.example — project root only (don't walk up, avoid false positives)
26
- const examplePath = join(cwd, '.env.example');
27
- if (!existsSync(examplePath)) process.exit(0);
28
-
29
- // Parse both files into key sets
30
- function parseEnvFile(path) {
31
- if (!existsSync(path)) return new Map();
32
- const map = new Map();
33
- const lines = readFileSync(path, 'utf8').split('\n');
34
- for (const line of lines) {
35
- const stripped = line.trim();
36
- if (!stripped || stripped.startsWith('#')) continue;
37
- const eq = stripped.indexOf('=');
38
- if (eq === -1) continue;
39
- const key = stripped.slice(0, eq).trim();
40
- const val = stripped.slice(eq + 1).trim();
41
- if (key) map.set(key, val);
42
- }
43
- return map;
44
- }
45
-
46
- const example = parseEnvFile(examplePath);
47
- if (example.size === 0) process.exit(0);
48
-
49
- const envFile = parseEnvFile(join(cwd, '.env'));
50
-
51
- // A key is "required" if its example value is a non-empty placeholder
52
- // (not already set to a real value — we don't block on keys with defaults)
53
- const PLACEHOLDER = /^(YOUR_|<|__|\$\{|REPLACE|ADD_|INSERT_|xxx|TODO)/i;
54
- const OPTIONAL_COMMENT = /#.*optional/i;
55
-
56
- function isRequired(key, exampleVal) {
57
- // If value looks like a real default (not a placeholder), it's optional
58
- if (!exampleVal) return true; // empty = required
59
- if (PLACEHOLDER.test(exampleVal)) return true;
60
- // If it has a real value in example, treat as optional with default
61
- return false;
62
- }
63
-
64
- const missing = [];
65
- for (const [key, exampleVal] of example) {
66
- if (!isRequired(key, exampleVal)) continue;
67
- // Check .env file first, then process.env
68
- const inEnvFile = envFile.has(key) && envFile.get(key) !== '' && !PLACEHOLDER.test(envFile.get(key));
69
- const inProcessEnv = process.env[key] && !PLACEHOLDER.test(process.env[key]);
70
- if (!inEnvFile && !inProcessEnv) {
71
- missing.push(key);
72
- }
73
- }
74
-
75
- if (missing.length === 0) process.exit(0);
76
-
77
- const noun = missing.length === 1 ? 'variable' : 'variables';
78
- const list = missing.map(k => ` - ${k}`).join('\n');
79
- process.stdout.write(
80
- `## Environment check — ${missing.length} required ${noun} not set\n${list}\nAdd these to .env before running the app.\n`
81
- );
82
-
83
- process.exit(0);
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * lib/hooks/read-tracker.mjs — Read tracker hook — tracks file reads for efficiency analysis.
4
- *
5
- * Runs as PostToolUse after Read tool calls. Logs each read to ~/.cx/read-log.json including file path, line count, and session timestamp for efficiency reporting.
6
- *
7
- * @p95ms 10
8
- * @maxBlockingScope none (PostToolUse, non-blocking)
9
- */
10
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
- import { createHash } from 'crypto';
12
- import { join, resolve } from 'path';
13
- import { homedir } from 'os';
14
- import { flushReadTrackerDeltas, recordReadDelta } from '../read-tracker-store.mjs';
15
-
16
- const CX_DIR = join(homedir(), '.cx');
17
- const HASH_STORE = join(CX_DIR, 'file-hashes.json');
18
- let input = {};
19
- try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
20
-
21
- if ((input?.tool_name || '') !== 'Read') process.exit(0);
22
-
23
- const rawPath = input?.tool_input?.file_path || '';
24
- if (!rawPath) process.exit(0);
25
-
26
- const absPath = rawPath.startsWith('/') ? rawPath : resolve(input?.cwd || process.cwd(), rawPath);
27
- if (!existsSync(absPath)) process.exit(0);
28
-
29
- try {
30
- const content = readFileSync(absPath, 'utf8');
31
- const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
32
- const nowIso = new Date().toISOString();
33
- const requestedLimit = Number(input?.tool_input?.limit || 0);
34
- const effectiveLimit = requestedLimit > 0 ? requestedLimit : 2000;
35
-
36
- mkdirSync(CX_DIR, { recursive: true });
37
- let store = {};
38
- try { store = JSON.parse(readFileSync(HASH_STORE, 'utf8')); } catch { /* fresh */ }
39
-
40
- store[absPath] = { hash, ts: nowIso, size: content.length };
41
-
42
- // Prune entries older than 2 hours
43
- const cutoff = Date.now() - 2 * 60 * 60 * 1000;
44
- for (const [k, v] of Object.entries(store)) {
45
- if (new Date(v.ts).getTime() < cutoff) delete store[k];
46
- }
47
-
48
- writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
49
- recordReadDelta({
50
- path: absPath,
51
- size: content.length,
52
- limit: effectiveLimit,
53
- ts: nowIso,
54
- }, process.env);
55
-
56
- if (process.env.CONSTRUCT_READ_TRACKER_FLUSH === '1') {
57
- flushReadTrackerDeltas({ nowIso, env: process.env });
58
- }
59
- } catch { /* best effort */ }
60
-
61
- process.exit(0);
@@ -1,96 +0,0 @@
1
- // lib/policy/unified-gates.mjs
2
- // Single policy declaration for all enforcement layers
3
-
4
- export const POLICIES = {
5
- commentStyle: {
6
- id: 'comment-style',
7
- description: 'Comments must follow project standards',
8
- layers: ['write', 'commit', 'ci'],
9
- bypass: 'CONSTRUCT_SKIP_COMMENT_STYLE',
10
- critical: false,
11
- },
12
- docCoupling: {
13
- id: 'doc-coupling',
14
- description: 'Code changes require documentation updates',
15
- layers: ['commit', 'push'],
16
- bypass: 'CONSTRUCT_SKIP_DOCS',
17
- critical: false,
18
- },
19
- secretScan: {
20
- id: 'secret-scan',
21
- description: 'No secrets in committed code',
22
- layers: ['commit', 'push', 'ci'],
23
- bypass: 'CONSTRUCT_SKIP_SECRET_SCAN',
24
- critical: true,
25
- },
26
- ciStatus: {
27
- id: 'ci-status',
28
- description: 'CI must be green before push',
29
- layers: ['push', 'session-end'],
30
- bypass: 'CONSTRUCT_SKIP_CI_CHECK',
31
- critical: true,
32
- },
33
- };
34
-
35
- export class UnifiedGateEngine {
36
- constructor(options = {}) {
37
- this.env = options.env || process.env;
38
- }
39
-
40
- isBypassed(policyId) {
41
- const policy = POLICIES[policyId];
42
- if (!policy) return { bypassed: false };
43
-
44
- if (policy.bypass && this.env[policy.bypass] === '1') {
45
- return { bypassed: true, envVar: policy.bypass };
46
- }
47
-
48
- if (this.env.CONSTRUCT_SKIP_ALL_GATES === '1') {
49
- return { bypassed: true, envVar: 'CONSTRUCT_SKIP_ALL_GATES' };
50
- }
51
-
52
- return { bypassed: false };
53
- }
54
-
55
- async evaluateLayer(layer, context = {}) {
56
- const results = [];
57
-
58
- for (const [id, policy] of Object.entries(POLICIES)) {
59
- if (policy.layers.includes(layer)) {
60
- const bypass = this.isBypassed(id);
61
- results.push({
62
- policy: id,
63
- passed: bypass.bypassed || true,
64
- bypassed: bypass.bypassed,
65
- bypassEnvVar: bypass.envVar,
66
- critical: policy.critical,
67
- });
68
- }
69
- }
70
-
71
- const failed = results.filter(r => !r.passed && r.critical);
72
-
73
- return {
74
- layer,
75
- passed: failed.length === 0,
76
- canProceed: failed.length === 0,
77
- results,
78
- summary: `${results.filter(r => r.passed).length}/${results.length} passed`,
79
- };
80
- }
81
- }
82
-
83
- export async function checkGates(layer, context = {}, options = {}) {
84
- const engine = new UnifiedGateEngine(options);
85
- return await engine.evaluateLayer(layer, context);
86
- }
87
-
88
- export function listPolicies() {
89
- return Object.entries(POLICIES).map(([id, policy]) => ({
90
- id,
91
- description: policy.description,
92
- layers: policy.layers,
93
- bypass: policy.bypass,
94
- critical: policy.critical,
95
- }));
96
- }