@entrorove/skill 0.1.22 → 0.1.24
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/AGREEMENT.md +6 -6
- package/CHANGELOG.md +10 -0
- package/SKILL.md +12 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +18 -83
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +22 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-cli.mjs +49 -16
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +124 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +97 -0
- package/local-formula-engine.mjs +111 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +107 -0
- package/local-ledger.mjs +2 -7
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +96 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +86 -0
- package/local-mutation-files.mjs +245 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +38 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +153 -3
- package/release-state.json +153 -17
- package/release.mjs +238 -4
- package/releases.json +12 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const feedbackNoticeVersion = '2026-10-07.1';
|
|
2
|
+
export const reviewUrl = 'https://entrorove.com/api/ide-reviews';
|
|
3
|
+
|
|
4
|
+
export function buildReview(data) {
|
|
5
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)
|
|
6
|
+
|| Object.keys(data).sort().join(',') !== 'comment,ide,invocationRequestId,noticeVersion,publicConsent,rating'
|
|
7
|
+
|| typeof data.invocationRequestId !== 'string'
|
|
8
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(data.invocationRequestId)
|
|
9
|
+
|| typeof data.ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(data.ide.trim())
|
|
10
|
+
|| !Number.isInteger(data.rating) || data.rating < 1 || data.rating > 5
|
|
11
|
+
|| typeof data.comment !== 'string' || data.comment.length < 4 || data.comment.length > 280
|
|
12
|
+
|| !/\S/.test(data.comment) || data.publicConsent !== true || data.noticeVersion !== feedbackNoticeVersion) {
|
|
13
|
+
throw new Error('Expected original invocationRequestId, ide, rating 1-5, unchanged user comment, publicConsent:true and current noticeVersion');
|
|
14
|
+
}
|
|
15
|
+
if (/[\u0000-\u001f\u007f]/u.test(data.comment)
|
|
16
|
+
|| /(?:https?:\/\/|www\.|[\w.+-]+@[\w.-]+\.[A-Za-z]{2,})/iu.test(data.comment)
|
|
17
|
+
|| /\b\d{10,}\b/u.test(data.comment)) throw new Error('Public feedback contains private contact data or unsupported characters');
|
|
18
|
+
return { url: reviewUrl, body: { invocationRequestId: data.invocationRequestId, ide: data.ide.trim(),
|
|
19
|
+
rating: data.rating, comment: data.comment, publicConsent: true, noticeVersion: data.noticeVersion } };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function submitSkillFeedback(data, dependencies) {
|
|
23
|
+
const review = buildReview(data);
|
|
24
|
+
await dependencies.install();
|
|
25
|
+
if (!await dependencies.agreementAccepted(dependencies.home)) return dependencies.agreementRequired(dependencies.home);
|
|
26
|
+
const token = await dependencies.readCredential(dependencies.home);
|
|
27
|
+
let response;
|
|
28
|
+
try {
|
|
29
|
+
response = await fetch(review.url, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
|
|
30
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify(review.body) });
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error('Public feedback outcome is unknown; this command has not retried publication');
|
|
33
|
+
}
|
|
34
|
+
if (!response.ok) throw new Error(`entrorove review failed: HTTP ${response.status}`);
|
|
35
|
+
const result = await response.json();
|
|
36
|
+
if (result?.published !== true || typeof result.id !== 'string') throw new Error('Invalid entrorove review response');
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const REQUEST_VERSION = 'archguard.skill.request/1.0'
|
|
4
|
+
const RESPONSE_VERSION = 'archguard.skill.response/1.0'
|
|
5
|
+
const COMPILER_VERSION = 'v7.0.44'
|
|
6
|
+
const CONTRACT_VERSION = 'archguard.contract/1.0'
|
|
7
|
+
const LEDGER_VERSION = 'archguard.checkpoint-ledger/1.0'
|
|
8
|
+
const SHA256 = /^[0-9a-f]{64}$/
|
|
9
|
+
const stringSchema = { type: 'string' }
|
|
10
|
+
const booleanSchema = { type: 'boolean' }
|
|
11
|
+
const integerSchema = { type: 'integer' }
|
|
12
|
+
const stringArraySchema = { type: 'array', items: stringSchema }
|
|
13
|
+
const strict = (properties, required = []) => ({ type: 'object', additionalProperties: false, properties, required })
|
|
14
|
+
|
|
15
|
+
const TEMPLATE_DEFINITIONS = Object.freeze({
|
|
16
|
+
'react-ts-vite': { frontend: { language: 'typescript', framework: 'react-18', scaffold: 'vite', uiLibrary: 'antd-5', styling: 'css-modules', stateManagement: 'zustand', forbiddenDeps: ['jquery', 'moment'] }, backend: null },
|
|
17
|
+
'vue3-ts-vite': { frontend: { language: 'typescript', framework: 'vue-3', scaffold: 'vite', uiLibrary: 'element-plus', styling: 'css-modules', stateManagement: 'pinia', forbiddenDeps: ['jquery', 'moment'] }, backend: null },
|
|
18
|
+
'nextjs-ts': { frontend: { language: 'typescript', framework: 'nextjs-15', scaffold: 'nextjs', uiLibrary: 'antd-5', styling: 'css-modules', stateManagement: 'react-context', forbiddenDeps: ['jquery', 'moment'] }, backend: { language: 'nodejs-20', framework: 'nextjs-route-handlers', orm: 'none', forbiddenDeps: ['request'], patterns: { required: ['schema-validation'], forbidden: ['raw-sql-concat'] } } },
|
|
19
|
+
'node-nestjs': { frontend: null, backend: { language: 'nodejs-20', framework: 'nestjs-10', orm: 'prisma', forbiddenDeps: ['request'], patterns: { required: ['dto-validation', 'global-exception-filter'], forbidden: ['raw-sql-concat'] } } },
|
|
20
|
+
'python-fastapi': { frontend: null, backend: { language: 'python-3.12', framework: 'fastapi', orm: 'sqlalchemy', forbiddenDeps: ['pickle5'], patterns: { required: ['schema-validation'], forbidden: ['raw-sql-concat'] } } },
|
|
21
|
+
'java-spring': { frontend: null, backend: { language: 'java-21', framework: 'spring-boot-3', orm: 'jpa', forbiddenDeps: ['commons-logging'], patterns: { required: ['dto-validation', 'global-exception-handler'], forbidden: ['raw-sql-concat'] } } },
|
|
22
|
+
'wechat-miniprogram': { frontend: { language: 'typescript', framework: 'wechat-miniprogram', scaffold: 'miniprogram', uiLibrary: 'native', styling: 'wxss', stateManagement: 'page-state', forbiddenDeps: ['jquery'] }, backend: null },
|
|
23
|
+
'tauri-react': { frontend: { language: 'typescript', framework: 'react-18', scaffold: 'tauri-2', uiLibrary: 'antd-5', styling: 'css-modules', stateManagement: 'zustand', forbiddenDeps: ['electron', 'jquery'] }, backend: { language: 'rust-2024', framework: 'tauri-2', orm: 'none', forbiddenDeps: [], patterns: { required: ['command-boundary'], forbidden: ['unsafe-without-audit'] } } },
|
|
24
|
+
})
|
|
25
|
+
const DEFAULT_BUDGETS = Object.freeze({ maxNestingDepth: 4, maxFileLines: 300, maxFunctionLines: 50, animations: 3, transitions: -1, bundleBudgetKB: 500 })
|
|
26
|
+
const DEPENDENCY_LOCKFILES = new Set(['pnpm-lock.yaml', 'package-lock.json', 'npm-shrinkwrap.json',
|
|
27
|
+
'yarn.lock', 'bun.lock', 'bun.lockb', 'Cargo.lock', 'go.sum', 'composer.lock', 'Gemfile.lock',
|
|
28
|
+
'Pipfile.lock', 'poetry.lock', 'uv.lock', 'pdm.lock', 'pubspec.lock', 'packages.lock.json', 'Package.resolved'])
|
|
29
|
+
const THIRD_PARTY_DIRECTORIES = new Set(['node_modules', 'vendor', 'third_party', 'third-party', '.venv', 'site-packages'])
|
|
30
|
+
const GENERATED_DIRECTORIES = new Set(['dist', '.umi', '.umi-production', '.runtime', '.next', 'coverage'])
|
|
31
|
+
const THIRD_PARTY_ROOTS = ['services/hermes-agent']
|
|
32
|
+
const GENERATED_ROOTS = ['apps/server/api']
|
|
33
|
+
const INSPECTION_SCOPES = ['first-party', 'dependency-lockfile', 'third-party', 'generated']
|
|
34
|
+
const BUILTIN_RULES = Object.freeze([
|
|
35
|
+
{ id: 'no-inline-style', category: 'standard', pattern: /\bstyle\s*=\s*(?:\{\{|["'])/g, message: 'Use the locked styling system instead of inline style.' },
|
|
36
|
+
{ id: 'no-hardcoded-color', category: 'standard', pattern: /#[0-9a-f]{3,8}\b|\brgb(?:a)?\s*\(|\bhsl(?:a)?\s*\(/gi, message: 'Use project theme tokens instead of hardcoded colors.' },
|
|
37
|
+
{ id: 'no-eval', category: 'security', pattern: /\beval\s*\(|new\s+Function\b/g, message: 'Dynamic code evaluation is forbidden.' },
|
|
38
|
+
{ id: 'no-debug', category: 'standard', pattern: /\bdebugger\b|\bconsole\.(?:log|debug)\s*\(/g, message: 'Remove debug statements.' },
|
|
39
|
+
{ id: 'no-magic-number', category: 'standard', pattern: /(?:^|[^\w.])(?:[2-9]|[1-9]\d{1,})(?:\.\d+)?(?:[^\w.]|$)/g, message: 'Name repeated or domain-significant numeric values.' },
|
|
40
|
+
])
|
|
41
|
+
const OPERATIONS = Object.freeze(['capabilities', 'help', 'intake', 'contract-create', 'contract-get', 'contract-update', 'template-list', 'checkpoint', 'rules-scan', 'complexity-report', 'drift-status', 'ledger-query'])
|
|
42
|
+
|
|
43
|
+
const patternSchema = strict({ required: stringArraySchema, forbidden: stringArraySchema }, ['required', 'forbidden'])
|
|
44
|
+
const stackSideSchema = strict({ language: stringSchema, framework: stringSchema, scaffold: stringSchema, uiLibrary: stringSchema, styling: stringSchema, stateManagement: stringSchema, orm: stringSchema, forbiddenDeps: stringArraySchema, patterns: patternSchema }, ['language', 'framework', 'forbiddenDeps'])
|
|
45
|
+
const stackSchema = strict({ frontend: { anyOf: [stackSideSchema, { type: 'null' }] }, backend: { anyOf: [stackSideSchema, { type: 'null' }] } }, ['frontend', 'backend'])
|
|
46
|
+
const ruleSchema = strict({ id: stringSchema, engine: { enum: ['regex', 'ast'] }, pattern: stringSchema, message: stringSchema, blocking: booleanSchema }, ['id', 'engine', 'pattern', 'message', 'blocking'])
|
|
47
|
+
const rulesSchema = strict({ blocking: stringArraySchema, strictStandard: booleanSchema, custom: { type: 'array', items: ruleSchema } }, ['blocking', 'strictStandard', 'custom'])
|
|
48
|
+
const budgetSchema = strict(Object.fromEntries(Object.keys(DEFAULT_BUDGETS).map((key) => [key, integerSchema])), Object.keys(DEFAULT_BUDGETS))
|
|
49
|
+
const auditSchema = { type: 'object', additionalProperties: { type: 'string' }, minProperties: 1 }
|
|
50
|
+
const CONTRACT_SCHEMA = strict({ schemaVersion: { const: CONTRACT_VERSION }, contractId: stringSchema, project: stringSchema, templateId: { enum: Object.keys(TEMPLATE_DEFINITIONS) }, revision: { type: 'integer', minimum: 1 }, stack: stackSchema, rules: rulesSchema, budgets: budgetSchema, audit: auditSchema }, ['schemaVersion', 'contractId', 'project', 'templateId', 'revision', 'stack', 'rules', 'budgets', 'audit'])
|
|
51
|
+
const overrideSchema = strict({ stack: stackSchema, rules: rulesSchema, budgets: budgetSchema, audit: auditSchema })
|
|
52
|
+
const fileSchema = strict({ path: stringSchema, content: stringSchema }, ['path', 'content'])
|
|
53
|
+
const findingSchema = strict({ severity: { enum: ['P0', 'P1', 'P2'] }, ruleId: stringSchema, entityRef: stringSchema, message: stringSchema, category: stringSchema, blocking: booleanSchema, evidence: { type: 'object', additionalProperties: true } }, ['severity', 'ruleId', 'entityRef', 'message', 'category', 'blocking', 'evidence'])
|
|
54
|
+
const metricSchema = strict({ path: stringSchema, inspectionScope: { enum: INSPECTION_SCOPES }, complexityChecked: booleanSchema, fileLines: integerSchema, maxNestingDepth: integerSchema, maxFunctionLines: integerSchema, animations: integerSchema, transitions: integerSchema }, ['path', 'inspectionScope', 'complexityChecked', 'fileLines', 'maxNestingDepth', 'maxFunctionLines', 'animations', 'transitions'])
|
|
55
|
+
const ledgerEntrySchema = strict({ schemaVersion: { const: LEDGER_VERSION }, checkpointId: stringSchema, blockId: stringSchema, path: stringSchema, contractRevision: integerSchema, beforeSha256: { type: 'string', pattern: SHA256.source }, afterSha256: { type: 'string', pattern: SHA256.source }, status: { enum: ['passed', 'blocked'] }, ruleIds: stringArraySchema, blockingRuleIds: stringArraySchema, rollbackRequired: booleanSchema }, ['schemaVersion', 'checkpointId', 'blockId', 'path', 'contractRevision', 'beforeSha256', 'afterSha256', 'status', 'ruleIds', 'blockingRuleIds', 'rollbackRequired'])
|
|
56
|
+
const ledgerSchema = { type: 'array', items: ledgerEntrySchema }
|
|
57
|
+
const digestSchema = { type: 'string', pattern: SHA256.source }
|
|
58
|
+
const nextSchema = strict({ operation: stringSchema, instruction: stringSchema }, ['operation'])
|
|
59
|
+
const driftSchema = strict({ trend: { enum: ['green', 'yellow', 'red'] }, totalBlocks: integerSchema, recentBlocks: integerSchema, blockedBlocks: integerSchema, violationRate: { type: 'number' }, consecutiveBlocks: integerSchema, repeatedRuleIds: stringArraySchema, action: stringSchema }, ['trend', 'totalBlocks', 'recentBlocks', 'blockedBlocks', 'violationRate', 'consecutiveBlocks', 'repeatedRuleIds', 'action'])
|
|
60
|
+
const checkpointOutputSchema = strict({ checkpoint: strict({ status: { enum: ['passed', 'blocked'] }, writeAllowed: booleanSchema, rollbackRequired: booleanSchema, findings: { type: 'array', items: findingSchema }, metrics: metricSchema, drift: driftSchema, instruction: stringSchema }, ['status', 'writeAllowed', 'rollbackRequired', 'findings', 'metrics', 'drift', 'instruction']), ledgerEntry: ledgerEntrySchema }, ['checkpoint', 'ledgerEntry'])
|
|
61
|
+
const scanInputSchema = strict({ contract: CONTRACT_SCHEMA, files: { type: 'array', minItems: 1, items: fileSchema } }, ['contract', 'files'])
|
|
62
|
+
const emptySchema = strict({})
|
|
63
|
+
const objectSchemaDescriptor = strict({ type: { const: 'object' }, additionalProperties: booleanSchema, properties: { type: 'object', additionalProperties: true }, required: stringArraySchema }, ['type', 'additionalProperties', 'properties', 'required'])
|
|
64
|
+
const schemaPair = strict({ input: objectSchemaDescriptor, output: objectSchemaDescriptor }, ['input', 'output'])
|
|
65
|
+
const capabilitiesOutputSchema = strict({ skill: strict({ name: stringSchema, version: stringSchema }, ['name', 'version']), operations: stringArraySchema, operationSchemas: { type: 'object', additionalProperties: schemaPair }, contractSchemaVersion: stringSchema, ledgerSchemaVersion: stringSchema, templates: stringArraySchema, executionBoundary: stringSchema, nextStep: nextSchema }, ['skill', 'operations', 'operationSchemas', 'contractSchemaVersion', 'ledgerSchemaVersion', 'templates', 'executionBoundary', 'nextStep'])
|
|
66
|
+
const OPERATION_SCHEMAS = Object.freeze({
|
|
67
|
+
capabilities: { input: emptySchema, output: capabilitiesOutputSchema },
|
|
68
|
+
help: { input: emptySchema, output: capabilitiesOutputSchema },
|
|
69
|
+
intake: { input: strict({ project: stringSchema, detectedFiles: stringArraySchema }, ['project']), output: strict({ project: stringSchema, questions: { type: 'array', items: strict({ id: stringSchema, prompt: stringSchema, required: booleanSchema, options: { type: 'array' } }, ['id', 'prompt', 'required']) }, nextStep: nextSchema }, ['project', 'questions', 'nextStep']) },
|
|
70
|
+
'contract-create': { input: strict({ project: stringSchema, templateId: { enum: Object.keys(TEMPLATE_DEFINITIONS) }, overrides: overrideSchema }, ['project', 'templateId']), output: strict({ contract: CONTRACT_SCHEMA, digest: digestSchema, nextStep: nextSchema }, ['contract', 'digest', 'nextStep']) },
|
|
71
|
+
'contract-get': { input: strict({ contract: CONTRACT_SCHEMA }, ['contract']), output: strict({ contract: CONTRACT_SCHEMA, digest: digestSchema }, ['contract', 'digest']) },
|
|
72
|
+
'contract-update': { input: strict({ contract: CONTRACT_SCHEMA, expectedDigest: digestSchema, patch: overrideSchema, confirmation: { const: 'confirm-architecture-contract-update' }, reason: stringSchema, actor: stringSchema }, ['contract', 'expectedDigest', 'patch', 'confirmation', 'reason', 'actor']), output: strict({ contract: CONTRACT_SCHEMA, digest: digestSchema, audit: auditSchema }, ['contract', 'digest', 'audit']) },
|
|
73
|
+
'template-list': { input: emptySchema, output: strict({ templates: { type: 'array', items: strict({ templateId: stringSchema, stack: stackSchema }, ['templateId', 'stack']) }, nextStep: nextSchema }, ['templates', 'nextStep']) },
|
|
74
|
+
checkpoint: { input: strict({ contract: CONTRACT_SCHEMA, block: strict({ blockId: stringSchema, path: stringSchema, content: stringSchema, beforeSha256: digestSchema }, ['blockId', 'path', 'content', 'beforeSha256']), history: ledgerSchema }, ['contract', 'block', 'history']), output: checkpointOutputSchema },
|
|
75
|
+
'rules-scan': { input: scanInputSchema, output: strict({ findings: { type: 'array', items: findingSchema }, summary: strict({ total: integerSchema, blocking: integerSchema }, ['total', 'blocking']) }, ['findings', 'summary']) },
|
|
76
|
+
'complexity-report': { input: scanInputSchema, output: strict({ metrics: { type: 'array', items: metricSchema }, findings: { type: 'array', items: findingSchema } }, ['metrics', 'findings']) },
|
|
77
|
+
'drift-status': { input: strict({ ledger: ledgerSchema }, ['ledger']), output: strict({ drift: driftSchema }, ['drift']) },
|
|
78
|
+
'ledger-query': { input: strict({ ledger: ledgerSchema, ruleId: stringSchema, status: { enum: ['passed', 'blocked'] } }, ['ledger']), output: strict({ schemaVersion: { const: LEDGER_VERSION }, entries: ledgerSchema, total: integerSchema }, ['schemaVersion', 'entries', 'total']) },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function canonicalJson(value) {
|
|
82
|
+
if (value === null || ['string', 'boolean'].includes(typeof value)) return JSON.stringify(value)
|
|
83
|
+
if (typeof value === 'number') {
|
|
84
|
+
if (!Number.isFinite(value)) throw new Error('JSON contains a non-finite number')
|
|
85
|
+
return JSON.stringify(value)
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
|
|
88
|
+
if (!value || typeof value !== 'object') throw new Error('Value must be finite JSON')
|
|
89
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`
|
|
90
|
+
}
|
|
91
|
+
const digest = (value) => createHash('sha256').update(canonicalJson(value)).digest('hex')
|
|
92
|
+
const contentDigest = (value) => createHash('sha256').update(value).digest('hex')
|
|
93
|
+
function text(value, context) {
|
|
94
|
+
if (typeof value !== 'string' || value !== value.trim() || !value) throw new Error(`${context} is required`)
|
|
95
|
+
return value
|
|
96
|
+
}
|
|
97
|
+
function record(value, context) {
|
|
98
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${context} must be an object`)
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
const response = (requestId, status, output, findings = []) => ({ ok: true, schemaVersion: RESPONSE_VERSION, requestId, status, output, findings })
|
|
102
|
+
const finding = (severity, ruleId, entityRef, message, category, blocking = true, evidence = {}) => ({ severity, ruleId, entityRef, message, category, blocking, evidence })
|
|
103
|
+
const pathSafe = (path) => typeof path === 'string' && path === path.normalize('NFC') && !path.startsWith('/') && !path.includes('\\') && path.split('/').every((part) => part && part !== '.' && part !== '..')
|
|
104
|
+
|
|
105
|
+
function sourceInspectionScope(path) {
|
|
106
|
+
if (!pathSafe(path)) return 'first-party'
|
|
107
|
+
const parts = path.split('/')
|
|
108
|
+
const directories = parts.slice(0, -1)
|
|
109
|
+
if (directories.some((part) => THIRD_PARTY_DIRECTORIES.has(part))
|
|
110
|
+
|| THIRD_PARTY_ROOTS.some((root) => path.startsWith(`${root}/`))) return 'third-party'
|
|
111
|
+
if (DEPENDENCY_LOCKFILES.has(parts.at(-1))) return 'dependency-lockfile'
|
|
112
|
+
if (directories.some((part) => GENERATED_DIRECTORIES.has(part))
|
|
113
|
+
|| GENERATED_ROOTS.some((root) => path.startsWith(`${root}/`))
|
|
114
|
+
|| /\.min\.(?:[cm]?js|css)$/.test(path)) return 'generated'
|
|
115
|
+
return 'first-party'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeCustomRule(value, index) {
|
|
119
|
+
const rule = record(value, `rules.custom[${index}]`)
|
|
120
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(text(rule.id, 'custom rule id')) || !['regex', 'ast'].includes(rule.engine) || typeof rule.pattern !== 'string' || rule.pattern.length > 500 || (rule.engine === 'regex' && /\\[1-9]|\(\?<|\(\?=|\(\?!/.test(rule.pattern)) || (rule.engine === 'ast' && !/^[A-Za-z][A-Za-z0-9]*$/.test(rule.pattern))) throw new Error(`rules.custom[${index}] is unsafe or unsupported`)
|
|
121
|
+
if (rule.engine === 'regex') new RegExp(rule.pattern, 'gu')
|
|
122
|
+
return { id: rule.id, engine: rule.engine, pattern: rule.pattern, message: text(rule.message, 'custom rule message'), blocking: rule.blocking === true }
|
|
123
|
+
}
|
|
124
|
+
function normalizeContract(value) {
|
|
125
|
+
const source = record(value, 'contract')
|
|
126
|
+
if (source.schemaVersion !== CONTRACT_VERSION || !TEMPLATE_DEFINITIONS[source.templateId] || !Number.isInteger(source.revision) || source.revision < 1 || !source.stack || !source.rules || !source.budgets || !source.audit) throw new Error('architecture contract is invalid')
|
|
127
|
+
const custom = Array.isArray(source.rules.custom) ? source.rules.custom.map(normalizeCustomRule) : []
|
|
128
|
+
const blocking = Array.isArray(source.rules.blocking) ? [...new Set(source.rules.blocking.map((rule) => text(rule, 'blocking rule')))].sort() : []
|
|
129
|
+
const budgets = Object.fromEntries(Object.keys(DEFAULT_BUDGETS).map((key) => {
|
|
130
|
+
const amount = source.budgets[key]
|
|
131
|
+
if (!Number.isInteger(amount) || amount < -1) throw new Error(`contract.budgets.${key} is invalid`)
|
|
132
|
+
return [key, amount]
|
|
133
|
+
}))
|
|
134
|
+
return { schemaVersion: CONTRACT_VERSION, contractId: text(source.contractId, 'contractId'), project: text(source.project, 'project'), templateId: source.templateId, revision: source.revision, stack: source.stack, rules: { blocking, strictStandard: source.rules.strictStandard === true, custom }, budgets, audit: record(source.audit, 'audit') }
|
|
135
|
+
}
|
|
136
|
+
function baseContract(project, templateId) {
|
|
137
|
+
const stack = TEMPLATE_DEFINITIONS[templateId]
|
|
138
|
+
if (!stack) throw new Error(`Unknown architecture template: ${templateId}`)
|
|
139
|
+
return normalizeContract({ schemaVersion: CONTRACT_VERSION, contractId: `${project}-architecture`, project, templateId, revision: 1, stack, rules: { blocking: ['no-eval', 'no-debug'], strictStandard: false, custom: [] }, budgets: DEFAULT_BUDGETS, audit: { createdBy: 'archguard', reason: 'initial-contract' } })
|
|
140
|
+
}
|
|
141
|
+
function forbiddenDependencies(contract) {
|
|
142
|
+
return [...new Set([...(contract.stack.frontend?.forbiddenDeps ?? []), ...(contract.stack.backend?.forbiddenDeps ?? [])])]
|
|
143
|
+
}
|
|
144
|
+
const dependencyKey = (value) => value.toLowerCase().replace(/[._]+/g, '-')
|
|
145
|
+
function dependencyMatches(actual, forbidden) {
|
|
146
|
+
const actualParts = actual.split(':')
|
|
147
|
+
return dependencyKey(actual) === dependencyKey(forbidden) || dependencyKey(actualParts.at(-1)) === dependencyKey(forbidden)
|
|
148
|
+
}
|
|
149
|
+
function manifestDependencies(file) {
|
|
150
|
+
if (/(^|\/)package\.json$/i.test(file.path)) {
|
|
151
|
+
const manifest = record(JSON.parse(file.content), 'package.json')
|
|
152
|
+
return ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].flatMap((key) => Object.keys(manifest[key] ?? {}))
|
|
153
|
+
}
|
|
154
|
+
if (/(^|\/)requirements(?:-[^/]*)?\.txt$/i.test(file.path)) return file.content.split(/\r?\n/).map((line) => line.replace(/\s+#.*$/, '').trim()).filter((line) => line && !line.startsWith('-')).map((line) => line.split(/[<>=!~\[;\s]/, 1)[0])
|
|
155
|
+
if (/(^|\/)pom\.xml$/i.test(file.path)) return [...file.content.matchAll(/<dependency\b[\s\S]*?<artifactId>\s*([^<\s]+)\s*<\/artifactId>[\s\S]*?<\/dependency>/gi)].map((match) => match[1])
|
|
156
|
+
if (/(^|\/)Cargo\.toml$/.test(file.path)) {
|
|
157
|
+
let dependencySection = false
|
|
158
|
+
return file.content.split(/\r?\n/).flatMap((line) => {
|
|
159
|
+
const section = line.trim().match(/^\[([^\]]+)]$/)
|
|
160
|
+
if (section) dependencySection = /(^|\.)((dev|build)-)?dependencies$/.test(section[1])
|
|
161
|
+
const dependency = dependencySection ? line.match(/^\s*([A-Za-z0-9_-]+)\s*=/)?.[1] : undefined
|
|
162
|
+
return dependency ? [dependency] : []
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
return []
|
|
166
|
+
}
|
|
167
|
+
function dependencyFindings(contract, file) {
|
|
168
|
+
const forbidden = forbiddenDependencies(contract)
|
|
169
|
+
let declared
|
|
170
|
+
try {
|
|
171
|
+
declared = manifestDependencies(file)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return [finding('P0', 'ARCH-DEPENDENCY-MANIFEST-INVALID', file.path, error instanceof Error ? error.message : 'Dependency manifest is invalid.', 'architecture')]
|
|
174
|
+
}
|
|
175
|
+
const found = new Set()
|
|
176
|
+
for (const dependency of forbidden) {
|
|
177
|
+
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
178
|
+
const imported = new RegExp(`(?:from\\s+|require\\(\\s*|import\\(\\s*)["']${escaped}(?:[\\/"'])`).test(file.content)
|
|
179
|
+
if (imported || declared.some((item) => dependencyMatches(item, dependency))) found.add(dependency)
|
|
180
|
+
}
|
|
181
|
+
return [...found].map((dependency) => finding('P0', 'ARCH-FORBIDDEN-DEPENDENCY', file.path, `Dependency ${dependency} is forbidden by the contract.`, 'architecture', true, { dependency }))
|
|
182
|
+
}
|
|
183
|
+
function languageFindings(contract, file) {
|
|
184
|
+
if (!pathSafe(file.path)) return [finding('P0', 'ARCH-PATH', String(file.path), 'File path is unsafe.', 'architecture')]
|
|
185
|
+
const findings = []
|
|
186
|
+
if (file.path === 'arch.contract.yaml') findings.push(finding('P0', 'ARCH-CONTRACT-MUTATION', file.path, 'The locked architecture contract cannot be changed by checkpoint.', 'architecture'))
|
|
187
|
+
const languages = [contract.stack.frontend?.language, contract.stack.backend?.language].filter(Boolean)
|
|
188
|
+
const extensions = new Set(languages.flatMap((language) => language === 'typescript' ? ['.ts', '.tsx'] : language.startsWith('nodejs') ? ['.js', '.mjs', '.cjs'] : language.startsWith('python') ? ['.py'] : language.startsWith('java') ? ['.java'] : language.startsWith('rust') ? ['.rs'] : []))
|
|
189
|
+
const codeExtension = file.path.match(/\.[A-Za-z0-9]+$/)?.[0]
|
|
190
|
+
if (codeExtension && ['.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.rs'].includes(codeExtension) && extensions.size && !extensions.has(codeExtension)) findings.push(finding('P0', 'ARCH-LANGUAGE', file.path, `File extension ${codeExtension} is outside the locked languages.`, 'architecture', true, { allowedExtensions: [...extensions] }))
|
|
191
|
+
return findings.concat(dependencyFindings(contract, file))
|
|
192
|
+
}
|
|
193
|
+
function ruleFindings(contract, file) {
|
|
194
|
+
const findings = []
|
|
195
|
+
for (const rule of BUILTIN_RULES) {
|
|
196
|
+
const matches = [...file.content.matchAll(new RegExp(rule.pattern.source, rule.pattern.flags))]
|
|
197
|
+
if (!matches.length) continue
|
|
198
|
+
const blocking = rule.category === 'security' || contract.rules.strictStandard || contract.rules.blocking.includes(rule.id)
|
|
199
|
+
findings.push(finding(blocking ? 'P1' : 'P2', rule.id, file.path, rule.message, rule.category, blocking, { matches: matches.length }))
|
|
200
|
+
}
|
|
201
|
+
for (const rule of contract.rules.custom) {
|
|
202
|
+
if (rule.engine !== 'regex') continue
|
|
203
|
+
const matches = [...file.content.matchAll(new RegExp(rule.pattern, 'gu'))]
|
|
204
|
+
if (matches.length) findings.push(finding(rule.blocking ? 'P1' : 'P2', rule.id, file.path, rule.message, 'custom', rule.blocking, { matches: matches.length }))
|
|
205
|
+
}
|
|
206
|
+
return findings
|
|
207
|
+
}
|
|
208
|
+
function complexity(file) {
|
|
209
|
+
const inspectionScope = sourceInspectionScope(file.path)
|
|
210
|
+
const lines = file.content.split(/\r?\n/)
|
|
211
|
+
let depth = 0; let maximumDepth = 0; let functionStart = null; let maximumFunctionLines = 0
|
|
212
|
+
for (const [index, line] of lines.entries()) {
|
|
213
|
+
if (functionStart === null && /\bfunction\b|=>\s*\{|\b(?:public|private|protected)?\s*(?:async\s+)?[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/.test(line)) functionStart = { line: index, depth }
|
|
214
|
+
for (const character of line.replace(/(['"`]).*?\1/g, '')) {
|
|
215
|
+
if (character === '{') { depth += 1; maximumDepth = Math.max(maximumDepth, depth) } else if (character === '}') depth = Math.max(0, depth - 1)
|
|
216
|
+
}
|
|
217
|
+
if (functionStart && depth <= functionStart.depth) { maximumFunctionLines = Math.max(maximumFunctionLines, index - functionStart.line + 1); functionStart = null }
|
|
218
|
+
}
|
|
219
|
+
if (functionStart) maximumFunctionLines = Math.max(maximumFunctionLines, lines.length - functionStart.line)
|
|
220
|
+
return { path: file.path, inspectionScope, complexityChecked: inspectionScope === 'first-party', fileLines: lines.length, maxNestingDepth: maximumDepth, maxFunctionLines: maximumFunctionLines, animations: (file.content.match(/@keyframes\b|\banimation(?:-name)?\s*:/g) ?? []).length, transitions: (file.content.match(/\btransition(?:-property)?\s*:/g) ?? []).length }
|
|
221
|
+
}
|
|
222
|
+
function complexityFindings(contract, metric) {
|
|
223
|
+
if (!metric.complexityChecked) return []
|
|
224
|
+
const checks = [['maxFileLines', 'fileLines'], ['maxNestingDepth', 'maxNestingDepth'], ['maxFunctionLines', 'maxFunctionLines'], ['animations', 'animations'], ['transitions', 'transitions']]
|
|
225
|
+
return checks.flatMap(([budgetKey, metricKey]) => contract.budgets[budgetKey] >= 0 && metric[metricKey] > contract.budgets[budgetKey] ? [finding('P1', `BUDGET-${budgetKey.toUpperCase()}`, metric.path, `${metricKey} ${metric[metricKey]} exceeds budget ${contract.budgets[budgetKey]}.`, 'complexity', true, { actual: metric[metricKey], budget: contract.budgets[budgetKey] })] : [])
|
|
226
|
+
}
|
|
227
|
+
function scanFiles(contract, files) {
|
|
228
|
+
if (!Array.isArray(files) || !files.length) throw new Error('files must be non-empty')
|
|
229
|
+
const normalized = files.map((value) => record(value, 'file'))
|
|
230
|
+
const metrics = normalized.map(complexity)
|
|
231
|
+
const findings = normalized.flatMap((file) => {
|
|
232
|
+
if (!pathSafe(file.path)) return languageFindings(contract, file)
|
|
233
|
+
if (sourceInspectionScope(file.path) !== 'first-party') return []
|
|
234
|
+
return [...languageFindings(contract, file), ...ruleFindings(contract, file)]
|
|
235
|
+
}).concat(metrics.flatMap((metric) => complexityFindings(contract, metric)))
|
|
236
|
+
return { findings, metrics }
|
|
237
|
+
}
|
|
238
|
+
function driftState(ledger) {
|
|
239
|
+
if (!Array.isArray(ledger)) throw new Error('ledger history is required')
|
|
240
|
+
const recent = ledger.slice(-10)
|
|
241
|
+
const trailing = []
|
|
242
|
+
for (let index = recent.length - 1; index >= 0 && recent[index]?.status === 'blocked'; index -= 1) trailing.unshift(recent[index])
|
|
243
|
+
const shared = trailing.length ? trailing.map((entry) => new Set(entry.blockingRuleIds ?? [])).reduce((left, right) => new Set([...left].filter((ruleId) => right.has(ruleId)))) : new Set()
|
|
244
|
+
const repeatedRuleIds = [...shared].sort()
|
|
245
|
+
const consecutiveBlocks = repeatedRuleIds.length ? trailing.length : 0
|
|
246
|
+
const trend = consecutiveBlocks >= 3 ? 'red' : consecutiveBlocks >= 2 ? 'yellow' : 'green'
|
|
247
|
+
const blockedBlocks = recent.filter((entry) => entry.status === 'blocked').length
|
|
248
|
+
return { trend, totalBlocks: ledger.length, recentBlocks: recent.length, blockedBlocks, violationRate: recent.length ? Math.round((blockedBlocks / recent.length) * 1e3) / 10 : 0, consecutiveBlocks, repeatedRuleIds, action: trend === 'red' ? 'interrupt-and-request-human-confirmation' : trend === 'yellow' ? 'warn-and-correct-next-block' : 'continue' }
|
|
249
|
+
}
|
|
250
|
+
function checkpoint(contract, blockValue, history, trustedAstFindings) {
|
|
251
|
+
const block = record(blockValue, 'checkpoint block')
|
|
252
|
+
if (!Array.isArray(history)) throw new Error('checkpoint history is required')
|
|
253
|
+
if (!pathSafe(block.path) || typeof block.content !== 'string' || !SHA256.test(block.beforeSha256)) throw new Error('checkpoint block is invalid')
|
|
254
|
+
const scan = scanFiles(contract, [{ path: block.path, content: block.content }])
|
|
255
|
+
const astRules = sourceInspectionScope(block.path) === 'first-party'
|
|
256
|
+
? contract.rules.custom.filter((rule) => rule.engine === 'ast') : []
|
|
257
|
+
if (astRules.length && !Array.isArray(trustedAstFindings)) scan.findings.push(finding('P1', 'ARCH-AST-LOCAL-RUNNER-REQUIRED', block.path, 'AST custom rules require the bundled trusted local checkpoint runner.', 'custom', true, { ruleIds: astRules.map((rule) => rule.id) }))
|
|
258
|
+
if (Array.isArray(trustedAstFindings)) for (const item of trustedAstFindings) scan.findings.push(record(item, 'trusted AST finding'))
|
|
259
|
+
const blocking = scan.findings.filter((item) => item.blocking)
|
|
260
|
+
const entry = { schemaVersion: LEDGER_VERSION, checkpointId: digest({ contract: digest(contract), block, trustedAstFindingsDigest: trustedAstFindings ? digest(trustedAstFindings) : null }), blockId: text(block.blockId, 'blockId'), path: block.path, contractRevision: contract.revision, beforeSha256: block.beforeSha256, afterSha256: contentDigest(block.content), status: blocking.length ? 'blocked' : 'passed', ruleIds: scan.findings.map((item) => item.ruleId).sort(), blockingRuleIds: blocking.map((item) => item.ruleId).sort(), rollbackRequired: blocking.length > 0 }
|
|
261
|
+
const drift = driftState([...history, entry])
|
|
262
|
+
if (drift.trend === 'red') entry.rollbackRequired = true
|
|
263
|
+
return { checkpoint: { status: entry.rollbackRequired ? 'blocked' : 'passed', writeAllowed: !entry.rollbackRequired, rollbackRequired: entry.rollbackRequired, findings: scan.findings, metrics: scan.metrics[0], drift, instruction: drift.trend === 'red' ? 'rollback-block-and-interrupt-aimlock' : entry.rollbackRequired ? 'rollback-block-and-correct' : 'accept-block' }, ledgerEntry: entry }
|
|
264
|
+
}
|
|
265
|
+
function capabilities() {
|
|
266
|
+
return { skill: { name: 'archguard', version: COMPILER_VERSION }, operations: OPERATIONS, operationSchemas: OPERATION_SCHEMAS, contractSchemaVersion: CONTRACT_VERSION, ledgerSchemaVersion: LEDGER_VERSION, templates: Object.keys(TEMPLATE_DEFINITIONS), executionBoundary: 'AST rules execute only through the bundled trusted local runner; remote JSON cannot supply AST findings.', nextStep: { operation: 'intake', instruction: 'Detect the project stack and lock an architecture contract before code generation.' } }
|
|
267
|
+
}
|
|
268
|
+
function requestEnvelope(request) {
|
|
269
|
+
if (!request || request.schemaVersion !== REQUEST_VERSION || typeof request.requestId !== 'string' || !OPERATIONS.includes(request.operation) || !request.input || typeof request.input !== 'object' || Array.isArray(request.input)) throw new Error('Invalid ArchGuard request envelope')
|
|
270
|
+
return request
|
|
271
|
+
}
|
|
272
|
+
function checkpointResponse(requestId, input, trustedAstFindings) {
|
|
273
|
+
if (Object.hasOwn(record(input.block, 'checkpoint block'), 'astFindings')) throw new Error('checkpoint.astFindings is not accepted from JSON callers')
|
|
274
|
+
const output = checkpoint(normalizeContract(input.contract), input.block, input.history, trustedAstFindings)
|
|
275
|
+
return response(requestId, output.checkpoint.status === 'passed' ? 'succeeded' : 'blocked', output, output.checkpoint.findings)
|
|
276
|
+
}
|
|
277
|
+
async function execute(request, trustedAstFindings) {
|
|
278
|
+
const { requestId, operation, input } = requestEnvelope(request)
|
|
279
|
+
if (operation === 'capabilities' || operation === 'help') return response(requestId, 'succeeded', capabilities())
|
|
280
|
+
if (operation === 'template-list') return response(requestId, 'succeeded', { templates: Object.entries(TEMPLATE_DEFINITIONS).map(([templateId, stack]) => ({ templateId, stack })), nextStep: { operation: 'contract-create' } })
|
|
281
|
+
if (operation === 'intake') return response(requestId, 'succeeded', { questions: [{ id: 'templateId', prompt: 'Which detected stack template must be locked?', required: true, options: Object.keys(TEMPLATE_DEFINITIONS) }, { id: 'strictStandard', prompt: 'Should standard-level findings block writes?', required: true, options: [false, true] }, { id: 'budgets', prompt: 'Confirm file, function, nesting, effect, and bundle budgets.', required: true }], project: text(input.project, 'project'), nextStep: { operation: 'contract-create' } })
|
|
282
|
+
if (operation === 'contract-create') {
|
|
283
|
+
const contract = baseContract(text(input.project, 'project'), text(input.templateId, 'templateId'))
|
|
284
|
+
const merged = input.overrides ? normalizeContract({ ...contract, ...input.overrides, stack: { ...contract.stack, ...(input.overrides.stack ?? {}) }, rules: { ...contract.rules, ...(input.overrides.rules ?? {}) }, budgets: { ...contract.budgets, ...(input.overrides.budgets ?? {}) } }) : contract
|
|
285
|
+
return response(requestId, 'succeeded', { contract: merged, digest: digest(merged), nextStep: { operation: 'checkpoint' } })
|
|
286
|
+
}
|
|
287
|
+
if (operation === 'contract-get') { const contract = normalizeContract(input.contract); return response(requestId, 'succeeded', { contract, digest: digest(contract) }) }
|
|
288
|
+
if (operation === 'contract-update') {
|
|
289
|
+
const contract = normalizeContract(input.contract)
|
|
290
|
+
if (input.expectedDigest !== digest(contract) || input.confirmation !== 'confirm-architecture-contract-update') throw new Error('Contract update authority is invalid')
|
|
291
|
+
const updated = normalizeContract({ ...contract, ...record(input.patch, 'patch'), revision: contract.revision + 1, audit: { updatedBy: text(input.actor, 'actor'), reason: text(input.reason, 'reason'), previousDigest: input.expectedDigest } })
|
|
292
|
+
return response(requestId, 'succeeded', { contract: updated, digest: digest(updated), audit: updated.audit })
|
|
293
|
+
}
|
|
294
|
+
if (operation === 'checkpoint') return checkpointResponse(requestId, input, trustedAstFindings)
|
|
295
|
+
if (operation === 'rules-scan' || operation === 'complexity-report') {
|
|
296
|
+
const scan = scanFiles(normalizeContract(input.contract), input.files)
|
|
297
|
+
const output = operation === 'rules-scan' ? { findings: scan.findings, summary: { total: scan.findings.length, blocking: scan.findings.filter((item) => item.blocking).length } } : { metrics: scan.metrics, findings: scan.findings.filter((item) => item.category === 'complexity') }
|
|
298
|
+
return response(requestId, scan.findings.some((item) => item.blocking) ? 'blocked' : 'succeeded', output, scan.findings)
|
|
299
|
+
}
|
|
300
|
+
if (operation === 'drift-status') return response(requestId, 'succeeded', { drift: driftState(input.ledger) })
|
|
301
|
+
if (!Array.isArray(input.ledger)) throw new Error('ledger is required')
|
|
302
|
+
const entries = input.ledger.filter((entry) => (!input.ruleId || entry.ruleIds?.includes(input.ruleId)) && (!input.status || entry.status === input.status))
|
|
303
|
+
return response(requestId, 'succeeded', { schemaVersion: LEDGER_VERSION, entries, total: entries.length })
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function run(request) {
|
|
307
|
+
return execute(request, undefined)
|
|
308
|
+
}
|
|
309
|
+
async function runTrustedLocalCheckpoint(request, trustedAstFindings) {
|
|
310
|
+
if (request?.operation !== 'checkpoint' || !Array.isArray(trustedAstFindings)) throw new Error('trusted local checkpoint authority is invalid')
|
|
311
|
+
return execute(request, trustedAstFindings.map((item) => record(item, 'trusted AST finding')))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { CONTRACT_VERSION, LEDGER_VERSION, OPERATION_SCHEMAS, TEMPLATE_DEFINITIONS, sourceInspectionScope, run, runTrustedLocalCheckpoint }
|