@devtrack-solution/codesdd 1.2.4-rc3 → 1.2.4
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/.sdd/skills/curated/devtrack-api/SKILL.md +91 -12
- package/.sdd/skills/curated/devtrack-api/agents/claude-code.yaml +2 -0
- package/.sdd/skills/curated/devtrack-api/agents/codex.yaml +2 -0
- package/.sdd/skills/curated/devtrack-api/agents/cursor.yaml +2 -0
- package/.sdd/skills/curated/devtrack-api/agents/gemini.yaml +2 -0
- package/.sdd/skills/curated/devtrack-api/agents/kimi.yaml +2 -0
- package/.sdd/skills/curated/devtrack-api/agents/openai.yaml +3 -3
- package/.sdd/skills/curated/devtrack-api/agents/opencode.yaml +2 -0
- package/.sdd/skills/curated/devtrack-api/references/application-presentation.md +59 -3
- package/.sdd/skills/curated/devtrack-api/references/consumer-sync-policy.md +15 -3
- package/.sdd/skills/curated/devtrack-api/references/contract-pack.yaml +1898 -2
- package/.sdd/skills/curated/devtrack-api/references/domain-modeling.md +3 -1
- package/.sdd/skills/curated/devtrack-api/references/field-validation-protocol.md +40 -0
- package/.sdd/skills/curated/devtrack-api/references/foundation-layout.md +20 -2
- package/.sdd/skills/curated/devtrack-api/references/generated-artifact-invalidation.md +97 -0
- package/.sdd/skills/curated/devtrack-api/references/implementation-checklist.md +30 -1
- package/.sdd/skills/curated/devtrack-api/references/portable-agent-contract.md +4 -3
- package/.sdd/skills/curated/devtrack-api/references/testing-validation.md +22 -1
- package/.sdd/skills/curated/devtrack-api/references/typeorm-infrastructure.md +9 -5
- package/README.md +122 -25
- package/dist/cli/program.js +180 -11
- package/dist/commands/config.js +27 -1
- package/dist/commands/sdd/execution.js +64 -2
- package/dist/commands/sdd.js +119 -4
- package/dist/core/cli/command-matrix.d.ts +18 -0
- package/dist/core/cli/command-matrix.js +148 -0
- package/dist/core/cli-command-quality.js +2 -0
- package/dist/core/config-schema.d.ts +14 -1
- package/dist/core/config-schema.js +32 -1
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +11 -0
- package/dist/core/global-config.d.ts +13 -0
- package/dist/core/init.d.ts +2 -2
- package/dist/core/init.js +13 -14
- package/dist/core/sdd/agent-binding.d.ts +9 -9
- package/dist/core/sdd/agent-runtime-contract.d.ts +4 -4
- package/dist/core/sdd/allocator-recovery.d.ts +14 -0
- package/dist/core/sdd/allocator-recovery.js +30 -0
- package/dist/core/sdd/allocator-security.d.ts +18 -0
- package/dist/core/sdd/allocator-security.js +36 -0
- package/dist/core/sdd/api-foundation-baseline.d.ts +111 -0
- package/dist/core/sdd/api-foundation-baseline.js +151 -0
- package/dist/core/sdd/api-foundation-parity.d.ts +114 -0
- package/dist/core/sdd/api-foundation-parity.js +131 -0
- package/dist/core/sdd/api-profile-catalog.d.ts +36 -0
- package/dist/core/sdd/api-profile-catalog.js +132 -0
- package/dist/core/sdd/api-profile-dry-run-projection.d.ts +93 -0
- package/dist/core/sdd/api-profile-dry-run-projection.js +370 -0
- package/dist/core/sdd/api-profile-recipes.d.ts +82 -0
- package/dist/core/sdd/api-profile-recipes.js +484 -0
- package/dist/core/sdd/artifact-id-allocator.d.ts +368 -0
- package/dist/core/sdd/artifact-id-allocator.js +510 -0
- package/dist/core/sdd/check.d.ts +50 -1
- package/dist/core/sdd/check.js +286 -9
- package/dist/core/sdd/deepagent-contracts.d.ts +4 -4
- package/dist/core/sdd/deepagents/reversa-subagents.d.ts +3 -3
- package/dist/core/sdd/default-bootstrap-files.d.ts +1 -1
- package/dist/core/sdd/default-bootstrap-files.js +0 -2
- package/dist/core/sdd/default-skills.js +7 -5
- package/dist/core/sdd/devtrack-api-appliance.d.ts +34 -0
- package/dist/core/sdd/devtrack-api-appliance.js +138 -34
- package/dist/core/sdd/devtrack-api-architecture.d.ts +16 -0
- package/dist/core/sdd/devtrack-api-architecture.js +86 -0
- package/dist/core/sdd/docs-sync.js +3 -3
- package/dist/core/sdd/enterprise-mutating-command-gate.d.ts +27 -0
- package/dist/core/sdd/enterprise-mutating-command-gate.js +104 -0
- package/dist/core/sdd/enterprise-provenance-gates.d.ts +20 -0
- package/dist/core/sdd/enterprise-provenance-gates.js +63 -0
- package/dist/core/sdd/enterprise-provisioning-policy.d.ts +26 -0
- package/dist/core/sdd/enterprise-provisioning-policy.js +104 -0
- package/dist/core/sdd/governance-schemas.d.ts +2 -2
- package/dist/core/sdd/governance-schemas.js +11 -2
- package/dist/core/sdd/json-schema.js +4 -0
- package/dist/core/sdd/legacy-operations.js +93 -4
- package/dist/core/sdd/package-security-gates.js +2 -0
- package/dist/core/sdd/package-structure-gate.d.ts +85 -3
- package/dist/core/sdd/package-structure-gate.js +386 -8
- package/dist/core/sdd/parallel-feat-automation.d.ts +6 -6
- package/dist/core/sdd/plugin-policy.js +6 -1
- package/dist/core/sdd/plugin-registry.d.ts +3 -3
- package/dist/core/sdd/quality-validation.d.ts +5 -5
- package/dist/core/sdd/release-readiness.d.ts +49 -0
- package/dist/core/sdd/release-readiness.js +303 -8
- package/dist/core/sdd/reversa-architecture-extractor.d.ts +13 -0
- package/dist/core/sdd/reversa-architecture-extractor.js +89 -0
- package/dist/core/sdd/reversa-artifact-writer.d.ts +18 -0
- package/dist/core/sdd/reversa-artifact-writer.js +40 -0
- package/dist/core/sdd/reversa-command-policy.d.ts +136 -0
- package/dist/core/sdd/reversa-command-policy.js +361 -0
- package/dist/core/sdd/reversa-data-extractor.d.ts +11 -0
- package/dist/core/sdd/reversa-data-extractor.js +73 -0
- package/dist/core/sdd/reversa-equivalence.d.ts +20 -0
- package/dist/core/sdd/reversa-equivalence.js +34 -0
- package/dist/core/sdd/reversa-evidence.d.ts +298 -0
- package/dist/core/sdd/reversa-evidence.js +118 -0
- package/dist/core/sdd/reversa-reconstruction.d.ts +29 -0
- package/dist/core/sdd/reversa-reconstruction.js +32 -0
- package/dist/core/sdd/reversa-rules-extractor.d.ts +12 -0
- package/dist/core/sdd/reversa-rules-extractor.js +86 -0
- package/dist/core/sdd/reversa-source-safety.d.ts +19 -0
- package/dist/core/sdd/reversa-source-safety.js +105 -0
- package/dist/core/sdd/reversa-surface-scout.d.ts +13 -0
- package/dist/core/sdd/reversa-surface-scout.js +85 -0
- package/dist/core/sdd/reversa-ux-mapper.d.ts +11 -0
- package/dist/core/sdd/reversa-ux-mapper.js +73 -0
- package/dist/core/sdd/sdk-agent-plugin-quality-gates.d.ts +1 -1
- package/dist/core/sdd/services/archive-quality-coherence.service.d.ts +17 -0
- package/dist/core/sdd/services/archive-quality-coherence.service.js +141 -0
- package/dist/core/sdd/services/decide.service.js +1 -1
- package/dist/core/sdd/services/finalize.service.d.ts +2 -0
- package/dist/core/sdd/services/finalize.service.js +48 -2
- package/dist/core/sdd/services/historical-quality-regression.service.d.ts +35 -0
- package/dist/core/sdd/services/historical-quality-regression.service.js +228 -0
- package/dist/core/sdd/services/ingest-deposito.service.js +1 -1
- package/dist/core/sdd/services/planning-execution-coherence.service.d.ts +45 -0
- package/dist/core/sdd/services/planning-execution-coherence.service.js +225 -0
- package/dist/core/sdd/state.js +15 -5
- package/dist/core/sdd/types.d.ts +3 -3
- package/dist/core/sdd/workspace-schemas.d.ts +45 -4
- package/dist/core/sdd/workspace-schemas.js +27 -6
- package/dist/core/shared/skill-generation.d.ts +2 -0
- package/dist/core/shared/skill-generation.js +19 -2
- package/dist/core/shared/tool-detection.d.ts +19 -0
- package/dist/core/shared/tool-detection.js +89 -0
- package/package.json +6 -5
- package/schemas/sdd/5-quality.schema.json +43 -0
- package/schemas/sdd/reversa-evidence-bundle.schema.json +466 -0
- package/schemas/sdd/workspace-catalog.schema.json +511 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { reversaSourceAttestationSchema } from './reversa-evidence.js';
|
|
3
|
+
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
|
4
|
+
const SECRET_PATH_SEGMENTS = new Set([
|
|
5
|
+
'.aws',
|
|
6
|
+
'.env',
|
|
7
|
+
'.ssh',
|
|
8
|
+
'credential',
|
|
9
|
+
'credentials',
|
|
10
|
+
'id_rsa',
|
|
11
|
+
'pem',
|
|
12
|
+
'private',
|
|
13
|
+
'secret',
|
|
14
|
+
'secrets',
|
|
15
|
+
'token',
|
|
16
|
+
]);
|
|
17
|
+
const SENSITIVE_KEY_TOKENS = ['key', 'secret', 'token', 'password', 'credential', 'private', 'auth'];
|
|
18
|
+
export function attestReversaSource(input) {
|
|
19
|
+
const normalizedPath = normalizeReversaSourcePath(input.sourcePath);
|
|
20
|
+
if (!normalizedPath) {
|
|
21
|
+
return blocked(input.sourcePath, 'Source path must be project-relative and must not contain traversal or absolute roots.');
|
|
22
|
+
}
|
|
23
|
+
if (isCanonicalStatePath(normalizedPath)) {
|
|
24
|
+
return blocked(normalizedPath, 'Reversa inputs cannot target canonical CodeSDD state under .sdd/state/.');
|
|
25
|
+
}
|
|
26
|
+
if (hasSecretPathSegment(normalizedPath)) {
|
|
27
|
+
return blocked(normalizedPath, 'Source path is blocked by Reversa secret-path policy.');
|
|
28
|
+
}
|
|
29
|
+
const redactedContent = typeof input.content === 'string' ? redactReversaSensitiveText(input.content) : undefined;
|
|
30
|
+
const redactionStatus = typeof input.content === 'string' && redactedContent !== input.content ? 'redacted' : 'not_required';
|
|
31
|
+
const attestation = reversaSourceAttestationSchema.parse({
|
|
32
|
+
source_ref: normalizedPath,
|
|
33
|
+
source_type: input.sourceType ?? inferSourceType(normalizedPath),
|
|
34
|
+
trust_level: input.trustLevel ?? 'hostile',
|
|
35
|
+
sha256: input.content ? createHash('sha256').update(redactedContent ?? input.content).digest('hex') : undefined,
|
|
36
|
+
redaction_status: redactionStatus,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
status: 'accepted',
|
|
40
|
+
normalized_path: normalizedPath,
|
|
41
|
+
reason: null,
|
|
42
|
+
attestation,
|
|
43
|
+
redacted_content: redactedContent,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function normalizeReversaSourcePath(value) {
|
|
47
|
+
const normalized = value.trim().replace(/\\/gu, '/').replace(/^\.\//u, '');
|
|
48
|
+
if (!normalized || normalized === '.' || normalized.startsWith('/') || WINDOWS_ABSOLUTE_PATH_PATTERN.test(normalized)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
52
|
+
if (segments.some((segment) => segment === '..')) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return segments.join('/');
|
|
56
|
+
}
|
|
57
|
+
export function redactReversaSensitiveText(value) {
|
|
58
|
+
return value
|
|
59
|
+
.split(/\r?\n/u)
|
|
60
|
+
.map((line) => {
|
|
61
|
+
const keyMatch = line.match(/^(\s*["']?([A-Za-z0-9_.-]+)["']?\s*[:=]\s*)(.+)$/u);
|
|
62
|
+
if (!keyMatch) {
|
|
63
|
+
return line.replace(/(bearer\s+)[A-Za-z0-9._~+/-]+=*/giu, '$1[REDACTED]');
|
|
64
|
+
}
|
|
65
|
+
const key = keyMatch[2] ?? '';
|
|
66
|
+
if (!SENSITIVE_KEY_TOKENS.some((token) => key.toLowerCase().includes(token))) {
|
|
67
|
+
return line;
|
|
68
|
+
}
|
|
69
|
+
return `${keyMatch[1]}[REDACTED]`;
|
|
70
|
+
})
|
|
71
|
+
.join('\n');
|
|
72
|
+
}
|
|
73
|
+
export function isReversaCanonicalStatePath(value) {
|
|
74
|
+
const normalized = normalizeReversaSourcePath(value);
|
|
75
|
+
return normalized ? isCanonicalStatePath(normalized) : false;
|
|
76
|
+
}
|
|
77
|
+
function blocked(sourcePath, reason) {
|
|
78
|
+
return {
|
|
79
|
+
status: 'blocked',
|
|
80
|
+
normalized_path: normalizeReversaSourcePath(sourcePath),
|
|
81
|
+
reason,
|
|
82
|
+
attestation: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function isCanonicalStatePath(value) {
|
|
86
|
+
return value === '.sdd/state' || value.startsWith('.sdd/state/');
|
|
87
|
+
}
|
|
88
|
+
function hasSecretPathSegment(value) {
|
|
89
|
+
return value
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.split('/')
|
|
92
|
+
.some((segment) => SECRET_PATH_SEGMENTS.has(segment) || segment.endsWith('.pem'));
|
|
93
|
+
}
|
|
94
|
+
function inferSourceType(path) {
|
|
95
|
+
if (/\.(md|mdx|txt|rst)$/iu.test(path))
|
|
96
|
+
return 'document';
|
|
97
|
+
if (/\.(ya?ml|json|toml|ini|env)$/iu.test(path))
|
|
98
|
+
return 'config';
|
|
99
|
+
if (/\.(log|out)$/iu.test(path))
|
|
100
|
+
return 'runtime_log';
|
|
101
|
+
if (/\.(sql|prisma)$/iu.test(path))
|
|
102
|
+
return 'database_schema';
|
|
103
|
+
return 'repository';
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=reversa-source-safety.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReversaEvidenceBundle } from './reversa-evidence.js';
|
|
2
|
+
export interface ReversaSurfaceSource {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BuildReversaSurfaceInventoryInput {
|
|
7
|
+
featureRef: string;
|
|
8
|
+
operationId: string;
|
|
9
|
+
generatedAt: string;
|
|
10
|
+
sources: ReversaSurfaceSource[];
|
|
11
|
+
}
|
|
12
|
+
export declare function buildReversaSurfaceInventory(input: BuildReversaSurfaceInventoryInput): ReversaEvidenceBundle;
|
|
13
|
+
//# sourceMappingURL=reversa-surface-scout.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { attestReversaSource } from './reversa-source-safety.js';
|
|
2
|
+
export function buildReversaSurfaceInventory(input) {
|
|
3
|
+
const attestations = input.sources.map((source) => attestReversaSource({
|
|
4
|
+
sourcePath: source.path,
|
|
5
|
+
content: source.content,
|
|
6
|
+
}));
|
|
7
|
+
const accepted = attestations.filter((entry) => entry.status === 'accepted' && entry.attestation);
|
|
8
|
+
const blocked = attestations.filter((entry) => entry.status === 'blocked');
|
|
9
|
+
const findings = accepted.flatMap((entry) => {
|
|
10
|
+
const source = input.sources.find((candidate) => entry.normalized_path === candidate.path.replace(/^\.\//u, ''));
|
|
11
|
+
return source ? scanSurface(entry.normalized_path, source.content) : [];
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
schema_version: 1,
|
|
15
|
+
contract: 'reversa-evidence-bundle/v1',
|
|
16
|
+
operation_id: input.operationId,
|
|
17
|
+
generated_at: input.generatedAt,
|
|
18
|
+
feature_ref: input.featureRef,
|
|
19
|
+
mode: 'read-only',
|
|
20
|
+
source_attestations: accepted.map((entry) => entry.attestation),
|
|
21
|
+
findings,
|
|
22
|
+
artifacts: [
|
|
23
|
+
{
|
|
24
|
+
path: `.sdd/evidence/reversa/${input.featureRef}/surface-inventory.yaml`,
|
|
25
|
+
artifact_type: 'inventory',
|
|
26
|
+
phase: 'surface',
|
|
27
|
+
produced_by: 'codesdd reversa surface scout',
|
|
28
|
+
write_scope: 'read_only',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
validations: [
|
|
32
|
+
{
|
|
33
|
+
id: 'REV-VAL-SURFACE-SAFETY',
|
|
34
|
+
phase: 'surface',
|
|
35
|
+
command: 'buildReversaSurfaceInventory',
|
|
36
|
+
status: blocked.length === 0 ? 'passed' : 'blocked',
|
|
37
|
+
issue_codes: blocked.length === 0 ? [] : ['UNSAFE_SOURCE_BLOCKED'],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
contradictions: [],
|
|
41
|
+
human_questions: [],
|
|
42
|
+
quality_refs: [`.sdd/active/${input.featureRef}/5-quality.yaml`],
|
|
43
|
+
residual_risks: blocked.map((entry) => ({
|
|
44
|
+
code: 'UNSAFE_SOURCE_BLOCKED',
|
|
45
|
+
severity: 'medium',
|
|
46
|
+
description: entry.reason ?? 'A legacy source was blocked by Reversa intake safety.',
|
|
47
|
+
mitigation: 'Review source path and provide a safe project-relative source before extraction.',
|
|
48
|
+
})),
|
|
49
|
+
metadata: {
|
|
50
|
+
counts: summarizeSurface(findings),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function scanSurface(sourceRef, content) {
|
|
55
|
+
const findings = [];
|
|
56
|
+
const routeMatches = [...content.matchAll(/\b(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/giu)];
|
|
57
|
+
const controllerMatches = [...content.matchAll(/@(Controller|Get|Post|Put|Patch|Delete)\s*\(\s*['"`]?([^'"`)]*)['"`]?\s*\)/giu)];
|
|
58
|
+
const jobMatches = [...content.matchAll(/\b(?:cron|schedule|job|queue|worker)\b/giu)];
|
|
59
|
+
const commandMatches = [...content.matchAll(/\b(?:command|program\.command|Command)\b/gu)];
|
|
60
|
+
const screenMatches = [...content.matchAll(/(?:component|screen|page|route)/giu)];
|
|
61
|
+
pushFinding(findings, 'API', 'API endpoints', routeMatches.length + controllerMatches.length, sourceRef);
|
|
62
|
+
pushFinding(findings, 'JOB', 'Jobs and workers', jobMatches.length, sourceRef);
|
|
63
|
+
pushFinding(findings, 'COMMAND', 'CLI commands', commandMatches.length, sourceRef);
|
|
64
|
+
pushFinding(findings, 'SCREEN', 'Screens and routes', screenMatches.length, sourceRef);
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
67
|
+
function pushFinding(findings, suffix, title, count, sourceRef) {
|
|
68
|
+
if (count === 0)
|
|
69
|
+
return;
|
|
70
|
+
findings.push({
|
|
71
|
+
id: `REV-FIND-SURFACE-${suffix}`,
|
|
72
|
+
phase: 'surface',
|
|
73
|
+
title,
|
|
74
|
+
summary: `${title} detected in ${sourceRef}: ${count}.`,
|
|
75
|
+
confidence: 'medium',
|
|
76
|
+
source_refs: [sourceRef],
|
|
77
|
+
contradiction_refs: [],
|
|
78
|
+
promoted_refs: [],
|
|
79
|
+
metadata: { count },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function summarizeSurface(findings) {
|
|
83
|
+
return Object.fromEntries(findings.map((finding) => [finding.id, Number(finding.metadata.count ?? 0)]));
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=reversa-surface-scout.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReversaEvidenceBundle } from './reversa-evidence.js';
|
|
2
|
+
export declare function buildReversaUxPermissionMap(input: {
|
|
3
|
+
featureRef: string;
|
|
4
|
+
operationId: string;
|
|
5
|
+
generatedAt: string;
|
|
6
|
+
sources: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}>;
|
|
10
|
+
}): ReversaEvidenceBundle;
|
|
11
|
+
//# sourceMappingURL=reversa-ux-mapper.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { attestReversaSource } from './reversa-source-safety.js';
|
|
2
|
+
export function buildReversaUxPermissionMap(input) {
|
|
3
|
+
const accepted = input.sources
|
|
4
|
+
.map((source) => ({ source, attestation: attestReversaSource({ sourcePath: source.path, content: source.content }) }))
|
|
5
|
+
.filter((entry) => entry.attestation.status === 'accepted' && entry.attestation.attestation);
|
|
6
|
+
const screens = accepted.flatMap(({ source, attestation }) => scanScreens(attestation.normalized_path, source.content));
|
|
7
|
+
const permissions = accepted.flatMap(({ source, attestation }) => scanPermissions(attestation.normalized_path, source.content));
|
|
8
|
+
const findings = [];
|
|
9
|
+
if (screens.length > 0) {
|
|
10
|
+
findings.push({
|
|
11
|
+
id: 'REV-FIND-UX-SCREENS',
|
|
12
|
+
phase: 'ux',
|
|
13
|
+
title: 'Screens and UI routes',
|
|
14
|
+
summary: `Detected ${screens.length} screen, component or route candidates.`,
|
|
15
|
+
confidence: 'medium',
|
|
16
|
+
source_refs: [...new Set(screens.map((entry) => entry.source_ref))],
|
|
17
|
+
contradiction_refs: [],
|
|
18
|
+
promoted_refs: [],
|
|
19
|
+
metadata: { screens },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (permissions.length > 0) {
|
|
23
|
+
findings.push({
|
|
24
|
+
id: 'REV-FIND-UX-PERMISSIONS',
|
|
25
|
+
phase: 'ux',
|
|
26
|
+
title: 'Permission and guard flows',
|
|
27
|
+
summary: `Detected ${permissions.length} permission or guard candidates.`,
|
|
28
|
+
confidence: 'medium',
|
|
29
|
+
source_refs: [...new Set(permissions.map((entry) => entry.source_ref))],
|
|
30
|
+
contradiction_refs: [],
|
|
31
|
+
promoted_refs: [],
|
|
32
|
+
metadata: { permissions },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
schema_version: 1,
|
|
37
|
+
contract: 'reversa-evidence-bundle/v1',
|
|
38
|
+
operation_id: input.operationId,
|
|
39
|
+
generated_at: input.generatedAt,
|
|
40
|
+
feature_ref: input.featureRef,
|
|
41
|
+
mode: 'read-only',
|
|
42
|
+
source_attestations: accepted.map((entry) => entry.attestation.attestation),
|
|
43
|
+
findings,
|
|
44
|
+
artifacts: [{
|
|
45
|
+
path: `.sdd/evidence/reversa/${input.featureRef}/ux-permission-map.yaml`,
|
|
46
|
+
artifact_type: 'ux_map',
|
|
47
|
+
phase: 'ux',
|
|
48
|
+
produced_by: 'codesdd reversa ux mapper',
|
|
49
|
+
write_scope: 'read_only',
|
|
50
|
+
}],
|
|
51
|
+
validations: [{
|
|
52
|
+
id: 'REV-VAL-UX-SAFETY',
|
|
53
|
+
phase: 'ux',
|
|
54
|
+
command: 'buildReversaUxPermissionMap',
|
|
55
|
+
status: accepted.length === input.sources.length ? 'passed' : 'blocked',
|
|
56
|
+
issue_codes: accepted.length === input.sources.length ? [] : ['UNSAFE_SOURCE_BLOCKED'],
|
|
57
|
+
}],
|
|
58
|
+
contradictions: [],
|
|
59
|
+
human_questions: [],
|
|
60
|
+
quality_refs: [`.sdd/active/${input.featureRef}/5-quality.yaml`],
|
|
61
|
+
residual_risks: [],
|
|
62
|
+
metadata: { screen_count: screens.length, permission_count: permissions.length },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function scanScreens(sourceRef, content) {
|
|
66
|
+
return [...content.matchAll(/\b(?:component|screen|page|route|path)\b\s*[:=]?\s*['"]?([A-Za-z0-9_/-]*)/giu)]
|
|
67
|
+
.map((match) => ({ name: match[1] || match[0], source_ref: sourceRef }));
|
|
68
|
+
}
|
|
69
|
+
function scanPermissions(sourceRef, content) {
|
|
70
|
+
return [...content.matchAll(/\b(?:guard|canActivate|permission|role|rbac|auth)\b/giu)]
|
|
71
|
+
.map((match) => ({ signal: match[0].toLowerCase(), source_ref: sourceRef }));
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=reversa-ux-mapper.js.map
|
|
@@ -41,8 +41,8 @@ export declare const sdkAgentPluginQualityGateInputSchema: z.ZodObject<{
|
|
|
41
41
|
}, z.core.$strip>>;
|
|
42
42
|
}, z.core.$strip>>>;
|
|
43
43
|
required_agent_providers: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
44
|
-
codex: "codex";
|
|
45
44
|
opencode: "opencode";
|
|
45
|
+
codex: "codex";
|
|
46
46
|
deepagents: "deepagents";
|
|
47
47
|
}>>>;
|
|
48
48
|
coverage_target: z.ZodDefault<z.ZodObject<{
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ArchiveQualityCoherenceIssueCode = 'ACCEPTANCE_NOT_MET_WITH_Q95_PASS' | 'OPEN_RISK_WITH_Q95_PASS' | 'PLACEHOLDER_EVIDENCE_WITH_Q95_PASS' | 'GENERIC_EVIDENCE_WITH_Q95_PASS';
|
|
2
|
+
export interface ArchiveQualityCoherenceIssue {
|
|
3
|
+
featureId: string;
|
|
4
|
+
qualityPath: string;
|
|
5
|
+
issueCode: ArchiveQualityCoherenceIssueCode;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ArchiveQualityCoherenceReport {
|
|
9
|
+
scanned: number;
|
|
10
|
+
analyzed: number;
|
|
11
|
+
grandfathered: number;
|
|
12
|
+
issues: ArchiveQualityCoherenceIssue[];
|
|
13
|
+
}
|
|
14
|
+
export declare function scanArchiveQualityCoherence(archivedDir: string, options?: {
|
|
15
|
+
grandfatherMaxFeat?: number;
|
|
16
|
+
}): Promise<ArchiveQualityCoherenceReport>;
|
|
17
|
+
//# sourceMappingURL=archive-quality-coherence.service.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseWorkspaceYamlDocument } from '../workspace-schemas.js';
|
|
5
|
+
const PLACEHOLDER_PATTERNS = [
|
|
6
|
+
/\(fill in/i,
|
|
7
|
+
/\(placeholder/i,
|
|
8
|
+
/\btodo\b/i,
|
|
9
|
+
/\btbd\b/i,
|
|
10
|
+
/\bpending\b/i,
|
|
11
|
+
/\bmust implement\b/i,
|
|
12
|
+
/\bto be executed\b/i,
|
|
13
|
+
/\bplanned\b/i,
|
|
14
|
+
/\bwill be\b/i,
|
|
15
|
+
/\bto do\b/i,
|
|
16
|
+
];
|
|
17
|
+
const GENERIC_EVIDENCE_PATTERNS = [
|
|
18
|
+
/must implement and record executable evidence/i,
|
|
19
|
+
/planned but not yet executed/i,
|
|
20
|
+
/is implemented only in the planned surfaces/i,
|
|
21
|
+
/planned implementation surface/i,
|
|
22
|
+
/pending\.?$/i,
|
|
23
|
+
];
|
|
24
|
+
const DEFAULT_GRANDFATHER_MAX_FEAT = 451;
|
|
25
|
+
function featNumericId(featureId) {
|
|
26
|
+
const match = featureId.match(/^FEAT-(\d{4,})$/);
|
|
27
|
+
if (!match)
|
|
28
|
+
return null;
|
|
29
|
+
return Number.parseInt(match[1] || '', 10);
|
|
30
|
+
}
|
|
31
|
+
function hasPattern(text, patterns) {
|
|
32
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
33
|
+
}
|
|
34
|
+
function qualityHasPlaceholderOrGenericEvidence(document) {
|
|
35
|
+
let hasPlaceholder = false;
|
|
36
|
+
let hasGeneric = false;
|
|
37
|
+
const evidenceTexts = [
|
|
38
|
+
...document.evidence_log.flatMap((entry) => [entry.kind, entry.result, entry.artifact ?? '']),
|
|
39
|
+
...document.acceptance_matrix.flatMap((entry) => [entry.criterion, entry.evidence]),
|
|
40
|
+
];
|
|
41
|
+
for (const text of evidenceTexts) {
|
|
42
|
+
if (!text)
|
|
43
|
+
continue;
|
|
44
|
+
if (!hasPlaceholder && hasPattern(text, PLACEHOLDER_PATTERNS)) {
|
|
45
|
+
hasPlaceholder = true;
|
|
46
|
+
}
|
|
47
|
+
if (!hasGeneric && hasPattern(text, GENERIC_EVIDENCE_PATTERNS)) {
|
|
48
|
+
hasGeneric = true;
|
|
49
|
+
}
|
|
50
|
+
if (hasPlaceholder && hasGeneric) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { hasPlaceholder, hasGeneric };
|
|
55
|
+
}
|
|
56
|
+
export async function scanArchiveQualityCoherence(archivedDir, options = {}) {
|
|
57
|
+
if (!existsSync(archivedDir)) {
|
|
58
|
+
return {
|
|
59
|
+
scanned: 0,
|
|
60
|
+
analyzed: 0,
|
|
61
|
+
grandfathered: 0,
|
|
62
|
+
issues: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const grandfatherMaxFeat = options.grandfatherMaxFeat ?? DEFAULT_GRANDFATHER_MAX_FEAT;
|
|
66
|
+
const issues = [];
|
|
67
|
+
const entries = await fs.readdir(archivedDir, { withFileTypes: true });
|
|
68
|
+
let scanned = 0;
|
|
69
|
+
let analyzed = 0;
|
|
70
|
+
let grandfathered = 0;
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory() || !entry.name.startsWith('FEAT-'))
|
|
73
|
+
continue;
|
|
74
|
+
scanned += 1;
|
|
75
|
+
const featureId = entry.name;
|
|
76
|
+
const numericId = featNumericId(featureId);
|
|
77
|
+
if (numericId !== null && numericId <= grandfatherMaxFeat) {
|
|
78
|
+
grandfathered += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const qualityPath = path.join(archivedDir, featureId, '5-quality.yaml');
|
|
82
|
+
if (!existsSync(qualityPath)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const raw = await fs.readFile(qualityPath, 'utf-8').catch(() => '');
|
|
86
|
+
if (!raw.trim()) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let quality;
|
|
90
|
+
try {
|
|
91
|
+
quality = parseWorkspaceYamlDocument('5-quality.yaml', raw);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
analyzed += 1;
|
|
97
|
+
if (quality.q95_ledger.status !== 'pass') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (quality.acceptance_matrix.some((entry) => entry.status === 'not_met')) {
|
|
101
|
+
issues.push({
|
|
102
|
+
featureId,
|
|
103
|
+
qualityPath,
|
|
104
|
+
issueCode: 'ACCEPTANCE_NOT_MET_WITH_Q95_PASS',
|
|
105
|
+
message: `${featureId} archived quality has q95_ledger.status=pass but acceptance_matrix contains not_met entries.`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (quality.requirement_validation_evidence_risk_matrix.some((entry) => entry.risk_status === 'open')) {
|
|
109
|
+
issues.push({
|
|
110
|
+
featureId,
|
|
111
|
+
qualityPath,
|
|
112
|
+
issueCode: 'OPEN_RISK_WITH_Q95_PASS',
|
|
113
|
+
message: `${featureId} archived quality has q95_ledger.status=pass but requirement risk_status=open remains unresolved.`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const evidenceSignals = qualityHasPlaceholderOrGenericEvidence(quality);
|
|
117
|
+
if (evidenceSignals.hasPlaceholder) {
|
|
118
|
+
issues.push({
|
|
119
|
+
featureId,
|
|
120
|
+
qualityPath,
|
|
121
|
+
issueCode: 'PLACEHOLDER_EVIDENCE_WITH_Q95_PASS',
|
|
122
|
+
message: `${featureId} archived quality has q95_ledger.status=pass but placeholder/future-tense evidence markers are present.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (evidenceSignals.hasGeneric) {
|
|
126
|
+
issues.push({
|
|
127
|
+
featureId,
|
|
128
|
+
qualityPath,
|
|
129
|
+
issueCode: 'GENERIC_EVIDENCE_WITH_Q95_PASS',
|
|
130
|
+
message: `${featureId} archived quality has q95_ledger.status=pass but generic non-executable evidence text is still present.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
scanned,
|
|
136
|
+
analyzed,
|
|
137
|
+
grandfathered,
|
|
138
|
+
issues,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=archive-quality-coherence.service.js.map
|
|
@@ -23,7 +23,7 @@ export class DecideService {
|
|
|
23
23
|
const debateContent = await fs.readFile(debateFile, 'utf-8');
|
|
24
24
|
const missingSections = validateDebateDocument(debateContent);
|
|
25
25
|
if (missingSections.length > 0) {
|
|
26
|
-
throw new Error(`Debate ${debateId}
|
|
26
|
+
throw new Error(`Debate ${debateId} failed the planning quality gate required for promotion. Fill required sections before deciding:\n- ${missingSections.join('\n- ')}`);
|
|
27
27
|
}
|
|
28
28
|
const now = nowIso();
|
|
29
29
|
const targetStatus = outcome === 'radar' || outcome === 'epic' ? 'APPROVED' : 'DISCARDED';
|
|
@@ -2,6 +2,7 @@ import { SddPaths, SddRuntimeConfig } from "../state.js";
|
|
|
2
2
|
import { BacklogItem, FinalizeQueueItem } from "../types.js";
|
|
3
3
|
import { SddStores } from "../store/sdd-stores.js";
|
|
4
4
|
import { type WorkspaceQualityDocument } from "../workspace-schemas.js";
|
|
5
|
+
export declare function reconcileAdrMarkdownForFinalize(existingAdr: string, now: string): string;
|
|
5
6
|
export declare class FinalizeService {
|
|
6
7
|
private readonly stores;
|
|
7
8
|
constructor(stores: SddStores);
|
|
@@ -135,4 +136,5 @@ export declare function missingCoverageTargets(document: WorkspaceQualityDocumen
|
|
|
135
136
|
export declare function coverageTargetMet(document: WorkspaceQualityDocument, kind: 'unit' | 'integration', target: number): boolean;
|
|
136
137
|
export declare function countQualityEvidenceRounds(document: WorkspaceQualityDocument): number;
|
|
137
138
|
export declare function missingSkillEvidence(document: WorkspaceQualityDocument): string[];
|
|
139
|
+
export declare function missingCriticalEvidenceProvenance(document: WorkspaceQualityDocument): string[];
|
|
138
140
|
//# sourceMappingURL=finalize.service.d.ts.map
|
|
@@ -43,6 +43,13 @@ function moveCompletedFinalizeItemsToHistory(state) {
|
|
|
43
43
|
}
|
|
44
44
|
state.items = activeItems;
|
|
45
45
|
}
|
|
46
|
+
export function reconcileAdrMarkdownForFinalize(existingAdr, now) {
|
|
47
|
+
return existingAdr
|
|
48
|
+
.replace(/^status:\s*IN_PROGRESS\b/m, 'status: DONE')
|
|
49
|
+
.replace(/^status:\s*DRAFT\b/m, 'status: DONE')
|
|
50
|
+
.replace(/^decision_status:\s*PROPOSED\b/m, 'decision_status: ACCEPTED')
|
|
51
|
+
.replace(/^updated_at:\s*["']?[\dTZ:.-]+["']?\s*$/m, `updated_at: ${now}`);
|
|
52
|
+
}
|
|
46
53
|
function buildPostFinalizeReplan(items, finalized, unlocked, generatedAt) {
|
|
47
54
|
const rank = 'impact';
|
|
48
55
|
const computed = computeReadyFeatures(items, { rank });
|
|
@@ -451,7 +458,13 @@ export class FinalizeService {
|
|
|
451
458
|
if (!options?.noAdr && !feature.requires_adr) {
|
|
452
459
|
const adrPath = path.join(paths.coreDir, 'adrs', `ADR-${feature.id}.md`);
|
|
453
460
|
const tx = new SddWriteTransaction();
|
|
454
|
-
|
|
461
|
+
try {
|
|
462
|
+
const existingAdr = await fs.readFile(adrPath, 'utf-8');
|
|
463
|
+
tx.writeFile(adrPath, reconcileAdrMarkdownForFinalize(existingAdr, now));
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
tx.writeFile(adrPath, buildAdrMarkdown(feature, unlockedByFeature, now));
|
|
467
|
+
}
|
|
455
468
|
await tx.commit(paths.projectRoot, paths.memoryRoot, 'finalize.service (ADR)');
|
|
456
469
|
}
|
|
457
470
|
await fs.mkdir(paths.archivedDir, { recursive: true });
|
|
@@ -701,6 +714,7 @@ export async function evaluateWorkspaceQualityFeedback(paths, config, feature) {
|
|
|
701
714
|
}
|
|
702
715
|
const missingTargets = missingCoverageTargets(document);
|
|
703
716
|
const missingSkills = missingSkillEvidence(document);
|
|
717
|
+
const criticalProvenanceReasons = missingCriticalEvidenceProvenance(document);
|
|
704
718
|
const matrixReasons = evaluateQualityWitnessMatrix(document);
|
|
705
719
|
const reasons = [];
|
|
706
720
|
const maxRounds = document.remediation_policy.max_rounds;
|
|
@@ -728,6 +742,9 @@ export async function evaluateWorkspaceQualityFeedback(paths, config, feature) {
|
|
|
728
742
|
if (missingSkills.length > 0) {
|
|
729
743
|
reasons.push(`required skill evidence missing for ${missingSkills.join(', ')}`);
|
|
730
744
|
}
|
|
745
|
+
if (criticalProvenanceReasons.length > 0) {
|
|
746
|
+
reasons.push(...criticalProvenanceReasons);
|
|
747
|
+
}
|
|
731
748
|
if (matrixReasons.length > 0) {
|
|
732
749
|
reasons.push(`quality-witness matrix blocked finalize: ${matrixReasons.join(' | ')}`);
|
|
733
750
|
}
|
|
@@ -942,7 +959,6 @@ const LEGACY_NAMING_PATTERNS = [
|
|
|
942
959
|
/\bcoach-sdd\b/u,
|
|
943
960
|
/\bcoach_sdd\b/u,
|
|
944
961
|
/\bsdd\s+coach\b/u,
|
|
945
|
-
/\.codesdd\b/u,
|
|
946
962
|
];
|
|
947
963
|
export function computeNamingAxis(feature, document) {
|
|
948
964
|
let score = 100;
|
|
@@ -1165,4 +1181,34 @@ export function missingSkillEvidence(document) {
|
|
|
1165
1181
|
.filter(Boolean));
|
|
1166
1182
|
return required.filter((skillId) => !provided.has(skillId.toLowerCase()));
|
|
1167
1183
|
}
|
|
1184
|
+
export function missingCriticalEvidenceProvenance(document) {
|
|
1185
|
+
const reasons = [];
|
|
1186
|
+
document.evidence_log.forEach((entry, index) => {
|
|
1187
|
+
if (entry.critical !== true) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const provenance = entry.provenance;
|
|
1191
|
+
if (!provenance) {
|
|
1192
|
+
reasons.push(`critical evidence_log[${index}] (${entry.kind}) missing provenance (command, exit_code, timestamp_iso, commit_sha, artifact_path, artifact_hash)`);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const missingFields = [];
|
|
1196
|
+
if (!provenance.command?.trim())
|
|
1197
|
+
missingFields.push('command');
|
|
1198
|
+
if (!Number.isInteger(provenance.exit_code))
|
|
1199
|
+
missingFields.push('exit_code');
|
|
1200
|
+
if (!provenance.timestamp_iso?.trim())
|
|
1201
|
+
missingFields.push('timestamp_iso');
|
|
1202
|
+
if (!provenance.commit_sha?.trim())
|
|
1203
|
+
missingFields.push('commit_sha');
|
|
1204
|
+
if (!provenance.artifact_path?.trim())
|
|
1205
|
+
missingFields.push('artifact_path');
|
|
1206
|
+
if (!provenance.artifact_hash?.trim())
|
|
1207
|
+
missingFields.push('artifact_hash');
|
|
1208
|
+
if (missingFields.length > 0) {
|
|
1209
|
+
reasons.push(`critical evidence_log[${index}] (${entry.kind}) missing provenance fields: ${missingFields.join(', ')}`);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
return reasons;
|
|
1213
|
+
}
|
|
1168
1214
|
//# sourceMappingURL=finalize.service.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type HistoricalQualityRegressionMode = 'recent-window' | 'full-history';
|
|
2
|
+
export type HistoricalQualityRegressionIssueCode = 'ACCEPTANCE_NOT_MET' | 'OPEN_RISK' | 'PLACEHOLDER_EVIDENCE' | 'MISSING_ADR' | 'FRONTEND_IMPACT_UNKNOWN' | 'MISSING_DONE_AT' | 'MISSING_ARCHIVED_AT' | 'STATE_CORE_DIVERGENCE';
|
|
3
|
+
export interface HistoricalQualityRegressionIssue {
|
|
4
|
+
feature_id: string;
|
|
5
|
+
code: HistoricalQualityRegressionIssueCode;
|
|
6
|
+
message: string;
|
|
7
|
+
quality_path?: string;
|
|
8
|
+
backlog_path?: string;
|
|
9
|
+
adr_path?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HistoricalQualityRegressionReport {
|
|
12
|
+
generated_at: string;
|
|
13
|
+
mode: HistoricalQualityRegressionMode;
|
|
14
|
+
recent_window_size: number | null;
|
|
15
|
+
scanned: number;
|
|
16
|
+
considered: number;
|
|
17
|
+
analyzed: number;
|
|
18
|
+
grandfathered: number;
|
|
19
|
+
issue_count: number;
|
|
20
|
+
grandfathering: {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
max_feat_id: number;
|
|
23
|
+
policy: string;
|
|
24
|
+
};
|
|
25
|
+
issues: HistoricalQualityRegressionIssue[];
|
|
26
|
+
}
|
|
27
|
+
export interface HistoricalQualityRegressionOptions {
|
|
28
|
+
mode?: HistoricalQualityRegressionMode;
|
|
29
|
+
recentWindowSize?: number;
|
|
30
|
+
grandfatherMaxFeat?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class HistoricalQualityRegressionService {
|
|
33
|
+
execute(projectRoot: string, options?: HistoricalQualityRegressionOptions): Promise<HistoricalQualityRegressionReport>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=historical-quality-regression.service.d.ts.map
|