kotonoha 0.4.1 → 0.4.2
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/CHANGELOG.md +28 -1
- package/README-ja.md +68 -12
- package/README.md +100 -17
- package/bin/kotonoha.js +4 -1
- package/package.json +6 -3
- package/skills/obsidian-bases/SKILL.md +140 -0
- package/skills/obsidian-bases/assets/templates/cards.base +8 -0
- package/skills/obsidian-bases/assets/templates/kanban.base +7 -0
- package/skills/obsidian-bases/assets/templates/list.base +8 -0
- package/skills/obsidian-bases/assets/templates/table.base +10 -0
- package/skills/obsidian-bases/references/execution-contract.json +246 -0
- package/skills/obsidian-bases/references/workflow.md +155 -0
- package/skills/obsidian-bases/scripts/check.cjs +690 -0
- package/skills/obsidian-bases/scripts/package-lock.json +104 -0
- package/skills/obsidian-bases/scripts/package.json +18 -0
- package/skills/obsidian-bases/scripts/verify.cjs +454 -0
- package/skills/obsidian-canvas/SKILL.md +134 -0
- package/skills/obsidian-canvas/assets/templates/comparison.canvas +11 -0
- package/skills/obsidian-canvas/assets/templates/mind-map.canvas +11 -0
- package/skills/obsidian-canvas/assets/templates/note-map.canvas +10 -0
- package/skills/obsidian-canvas/assets/templates/workflow.canvas +11 -0
- package/skills/obsidian-canvas/references/execution-contract.json +404 -0
- package/skills/obsidian-canvas/references/workflow.md +187 -0
- package/skills/obsidian-canvas/scripts/canvas.py +1479 -0
- package/skills/obsidian-canvas/scripts/canvas.py.lock +143 -0
- package/skills/obsidian-canvas/scripts/test_canvas.py +2047 -0
- package/skills/obsidian-canvas/scripts/test_canvas.py.lock +143 -0
- package/skills/obsidian-vault/SKILL.md +149 -93
- package/skills/obsidian-vault/references/workflow.md +59 -4
- package/skills/obsidian-vault/scripts/test_vault.py +76 -0
- package/skills/obsidian-vault/scripts/vault.py +118 -71
- package/skills/tech-writer/SKILL.md +12 -2
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createHash } = require('node:crypto');
|
|
6
|
+
const { TextDecoder } = require('node:util');
|
|
7
|
+
|
|
8
|
+
const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
9
|
+
const mapping = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
11
|
+
const stringify = value => JSON.stringify(value) + '\n';
|
|
12
|
+
const issue = (code, category, at = [], related = [], line = null, column = null) =>
|
|
13
|
+
({ code, category, path: at, line, column, related });
|
|
14
|
+
|
|
15
|
+
function report() {
|
|
16
|
+
return {
|
|
17
|
+
schema_version: 1,
|
|
18
|
+
status: 'failed',
|
|
19
|
+
exit_code: 1,
|
|
20
|
+
structure: 'unprocessed',
|
|
21
|
+
source_sha256: null,
|
|
22
|
+
static_checks_passed: false,
|
|
23
|
+
issues: [],
|
|
24
|
+
unprocessed: [],
|
|
25
|
+
read_only: true,
|
|
26
|
+
authorization: 'not_assessed',
|
|
27
|
+
write_status: 'not_requested',
|
|
28
|
+
render_check: 'not_performed',
|
|
29
|
+
expression_evaluation: 'not_performed',
|
|
30
|
+
proofread_gate: 'not_requested',
|
|
31
|
+
application_version: null,
|
|
32
|
+
application_version_source: 'user_declared'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function early(code) {
|
|
37
|
+
const result = report();
|
|
38
|
+
result.issues = [issue(code, {
|
|
39
|
+
invalid_input: 'input', dependency_missing: 'capability', runtime_error: 'runtime'
|
|
40
|
+
}[code])];
|
|
41
|
+
if (code === 'dependency_missing') {
|
|
42
|
+
result.status = 'not_performed';
|
|
43
|
+
result.exit_code = 4;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadDependencies() {
|
|
49
|
+
const modules = Object.create(null);
|
|
50
|
+
const versions = { yaml: '2.9.1', jsep: '1.4.0', ajv: '8.20.0', 'jsonc-parser': '3.3.1' };
|
|
51
|
+
for (const [name, version] of Object.entries(versions)) {
|
|
52
|
+
const directory = path.join(__dirname, 'node_modules', name);
|
|
53
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
|
|
54
|
+
if (metadata.version !== version) throw new Error('dependency');
|
|
55
|
+
modules[name] = require(name === 'ajv' ? path.join(directory, 'dist', '2020.js') : directory);
|
|
56
|
+
}
|
|
57
|
+
return modules;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validators(contract, Ajv) {
|
|
61
|
+
const schema = Object.fromEntries(Object.entries(contract).filter(([key]) => !key.startsWith('x-')));
|
|
62
|
+
const ajv = new Ajv({ strict: true, strictRequired: false, allErrors: true,
|
|
63
|
+
allowUnionTypes: true, ownProperties: true });
|
|
64
|
+
return Object.fromEntries(['Request', 'Document', 'Report'].map(name =>
|
|
65
|
+
[name, ajv.compile({ ...schema, $ref: '#/$defs/' + name })]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readControl(maximum) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let size = 0;
|
|
72
|
+
let settled = false;
|
|
73
|
+
const fail = () => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
chunks.length = 0;
|
|
77
|
+
process.stdin.pause();
|
|
78
|
+
reject(new Error('input'));
|
|
79
|
+
};
|
|
80
|
+
process.stdin.on('error', fail);
|
|
81
|
+
process.stdin.on('data', chunk => {
|
|
82
|
+
if (settled) return;
|
|
83
|
+
size += chunk.length;
|
|
84
|
+
if (size > maximum) return fail();
|
|
85
|
+
chunks.push(chunk);
|
|
86
|
+
});
|
|
87
|
+
process.stdin.on('end', () => {
|
|
88
|
+
if (settled) return;
|
|
89
|
+
settled = true;
|
|
90
|
+
resolve(Buffer.concat(chunks, size));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseControl(bytes, jsonc, validate) {
|
|
96
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
97
|
+
const errors = [];
|
|
98
|
+
const tree = jsonc.parseTree(text, errors, { disallowComments: true, allowTrailingComma: false });
|
|
99
|
+
if (!tree || errors.length) throw new Error('input');
|
|
100
|
+
const stack = [tree];
|
|
101
|
+
while (stack.length) {
|
|
102
|
+
const node = stack.pop();
|
|
103
|
+
if (node.type === 'object') {
|
|
104
|
+
const keys = new Set();
|
|
105
|
+
for (const property of node.children || []) {
|
|
106
|
+
const key = property.children[0].value;
|
|
107
|
+
if (keys.has(key)) throw new Error('input');
|
|
108
|
+
keys.add(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (node.children) stack.push(...node.children);
|
|
112
|
+
}
|
|
113
|
+
const request = JSON.parse(text);
|
|
114
|
+
if (!validate(request)) throw new Error('input');
|
|
115
|
+
return request;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function compareString(a, b) {
|
|
119
|
+
const left = Array.from(a, x => x.codePointAt(0));
|
|
120
|
+
const right = Array.from(b, x => x.codePointAt(0));
|
|
121
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
122
|
+
if (left[i] !== right[i]) return left[i] < right[i] ? -1 : 1;
|
|
123
|
+
}
|
|
124
|
+
return left.length - right.length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function comparePath(a, b) {
|
|
128
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
129
|
+
if (a[i] === b[i]) continue;
|
|
130
|
+
if (typeof a[i] !== typeof b[i]) return typeof a[i] === 'string' ? -1 : 1;
|
|
131
|
+
return typeof a[i] === 'number' ? a[i] - b[i] : compareString(a[i], b[i]);
|
|
132
|
+
}
|
|
133
|
+
return a.length - b.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function outputExceeds(value, maximum) {
|
|
137
|
+
let size = 1; // The trailing LF is part of the output cap.
|
|
138
|
+
const pending = [value];
|
|
139
|
+
while (pending.length) {
|
|
140
|
+
const current = pending.pop();
|
|
141
|
+
if (Array.isArray(current)) {
|
|
142
|
+
size += 2 + Math.max(0, current.length - 1);
|
|
143
|
+
pending.push(...current);
|
|
144
|
+
} else if (mapping(current)) {
|
|
145
|
+
const entries = Object.entries(current);
|
|
146
|
+
size += 2 + Math.max(0, entries.length - 1) + entries.length;
|
|
147
|
+
for (const [key, child] of entries) pending.push(key, child);
|
|
148
|
+
} else size += Buffer.byteLength(JSON.stringify(current));
|
|
149
|
+
if (size > maximum) return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function numericFact(node) {
|
|
155
|
+
if (!node || (typeof node.value !== 'number' && typeof node.value !== 'bigint')) return null;
|
|
156
|
+
const source = node.source;
|
|
157
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(source);
|
|
158
|
+
if (!match || !Number.isFinite(Number(source))) return { unsupported: true };
|
|
159
|
+
let digits = (match[2] + (match[3] || '')).replace(/^0+/, '');
|
|
160
|
+
if (!digits) return { integral: true, value: 0 };
|
|
161
|
+
const exponent = Number(match[4] || 0) - (match[3] || '').length;
|
|
162
|
+
if (exponent < 0) {
|
|
163
|
+
const needed = -exponent;
|
|
164
|
+
if (needed > digits.length || !/^0*$/.test(digits.slice(-needed))) {
|
|
165
|
+
return { integral: false, value: Number(source) };
|
|
166
|
+
}
|
|
167
|
+
digits = digits.slice(0, digits.length - needed);
|
|
168
|
+
} else {
|
|
169
|
+
if (digits.length + exponent > 16) return { unsupported: true };
|
|
170
|
+
digits += '0'.repeat(exponent);
|
|
171
|
+
}
|
|
172
|
+
digits = digits.replace(/^0+/, '') || '0';
|
|
173
|
+
if (digits.length > 16 || (digits.length === 16 && digits > '9007199254740991')) {
|
|
174
|
+
return { unsupported: true };
|
|
175
|
+
}
|
|
176
|
+
return { integral: true, value: Number((match[1] === '-' ? '-' : '') + digits) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function expression(text, context, jsep, contract) {
|
|
180
|
+
const limits = contract['x-limits'];
|
|
181
|
+
if (text.length > limits.expression_chars) return { codes: ['limit_exceeded'] };
|
|
182
|
+
let quote = '';
|
|
183
|
+
let nesting = 0;
|
|
184
|
+
for (let i = 0; i < text.length; i++) {
|
|
185
|
+
const character = text[i];
|
|
186
|
+
if (quote) {
|
|
187
|
+
if (character === '\\') i++;
|
|
188
|
+
else if (character === quote) quote = '';
|
|
189
|
+
} else if (character === '"' || character === "'") quote = character;
|
|
190
|
+
else if (character === '(' || character === '[') {
|
|
191
|
+
if (++nesting > 64) return { codes: ['limit_exceeded'] };
|
|
192
|
+
} else if (character === ')' || character === ']') nesting--;
|
|
193
|
+
}
|
|
194
|
+
let ast;
|
|
195
|
+
try {
|
|
196
|
+
ast = jsep(text);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof RangeError) throw error;
|
|
199
|
+
return { codes: ['unsupported_expression'] };
|
|
200
|
+
}
|
|
201
|
+
const nodes = [];
|
|
202
|
+
const stack = [[ast, 1, null, null]];
|
|
203
|
+
while (stack.length) {
|
|
204
|
+
const [node, depth, parent, field] = stack.pop();
|
|
205
|
+
if (!node || typeof node.type !== 'string') continue;
|
|
206
|
+
nodes.push([node, parent, field]);
|
|
207
|
+
if (nodes.length > limits.expression_nodes || depth > limits.expression_depth) {
|
|
208
|
+
return { codes: ['limit_exceeded'] };
|
|
209
|
+
}
|
|
210
|
+
for (const [key, child] of Object.entries(node)) {
|
|
211
|
+
if (Array.isArray(child)) {
|
|
212
|
+
for (let i = child.length - 1; i >= 0; i--) {
|
|
213
|
+
if (child[i] && typeof child[i] === 'object') stack.push([child[i], depth + 1, node, key]);
|
|
214
|
+
}
|
|
215
|
+
} else if (child && typeof child === 'object') stack.push([child, depth + 1, node, key]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const grammar = contract['x-expression'];
|
|
219
|
+
const codes = new Set();
|
|
220
|
+
const refs = new Set();
|
|
221
|
+
const unsupported = code => codes.add(code || 'unsupported_expression');
|
|
222
|
+
for (const [node, parent, field] of nodes) {
|
|
223
|
+
if (!grammar.ast.includes(node.type) || node.optional) {
|
|
224
|
+
unsupported();
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
switch (node.type) {
|
|
228
|
+
case 'Literal':
|
|
229
|
+
if (!grammar.literal_types.includes(typeof node.value) || node.value === null) unsupported();
|
|
230
|
+
else if (typeof node.value === 'number' && (!Number.isFinite(node.value) ||
|
|
231
|
+
/^0[xob]/i.test(node.raw || ''))) unsupported('unsupported_number');
|
|
232
|
+
break;
|
|
233
|
+
case 'Identifier': {
|
|
234
|
+
if (!/^[$_\p{ID_Start}][$\u200c\u200d_\p{ID_Continue}]*$/u.test(node.name)) unsupported();
|
|
235
|
+
const property = parent && parent.type === 'MemberExpression' && field === 'property' && !parent.computed;
|
|
236
|
+
if (node.name === 'values' && context !== 'summary' && !property) unsupported();
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'UnaryExpression':
|
|
240
|
+
if (!grammar.unary.includes(node.operator)) unsupported('unsupported_operator');
|
|
241
|
+
break;
|
|
242
|
+
case 'BinaryExpression':
|
|
243
|
+
if (!grammar.binary.includes(node.operator)) unsupported('unsupported_operator');
|
|
244
|
+
break;
|
|
245
|
+
case 'ArrayExpression':
|
|
246
|
+
if (node.elements.some(element => element == null)) unsupported();
|
|
247
|
+
break;
|
|
248
|
+
case 'MemberExpression': {
|
|
249
|
+
const directFormula = node.object.type === 'Identifier' && node.object.name === 'formula';
|
|
250
|
+
let name;
|
|
251
|
+
if (node.computed) {
|
|
252
|
+
if (node.property.type !== 'Literal' || !(typeof node.property.value === 'string' ||
|
|
253
|
+
(!directFormula && Number.isSafeInteger(node.property.value) && node.property.value >= 0))) unsupported();
|
|
254
|
+
else name = node.property.value;
|
|
255
|
+
} else if (node.property.type !== 'Identifier') unsupported();
|
|
256
|
+
else name = node.property.name;
|
|
257
|
+
if (directFormula && typeof name === 'string') refs.add(name);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case 'CallExpression': {
|
|
261
|
+
const callee = node.callee;
|
|
262
|
+
let arity;
|
|
263
|
+
if (callee.type === 'Identifier') {
|
|
264
|
+
arity = own(grammar.global_calls, callee.name) ? grammar.global_calls[callee.name] : null;
|
|
265
|
+
if (!arity) unsupported('unsupported_function');
|
|
266
|
+
} else if (callee.type === 'MemberExpression' && !callee.computed && !callee.optional &&
|
|
267
|
+
callee.property.type === 'Identifier') {
|
|
268
|
+
const name = callee.property.name;
|
|
269
|
+
arity = own(grammar.method_calls, name) ? grammar.method_calls[name] : null;
|
|
270
|
+
if (name === 'mean' && !(context === 'summary' &&
|
|
271
|
+
callee.object.type === 'Identifier' && callee.object.name === 'values')) {
|
|
272
|
+
unsupported();
|
|
273
|
+
arity = null;
|
|
274
|
+
} else if (!arity) unsupported('unsupported_function');
|
|
275
|
+
} else unsupported();
|
|
276
|
+
if (arity && (node.arguments.length < arity[0] || node.arguments.length > arity[1])) {
|
|
277
|
+
unsupported('unsupported_function');
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { codes: [...codes], refs };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function validateDocument(request, modules, contract, guards) {
|
|
287
|
+
const result = report();
|
|
288
|
+
result.application_version = request.application_version;
|
|
289
|
+
result.proofread_gate = request.intent === 'proofread' ? 'external_required' : 'not_requested';
|
|
290
|
+
const findings = new Map();
|
|
291
|
+
let invalid = false;
|
|
292
|
+
let resource = false;
|
|
293
|
+
let operationMissing = false;
|
|
294
|
+
const add = (code, category, at = [], related = [], line = null, column = null) => {
|
|
295
|
+
const entry = issue(code, category, at, [...new Set(related)].sort(compareString), line, column);
|
|
296
|
+
findings.set(JSON.stringify(entry), entry);
|
|
297
|
+
if (category === 'format' || category === 'expression') invalid = true;
|
|
298
|
+
if (code === 'limit_exceeded') resource = true;
|
|
299
|
+
};
|
|
300
|
+
const bad = (code, at) => add(code, 'format', at);
|
|
301
|
+
const unsupported = (code, at) => add(code, 'unsupported', at);
|
|
302
|
+
const hasUnsupported = () => [...findings.values()].some(item => item.category === 'unsupported');
|
|
303
|
+
const finish = (hasViews = false) => {
|
|
304
|
+
result.issues = [...findings.values()].sort((a, b) =>
|
|
305
|
+
comparePath(a.path, b.path) || compareString(a.code, b.code) || comparePath(a.related, b.related));
|
|
306
|
+
const unprocessed = new Map(result.issues.filter(item => item.category === 'unsupported')
|
|
307
|
+
.map(item => [JSON.stringify(item.path), item.path]));
|
|
308
|
+
result.unprocessed = [...unprocessed.values()].sort(comparePath);
|
|
309
|
+
result.structure = invalid && !operationMissing ? 'invalid' : hasUnsupported() ? 'unsupported' : 'supported';
|
|
310
|
+
result.exit_code = invalid ? 2 : hasUnsupported() ? 3 :
|
|
311
|
+
result.issues.some(item => item.code === 'application_version_insufficient') ? 4 :
|
|
312
|
+
result.issues.some(item => item.code === 'application_version_unknown') ? 3 : 0;
|
|
313
|
+
result.status = ({ 0: 'completed', 2: 'not_performed', 3: 'partial', 4: 'not_performed' })[result.exit_code];
|
|
314
|
+
result.static_checks_passed = result.exit_code === 0 && hasViews;
|
|
315
|
+
if (result.issues.length > contract['x-limits'].diagnostics ||
|
|
316
|
+
outputExceeds(result, contract['x-limits'].output_bytes)) {
|
|
317
|
+
result.issues = [issue('limit_exceeded', 'unsupported')];
|
|
318
|
+
result.unprocessed = [[]];
|
|
319
|
+
result.static_checks_passed = false;
|
|
320
|
+
result.structure = invalid ? 'invalid' : 'unsupported';
|
|
321
|
+
result.exit_code = invalid ? 2 : 3;
|
|
322
|
+
result.status = invalid ? 'not_performed' : 'partial';
|
|
323
|
+
}
|
|
324
|
+
if (!guards.Report(result)) throw new Error('report');
|
|
325
|
+
return result;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const text = request.document;
|
|
329
|
+
if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(text)) {
|
|
330
|
+
unsupported('unsupported_unicode', []);
|
|
331
|
+
return finish();
|
|
332
|
+
}
|
|
333
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
334
|
+
result.source_sha256 = hash(bytes);
|
|
335
|
+
if (text.startsWith('\uFEFF')) {
|
|
336
|
+
unsupported('unsupported_unicode', []);
|
|
337
|
+
return finish();
|
|
338
|
+
}
|
|
339
|
+
if (bytes.length > contract['x-limits'].document_bytes) {
|
|
340
|
+
unsupported('limit_exceeded', []);
|
|
341
|
+
return finish();
|
|
342
|
+
}
|
|
343
|
+
const yaml = modules.yaml;
|
|
344
|
+
const documents = yaml.parseAllDocuments(text, { version: '1.2', schema: 'core',
|
|
345
|
+
uniqueKeys: true, strict: true, keepSourceTokens: true, intAsBigInt: true });
|
|
346
|
+
const errors = documents.flatMap(document => document.errors).sort((a, b) => a.pos[0] - b.pos[0]);
|
|
347
|
+
if (errors.length) {
|
|
348
|
+
const error = errors[0];
|
|
349
|
+
const before = text.slice(0, error.pos[0]);
|
|
350
|
+
const line = before.split('\n').length;
|
|
351
|
+
const column = error.pos[0] - before.lastIndexOf('\n');
|
|
352
|
+
add(error.code === 'DUPLICATE_KEY' ? 'duplicate_yaml_key' : 'invalid_yaml', 'format', [], [], line, column);
|
|
353
|
+
return finish();
|
|
354
|
+
}
|
|
355
|
+
if (documents.length > 1) {
|
|
356
|
+
unsupported('multiple_yaml_documents', []);
|
|
357
|
+
return finish();
|
|
358
|
+
}
|
|
359
|
+
for (const token of new yaml.Parser().parse(text)) {
|
|
360
|
+
if (token.type === 'directive') {
|
|
361
|
+
unsupported('unsupported_yaml_feature', []);
|
|
362
|
+
return finish();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const root = documents[0] ? documents[0].contents : null;
|
|
366
|
+
const stack = [[root, 0]];
|
|
367
|
+
let count = 0;
|
|
368
|
+
let over = false;
|
|
369
|
+
let feature = false;
|
|
370
|
+
while (stack.length) {
|
|
371
|
+
const [node, parentDepth] = stack.pop();
|
|
372
|
+
count++;
|
|
373
|
+
const collection = yaml.isMap(node) || yaml.isSeq(node);
|
|
374
|
+
const depth = parentDepth + (collection ? 1 : 0);
|
|
375
|
+
if (count > contract['x-limits'].yaml_nodes || depth > contract['x-limits'].yaml_depth) over = true;
|
|
376
|
+
if (node && (node.anchor || node.tag || yaml.isAlias(node))) feature = true;
|
|
377
|
+
if (yaml.isMap(node)) {
|
|
378
|
+
for (let i = node.items.length - 1; i >= 0; i--) {
|
|
379
|
+
const pair = node.items[i];
|
|
380
|
+
if (yaml.isScalar(pair.key) && pair.key.value === '<<') feature = true;
|
|
381
|
+
stack.push([pair.value, depth], [pair.key, depth]);
|
|
382
|
+
}
|
|
383
|
+
} else if (yaml.isSeq(node)) {
|
|
384
|
+
for (let i = node.items.length - 1; i >= 0; i--) stack.push([node.items[i], depth]);
|
|
385
|
+
}
|
|
386
|
+
if (feature) break;
|
|
387
|
+
}
|
|
388
|
+
if (feature || over) {
|
|
389
|
+
unsupported(feature ? 'unsupported_yaml_feature' : 'limit_exceeded', []);
|
|
390
|
+
return finish();
|
|
391
|
+
}
|
|
392
|
+
const numeric = new Map();
|
|
393
|
+
const conversion = [[root, [], null, null]];
|
|
394
|
+
let data;
|
|
395
|
+
while (conversion.length) {
|
|
396
|
+
const [node, at, parent, key] = conversion.pop();
|
|
397
|
+
let value;
|
|
398
|
+
if (yaml.isMap(node)) {
|
|
399
|
+
value = Object.create(null);
|
|
400
|
+
for (let i = node.items.length - 1; i >= 0; i--) {
|
|
401
|
+
const pair = node.items[i];
|
|
402
|
+
if (!yaml.isScalar(pair.key) || typeof pair.key.value !== 'string') bad('invalid_type', at);
|
|
403
|
+
else conversion.push([pair.value, [...at, pair.key.value], value, pair.key.value]);
|
|
404
|
+
}
|
|
405
|
+
} else if (yaml.isSeq(node)) {
|
|
406
|
+
value = new Array(node.items.length);
|
|
407
|
+
for (let i = node.items.length - 1; i >= 0; i--) conversion.push([node.items[i], [...at, i], value, i]);
|
|
408
|
+
} else {
|
|
409
|
+
value = node ? node.value : null;
|
|
410
|
+
const fact = numericFact(node);
|
|
411
|
+
if (fact) {
|
|
412
|
+
numeric.set(JSON.stringify(at), fact);
|
|
413
|
+
value = fact.unsupported ? null : fact.value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (parent) parent[key] = value;
|
|
417
|
+
else data = value;
|
|
418
|
+
}
|
|
419
|
+
const numberUnsupported = at => {
|
|
420
|
+
const fact = numeric.get(JSON.stringify(at));
|
|
421
|
+
if (!fact || !fact.unsupported) return false;
|
|
422
|
+
unsupported('unsupported_number', at);
|
|
423
|
+
return true;
|
|
424
|
+
};
|
|
425
|
+
const type = (value, kind, at) => {
|
|
426
|
+
if (numberUnsupported(at)) return false;
|
|
427
|
+
const valid = kind === 'object' ? mapping(value) : kind === 'array' ? Array.isArray(value) : typeof value === kind;
|
|
428
|
+
if (!valid) bad('invalid_type', at);
|
|
429
|
+
return valid;
|
|
430
|
+
};
|
|
431
|
+
const string = (value, at) => {
|
|
432
|
+
if (!type(value, 'string', at)) return false;
|
|
433
|
+
if (!value.length) {
|
|
434
|
+
unsupported('unsupported_value', at);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
};
|
|
439
|
+
const keys = (value, allowed, at) => {
|
|
440
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) unsupported('unknown_key', [...at, key]);
|
|
441
|
+
};
|
|
442
|
+
const expressions = [];
|
|
443
|
+
const references = [];
|
|
444
|
+
const summaryReferences = [];
|
|
445
|
+
let formulaNames = new Set();
|
|
446
|
+
let summaryNames = new Set();
|
|
447
|
+
let formulaComplete = true;
|
|
448
|
+
let summaryComplete = true;
|
|
449
|
+
const propertyRef = (name, at) => {
|
|
450
|
+
if (name.startsWith('formula.')) references.push({ path: at, refs: new Set([name.slice(8)]) });
|
|
451
|
+
};
|
|
452
|
+
const filter = (value, at) => {
|
|
453
|
+
const pending = [[value, at]];
|
|
454
|
+
while (pending.length) {
|
|
455
|
+
const [current, location] = pending.pop();
|
|
456
|
+
if (typeof current === 'string') {
|
|
457
|
+
if (string(current, location)) expressions.push({ text: current, path: location, context: 'filter' });
|
|
458
|
+
} else if (type(current, 'object', location)) {
|
|
459
|
+
const names = Object.keys(current);
|
|
460
|
+
if (names.length !== 1) unsupported('unsupported_value', location);
|
|
461
|
+
keys(current, ['and', 'or', 'not'], location);
|
|
462
|
+
for (const name of names) {
|
|
463
|
+
if (!['and', 'or', 'not'].includes(name)) continue;
|
|
464
|
+
const child = [...location, name];
|
|
465
|
+
if (type(current[name], 'array', child)) {
|
|
466
|
+
for (let i = current[name].length - 1; i >= 0; i--) pending.push([current[name][i], [...child, i]]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
if (!type(data, 'object', [])) return finish();
|
|
473
|
+
keys(data, Object.keys(contract.$defs.Document.properties), []);
|
|
474
|
+
if (own(data, 'formulas') && mapping(data.formulas) &&
|
|
475
|
+
Object.keys(data.formulas).length > contract['x-limits'].formulas) {
|
|
476
|
+
unsupported('limit_exceeded', ['formulas']);
|
|
477
|
+
return finish();
|
|
478
|
+
}
|
|
479
|
+
for (const name of ['formulas', 'summaries']) {
|
|
480
|
+
if (!own(data, name)) continue;
|
|
481
|
+
if (!type(data[name], 'object', [name])) {
|
|
482
|
+
if (name === 'formulas') formulaComplete = false;
|
|
483
|
+
else summaryComplete = false;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const names = new Set(Object.keys(data[name]));
|
|
487
|
+
if (name === 'formulas') formulaNames = names;
|
|
488
|
+
else summaryNames = names;
|
|
489
|
+
for (const key of names) {
|
|
490
|
+
const at = [name, key];
|
|
491
|
+
const validName = string(key, at);
|
|
492
|
+
const validValue = string(data[name][key], at);
|
|
493
|
+
if (!validName) {
|
|
494
|
+
if (name === 'formulas') formulaComplete = false;
|
|
495
|
+
else summaryComplete = false;
|
|
496
|
+
}
|
|
497
|
+
if (validName && validValue) expressions.push({
|
|
498
|
+
text: data[name][key], path: at, context: name === 'summaries' ? 'summary' : 'formula', name: key
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (own(data, 'filters')) filter(data.filters, ['filters']);
|
|
503
|
+
if (own(data, 'properties') && type(data.properties, 'object', ['properties'])) {
|
|
504
|
+
for (const [key, value] of Object.entries(data.properties)) {
|
|
505
|
+
const at = ['properties', key];
|
|
506
|
+
if (string(key, at)) propertyRef(key, at);
|
|
507
|
+
if (!type(value, 'object', at)) continue;
|
|
508
|
+
keys(value, ['displayName'], at);
|
|
509
|
+
if (own(value, 'displayName')) string(value.displayName, [...at, 'displayName']);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const views = [];
|
|
513
|
+
if (own(data, 'views') && type(data.views, 'array', ['views'])) {
|
|
514
|
+
const names = new Map();
|
|
515
|
+
for (let i = 0; i < data.views.length; i++) {
|
|
516
|
+
const view = data.views[i];
|
|
517
|
+
const at = ['views', i];
|
|
518
|
+
if (!type(view, 'object', at)) continue;
|
|
519
|
+
views.push([view, at]);
|
|
520
|
+
keys(view, Object.keys(contract.$defs.View.properties), at);
|
|
521
|
+
for (const key of ['type', 'name']) if (!own(view, key)) bad('missing_required', [...at, key]);
|
|
522
|
+
if (own(view, 'type') && type(view.type, 'string', [...at, 'type']) &&
|
|
523
|
+
!own(contract['x-minimum-app'], view.type)) unsupported('unsupported_view', [...at, 'type']);
|
|
524
|
+
if (own(view, 'name') && string(view.name, [...at, 'name'])) {
|
|
525
|
+
if (names.has(view.name)) add('duplicate_view_name', 'caution', [...at, 'name'], [String(names.get(view.name))]);
|
|
526
|
+
else names.set(view.name, i);
|
|
527
|
+
}
|
|
528
|
+
if (view.type === 'kanban' && !own(view, 'groupBy')) bad('missing_required', [...at, 'groupBy']);
|
|
529
|
+
else if (own(view, 'groupOrder') && !own(view, 'groupBy')) unsupported('unsupported_value', [...at, 'groupOrder']);
|
|
530
|
+
if (own(view, 'filters')) filter(view.filters, [...at, 'filters']);
|
|
531
|
+
if (own(view, 'order') && type(view.order, 'array', [...at, 'order'])) {
|
|
532
|
+
view.order.forEach((name, index) => {
|
|
533
|
+
const location = [...at, 'order', index];
|
|
534
|
+
if (string(name, location)) propertyRef(name, location);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (own(view, 'limit')) {
|
|
538
|
+
const location = [...at, 'limit'];
|
|
539
|
+
if (!numberUnsupported(location)) {
|
|
540
|
+
const fact = numeric.get(JSON.stringify(location));
|
|
541
|
+
if (!fact || !fact.integral) bad('invalid_type', location);
|
|
542
|
+
else if (fact.value < 1 || fact.value > 1000000) unsupported('unsupported_value', location);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (own(view, 'groupBy') && type(view.groupBy, 'object', [...at, 'groupBy'])) {
|
|
546
|
+
const group = view.groupBy;
|
|
547
|
+
const location = [...at, 'groupBy'];
|
|
548
|
+
keys(group, ['property', 'direction'], location);
|
|
549
|
+
if (!own(group, 'property')) bad('missing_required', [...location, 'property']);
|
|
550
|
+
else if (string(group.property, [...location, 'property'])) propertyRef(group.property, [...location, 'property']);
|
|
551
|
+
if (own(group, 'direction')) {
|
|
552
|
+
if (type(group.direction, 'string', [...location, 'direction']) &&
|
|
553
|
+
!['ASC', 'DESC'].includes(group.direction)) bad('invalid_enum', [...location, 'direction']);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (own(view, 'groupOrder') && type(view.groupOrder, 'array', [...at, 'groupOrder'])) {
|
|
557
|
+
view.groupOrder.forEach((value, index) => {
|
|
558
|
+
const location = [...at, 'groupOrder', index];
|
|
559
|
+
if (!numberUnsupported(location) && value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
|
560
|
+
bad('invalid_type', location);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (own(view, 'summaries') && type(view.summaries, 'object', [...at, 'summaries'])) {
|
|
565
|
+
for (const [target, name] of Object.entries(view.summaries)) {
|
|
566
|
+
const location = [...at, 'summaries', target];
|
|
567
|
+
if (string(target, location)) propertyRef(target, location);
|
|
568
|
+
if (string(name, location)) summaryReferences.push({ name, path: location });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const accepted = [];
|
|
574
|
+
for (const entry of expressions) {
|
|
575
|
+
const parsed = expression(entry.text, entry.context, modules.jsep, contract);
|
|
576
|
+
if (parsed.codes.length) {
|
|
577
|
+
for (const code of parsed.codes) unsupported(code, entry.path);
|
|
578
|
+
} else accepted.push({ ...entry, refs: parsed.refs });
|
|
579
|
+
}
|
|
580
|
+
if (!resource && formulaComplete) {
|
|
581
|
+
for (const entry of [...accepted, ...references]) {
|
|
582
|
+
const missing = [...entry.refs].filter(name => !formulaNames.has(name));
|
|
583
|
+
if (missing.length) add('formula_missing', 'expression', entry.path, missing);
|
|
584
|
+
}
|
|
585
|
+
if (!resource && summaryComplete) {
|
|
586
|
+
for (const entry of summaryReferences) {
|
|
587
|
+
if (!summaryNames.has(entry.name) && !contract['x-summary-names'].includes(entry.name)) {
|
|
588
|
+
add('summary_missing', 'expression', entry.path, [entry.name]);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const graph = new Map(accepted.filter(entry => entry.context === 'formula').map(entry => [entry.name, entry.refs]));
|
|
593
|
+
const visited = new Set();
|
|
594
|
+
const order = [];
|
|
595
|
+
for (const name of graph.keys()) {
|
|
596
|
+
if (visited.has(name)) continue;
|
|
597
|
+
const pending = [[name, false]];
|
|
598
|
+
while (pending.length) {
|
|
599
|
+
const [current, done] = pending.pop();
|
|
600
|
+
if (done) { order.push(current); continue; }
|
|
601
|
+
if (visited.has(current)) continue;
|
|
602
|
+
visited.add(current);
|
|
603
|
+
pending.push([current, true]);
|
|
604
|
+
for (const next of graph.get(current)) if (graph.has(next) && !visited.has(next)) pending.push([next, false]);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const reverse = new Map([...graph.keys()].map(name => [name, []]));
|
|
608
|
+
for (const [name, refs] of graph) for (const next of refs) if (reverse.has(next)) reverse.get(next).push(name);
|
|
609
|
+
visited.clear();
|
|
610
|
+
for (const name of order.reverse()) {
|
|
611
|
+
if (visited.has(name)) continue;
|
|
612
|
+
const component = [];
|
|
613
|
+
const pending = [name];
|
|
614
|
+
while (pending.length) {
|
|
615
|
+
const current = pending.pop();
|
|
616
|
+
if (visited.has(current)) continue;
|
|
617
|
+
visited.add(current);
|
|
618
|
+
component.push(current);
|
|
619
|
+
pending.push(...reverse.get(current).filter(next => !visited.has(next)));
|
|
620
|
+
}
|
|
621
|
+
if (component.length > 1 || graph.get(name).has(name)) add('formula_cycle', 'expression', ['formulas'], component);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (!invalid && !hasUnsupported()) {
|
|
625
|
+
if (!guards.Document(data)) throw new Error('document');
|
|
626
|
+
if (!views.length) {
|
|
627
|
+
if (request.intent === 'inspect') add('no_views', 'caution', ['views']);
|
|
628
|
+
else {
|
|
629
|
+
bad('missing_required', ['views']);
|
|
630
|
+
operationMissing = true;
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
const version = value => value.split('.').map(Number).concat([0]).slice(0, 3);
|
|
634
|
+
const compareVersion = (a, b) => {
|
|
635
|
+
const x = version(a), y = version(b);
|
|
636
|
+
return x[0] - y[0] || x[1] - y[1] || x[2] - y[2];
|
|
637
|
+
};
|
|
638
|
+
const minimum = views.map(([view]) => contract['x-minimum-app'][view.type]).sort(compareVersion).pop();
|
|
639
|
+
if (request.application_version === null) add('application_version_unknown', 'capability', [], [minimum]);
|
|
640
|
+
else for (const [view, at] of views) {
|
|
641
|
+
const needed = contract['x-minimum-app'][view.type];
|
|
642
|
+
if (compareVersion(request.application_version, needed) < 0) {
|
|
643
|
+
add('application_version_insufficient', 'capability', [...at, 'type'], [needed]);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return finish(views.length > 0);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function emit(value) {
|
|
652
|
+
process.stdout.write(stringify(value));
|
|
653
|
+
process.exitCode = value.exit_code || 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function main() {
|
|
657
|
+
const args = process.argv.slice(2);
|
|
658
|
+
if (args.length === 1 && args[0] === '--help') {
|
|
659
|
+
emit({ usage: 'node <bases-dir>/scripts/check.cjs --stdin', read_only: true });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (args.length !== 1 || args[0] !== '--stdin') {
|
|
663
|
+
emit(early('invalid_input'));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
let modules;
|
|
667
|
+
try {
|
|
668
|
+
modules = loadDependencies();
|
|
669
|
+
} catch {
|
|
670
|
+
emit(early('dependency_missing'));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const contract = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'references', 'execution-contract.json'), 'utf8'));
|
|
675
|
+
const guards = validators(contract, modules.ajv);
|
|
676
|
+
let request;
|
|
677
|
+
try {
|
|
678
|
+
request = parseControl(await readControl(contract['x-commands']['--stdin'].stdin_max_bytes),
|
|
679
|
+
modules['jsonc-parser'], guards.Request);
|
|
680
|
+
} catch {
|
|
681
|
+
emit(early('invalid_input'));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
emit(validateDocument(request, modules, contract, guards));
|
|
685
|
+
} catch {
|
|
686
|
+
emit(early('runtime_error'));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
main().catch(() => emit(early('runtime_error')));
|