@nforma.ai/nforma 0.2.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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation module for debt entries and ledger
|
|
3
|
+
* Implements runtime validation against debt.schema.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const VALID_STATUSES = ['open', 'acknowledged', 'resolving', 'resolved'];
|
|
7
|
+
const VALID_ENVIRONMENTS = ['production', 'staging', 'development', 'test', 'local'];
|
|
8
|
+
const VALID_SOURCE_TYPES = ['github', 'sentry', 'sentry-feedback', 'prometheus', 'grafana', 'logstash', 'bash'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a string is valid ISO8601 date-time format
|
|
12
|
+
* @param {string} dateStr - String to validate
|
|
13
|
+
* @returns {boolean} true if valid ISO8601
|
|
14
|
+
*/
|
|
15
|
+
function isValidISO8601(dateStr) {
|
|
16
|
+
if (typeof dateStr !== 'string') return false;
|
|
17
|
+
// Check basic ISO8601 format: YYYY-MM-DDTHH:MM:SSZ or with timezone offset
|
|
18
|
+
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
19
|
+
if (!iso8601Regex.test(dateStr)) return false;
|
|
20
|
+
// Also verify it's a valid date
|
|
21
|
+
const date = new Date(dateStr);
|
|
22
|
+
return !isNaN(date.getTime());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate a debt entry object
|
|
27
|
+
* @param {object} entry - Debt entry to validate
|
|
28
|
+
* @returns {boolean|string[]} true if valid, or array of error strings if invalid
|
|
29
|
+
*/
|
|
30
|
+
function validateDebtEntry(entry) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
|
|
33
|
+
// Type check: must be object
|
|
34
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
35
|
+
return ['entry must be an object'];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check required fields: id
|
|
39
|
+
if (!entry.id || typeof entry.id !== 'string') {
|
|
40
|
+
errors.push('id required (string)');
|
|
41
|
+
} else {
|
|
42
|
+
// Validate id pattern: UUID v4 format
|
|
43
|
+
const idPattern = /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/;
|
|
44
|
+
if (!idPattern.test(entry.id)) {
|
|
45
|
+
errors.push('id must match UUID format');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check required fields: fingerprint
|
|
50
|
+
if (!entry.fingerprint || typeof entry.fingerprint !== 'string') {
|
|
51
|
+
errors.push('fingerprint required (string)');
|
|
52
|
+
} else {
|
|
53
|
+
// Validate fingerprint pattern: 16-64 hex chars
|
|
54
|
+
const fpPattern = /^[a-z0-9]{16,64}$/;
|
|
55
|
+
if (!fpPattern.test(entry.fingerprint)) {
|
|
56
|
+
errors.push('fingerprint must be 16-64 lowercase hex characters');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check required fields: title
|
|
61
|
+
if (!entry.title || typeof entry.title !== 'string' || entry.title.length < 1) {
|
|
62
|
+
errors.push('title required (non-empty string)');
|
|
63
|
+
} else if (entry.title.length > 256) {
|
|
64
|
+
errors.push('title must be <= 256 characters');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check required fields: occurrences
|
|
68
|
+
if (typeof entry.occurrences !== 'number' || entry.occurrences < 1) {
|
|
69
|
+
errors.push('occurrences required (integer >= 1)');
|
|
70
|
+
} else if (!Number.isInteger(entry.occurrences)) {
|
|
71
|
+
errors.push('occurrences must be an integer');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check required fields: first_seen
|
|
75
|
+
if (!entry.first_seen) {
|
|
76
|
+
errors.push('first_seen required (ISO8601 string)');
|
|
77
|
+
} else if (!isValidISO8601(entry.first_seen)) {
|
|
78
|
+
errors.push('first_seen must be ISO8601 format');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check required fields: last_seen
|
|
82
|
+
if (!entry.last_seen) {
|
|
83
|
+
errors.push('last_seen required (ISO8601 string)');
|
|
84
|
+
} else if (!isValidISO8601(entry.last_seen)) {
|
|
85
|
+
errors.push('last_seen must be ISO8601 format');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check timestamp ordering: last_seen >= first_seen
|
|
89
|
+
if (entry.first_seen && entry.last_seen && isValidISO8601(entry.first_seen) && isValidISO8601(entry.last_seen)) {
|
|
90
|
+
const firstDate = new Date(entry.first_seen);
|
|
91
|
+
const lastDate = new Date(entry.last_seen);
|
|
92
|
+
if (lastDate < firstDate) {
|
|
93
|
+
errors.push('last_seen must be >= first_seen');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check required fields: environments
|
|
98
|
+
if (!Array.isArray(entry.environments) || entry.environments.length === 0) {
|
|
99
|
+
errors.push('environments required (non-empty array)');
|
|
100
|
+
} else {
|
|
101
|
+
// Validate each environment value
|
|
102
|
+
for (const env of entry.environments) {
|
|
103
|
+
if (typeof env !== 'string' || !VALID_ENVIRONMENTS.includes(env)) {
|
|
104
|
+
errors.push(`invalid environment value: ${env} (must be one of: ${VALID_ENVIRONMENTS.join(', ')})`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check required fields: status
|
|
110
|
+
if (!entry.status || typeof entry.status !== 'string') {
|
|
111
|
+
errors.push('status required (string)');
|
|
112
|
+
} else if (!VALID_STATUSES.includes(entry.status)) {
|
|
113
|
+
errors.push(`status must be one of: ${VALID_STATUSES.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check required fields: source_entries
|
|
117
|
+
if (!Array.isArray(entry.source_entries) || entry.source_entries.length === 0) {
|
|
118
|
+
errors.push('source_entries required (non-empty array)');
|
|
119
|
+
} else {
|
|
120
|
+
// Validate each source entry
|
|
121
|
+
for (let i = 0; i < entry.source_entries.length; i++) {
|
|
122
|
+
const se = entry.source_entries[i];
|
|
123
|
+
if (typeof se !== 'object' || se === null) {
|
|
124
|
+
errors.push(`source_entries[${i}] must be an object`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!se.source_type || typeof se.source_type !== 'string' || !VALID_SOURCE_TYPES.includes(se.source_type)) {
|
|
128
|
+
errors.push(`source_entries[${i}].source_type required and must be one of: ${VALID_SOURCE_TYPES.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
if (!se.source_id || typeof se.source_id !== 'string' || se.source_id.length === 0) {
|
|
131
|
+
errors.push(`source_entries[${i}].source_id required (non-empty string)`);
|
|
132
|
+
}
|
|
133
|
+
if (!se.observed_at || !isValidISO8601(se.observed_at)) {
|
|
134
|
+
errors.push(`source_entries[${i}].observed_at required (ISO8601 format)`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check optional fields: formal_ref
|
|
140
|
+
if (entry.hasOwnProperty('formal_ref')) {
|
|
141
|
+
if (entry.formal_ref !== null && typeof entry.formal_ref !== 'string') {
|
|
142
|
+
errors.push('formal_ref must be string or null');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check optional fields: formal_ref_source
|
|
147
|
+
if (entry.hasOwnProperty('formal_ref_source')) {
|
|
148
|
+
const validSources = ['manual', 'auto-detect', 'spec-inferred'];
|
|
149
|
+
if (entry.formal_ref_source !== null &&
|
|
150
|
+
(typeof entry.formal_ref_source !== 'string' || !validSources.includes(entry.formal_ref_source))) {
|
|
151
|
+
errors.push('formal_ref_source must be "manual", "auto-detect", "spec-inferred", or null');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check for additional properties (additionalProperties: false)
|
|
156
|
+
const allowedProps = new Set([
|
|
157
|
+
'id', 'fingerprint', 'title', 'occurrences', 'first_seen', 'last_seen',
|
|
158
|
+
'environments', 'status', 'formal_ref', 'formal_ref_source', 'source_entries', 'resolved_at'
|
|
159
|
+
]);
|
|
160
|
+
for (const key of Object.keys(entry)) {
|
|
161
|
+
if (!allowedProps.has(key)) {
|
|
162
|
+
errors.push(`additional property not allowed: ${key}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return errors.length === 0 ? true : errors;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate a debt ledger object
|
|
171
|
+
* @param {object} ledger - Debt ledger to validate
|
|
172
|
+
* @returns {boolean|string[]} true if valid, or array of error strings if invalid
|
|
173
|
+
*/
|
|
174
|
+
function validateDebtLedger(ledger) {
|
|
175
|
+
const errors = [];
|
|
176
|
+
|
|
177
|
+
// Type check: must be object
|
|
178
|
+
if (typeof ledger !== 'object' || ledger === null) {
|
|
179
|
+
return ['ledger must be an object'];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check schema_version
|
|
183
|
+
if (ledger.schema_version !== '1') {
|
|
184
|
+
errors.push('schema_version must be "1"');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check debt_entries is array
|
|
188
|
+
if (!Array.isArray(ledger.debt_entries)) {
|
|
189
|
+
errors.push('debt_entries must be an array');
|
|
190
|
+
} else {
|
|
191
|
+
// Validate each entry
|
|
192
|
+
for (let i = 0; i < ledger.debt_entries.length; i++) {
|
|
193
|
+
const entryErrors = validateDebtEntry(ledger.debt_entries[i]);
|
|
194
|
+
if (entryErrors !== true) {
|
|
195
|
+
errors.push(`debt_entries[${i}]: ${entryErrors.join('; ')}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return errors.length === 0 ? true : errors;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
validateDebtEntry,
|
|
205
|
+
validateDebtLedger,
|
|
206
|
+
isValidISO8601
|
|
207
|
+
};
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* validate-invariant.cjs
|
|
6
|
+
*
|
|
7
|
+
* Two-layer invariant gate for requirements:
|
|
8
|
+
* Layer 1: Fast regex pass — rejects obvious non-invariants (<1ms)
|
|
9
|
+
* Layer 2: Heuristic borderline detection — flags cases for Haiku sub-agent review
|
|
10
|
+
*
|
|
11
|
+
* The script itself does NOT call Haiku. It outputs BORDERLINE verdicts for cases
|
|
12
|
+
* that need classification. The calling workflow (add-requirement.md, map-requirements.md)
|
|
13
|
+
* spawns a Haiku sub-agent via the Agent tool for those cases.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node bin/validate-invariant.cjs --id=BLD-01 --text="hooks/dist/ rebuilt from current source"
|
|
17
|
+
* node bin/validate-invariant.cjs --batch --envelope=.planning/formal/requirements.json
|
|
18
|
+
* node bin/validate-invariant.cjs --batch --strict --envelope=.planning/formal/requirements.json
|
|
19
|
+
* node bin/validate-invariant.cjs --test
|
|
20
|
+
*
|
|
21
|
+
* Verdicts:
|
|
22
|
+
* INVARIANT — requirement has invariant language, passed regex
|
|
23
|
+
* NON_INVARIANT — caught by regex fast-pass
|
|
24
|
+
* BORDERLINE — needs Haiku sub-agent classification (no invariant language, past-tense heavy)
|
|
25
|
+
*
|
|
26
|
+
* Exit codes:
|
|
27
|
+
* 0 — validation complete (results printed)
|
|
28
|
+
* 1 — operational error
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Layer 1: Regex fast-pass
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const REGEX_RULES = [
|
|
39
|
+
{
|
|
40
|
+
name: 'past_achievement',
|
|
41
|
+
pattern: /\b(ACHIEVED|IMPLEMENTED|DELIVERED)\b/,
|
|
42
|
+
reason: 'Past achievement — archive as milestone finding',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'release_task',
|
|
46
|
+
pattern: /\b(bumpe?d?\s+from\s+.*\s+to|git\s+tag|published\s+to\s+npm)\b/i,
|
|
47
|
+
reason: 'Release task — belongs in changelog',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'migration_task',
|
|
51
|
+
pattern: /\b(git\s+mv|ported\s+to|archive[d]?\s+(in|to)|renamed.*preserved)\b/i,
|
|
52
|
+
reason: 'One-time migration — already completed',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'changelog',
|
|
56
|
+
pattern: /\bCHANGELOG\b/,
|
|
57
|
+
reason: 'Changelog task — documentation, not invariant',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'build_ci_gate',
|
|
61
|
+
pattern: /\b(npm\s+test\s+passes|rebuilt\s+from\s+current\s+source)\b/i,
|
|
62
|
+
reason: 'CI gate — acceptance criteria, not system property',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'audit_finding',
|
|
66
|
+
pattern: /\b(no\s+drift\s+detected|audited\s+against|verified\s+by\s+spot.check)\b/i,
|
|
67
|
+
reason: 'Audit finding — a snapshot, not ongoing constraint',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'past_improvement',
|
|
71
|
+
pattern: /\b(updated\s+to\s+(parallel|new|read|use)|hardened|validated\s+with.*test|gains\s+a\s+.*\s+field|improvement\s+areas\s+identified)\b/i,
|
|
72
|
+
reason: 'Past improvement — describes what was done, not what must hold',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'past_completion',
|
|
76
|
+
pattern: /\b(added\s+to\s+`|removed\s+from\s+`|created\s+with|no\s+old\s+names\s+remain)\b/i,
|
|
77
|
+
reason: 'Past completion — describes a completed action, not ongoing constraint',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run regex fast-pass on a requirement's text.
|
|
83
|
+
* @param {string} text - Requirement text
|
|
84
|
+
* @returns {{ matched: boolean, reason?: string, rule?: string }}
|
|
85
|
+
*/
|
|
86
|
+
function regexPass(text) {
|
|
87
|
+
// Strip backticks so patterns match content inside code spans
|
|
88
|
+
const stripped = text.replace(/`/g, '');
|
|
89
|
+
for (const rule of REGEX_RULES) {
|
|
90
|
+
if (rule.pattern.test(stripped)) {
|
|
91
|
+
return { matched: true, reason: rule.reason, rule: rule.name };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { matched: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// Borderline detection heuristic
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const INVARIANT_LANGUAGE = /\b(must|shall|always|never|ensures?|prevents?|rejects?|blocks?|validates?|enforces?|requires?|guarantees?|maintains?)\b/i;
|
|
102
|
+
|
|
103
|
+
// Past-tense action verbs (exclude common participial adjectives used as modifiers)
|
|
104
|
+
const PAST_TENSE_ACTIONS = /\b(updated|created|added|removed|renamed|reviewed|identified|ported|archived|implemented|delivered|achieved|migrated|completed|hardened|validated|published|bumped|cleared|finalized)\b/gi;
|
|
105
|
+
const PRESENT_TENSE = /\b(is|are|has|have|does|do|can|will|may|should|activates?|writes?|reads?|runs?|tracks?|checks?|detects?|responds?)\b/gi;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a requirement is borderline (needs Haiku sub-agent review).
|
|
109
|
+
* Returns true if it lacks invariant language and has majority past-tense action verbs.
|
|
110
|
+
* @param {string} text - Requirement text
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isBorderline(text) {
|
|
114
|
+
if (INVARIANT_LANGUAGE.test(text)) return false;
|
|
115
|
+
|
|
116
|
+
const pastMatches = (text.match(PAST_TENSE_ACTIONS) || []).length;
|
|
117
|
+
const presentMatches = (text.match(PRESENT_TENSE) || []).length;
|
|
118
|
+
|
|
119
|
+
// If majority past-tense action verbs or no strong present-tense verbs, it's borderline
|
|
120
|
+
return pastMatches > presentMatches || (pastMatches > 0 && presentMatches === 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the Haiku sub-agent prompt for a borderline requirement.
|
|
125
|
+
* Callers pass this to Agent(model: "haiku") in the workflow.
|
|
126
|
+
* @param {string} id - Requirement ID
|
|
127
|
+
* @param {string} text - Requirement text
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function buildHaikuPrompt(id, text) {
|
|
131
|
+
return `You are a requirements invariant classifier.
|
|
132
|
+
|
|
133
|
+
A VALID requirement is an INVARIANT — a property that must hold at any point in time.
|
|
134
|
+
Test: "At any point, if you inspect the system, this property holds."
|
|
135
|
+
|
|
136
|
+
A NON-INVARIANT is a task, migration, past achievement, or process step.
|
|
137
|
+
|
|
138
|
+
Requirement: ${id}: ${text}
|
|
139
|
+
|
|
140
|
+
Classify as exactly one of:
|
|
141
|
+
- INVARIANT: <one-line reason>
|
|
142
|
+
- NON_INVARIANT: <one-line reason>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
// Public API
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate a single requirement (regex + borderline heuristic only).
|
|
151
|
+
* Does NOT call Haiku — returns BORDERLINE for cases needing sub-agent review.
|
|
152
|
+
* @param {{ id: string, text: string }} req
|
|
153
|
+
* @returns {{ verdict: string, reason?: string, layer?: string }}
|
|
154
|
+
*/
|
|
155
|
+
function validateInvariant(req) {
|
|
156
|
+
// Layer 1: Regex fast-pass
|
|
157
|
+
const regexResult = regexPass(req.text);
|
|
158
|
+
if (regexResult.matched) {
|
|
159
|
+
return { verdict: 'NON_INVARIANT', reason: regexResult.reason, layer: 'regex' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Layer 2: Borderline detection → needs Haiku sub-agent
|
|
163
|
+
if (isBorderline(req.text)) {
|
|
164
|
+
return { verdict: 'BORDERLINE', reason: 'Lacks invariant language with past-tense verbs — needs Haiku sub-agent classification', layer: 'heuristic' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Has invariant language and passed regex → INVARIANT
|
|
168
|
+
return { verdict: 'INVARIANT' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate a batch of requirements (regex + borderline heuristic only).
|
|
173
|
+
* @param {Array<{ id: string, text: string }>} requirements
|
|
174
|
+
* @returns {Array<{ id: string, verdict: string, reason?: string, layer?: string }>}
|
|
175
|
+
*/
|
|
176
|
+
function validateInvariantBatch(requirements) {
|
|
177
|
+
return requirements.map(req => ({ id: req.id, ...validateInvariant(req) }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
// Built-in test suite
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function runTests() {
|
|
185
|
+
const archivePath = path.join(__dirname, '..', '.planning', 'formal', 'archived-non-invariants.json');
|
|
186
|
+
if (!fs.existsSync(archivePath)) {
|
|
187
|
+
console.error('Test data not found: .planning/formal/archived-non-invariants.json');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const archived = JSON.parse(fs.readFileSync(archivePath, 'utf8'));
|
|
192
|
+
const nonInvariants = archived.entries || [];
|
|
193
|
+
|
|
194
|
+
console.log('=== Invariant Gate Test Suite ===\n');
|
|
195
|
+
|
|
196
|
+
// Test 1: All archived non-invariants should be caught by regex
|
|
197
|
+
console.log(`Test 1: Regex catches archived non-invariants (${nonInvariants.length} entries)`);
|
|
198
|
+
let regexCaught = 0;
|
|
199
|
+
let regexMissed = [];
|
|
200
|
+
for (const entry of nonInvariants) {
|
|
201
|
+
const result = regexPass(entry.text);
|
|
202
|
+
if (result.matched) {
|
|
203
|
+
regexCaught++;
|
|
204
|
+
} else {
|
|
205
|
+
regexMissed.push(entry.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log(` Caught: ${regexCaught}/${nonInvariants.length}`);
|
|
209
|
+
if (regexMissed.length > 0) {
|
|
210
|
+
console.log(` Missed (would go to Haiku sub-agent): ${regexMissed.join(', ')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Test 2: Known good invariants should pass
|
|
214
|
+
const goodInvariants = [
|
|
215
|
+
{ id: 'ACT-01', text: '`.planning/current-activity.json` is written atomically at every major workflow state transition' },
|
|
216
|
+
{ id: 'CONF-05', text: 'Config changes must trigger validation before being applied' },
|
|
217
|
+
{ id: 'STATE-04', text: 'State transitions must be atomic — partial transitions are never persisted' },
|
|
218
|
+
{ id: 'ENFC-01', text: 'Quorum enforcement must block plan execution when quorum is not met' },
|
|
219
|
+
{ id: 'VERIFY-01', text: 'Verification always runs after phase execution completes' },
|
|
220
|
+
{ id: 'AGENT-01', text: 'User can add a new claude-mcp-server instance (name, provider, model, key)' },
|
|
221
|
+
{ id: 'BREAKER-01', text: 'Circuit breaker activates when 3+ alternating oscillation groups are detected' },
|
|
222
|
+
{ id: 'HOOK-01', text: 'Hooks must never block session start on transient errors' },
|
|
223
|
+
{ id: 'MCP-01', text: 'MCP server must respond to health_check within 10 seconds' },
|
|
224
|
+
{ id: 'QUICK-01', text: 'Quick tasks must create atomic commits for each logical change' },
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
console.log(`\nTest 2: Known good invariants pass regex (${goodInvariants.length} entries)`);
|
|
228
|
+
let goodPassed = 0;
|
|
229
|
+
let goodFailed = [];
|
|
230
|
+
for (const entry of goodInvariants) {
|
|
231
|
+
const result = regexPass(entry.text);
|
|
232
|
+
if (!result.matched) {
|
|
233
|
+
goodPassed++;
|
|
234
|
+
} else {
|
|
235
|
+
goodFailed.push({ id: entry.id, reason: result.reason });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
console.log(` Passed: ${goodPassed}/${goodInvariants.length}`);
|
|
239
|
+
if (goodFailed.length > 0) {
|
|
240
|
+
console.log(` FALSE POSITIVES:`);
|
|
241
|
+
for (const f of goodFailed) {
|
|
242
|
+
console.log(` ${f.id}: incorrectly rejected — "${f.reason}"`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Test 3: isBorderline heuristic
|
|
247
|
+
console.log('\nTest 3: Borderline detection heuristic');
|
|
248
|
+
const borderlineCases = [
|
|
249
|
+
{ text: 'R3.6 quorum enforcement reviewed and improvement areas identified', expected: true },
|
|
250
|
+
{ text: 'Quorum enforcement must block plan execution when quorum is not met', expected: false },
|
|
251
|
+
{ text: 'All source files updated to use new slot names', expected: true },
|
|
252
|
+
{ text: 'Circuit breaker activates when 3+ alternating groups detected', expected: false },
|
|
253
|
+
];
|
|
254
|
+
let heuristicCorrect = 0;
|
|
255
|
+
for (const tc of borderlineCases) {
|
|
256
|
+
const result = isBorderline(tc.text);
|
|
257
|
+
if (result === tc.expected) {
|
|
258
|
+
heuristicCorrect++;
|
|
259
|
+
} else {
|
|
260
|
+
console.log(` MISMATCH: "${tc.text.slice(0, 50)}..." — expected ${tc.expected}, got ${result}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
console.log(` Correct: ${heuristicCorrect}/${borderlineCases.length}`);
|
|
264
|
+
|
|
265
|
+
// Summary
|
|
266
|
+
const totalTests = nonInvariants.length + goodInvariants.length + borderlineCases.length;
|
|
267
|
+
const totalCorrect = regexCaught + goodPassed + heuristicCorrect;
|
|
268
|
+
console.log(`\n=== Summary: ${totalCorrect}/${totalTests} correct ===`);
|
|
269
|
+
|
|
270
|
+
if (regexMissed.length > 0) {
|
|
271
|
+
console.log(`\nNote: ${regexMissed.length} archived non-invariants not caught by regex.`);
|
|
272
|
+
console.log('These would be classified by a Haiku sub-agent in the workflow.');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
277
|
+
// CLI entrypoint
|
|
278
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
function main() {
|
|
281
|
+
const args = {};
|
|
282
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
283
|
+
const arg = process.argv[i];
|
|
284
|
+
if (arg.startsWith('--')) {
|
|
285
|
+
const eqIdx = arg.indexOf('=');
|
|
286
|
+
if (eqIdx !== -1) {
|
|
287
|
+
args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
288
|
+
} else {
|
|
289
|
+
args[arg.slice(2)] = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --test: run built-in test suite
|
|
295
|
+
if (args.test) {
|
|
296
|
+
runTests();
|
|
297
|
+
process.exit(0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --batch: validate entire envelope
|
|
301
|
+
if (args.batch) {
|
|
302
|
+
const envelopePath = args.envelope || '.planning/formal/requirements.json';
|
|
303
|
+
if (!fs.existsSync(envelopePath)) {
|
|
304
|
+
console.error(`Envelope not found: ${envelopePath}`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const envelope = JSON.parse(fs.readFileSync(envelopePath, 'utf8'));
|
|
309
|
+
const requirements = envelope.requirements || [];
|
|
310
|
+
|
|
311
|
+
console.log(`Validating ${requirements.length} requirements...\n`);
|
|
312
|
+
|
|
313
|
+
const results = validateInvariantBatch(requirements);
|
|
314
|
+
const nonInvariants = results.filter(r => r.verdict === 'NON_INVARIANT');
|
|
315
|
+
const borderline = results.filter(r => r.verdict === 'BORDERLINE');
|
|
316
|
+
|
|
317
|
+
if (nonInvariants.length === 0 && borderline.length === 0) {
|
|
318
|
+
console.log('All requirements passed invariant gate.');
|
|
319
|
+
} else {
|
|
320
|
+
if (nonInvariants.length > 0) {
|
|
321
|
+
console.log(`NON-INVARIANTS DETECTED (${nonInvariants.length}):\n`);
|
|
322
|
+
for (const ni of nonInvariants) {
|
|
323
|
+
const req = requirements.find(r => r.id === ni.id);
|
|
324
|
+
console.log(` ${ni.id}: ${req?.text?.slice(0, 80)}...`);
|
|
325
|
+
console.log(` Reason: ${ni.reason} [${ni.layer}]`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (borderline.length > 0) {
|
|
329
|
+
console.log(`\nBORDERLINE — need Haiku sub-agent review (${borderline.length}):`);
|
|
330
|
+
for (const b of borderline) {
|
|
331
|
+
const req = requirements.find(r => r.id === b.id);
|
|
332
|
+
console.log(` ${b.id}: ${req?.text?.slice(0, 80)}...`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --strict: archive non-invariants and remove from envelope
|
|
338
|
+
if (args.strict && nonInvariants.length > 0) {
|
|
339
|
+
const archivePath = args['archive-path'] || '.planning/formal/archived-non-invariants.json';
|
|
340
|
+
let archive = { archived_at: null, reason: '', entries: [] };
|
|
341
|
+
if (fs.existsSync(archivePath)) {
|
|
342
|
+
archive = JSON.parse(fs.readFileSync(archivePath, 'utf8'));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Add any non-invariants not yet in the archive
|
|
346
|
+
const existingIds = new Set((archive.entries || []).map(e => e.id));
|
|
347
|
+
const toArchive = nonInvariants
|
|
348
|
+
.map(ni => requirements.find(r => r.id === ni.id))
|
|
349
|
+
.filter(r => r && !existingIds.has(r.id));
|
|
350
|
+
|
|
351
|
+
if (toArchive.length > 0) {
|
|
352
|
+
archive.entries = [...(archive.entries || []), ...toArchive];
|
|
353
|
+
archive.archived_at = new Date().toISOString();
|
|
354
|
+
archive.reason = archive.reason || 'Non-invariant entries removed by invariant gate';
|
|
355
|
+
|
|
356
|
+
const dir = path.dirname(archivePath);
|
|
357
|
+
const tmpArch = path.join(dir, '.archived-non-invariants.json.tmp');
|
|
358
|
+
fs.writeFileSync(tmpArch, JSON.stringify(archive, null, 2) + '\n', 'utf8');
|
|
359
|
+
fs.renameSync(tmpArch, archivePath);
|
|
360
|
+
console.log(`\nArchived ${toArchive.length} new non-invariants to ${archivePath}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Always remove non-invariants from envelope (even if already in archive)
|
|
364
|
+
const nonInvariantIds = new Set(nonInvariants.map(ni => ni.id));
|
|
365
|
+
envelope.requirements = requirements.filter(r => !nonInvariantIds.has(r.id));
|
|
366
|
+
|
|
367
|
+
const dir = path.dirname(envelopePath);
|
|
368
|
+
const tmpEnv = path.join(dir, '.requirements.json.tmp');
|
|
369
|
+
fs.writeFileSync(tmpEnv, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
|
|
370
|
+
fs.renameSync(tmpEnv, envelopePath);
|
|
371
|
+
|
|
372
|
+
console.log(`Envelope reduced: ${requirements.length} → ${envelope.requirements.length}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const invariantCount = results.length - nonInvariants.length - borderline.length;
|
|
376
|
+
console.log(`\nSummary: ${nonInvariants.length} non-invariant, ${borderline.length} borderline, ${invariantCount} invariant`);
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Single requirement mode
|
|
381
|
+
if (!args.id || !args.text) {
|
|
382
|
+
console.error('Usage: node bin/validate-invariant.cjs --id=ID --text="requirement text"');
|
|
383
|
+
console.error(' node bin/validate-invariant.cjs --batch [--envelope=path] [--strict]');
|
|
384
|
+
console.error(' node bin/validate-invariant.cjs --test');
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const result = validateInvariant({ id: args.id, text: args.text });
|
|
389
|
+
|
|
390
|
+
if (result.verdict === 'INVARIANT') {
|
|
391
|
+
console.log('Invariant check: PASS');
|
|
392
|
+
} else if (result.verdict === 'BORDERLINE') {
|
|
393
|
+
console.log('Invariant check: BORDERLINE — needs Haiku sub-agent classification');
|
|
394
|
+
console.log(` Haiku prompt: ${buildHaikuPrompt(args.id, args.text).slice(0, 100)}...`);
|
|
395
|
+
} else {
|
|
396
|
+
console.log('Invariant check: FAIL');
|
|
397
|
+
console.log(` Reason: ${result.reason}`);
|
|
398
|
+
console.log(` Layer: ${result.layer}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
405
|
+
// Exports
|
|
406
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
module.exports = {
|
|
409
|
+
regexPass,
|
|
410
|
+
isBorderline,
|
|
411
|
+
buildHaikuPrompt,
|
|
412
|
+
validateInvariant,
|
|
413
|
+
validateInvariantBatch,
|
|
414
|
+
INVARIANT_LANGUAGE,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (require.main === module) {
|
|
418
|
+
main();
|
|
419
|
+
}
|