@ductape/cli 0.3.36 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { findProjectConfig } from '../lib/config.js';
|
|
2
|
+
import { fail } from '../lib/output.js';
|
|
3
|
+
import { formatSessionConformance, validateSessionConformance } from '../lib/session-conformance.js';
|
|
4
|
+
export async function runSessionsValidate(opts = {}) {
|
|
5
|
+
const found = findProjectConfig();
|
|
6
|
+
if (!found)
|
|
7
|
+
fail('No linked project. Run `ductape link` from your project directory (or `ductape init`).');
|
|
8
|
+
const result = validateSessionConformance(found.dir);
|
|
9
|
+
console.log(opts.json ? JSON.stringify(result, null, 2) : formatSessionConformance(result));
|
|
10
|
+
if (!result.ok)
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { runDb, runDbActions, runDbContext } from './commands/db.js';
|
|
|
19
19
|
import { runComponentActions } from './commands/component-actions.js';
|
|
20
20
|
import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
|
|
21
21
|
import { runFeaturesSync, runFeaturesValidate } from './commands/features-sync.js';
|
|
22
|
+
import { runSessionsValidate } from './commands/sessions-validate.js';
|
|
22
23
|
import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
|
|
23
24
|
import { runGraph } from './commands/graph.js';
|
|
24
25
|
import { runSecretImportEnv, runSecrets } from './commands/secrets.js';
|
|
@@ -895,6 +896,12 @@ db
|
|
|
895
896
|
return runDb(verb, { file: opts.file, json: opts.json });
|
|
896
897
|
}));
|
|
897
898
|
const features = program.command('features').description('Code-first Ductape Features (ductape/features/)');
|
|
899
|
+
const sessions = program.command('sessions').description('Session lifecycle and propagation checks');
|
|
900
|
+
sessions
|
|
901
|
+
.command('validate')
|
|
902
|
+
.description('Read-only project-wide audit of Ductape SDK session propagation')
|
|
903
|
+
.option('--json', 'JSON output')
|
|
904
|
+
.action(wrap((opts) => runSessionsValidate({ json: Boolean(opts.json) })));
|
|
898
905
|
features
|
|
899
906
|
.command('validate')
|
|
900
907
|
.description('Read-only AST validation of code-first Feature control flow; performs no remote mutation')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SessionClassification = 'explicit' | 'inherited' | 'delegated' | 'system' | 'missing';
|
|
2
|
+
export interface SessionFinding {
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
call: string;
|
|
6
|
+
classification: SessionClassification;
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionValidationResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
root: string;
|
|
12
|
+
summary: Record<SessionClassification | 'calls', number>;
|
|
13
|
+
findings: SessionFinding[];
|
|
14
|
+
}
|
|
15
|
+
export declare function validateSessionConformance(rootDir: string): SessionValidationResult;
|
|
16
|
+
export declare function formatSessionConformance(result: SessionValidationResult): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
4
|
+
const IGNORED = new Set(['.git', 'dist', 'build', 'coverage', 'node_modules']);
|
|
5
|
+
const CALL = /\b(?:ductape\.)?(api|feature|database|databases|graph|vector|storage|events|notifications|quota|fallback)\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
6
|
+
function walk(root, files) {
|
|
7
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
8
|
+
if (entry.isSymbolicLink() || IGNORED.has(entry.name))
|
|
9
|
+
continue;
|
|
10
|
+
const target = path.join(root, entry.name);
|
|
11
|
+
if (entry.isDirectory())
|
|
12
|
+
walk(target, files);
|
|
13
|
+
else if (EXTENSIONS.has(path.extname(entry.name)))
|
|
14
|
+
files.push(target);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function invocation(source, open) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let quote = '';
|
|
20
|
+
let escaped = false;
|
|
21
|
+
for (let i = open; i < source.length; i += 1) {
|
|
22
|
+
const char = source[i];
|
|
23
|
+
if (quote) {
|
|
24
|
+
if (escaped)
|
|
25
|
+
escaped = false;
|
|
26
|
+
else if (char === '\\')
|
|
27
|
+
escaped = true;
|
|
28
|
+
else if (char === quote)
|
|
29
|
+
quote = '';
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
33
|
+
quote = char;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === '(')
|
|
37
|
+
depth += 1;
|
|
38
|
+
if (char === ')' && --depth === 0)
|
|
39
|
+
return source.slice(open + 1, i);
|
|
40
|
+
}
|
|
41
|
+
return source.slice(open + 1);
|
|
42
|
+
}
|
|
43
|
+
export function validateSessionConformance(rootDir) {
|
|
44
|
+
const root = path.resolve(rootDir);
|
|
45
|
+
const files = [];
|
|
46
|
+
walk(root, files);
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = CALL.exec(source))) {
|
|
52
|
+
const call = `${match[1]}.${match[2]}`;
|
|
53
|
+
const line = source.slice(0, match.index).split('\n').length;
|
|
54
|
+
const args = invocation(source, source.indexOf('(', match.index));
|
|
55
|
+
const nearby = source.slice(Math.max(0, match.index - 300), match.index);
|
|
56
|
+
let classification = 'missing';
|
|
57
|
+
let reason;
|
|
58
|
+
if (/\bsession\s*:/.test(args))
|
|
59
|
+
classification = 'explicit';
|
|
60
|
+
else if (/\bctx\.$/.test(nearby))
|
|
61
|
+
classification = 'inherited';
|
|
62
|
+
else {
|
|
63
|
+
const marker = nearby.match(/ductape-session:\s*(delegated|system)\s+([^\n]+)/i);
|
|
64
|
+
if (marker) {
|
|
65
|
+
classification = marker[1].toLowerCase();
|
|
66
|
+
reason = marker[2].trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
findings.push({ file: path.relative(root, file).split(path.sep).join('/'), line, call, classification, reason });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const summary = { calls: findings.length, explicit: 0, inherited: 0, delegated: 0, system: 0, missing: 0 };
|
|
73
|
+
for (const finding of findings)
|
|
74
|
+
summary[finding.classification] += 1;
|
|
75
|
+
return { ok: summary.missing === 0, root, summary, findings };
|
|
76
|
+
}
|
|
77
|
+
export function formatSessionConformance(result) {
|
|
78
|
+
const lines = [`Session propagation: ${result.ok ? 'PASS' : 'FAIL'} (${result.summary.calls} calls)`];
|
|
79
|
+
for (const finding of result.findings.filter((item) => item.classification === 'missing'))
|
|
80
|
+
lines.push(` ${finding.file}:${finding.line} ${finding.call} has no explicit, inherited, delegated, or system session classification`);
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
package/package.json
CHANGED