sinapse-ai 1.6.1 → 1.8.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/.claude/CLAUDE.md +5 -11
- package/.claude/hooks/README.md +14 -1
- package/.claude/hooks/code-intel-pretool.cjs +115 -0
- package/.claude/hooks/enforce-delegation.cjs +31 -3
- package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
- package/.claude/hooks/enforce-permission-mode.cjs +249 -0
- package/.claude/hooks/secret-scanning.cjs +34 -43
- package/.claude/hooks/synapse-engine.cjs +23 -23
- package/.claude/hooks/telemetry-post-tool.cjs +128 -0
- package/.claude/hooks/telemetry-stop.cjs +132 -0
- package/.claude/hooks/verify-packages.cjs +9 -2
- package/.claude/rules/documentation-first.md +1 -1
- package/.claude/rules/hook-governance.md +2 -0
- package/.sinapse-ai/cli/commands/health/index.js +24 -0
- package/.sinapse-ai/core/README.md +11 -0
- package/.sinapse-ai/core/config/config-loader.js +19 -0
- package/.sinapse-ai/core/config/merge-utils.js +8 -0
- package/.sinapse-ai/core/errors/constants.js +147 -0
- package/.sinapse-ai/core/errors/error-registry.js +176 -0
- package/.sinapse-ai/core/errors/index.js +50 -0
- package/.sinapse-ai/core/errors/serializer.js +147 -0
- package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
- package/.sinapse-ai/core/errors/utils.js +187 -0
- package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
- package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
- package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
- package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
- package/.sinapse-ai/core/execution/wave-executor.js +4 -1
- package/.sinapse-ai/core/grounding/README.md +71 -11
- package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
- package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
- package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
- package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
- package/.sinapse-ai/core/health-check/healers/index.js +40 -3
- package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
- package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
- package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
- package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
- package/.sinapse-ai/core/ids/index.js +30 -0
- package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
- package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
- package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
- package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
- package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
- package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
- package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
- package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
- package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
- package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
- package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
- package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
- package/.sinapse-ai/core/registry/registry-loader.js +71 -5
- package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
- package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
- package/.sinapse-ai/core/synapse/context/index.js +19 -0
- package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
- package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
- package/.sinapse-ai/core/synapse/engine.js +43 -3
- package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
- package/.sinapse-ai/core/utils/output-formatter.js +8 -290
- package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
- package/.sinapse-ai/core-config.yaml +68 -1
- package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
- package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
- package/.sinapse-ai/development/agents/developer.md +2 -0
- package/.sinapse-ai/development/agents/devops.md +9 -0
- package/.sinapse-ai/development/external-executors/README.md +18 -0
- package/.sinapse-ai/development/external-executors/codex.md +56 -0
- package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
- package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
- package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
- package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
- package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
- package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
- package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
- package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
- package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
- package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
- package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
- package/.sinapse-ai/install-manifest.yaml +218 -114
- package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
- package/.sinapse-ai/scripts/pm.sh +18 -6
- package/bin/cli.js +17 -0
- package/bin/commands/agents.js +96 -0
- package/bin/commands/doctor.js +15 -0
- package/bin/commands/ideate.js +129 -0
- package/bin/commands/uninstall.js +40 -0
- package/bin/postinstall.js +50 -4
- package/bin/sinapse.js +146 -2
- package/bin/utils/secret-scanner-core.js +253 -0
- package/bin/utils/staged-secret-scan.js +106 -40
- package/docs/framework/collaboration-autonomy-plan.md +18 -18
- package/docs/guides/parallel-workflow.md +6 -6
- package/package.json +22 -5
- package/packages/installer/src/installer/git-hooks-installer.js +384 -0
- package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
- package/packages/installer/src/wizard/ide-config-generator.js +23 -0
- package/packages/installer/src/wizard/validators.js +38 -1
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
- package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
- package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
- package/scripts/eval-runner.js +422 -0
- package/scripts/generate-install-manifest.js +13 -9
- package/scripts/generate-synapse-runtime.js +51 -0
- package/scripts/regenerate-orqx-stubs.ps1 +6 -5
- package/scripts/validate-all.js +1 -0
- package/scripts/validate-evals.js +466 -0
- package/scripts/validate-schemas.js +539 -0
- package/scripts/validate-squad-orqx.js +9 -2
- package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
- package/squads/squad-brand/templates/client-delivery-template.md +1 -1
- package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
- package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
- package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
- package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
- package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
- package/docs/chrome-brain-upgrade-plan.md +0 -624
- package/docs/constitution-compliance.md +0 -87
- package/docs/mega-upgrade-orchestration-plan.md +0 -71
- package/docs/research-synthesis-for-upgrade.md +0 -511
- package/docs/security-audit-report.md +0 -306
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Eval Harness — Camada-1 LINTER (Structural Contract Validator)
|
|
6
|
+
*
|
|
7
|
+
* Deterministic CI gate. Validates EVERY eval contract under tests/evals/<gate>/:
|
|
8
|
+
* - evals.json : gateId/phase/cases shape + per-case + per-expectation rules.
|
|
9
|
+
* - triggers.json: positives[]/negatives[] non-empty, no overlap.
|
|
10
|
+
*
|
|
11
|
+
* Single source of truth for predicate kinds + arg requirements:
|
|
12
|
+
* tests/evals/_lib/predicates.js (PREDICATE_KINDS, ARG_REQUIRED).
|
|
13
|
+
* The closed kind set is NEVER duplicated here — it is imported.
|
|
14
|
+
*
|
|
15
|
+
* Hard guarantees:
|
|
16
|
+
* - Node puro, Windows-nativo, zero NEW dependency.
|
|
17
|
+
* - The >=3-expectations check and the kind-in-closed-set check are EXPLICIT
|
|
18
|
+
* in this code (not delegated to the JSON Schema), per contract.
|
|
19
|
+
* - The tests/evals/_lib/eval-contract.schema.json is used as an OPTIONAL
|
|
20
|
+
* reference layer when a draft-07 validator (ajv) is already installed.
|
|
21
|
+
* If ajv is absent, validation proceeds fully via the explicit checks.
|
|
22
|
+
*
|
|
23
|
+
* EXIT CODES:
|
|
24
|
+
* 0 -> all contracts valid.
|
|
25
|
+
* 1 -> at least one violation (each printed with file path + actionable fix).
|
|
26
|
+
* 2 -> harness/internal error (e.g. predicates.js missing/unreadable).
|
|
27
|
+
*
|
|
28
|
+
* Wired into package.json as:
|
|
29
|
+
* "validate:evals": "node scripts/validate-evals.js"
|
|
30
|
+
*
|
|
31
|
+
* @module scripts/validate-evals
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Paths
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
42
|
+
const EVALS_ROOT = path.join(REPO_ROOT, 'tests', 'evals');
|
|
43
|
+
const PREDICATES_PATH = path.join(EVALS_ROOT, '_lib', 'predicates.js');
|
|
44
|
+
const SCHEMA_PATH = path.join(EVALS_ROOT, '_lib', 'eval-contract.schema.json');
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Single source of truth — closed predicate set imported from the foundation.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
let PREDICATE_KINDS;
|
|
51
|
+
let ARG_REQUIRED;
|
|
52
|
+
try {
|
|
53
|
+
const predicates = require(PREDICATES_PATH);
|
|
54
|
+
PREDICATE_KINDS = predicates.PREDICATE_KINDS;
|
|
55
|
+
ARG_REQUIRED = predicates.ARG_REQUIRED;
|
|
56
|
+
if (!Array.isArray(PREDICATE_KINDS) || PREDICATE_KINDS.length === 0) {
|
|
57
|
+
throw new Error('PREDICATE_KINDS is not a non-empty array');
|
|
58
|
+
}
|
|
59
|
+
if (!ARG_REQUIRED || typeof ARG_REQUIRED !== 'object') {
|
|
60
|
+
throw new Error('ARG_REQUIRED is not an object');
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
'[validate-evals] FATAL: could not load the closed predicate set from\n' +
|
|
65
|
+
` ${PREDICATES_PATH}\n` +
|
|
66
|
+
` reason: ${err.message}\n` +
|
|
67
|
+
' The foundation predicate library must exist and export PREDICATE_KINDS + ARG_REQUIRED.\n',
|
|
68
|
+
);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const KIND_SET = new Set(PREDICATE_KINDS);
|
|
73
|
+
|
|
74
|
+
// Kinds whose required arg is an INTEGER vs a non-empty STRING (regex source).
|
|
75
|
+
// Derived from the foundation contract; only applies to ARG_REQUIRED kinds.
|
|
76
|
+
const INT_ARG_KINDS = new Set(['opportunities_min', 'execution_under_ms']);
|
|
77
|
+
const STR_ARG_KINDS = new Set(['warning_matches', 'correction_prompt_matches']);
|
|
78
|
+
|
|
79
|
+
// Phase enum (mirrors GateEvaluator phases + schema). Used for triggers + evals.
|
|
80
|
+
const PHASE_ENUM = ['epic_creation', 'story_creation', 'story_validation', '2_development'];
|
|
81
|
+
const PHASE_SET = new Set(PHASE_ENUM);
|
|
82
|
+
const GATE_ID_RE = /^G[1-9][0-9]*$/;
|
|
83
|
+
const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Violation collection
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/** @type {Array<{file:string, message:string}>} */
|
|
90
|
+
const violations = [];
|
|
91
|
+
|
|
92
|
+
function addViolation(file, message) {
|
|
93
|
+
violations.push({ file, message });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Optional Ajv reference layer (only if already installed — zero new dep).
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
let ajvLayer = null;
|
|
101
|
+
try {
|
|
102
|
+
ajvLayer = buildAjvLayer();
|
|
103
|
+
} catch (_err) {
|
|
104
|
+
ajvLayer = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildAjvLayer() {
|
|
108
|
+
const Ajv = require('ajv');
|
|
109
|
+
if (!fs.existsSync(SCHEMA_PATH)) return null;
|
|
110
|
+
const schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
111
|
+
// strict:false silences Ajv 8 strictTypes noise on the union-typed `arg`
|
|
112
|
+
// (the per-kind allOf already narrows it); explicit checks remain authoritative.
|
|
113
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
114
|
+
ajv.addSchema(schema, 'eval-contract');
|
|
115
|
+
const ref = (def) => ajv.compile({ $ref: `eval-contract#/definitions/${def}` });
|
|
116
|
+
return { validateEval: ref('EvalFile'), validateTriggers: ref('TriggersFile') };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ajvErrorsToLines(errors) {
|
|
120
|
+
if (!errors || !errors.length) return [];
|
|
121
|
+
return errors.map((e) => {
|
|
122
|
+
const where = e.instancePath || e.dataPath || '(root)';
|
|
123
|
+
return `schema: ${where} ${e.message}`;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Filesystem helpers
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
function readJson(file) {
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { ok: false, error: `cannot read file: ${err.message}` };
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return { ok: true, data: JSON.parse(raw) };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return { ok: false, error: `invalid JSON: ${err.message}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isPlainObject(v) {
|
|
146
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Discover gate folders under tests/evals/ (every dir except _lib / _*). */
|
|
150
|
+
function discoverGateDirs() {
|
|
151
|
+
let entries;
|
|
152
|
+
try {
|
|
153
|
+
entries = fs.readdirSync(EVALS_ROOT, { withFileTypes: true });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
process.stderr.write(`[validate-evals] FATAL: cannot read ${EVALS_ROOT}: ${err.message}\n`);
|
|
156
|
+
process.exit(2);
|
|
157
|
+
}
|
|
158
|
+
return entries
|
|
159
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('_'))
|
|
160
|
+
.map((d) => path.join(EVALS_ROOT, d.name))
|
|
161
|
+
.sort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Structural validators (EXPLICIT — these are the contract, not the schema).
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
function validateExpectation(file, caseId, caseIndex, exp, expIndex, seenExpIds) {
|
|
169
|
+
const loc = `case[${caseIndex}] '${caseId}' -> expectation[${expIndex}]`;
|
|
170
|
+
|
|
171
|
+
if (!isPlainObject(exp)) {
|
|
172
|
+
addViolation(file, `${loc}: must be an object.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// id
|
|
177
|
+
if (typeof exp.id !== 'string' || exp.id.length === 0) {
|
|
178
|
+
addViolation(file, `${loc}: missing/empty 'id' (string required, unique within the case).`);
|
|
179
|
+
} else if (seenExpIds.has(exp.id)) {
|
|
180
|
+
addViolation(file, `${loc}: duplicate expectation id '${exp.id}' within case '${caseId}'.`);
|
|
181
|
+
} else {
|
|
182
|
+
seenExpIds.add(exp.id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// kind — EXPLICIT closed-set check against the foundation.
|
|
186
|
+
if (typeof exp.kind !== 'string' || exp.kind.length === 0) {
|
|
187
|
+
addViolation(file, `${loc}: missing 'kind' (string required).`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!KIND_SET.has(exp.kind)) {
|
|
191
|
+
addViolation(
|
|
192
|
+
file,
|
|
193
|
+
`${loc}: unknown predicate kind '${exp.kind}'. Allowed (closed set from predicates.js): ${PREDICATE_KINDS.join(', ')}.`,
|
|
194
|
+
);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// arg — required + typed per the foundation's ARG_REQUIRED contract.
|
|
199
|
+
const needsArg = ARG_REQUIRED[exp.kind] === true;
|
|
200
|
+
const hasArg = exp.arg !== undefined;
|
|
201
|
+
|
|
202
|
+
if (needsArg && !hasArg) {
|
|
203
|
+
addViolation(
|
|
204
|
+
file,
|
|
205
|
+
`${loc}: predicate '${exp.kind}' requires an 'arg' field${
|
|
206
|
+
INT_ARG_KINDS.has(exp.kind) ? ' (integer >= 0)' : STR_ARG_KINDS.has(exp.kind) ? ' (non-empty regex string)' : ''
|
|
207
|
+
}.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (needsArg && hasArg) {
|
|
212
|
+
if (INT_ARG_KINDS.has(exp.kind)) {
|
|
213
|
+
if (typeof exp.arg !== 'number' || !Number.isInteger(exp.arg) || exp.arg < 0) {
|
|
214
|
+
addViolation(
|
|
215
|
+
file,
|
|
216
|
+
`${loc}: predicate '${exp.kind}' arg must be an integer >= 0 (got ${JSON.stringify(exp.arg)}).`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} else if (STR_ARG_KINDS.has(exp.kind)) {
|
|
220
|
+
if (typeof exp.arg !== 'string' || exp.arg.length === 0) {
|
|
221
|
+
addViolation(
|
|
222
|
+
file,
|
|
223
|
+
`${loc}: predicate '${exp.kind}' arg must be a non-empty regex string (got ${JSON.stringify(exp.arg)}).`,
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
// Best-effort: ensure the regex source compiles (data only — new RegExp).
|
|
227
|
+
try {
|
|
228
|
+
|
|
229
|
+
new RegExp(exp.arg);
|
|
230
|
+
} catch (reErr) {
|
|
231
|
+
addViolation(
|
|
232
|
+
file,
|
|
233
|
+
`${loc}: predicate '${exp.kind}' arg is not a valid regex: ${reErr.message} (source: ${JSON.stringify(exp.arg)}).`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// arg present but NOT expected -> warn-as-violation (keeps contract tight).
|
|
241
|
+
if (!needsArg && hasArg) {
|
|
242
|
+
addViolation(
|
|
243
|
+
file,
|
|
244
|
+
`${loc}: predicate '${exp.kind}' does not accept an 'arg' field — remove it (got ${JSON.stringify(exp.arg)}).`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function validateCase(file, caseObj, caseIndex, seenCaseIds) {
|
|
250
|
+
if (!isPlainObject(caseObj)) {
|
|
251
|
+
addViolation(file, `case[${caseIndex}]: must be an object.`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// id (kebab-case, unique)
|
|
256
|
+
const caseId = typeof caseObj.id === 'string' ? caseObj.id : `<index ${caseIndex}>`;
|
|
257
|
+
if (typeof caseObj.id !== 'string' || caseObj.id.length === 0) {
|
|
258
|
+
addViolation(file, `case[${caseIndex}]: missing 'id' (kebab-case string required).`);
|
|
259
|
+
} else if (!KEBAB_RE.test(caseObj.id)) {
|
|
260
|
+
addViolation(file, `case[${caseIndex}] '${caseObj.id}': 'id' must be kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$).`);
|
|
261
|
+
} else if (seenCaseIds.has(caseObj.id)) {
|
|
262
|
+
addViolation(file, `case[${caseIndex}] '${caseObj.id}': duplicate case id within the file.`);
|
|
263
|
+
} else {
|
|
264
|
+
seenCaseIds.add(caseObj.id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// context (object, passed verbatim to gate.verify)
|
|
268
|
+
if (!isPlainObject(caseObj.context)) {
|
|
269
|
+
addViolation(file, `case[${caseIndex}] '${caseId}': missing 'context' (object required — passed to gate.verify()).`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// expectations (array, >= 3 — EXPLICIT)
|
|
273
|
+
if (!Array.isArray(caseObj.expectations)) {
|
|
274
|
+
addViolation(file, `case[${caseIndex}] '${caseId}': missing 'expectations' (array required).`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (caseObj.expectations.length < 3) {
|
|
278
|
+
addViolation(
|
|
279
|
+
file,
|
|
280
|
+
`case[${caseIndex}] '${caseId}': needs at least 3 expectations (has ${caseObj.expectations.length}). Add ${
|
|
281
|
+
3 - caseObj.expectations.length
|
|
282
|
+
} more.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const seenExpIds = new Set();
|
|
287
|
+
caseObj.expectations.forEach((exp, i) => validateExpectation(file, caseId, caseIndex, exp, i, seenExpIds));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function validateEvalFile(file, data) {
|
|
291
|
+
if (!isPlainObject(data)) {
|
|
292
|
+
addViolation(file, 'root must be a JSON object.');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// gateId
|
|
297
|
+
if (typeof data.gateId !== 'string' || data.gateId.length === 0) {
|
|
298
|
+
addViolation(file, 'missing \'gateId\' (string required, e.g. "G5").');
|
|
299
|
+
} else if (!GATE_ID_RE.test(data.gateId)) {
|
|
300
|
+
addViolation(file, `'gateId' must match ^G[1-9][0-9]*$ (got ${JSON.stringify(data.gateId)}).`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// phase
|
|
304
|
+
if (typeof data.phase !== 'string' || data.phase.length === 0) {
|
|
305
|
+
addViolation(file, 'missing \'phase\' (string required).');
|
|
306
|
+
} else if (!PHASE_SET.has(data.phase)) {
|
|
307
|
+
addViolation(file, `'phase' must be one of: ${PHASE_ENUM.join(', ')} (got ${JSON.stringify(data.phase)}).`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// cases (array >= 1)
|
|
311
|
+
if (!Array.isArray(data.cases)) {
|
|
312
|
+
addViolation(file, 'missing \'cases\' (array required).');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (data.cases.length < 1) {
|
|
316
|
+
addViolation(file, '\'cases\' must contain at least 1 case (has 0).');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const seenCaseIds = new Set();
|
|
320
|
+
data.cases.forEach((c, i) => validateCase(file, c, i, seenCaseIds));
|
|
321
|
+
|
|
322
|
+
// Optional Ajv reference layer.
|
|
323
|
+
if (ajvLayer && ajvLayer.validateEval) {
|
|
324
|
+
const okAjv = ajvLayer.validateEval(data);
|
|
325
|
+
if (!okAjv) {
|
|
326
|
+
ajvErrorsToLines(ajvLayer.validateEval.errors).forEach((line) => addViolation(file, line));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function validateTriggersFile(file, data) {
|
|
332
|
+
if (!isPlainObject(data)) {
|
|
333
|
+
addViolation(file, 'root must be a JSON object.');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// gateId
|
|
338
|
+
if (typeof data.gateId !== 'string' || data.gateId.length === 0) {
|
|
339
|
+
addViolation(file, 'missing \'gateId\' (string required, e.g. "G5").');
|
|
340
|
+
} else if (!GATE_ID_RE.test(data.gateId)) {
|
|
341
|
+
addViolation(file, `'gateId' must match ^G[1-9][0-9]*$ (got ${JSON.stringify(data.gateId)}).`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const checkPhaseArray = (key, requireNonEmpty) => {
|
|
345
|
+
const arr = data[key];
|
|
346
|
+
if (!Array.isArray(arr)) {
|
|
347
|
+
addViolation(file, `missing '${key}' (array of phases required).`);
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
if (requireNonEmpty && arr.length === 0) {
|
|
351
|
+
addViolation(file, `'${key}' must be non-empty (at least 1 phase).`);
|
|
352
|
+
}
|
|
353
|
+
arr.forEach((p, i) => {
|
|
354
|
+
if (typeof p !== 'string' || !PHASE_SET.has(p)) {
|
|
355
|
+
addViolation(
|
|
356
|
+
file,
|
|
357
|
+
`'${key}[${i}]' must be one of: ${PHASE_ENUM.join(', ')} (got ${JSON.stringify(p)}).`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
return arr;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const positives = checkPhaseArray('positives', true);
|
|
365
|
+
// Contract: both non-empty. Schema only requires positives minItems 1, but the
|
|
366
|
+
// task spec requires negatives non-empty too -> enforce explicitly.
|
|
367
|
+
const negatives = checkPhaseArray('negatives', true);
|
|
368
|
+
|
|
369
|
+
// No overlap between positives and negatives.
|
|
370
|
+
const posSet = new Set(positives.filter((p) => typeof p === 'string'));
|
|
371
|
+
const overlap = negatives.filter((p) => typeof p === 'string' && posSet.has(p));
|
|
372
|
+
if (overlap.length > 0) {
|
|
373
|
+
addViolation(
|
|
374
|
+
file,
|
|
375
|
+
`'positives' and 'negatives' overlap on: ${[...new Set(overlap)].join(', ')}. A phase cannot be both.`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Optional Ajv reference layer.
|
|
380
|
+
if (ajvLayer && ajvLayer.validateTriggers) {
|
|
381
|
+
const okAjv = ajvLayer.validateTriggers(data);
|
|
382
|
+
if (!okAjv) {
|
|
383
|
+
ajvErrorsToLines(ajvLayer.validateTriggers.errors).forEach((line) => addViolation(file, line));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Main
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
function rel(p) {
|
|
393
|
+
return path.relative(REPO_ROOT, p).split(path.sep).join('/');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function main() {
|
|
397
|
+
const gateDirs = discoverGateDirs();
|
|
398
|
+
|
|
399
|
+
if (gateDirs.length === 0) {
|
|
400
|
+
process.stdout.write(`[validate-evals] No gate folders found under ${rel(EVALS_ROOT)}/ (nothing to validate).\n`);
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let filesChecked = 0;
|
|
405
|
+
|
|
406
|
+
for (const dir of gateDirs) {
|
|
407
|
+
const evalsFile = path.join(dir, 'evals.json');
|
|
408
|
+
const triggersFile = path.join(dir, 'triggers.json');
|
|
409
|
+
|
|
410
|
+
// evals.json — required per gate folder.
|
|
411
|
+
if (!fs.existsSync(evalsFile)) {
|
|
412
|
+
addViolation(rel(evalsFile), `missing required file 'evals.json' in gate folder ${rel(dir)}/.`);
|
|
413
|
+
} else {
|
|
414
|
+
filesChecked += 1;
|
|
415
|
+
const parsed = readJson(evalsFile);
|
|
416
|
+
if (!parsed.ok) {
|
|
417
|
+
addViolation(rel(evalsFile), parsed.error);
|
|
418
|
+
} else {
|
|
419
|
+
validateEvalFile(rel(evalsFile), parsed.data);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// triggers.json — required per gate folder.
|
|
424
|
+
if (!fs.existsSync(triggersFile)) {
|
|
425
|
+
addViolation(rel(triggersFile), `missing required file 'triggers.json' in gate folder ${rel(dir)}/.`);
|
|
426
|
+
} else {
|
|
427
|
+
filesChecked += 1;
|
|
428
|
+
const parsed = readJson(triggersFile);
|
|
429
|
+
if (!parsed.ok) {
|
|
430
|
+
addViolation(rel(triggersFile), parsed.error);
|
|
431
|
+
} else {
|
|
432
|
+
validateTriggersFile(rel(triggersFile), parsed.data);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Report.
|
|
438
|
+
const refLayer = ajvLayer ? 'ajv (draft-07) reference layer: ON' : 'ajv reference layer: OFF (explicit checks only)';
|
|
439
|
+
process.stdout.write(
|
|
440
|
+
`[validate-evals] gate folders: ${gateDirs.length} | files checked: ${filesChecked} | ${refLayer}\n`,
|
|
441
|
+
);
|
|
442
|
+
process.stdout.write(`[validate-evals] predicate kinds (closed set, ${PREDICATE_KINDS.length}): ${PREDICATE_KINDS.join(', ')}\n`);
|
|
443
|
+
|
|
444
|
+
if (violations.length === 0) {
|
|
445
|
+
process.stdout.write('[validate-evals] OK — all eval contracts are structurally valid.\n');
|
|
446
|
+
process.exit(0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
process.stdout.write(`\n[validate-evals] FAIL — ${violations.length} violation(s):\n`);
|
|
450
|
+
// Group by file for readability.
|
|
451
|
+
const byFile = new Map();
|
|
452
|
+
for (const v of violations) {
|
|
453
|
+
if (!byFile.has(v.file)) byFile.set(v.file, []);
|
|
454
|
+
byFile.get(v.file).push(v.message);
|
|
455
|
+
}
|
|
456
|
+
for (const [file, msgs] of byFile) {
|
|
457
|
+
process.stdout.write(`\n ${file}\n`);
|
|
458
|
+
for (const m of msgs) {
|
|
459
|
+
process.stdout.write(` - ${m}\n`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
process.stdout.write('\n');
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
main();
|