kotonoha 0.4.0 → 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 +47 -1
- package/README-ja.md +82 -4
- package/README.md +123 -6
- package/bin/kotonoha.js +4 -1
- package/package.json +8 -3
- package/skills/japanese-prose/scripts/invariants.py +24 -6
- 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 +162 -0
- package/skills/obsidian-vault/references/execution-contract.json +479 -0
- package/skills/obsidian-vault/references/workflow.md +206 -0
- package/skills/obsidian-vault/scripts/test_vault.py +569 -0
- package/skills/obsidian-vault/scripts/test_vault.py.lock +143 -0
- package/skills/obsidian-vault/scripts/vault.py +1363 -0
- package/skills/obsidian-vault/scripts/vault.py.lock +143 -0
- package/skills/tech-writer/SKILL.md +26 -7
- package/skills/tech-writer/assets/templates/obsidian.md +37 -0
- package/skills/tech-writer/references/doctypes/obsidian.md +106 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kotonoha-obsidian-bases-checker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "kotonoha-obsidian-bases-checker",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"ajv": "8.20.0",
|
|
12
|
+
"jsep": "1.4.0",
|
|
13
|
+
"jsonc-parser": "3.3.1",
|
|
14
|
+
"yaml": "2.9.1"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"node_modules/ajv": {
|
|
21
|
+
"version": "8.20.0",
|
|
22
|
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
|
23
|
+
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"fast-deep-equal": "^3.1.3",
|
|
27
|
+
"fast-uri": "^3.0.1",
|
|
28
|
+
"json-schema-traverse": "^1.0.0",
|
|
29
|
+
"require-from-string": "^2.0.2"
|
|
30
|
+
},
|
|
31
|
+
"funding": {
|
|
32
|
+
"type": "github",
|
|
33
|
+
"url": "https://github.com/sponsors/epoberezkin"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"node_modules/fast-deep-equal": {
|
|
37
|
+
"version": "3.1.3",
|
|
38
|
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
39
|
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
},
|
|
42
|
+
"node_modules/fast-uri": {
|
|
43
|
+
"version": "3.1.8",
|
|
44
|
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
|
45
|
+
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
|
46
|
+
"funding": [
|
|
47
|
+
{
|
|
48
|
+
"type": "github",
|
|
49
|
+
"url": "https://github.com/sponsors/fastify"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "opencollective",
|
|
53
|
+
"url": "https://opencollective.com/fastify"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"license": "BSD-3-Clause"
|
|
57
|
+
},
|
|
58
|
+
"node_modules/jsep": {
|
|
59
|
+
"version": "1.4.0",
|
|
60
|
+
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
|
|
61
|
+
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
|
|
62
|
+
"license": "MIT",
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">= 10.16.0"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"node_modules/json-schema-traverse": {
|
|
68
|
+
"version": "1.0.0",
|
|
69
|
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
|
70
|
+
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
|
71
|
+
"license": "MIT"
|
|
72
|
+
},
|
|
73
|
+
"node_modules/jsonc-parser": {
|
|
74
|
+
"version": "3.3.1",
|
|
75
|
+
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
|
76
|
+
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
|
77
|
+
"license": "MIT"
|
|
78
|
+
},
|
|
79
|
+
"node_modules/require-from-string": {
|
|
80
|
+
"version": "2.0.2",
|
|
81
|
+
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
|
82
|
+
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
|
83
|
+
"license": "MIT",
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=0.10.0"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"node_modules/yaml": {
|
|
89
|
+
"version": "2.9.1",
|
|
90
|
+
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
|
91
|
+
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
|
92
|
+
"license": "ISC",
|
|
93
|
+
"bin": {
|
|
94
|
+
"yaml": "bin.mjs"
|
|
95
|
+
},
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": ">= 14.6"
|
|
98
|
+
},
|
|
99
|
+
"funding": {
|
|
100
|
+
"url": "https://github.com/sponsors/eemeli"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kotonoha-obsidian-bases-checker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Read-only, stdin-only static checker for the supported Bases subset.",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test verify.cjs"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"ajv": "8.20.0",
|
|
14
|
+
"jsep": "1.4.0",
|
|
15
|
+
"jsonc-parser": "3.3.1",
|
|
16
|
+
"yaml": "2.9.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const { once } = require('node:events');
|
|
10
|
+
const { spawn } = require('node:child_process');
|
|
11
|
+
|
|
12
|
+
const repository = path.resolve(__dirname, '..', '..', '..');
|
|
13
|
+
const docs = path.join(repository, 'docs', 'obsidian-bases');
|
|
14
|
+
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
15
|
+
const cases = read(path.join(docs, 'acceptance-cases.json'));
|
|
16
|
+
const boundaries = read(path.join(docs, 'boundary-cases.json'));
|
|
17
|
+
const measured = read(path.join(docs, 'boundary-verification.json'));
|
|
18
|
+
const correctedMeasurements = new Map(read(path.join(docs, 'boundary-correction-verification.json'))
|
|
19
|
+
.cases.map(entry => [entry.id, entry]));
|
|
20
|
+
const contract = read(path.join(__dirname, '..', 'references', 'execution-contract.json'));
|
|
21
|
+
const Ajv = require(path.join(__dirname, 'node_modules', 'ajv', 'dist', '2020.js'));
|
|
22
|
+
const ajv = new Ajv({ strict: true, strictRequired: false, allErrors: true, allowUnionTypes: true, ownProperties: true });
|
|
23
|
+
const schema = Object.fromEntries(Object.entries(contract).filter(([key]) => !key.startsWith('x-')));
|
|
24
|
+
const validReport = ajv.compile({ ...schema, $ref: '#/$defs/Report' });
|
|
25
|
+
const validUsage = ajv.compile({ ...schema, $ref: '#/$defs/Usage' });
|
|
26
|
+
const checker = path.join(__dirname, 'check.cjs');
|
|
27
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
28
|
+
const canonical = value => JSON.stringify(value) + '\n';
|
|
29
|
+
const B = 'views: [{type: table, name: A}]\n';
|
|
30
|
+
const E = expression => 'formulas:\n x: ' + JSON.stringify(expression) + '\n' + B;
|
|
31
|
+
const request = (document = B, overrides = {}) =>
|
|
32
|
+
({ schema_version: 1, intent: 'inspect', document, application_version: '1.14.0', ...overrides });
|
|
33
|
+
const diagnostic = (code, category, at = [], related = [], line = null, column = null) =>
|
|
34
|
+
({ code, category, path: at, line, column, related });
|
|
35
|
+
const normalizeIssue = entry => diagnostic(entry.code, entry.category, entry.path,
|
|
36
|
+
entry.related || [], entry.line ?? null, entry.column ?? null);
|
|
37
|
+
|
|
38
|
+
function expectedReport(input, expected) {
|
|
39
|
+
const early = expected.structure === 'unprocessed';
|
|
40
|
+
return {
|
|
41
|
+
schema_version: 1,
|
|
42
|
+
status: ({ 0: 'completed', 1: 'failed', 2: 'not_performed', 3: 'partial', 4: 'not_performed' })[expected.exit_code],
|
|
43
|
+
exit_code: expected.exit_code,
|
|
44
|
+
structure: expected.structure,
|
|
45
|
+
source_sha256: early || expected.nullHash ? null : sha(Buffer.from(input.document, 'utf8')),
|
|
46
|
+
static_checks_passed: expected.static_checks_passed,
|
|
47
|
+
issues: expected.issues.map(normalizeIssue),
|
|
48
|
+
unprocessed: expected.unprocessed || [],
|
|
49
|
+
read_only: true,
|
|
50
|
+
authorization: 'not_assessed',
|
|
51
|
+
write_status: 'not_requested',
|
|
52
|
+
render_check: 'not_performed',
|
|
53
|
+
expression_evaluation: 'not_performed',
|
|
54
|
+
proofread_gate: !early && input.intent === 'proofread' ? 'external_required' : 'not_requested',
|
|
55
|
+
application_version: early ? null : input.application_version,
|
|
56
|
+
application_version_source: 'user_declared'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function earlyReport(code = 'invalid_input') {
|
|
61
|
+
return expectedReport(null, {
|
|
62
|
+
exit_code: code === 'dependency_missing' ? 4 : 1,
|
|
63
|
+
structure: 'unprocessed', static_checks_passed: false,
|
|
64
|
+
issues: [diagnostic(code, { invalid_input: 'input', dependency_missing: 'capability', runtime_error: 'runtime' }[code])]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function run(input, args = ['--stdin'], entry = checker, options = {}) {
|
|
69
|
+
const execution = spawnSync(process.execPath, [entry, ...args], {
|
|
70
|
+
input: typeof input === 'string' || Buffer.isBuffer(input) ? input : JSON.stringify(input),
|
|
71
|
+
encoding: 'utf8', maxBuffer: 3 * 1048576, timeout: 30000, ...options
|
|
72
|
+
});
|
|
73
|
+
assert.ifError(execution.error);
|
|
74
|
+
assert.equal(execution.signal, null);
|
|
75
|
+
assert.equal(execution.stderr, '');
|
|
76
|
+
return execution;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function compare(execution, expected) {
|
|
80
|
+
const actual = JSON.parse(execution.stdout);
|
|
81
|
+
assert.equal(validReport(actual), true, JSON.stringify(validReport.errors));
|
|
82
|
+
assert.equal(execution.status, expected.exit_code);
|
|
83
|
+
assert.deepEqual(actual, expected);
|
|
84
|
+
assert.equal(execution.stdout, canonical(expected));
|
|
85
|
+
return actual;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const complete = () => ({ exit_code: 0, structure: 'supported', static_checks_passed: true, issues: [] });
|
|
89
|
+
const unsupported = (code, at) => ({
|
|
90
|
+
exit_code: 3, structure: 'unsupported', static_checks_passed: false,
|
|
91
|
+
issues: [diagnostic(code, 'unsupported', at)], unprocessed: [at]
|
|
92
|
+
});
|
|
93
|
+
const invalid = (code, at, related = [], category = 'format') => ({
|
|
94
|
+
exit_code: 2, structure: 'invalid', static_checks_passed: false,
|
|
95
|
+
issues: [diagnostic(code, category, at, related)]
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
for (const entry of cases.diagnostic_cases) {
|
|
99
|
+
test(entry.id + ': independent diagnostic oracle', () => {
|
|
100
|
+
let document = entry.raw ?? cases.documents[entry.document];
|
|
101
|
+
for (const [before, after] of entry.replace || []) {
|
|
102
|
+
assert.equal(document.split(before).length, 2, 'fixture replacement must occur exactly once');
|
|
103
|
+
document = document.replace(before, after);
|
|
104
|
+
}
|
|
105
|
+
document = (entry.prefix || '') + document + (entry.append || '');
|
|
106
|
+
const input = request(document, {
|
|
107
|
+
intent: entry.intent || 'inspect',
|
|
108
|
+
application_version: Object.prototype.hasOwnProperty.call(entry, 'application_version') ? entry.application_version : '1.14.0'
|
|
109
|
+
});
|
|
110
|
+
compare(run(input), expectedReport(input, entry.expected));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// These constructors expand the approved notation; no product output is used.
|
|
115
|
+
const r01 = B + '#' + 'a'.repeat(1048576 - Buffer.byteLength(B) - 2) + '\n';
|
|
116
|
+
// Approved boundary-correction.json supersedes only R19/R20/R23 inputs and hashes.
|
|
117
|
+
const r19 = '? ' + 'a'.repeat(523997) + '\n: 0\n';
|
|
118
|
+
const materialized = [
|
|
119
|
+
request(r01), request(r01 + ' '),
|
|
120
|
+
request('x: ' + '['.repeat(63) + '0' + ']'.repeat(63) + '\n'),
|
|
121
|
+
request('x: ' + '['.repeat(64) + '0' + ']'.repeat(64) + '\n'),
|
|
122
|
+
request('x: [' + Array(9997).fill('0').join(',') + ']\n'),
|
|
123
|
+
request('x: [' + Array(9998).fill('0').join(',') + ']\n'),
|
|
124
|
+
request(E('"' + 'a'.repeat(4094) + '"')),
|
|
125
|
+
request(E('"' + 'a'.repeat(4095) + '"')),
|
|
126
|
+
request(E('[' + Array(1023).fill('1').join(',') + ']')),
|
|
127
|
+
request(E('[' + Array(1024).fill('1').join(',') + ']')),
|
|
128
|
+
request(E('1+'.repeat(63) + '1')), request(E('1+'.repeat(64) + '1')),
|
|
129
|
+
request(E('('.repeat(64) + '1' + ')'.repeat(64))),
|
|
130
|
+
request(E('('.repeat(65) + '1' + ')'.repeat(65))),
|
|
131
|
+
request('formulas:\n' + Array.from({ length: 256 }, (_, i) => ' f' + i + ': "1"\n').join('') + B),
|
|
132
|
+
request('formulas:\n' + Array.from({ length: 257 }, (_, i) => ' f' + i + ': "1"\n').join('') + B),
|
|
133
|
+
request('views:\n' + ' - {type: table, name: A}\n'.repeat(257)),
|
|
134
|
+
request('views:\n' + ' - {type: table, name: A}\n'.repeat(258)),
|
|
135
|
+
request(r19), request(r19, { application_version: '01.14.0' }),
|
|
136
|
+
request(), request(), request(r19 + 'views: [{type: table, name: true}]\n')
|
|
137
|
+
];
|
|
138
|
+
const stdinAtCap = JSON.stringify(request()).padEnd(16777216, ' ');
|
|
139
|
+
assert.equal(Buffer.byteLength(stdinAtCap), 16777216);
|
|
140
|
+
assert.equal(boundaries.cases.length, materialized.length);
|
|
141
|
+
for (let i = 0; i < materialized.length; i++) {
|
|
142
|
+
const oracle = boundaries.cases[i];
|
|
143
|
+
const evidence = correctedMeasurements.get(oracle.id) || measured.cases[i];
|
|
144
|
+
test(oracle.id + ': actual inclusive resource boundary and independent report hash', () => {
|
|
145
|
+
const input = materialized[i];
|
|
146
|
+
let expected = complete();
|
|
147
|
+
if (oracle.expected === 'resource') expected = unsupported('limit_exceeded', oracle.P);
|
|
148
|
+
else if (oracle.id === 'R03' || oracle.id === 'R05') expected = unsupported('unknown_key', ['x']);
|
|
149
|
+
else if (oracle.id === 'R17') expected.issues = Array.from({ length: 256 }, (_, index) =>
|
|
150
|
+
diagnostic('duplicate_view_name', 'caution', ['views', index + 1, 'name'], ['0']));
|
|
151
|
+
else if (oracle.id === 'R19') expected = unsupported('unknown_key', ['a'.repeat(523997)]);
|
|
152
|
+
else if (oracle.id === 'R22') expected = {
|
|
153
|
+
exit_code: 1, structure: 'unprocessed', static_checks_passed: false,
|
|
154
|
+
issues: [diagnostic('invalid_input', 'input')]
|
|
155
|
+
};
|
|
156
|
+
else if (oracle.id === 'R23') expected = {
|
|
157
|
+
...unsupported('limit_exceeded', []), exit_code: 2, structure: 'invalid'
|
|
158
|
+
};
|
|
159
|
+
const wanted = expectedReport(input, expected);
|
|
160
|
+
assert.equal(Buffer.byteLength(input.document), evidence.document_bytes);
|
|
161
|
+
assert.equal(wanted.source_sha256, evidence.source_sha256);
|
|
162
|
+
assert.equal(wanted.exit_code, evidence.expected_exit_code);
|
|
163
|
+
assert.equal(wanted.structure, evidence.expected_structure);
|
|
164
|
+
assert.equal(Buffer.byteLength(canonical(wanted)), evidence.expected_report_bytes);
|
|
165
|
+
assert.equal(sha(canonical(wanted)), evidence.report_sha256, 'independent expected Report must match approved hash');
|
|
166
|
+
const raw = oracle.id === 'R21' ? stdinAtCap : oracle.id === 'R22' ? stdinAtCap + ' ' : input;
|
|
167
|
+
const execution = run(raw);
|
|
168
|
+
compare(execution, wanted);
|
|
169
|
+
assert.equal(sha(execution.stdout), evidence.report_sha256);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test('original implicit overlong key remains invalid YAML under strict parsing', () => {
|
|
174
|
+
const input = request('a'.repeat(523997) + ': 0\n');
|
|
175
|
+
const expected = {
|
|
176
|
+
...invalid('invalid_yaml', []),
|
|
177
|
+
issues: [diagnostic('invalid_yaml', 'format', [], [], 1, 1)]
|
|
178
|
+
};
|
|
179
|
+
compare(run(input), expectedReport(input, expected));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('C01: valid request', () => compare(run(request()), expectedReport(request(), complete())));
|
|
183
|
+
test('output bytes: strict-valid explicit long key at exactly 1MiB, plus one metadata byte, and invalid dominance', () => {
|
|
184
|
+
const key = 'a'.repeat(523997);
|
|
185
|
+
const document = '? ' + key + '\n: 0\n';
|
|
186
|
+
const atCap = request(document);
|
|
187
|
+
const expected = expectedReport(atCap, unsupported('unknown_key', [key]));
|
|
188
|
+
assert.equal(Buffer.byteLength(canonical(expected)), 1048576);
|
|
189
|
+
compare(run(atCap), expected);
|
|
190
|
+
const above = request(document, { application_version: '01.14.0' });
|
|
191
|
+
const unbounded = expectedReport(above, unsupported('unknown_key', [key]));
|
|
192
|
+
assert.equal(Buffer.byteLength(canonical(unbounded)), 1048577);
|
|
193
|
+
compare(run(above), expectedReport(above, unsupported('limit_exceeded', [])));
|
|
194
|
+
const mixed = request(document + 'views: [{type: table, name: true}]\n');
|
|
195
|
+
compare(run(mixed), expectedReport(mixed, {
|
|
196
|
+
...unsupported('limit_exceeded', []), exit_code: 2, structure: 'invalid'
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
test('C02: help ignores stdin and does not require dependencies', () => {
|
|
200
|
+
const execution = run('not JSON', ['--help']);
|
|
201
|
+
assert.equal(execution.status, 0);
|
|
202
|
+
assert.equal(validUsage(JSON.parse(execution.stdout)), true);
|
|
203
|
+
assert.equal(execution.stdout, canonical(boundaries.command_reports.help));
|
|
204
|
+
});
|
|
205
|
+
const badArguments = [
|
|
206
|
+
[], ['--std'], ['--stdin', '--stdin'], ['--path', '/must-not-open'],
|
|
207
|
+
['--stdin', 'extra'], ['--help', '--stdin'], ['--help', '--help'], ['--stdin=true'],
|
|
208
|
+
['--'], ['--unknown'], ['--STDIN']
|
|
209
|
+
];
|
|
210
|
+
for (const args of badArguments) test('C03-C07: rejected arguments ' + JSON.stringify(args), () =>
|
|
211
|
+
compare(run('', args), earlyReport()));
|
|
212
|
+
|
|
213
|
+
const validJSON = JSON.stringify(request());
|
|
214
|
+
const badControls = [
|
|
215
|
+
['duplicate root key', validJSON.replace('"schema_version":1', '"schema_version":1,"schema_version":1')],
|
|
216
|
+
['duplicate nested key', validJSON.replace('"document":', '"unexpected":{"a":1,"a":2},"document":')],
|
|
217
|
+
['escaped duplicate key', validJSON.replace('"schema_version":1', '"schema_version":1,"schema_\\u0076ersion":1')],
|
|
218
|
+
['JSON comment', '/* no */' + validJSON],
|
|
219
|
+
['JSON trailing comma', validJSON.slice(0, -1) + ',}'],
|
|
220
|
+
['invalid UTF8', Buffer.from([0xc0, 0xaf])],
|
|
221
|
+
['truncated UTF8', Buffer.from([0xe3, 0x81])],
|
|
222
|
+
['encoded surrogate', Buffer.from([0xed, 0xa0, 0x80])],
|
|
223
|
+
['empty', ''], ['whitespace', ' \n\t'], ['literal null', 'null'],
|
|
224
|
+
['control BOM', '\uFEFF' + validJSON], ['extra JSON value', validJSON + '{}'],
|
|
225
|
+
['unknown key', JSON.stringify({ ...request(), vault_path: '/must-not-open' })],
|
|
226
|
+
['wrong document type', JSON.stringify(request(1))],
|
|
227
|
+
['wrong schema type', JSON.stringify(request(B, { schema_version: '1' }))],
|
|
228
|
+
['unsafe schema integer', validJSON.replace('"schema_version":1', '"schema_version":9007199254740993')],
|
|
229
|
+
['unsafe version number', JSON.stringify(request(B, { application_version: 9007199254740992 }))],
|
|
230
|
+
['wrong intent', JSON.stringify(request(B, { intent: 'save' }))],
|
|
231
|
+
['wrong version syntax', JSON.stringify(request(B, { application_version: 'v1.14.0' }))],
|
|
232
|
+
['oversized version component', JSON.stringify(request(B, { application_version: '1000.14.0' }))],
|
|
233
|
+
...['schema_version', 'intent', 'document', 'application_version'].map(key => {
|
|
234
|
+
const input = request(); delete input[key]; return ['missing ' + key, JSON.stringify(input)];
|
|
235
|
+
})
|
|
236
|
+
];
|
|
237
|
+
for (const [name, raw] of badControls) test('C08-C11: strict control ' + name, () =>
|
|
238
|
+
compare(run(raw), earlyReport()));
|
|
239
|
+
|
|
240
|
+
function fixture(callback, { dependencies = false } = {}) {
|
|
241
|
+
const directory = path.join(__dirname, 'node_modules', '.bases-verify-' + randomUUID());
|
|
242
|
+
fs.mkdirSync(path.join(directory, 'scripts'), { recursive: true });
|
|
243
|
+
const entry = path.join(directory, 'scripts', 'check.cjs');
|
|
244
|
+
fs.copyFileSync(checker, entry);
|
|
245
|
+
if (dependencies) {
|
|
246
|
+
fs.mkdirSync(path.join(directory, 'scripts', 'node_modules'));
|
|
247
|
+
for (const name of Object.keys(contract['x-runtime'].dependencies)) {
|
|
248
|
+
fs.symlinkSync(path.join(__dirname, 'node_modules', name),
|
|
249
|
+
path.join(directory, 'scripts', 'node_modules', name), 'dir');
|
|
250
|
+
}
|
|
251
|
+
fs.mkdirSync(path.join(directory, 'references'));
|
|
252
|
+
fs.copyFileSync(path.join(__dirname, '..', 'references', 'execution-contract.json'),
|
|
253
|
+
path.join(directory, 'references', 'execution-contract.json'));
|
|
254
|
+
}
|
|
255
|
+
try { return callback(entry, directory); }
|
|
256
|
+
finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
test('C12: actual isolated copy without local dependencies never uses ancestors or NODE_PATH', () => {
|
|
260
|
+
fixture(entry => {
|
|
261
|
+
compare(run(request(), ['--stdin'], entry, { env: { ...process.env, NODE_PATH: path.join(__dirname, 'node_modules') } }),
|
|
262
|
+
earlyReport('dependency_missing'));
|
|
263
|
+
compare(run('not JSON', ['--stdin'], entry), earlyReport('dependency_missing'));
|
|
264
|
+
compare(run('', [], entry), earlyReport());
|
|
265
|
+
const execution = run('not JSON', ['--help'], entry);
|
|
266
|
+
assert.equal(execution.status, 0);
|
|
267
|
+
assert.equal(execution.stdout, canonical(boundaries.command_reports.help));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
for (const name of Object.keys(contract['x-runtime'].dependencies)) {
|
|
272
|
+
for (const state of ['missing', 'wrong version']) {
|
|
273
|
+
test('C12: actual local ' + name + ' ' + state, () => {
|
|
274
|
+
fixture((entry, directory) => {
|
|
275
|
+
const dependency = path.join(directory, 'scripts', 'node_modules', name);
|
|
276
|
+
fs.unlinkSync(dependency);
|
|
277
|
+
if (state === 'wrong version') {
|
|
278
|
+
fs.mkdirSync(dependency);
|
|
279
|
+
fs.writeFileSync(path.join(dependency, 'package.json'), JSON.stringify({ name, version: '0.0.0' }));
|
|
280
|
+
}
|
|
281
|
+
compare(run(request(), ['--stdin'], entry), earlyReport('dependency_missing'));
|
|
282
|
+
}, { dependencies: true });
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
test('unexpected parser RangeError and corrupt contract clear all document metadata', () => {
|
|
288
|
+
fixture((entry, directory) => {
|
|
289
|
+
const dependency = path.join(directory, 'scripts', 'node_modules', 'yaml');
|
|
290
|
+
fs.unlinkSync(dependency);
|
|
291
|
+
fs.mkdirSync(dependency);
|
|
292
|
+
fs.writeFileSync(path.join(dependency, 'package.json'), JSON.stringify({ version: '2.9.1', main: 'index.cjs' }));
|
|
293
|
+
fs.writeFileSync(path.join(dependency, 'index.cjs'),
|
|
294
|
+
'exports.parseAllDocuments = () => { throw new RangeError("do not expose source"); };');
|
|
295
|
+
compare(run(request('SECRET', { intent: 'proofread', application_version: '1.2.3' }), ['--stdin'], entry),
|
|
296
|
+
earlyReport('runtime_error'));
|
|
297
|
+
fs.writeFileSync(path.join(directory, 'references', 'execution-contract.json'), 'invalid');
|
|
298
|
+
compare(run(request(), ['--stdin'], entry), earlyReport('runtime_error'));
|
|
299
|
+
}, { dependencies: true });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('stdin stream errors return exact invalid_control without leaking input', async () => {
|
|
303
|
+
let directory;
|
|
304
|
+
// Keep this fixture alive until the child closes; synchronous fixture cleanup is unsuitable here.
|
|
305
|
+
directory = path.join(__dirname, 'node_modules', '.bases-verify-' + randomUUID());
|
|
306
|
+
fs.mkdirSync(directory);
|
|
307
|
+
const preload = path.join(directory, 'stream.cjs');
|
|
308
|
+
fs.writeFileSync(preload, 'process.nextTick(() => process.stdin.emit("error", new Error("SECRET")));');
|
|
309
|
+
try {
|
|
310
|
+
const child = spawn(process.execPath, ['--require', preload, checker, '--stdin'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
311
|
+
let stdout = '', stderr = '';
|
|
312
|
+
child.stdout.on('data', data => { stdout += data; });
|
|
313
|
+
child.stderr.on('data', data => { stderr += data; });
|
|
314
|
+
child.stdin.on('error', () => {});
|
|
315
|
+
child.stdin.end(validJSON);
|
|
316
|
+
const [code, signal] = await once(child, 'close');
|
|
317
|
+
compare({ stdout, stderr, status: code, signal }, earlyReport());
|
|
318
|
+
assert.equal(stderr, '');
|
|
319
|
+
} finally { fs.rmSync(directory, { recursive: true, force: true }); }
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const expressionCases = [
|
|
323
|
+
['quoted delimiters and commas are data', '["(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((", ",]"]', null],
|
|
324
|
+
['static computed index', 'note.items[0]', null],
|
|
325
|
+
['direct formula bracket reference', 'formula["ghost"]', 'formula_missing'],
|
|
326
|
+
['literal does not reference formula', '"formula.ghost"', null],
|
|
327
|
+
['null literal', 'null', 'unsupported_expression'],
|
|
328
|
+
['sparse middle', '[1,,2]', 'unsupported_expression'],
|
|
329
|
+
['sparse first', '[,1]', 'unsupported_expression'],
|
|
330
|
+
['optional chain', 'note?.name', 'unsupported_expression'],
|
|
331
|
+
['computed function target', 'note["lower"]()', 'unsupported_expression'],
|
|
332
|
+
['dynamic index', 'note[index]', 'unsupported_expression'],
|
|
333
|
+
['negative index', 'note[-1]', 'unsupported_expression'],
|
|
334
|
+
['compound', 'a;b', 'unsupported_expression'],
|
|
335
|
+
['assignment', 'a = 1', 'unsupported_expression'],
|
|
336
|
+
['unsupported unary', '~a', 'unsupported_operator'],
|
|
337
|
+
['unsupported binary', 'a === b', 'unsupported_operator'],
|
|
338
|
+
['unlisted documented call', 'note.isEmpty()', 'unsupported_function'],
|
|
339
|
+
['unlisted startsWith', 'note.startsWith("a")', 'unsupported_function'],
|
|
340
|
+
['bad global arity', 'today(1)', 'unsupported_function'],
|
|
341
|
+
['bad method arity', 'note.lower(1)', 'unsupported_function'],
|
|
342
|
+
['method call lower bound', 'note.hasTag()', 'unsupported_function'],
|
|
343
|
+
['method call upper bound', 'note.hasTag(' + Array(129).fill('"x"').join(',') + ')', 'unsupported_function'],
|
|
344
|
+
['nonfinite number', '1e999', 'unsupported_number'],
|
|
345
|
+
['unparsed nondecimal number', '0x10', 'unsupported_expression'],
|
|
346
|
+
['no runtime evaluation of division', '1 / 0', null],
|
|
347
|
+
['this does not fetch an active file', 'this.file.name', null],
|
|
348
|
+
['no execution of process call', 'process.exit(91)', 'unsupported_function'],
|
|
349
|
+
['no module loading from expressions', 'require("node:fs")', 'unsupported_function'],
|
|
350
|
+
['unsupported AST never invents missing refs', 'formula.ghost ? 1 : 2', 'unsupported_expression'],
|
|
351
|
+
['unsupported call never invents missing refs', 'fetch(formula.ghost)', 'unsupported_function'],
|
|
352
|
+
['identifier Unicode', '価格 * 数量', null],
|
|
353
|
+
['identifier not arbitrary nonASCII', '😀', 'unsupported_expression']
|
|
354
|
+
];
|
|
355
|
+
for (const [name, expression, code] of expressionCases) {
|
|
356
|
+
test('expression: ' + name, () => {
|
|
357
|
+
const input = request(E(expression));
|
|
358
|
+
const expected = code === 'formula_missing' ? invalid(code, ['formulas', 'x'], ['ghost'], 'expression') :
|
|
359
|
+
code ? unsupported(code, ['formulas', 'x']) : complete();
|
|
360
|
+
compare(run(input), expectedReport(input, expected));
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const numericCases = [
|
|
365
|
+
['20e0', null], ['2e1', null], ['200e-1', null], ['0.2e2', null],
|
|
366
|
+
['20.000000000000000000', null],
|
|
367
|
+
['20.000000000000000001', 'invalid_type'],
|
|
368
|
+
['1e-999', 'invalid_type'], ['9007199254740992', 'unsupported_number'],
|
|
369
|
+
['90071992547409920e-1', 'unsupported_number'], ['0x14', 'unsupported_number'],
|
|
370
|
+
['0o24', 'unsupported_number'], ['.inf', 'unsupported_number'], ['.nan', 'unsupported_number'],
|
|
371
|
+
['0', 'unsupported_value'], ['-1', 'unsupported_value'], ['1000001', 'unsupported_value'],
|
|
372
|
+
['1', null], ['1000000', null], ['false', 'invalid_type'], ['"20"', 'invalid_type']
|
|
373
|
+
];
|
|
374
|
+
for (const [lexeme, code] of numericCases) test('exact numeric lexeme: ' + lexeme, () => {
|
|
375
|
+
const input = request('views: [{type: table, name: A, limit: ' + lexeme + '}]\n');
|
|
376
|
+
const expected = !code ? complete() : code === 'invalid_type' ? invalid(code, ['views', 0, 'limit']) :
|
|
377
|
+
unsupported(code, ['views', 0, 'limit']);
|
|
378
|
+
compare(run(input), expectedReport(input, expected));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const extraCases = [
|
|
382
|
+
['empty document', '', invalid('invalid_type', [])],
|
|
383
|
+
['sequence root', '[]\n', invalid('invalid_type', [])],
|
|
384
|
+
['scalar root', 'null\n', invalid('invalid_type', [])],
|
|
385
|
+
['nonstring map key', '1: {}\n', invalid('invalid_type', [])],
|
|
386
|
+
['explicit core tag', 'views: !!seq []\n', unsupported('unsupported_yaml_feature', [])],
|
|
387
|
+
['YAML directive', '%YAML 1.2\n---\n' + B, unsupported('unsupported_yaml_feature', [])],
|
|
388
|
+
['TAG directive', '%TAG !e! tag:example.invalid,2026:\n---\n' + B, unsupported('unsupported_yaml_feature', [])],
|
|
389
|
+
['unknown directive', '%PRIVATE data\n---\n' + B, unsupported('unsupported_yaml_feature', [])],
|
|
390
|
+
['prototype user dictionaries', 'summaries: {__proto__: "values.mean()"}\nproperties: {constructor: {displayName: A}}\n' +
|
|
391
|
+
'views: [{type: table, name: A, summaries: {prototype: __proto__}}]\n', complete()],
|
|
392
|
+
['unknown key with no views has no operation issue', 'extension: {}\n', unsupported('unknown_key', ['extension'])],
|
|
393
|
+
['known unsupported expression definition is still known', 'formulas: {x: "fetch()", y: "formula.x"}\n' + B,
|
|
394
|
+
unsupported('unsupported_function', ['formulas', 'x'])],
|
|
395
|
+
['unsupported namespace suppresses missing refs', 'formulas: []\nviews: [{type: table, name: A, order: [formula.ghost]}]\n',
|
|
396
|
+
invalid('invalid_type', ['formulas'])],
|
|
397
|
+
['self cycle', E('formula.x'), invalid('formula_cycle', ['formulas'], ['x'], 'expression')],
|
|
398
|
+
['per-expression cap suppresses references/version', E('formula.ghost+' + '1+'.repeat(2048) + '1'),
|
|
399
|
+
unsupported('limit_exceeded', ['formulas', 'x'])],
|
|
400
|
+
['lone high surrogate', B + '# \ud800', { ...unsupported('unsupported_unicode', []), nullHash: true }],
|
|
401
|
+
['lone low surrogate', B + '# \udfff', { ...unsupported('unsupported_unicode', []), nullHash: true }],
|
|
402
|
+
['valid astral Unicode', 'views: [{type: table, name: "😀"}]\n', complete()],
|
|
403
|
+
['noninitial FEFF is a string character, not a byte-order mark', 'views: [{type: table, name: "A\uFEFFB"}]\n', complete()],
|
|
404
|
+
['no mutation of quoted document text', B + '# %YAML 1.2 &anchor !tag << *alias\n', complete()],
|
|
405
|
+
['CRLF source is hashed exactly', B.replace(/\n/g, '\r\n'), complete()]
|
|
406
|
+
];
|
|
407
|
+
for (const [name, document, expected] of extraCases) test('structure: ' + name, () => {
|
|
408
|
+
const input = request(document);
|
|
409
|
+
compare(run(input), expectedReport(input, expected));
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
for (const intent of ['inspect', 'create', 'edit', 'proofread']) {
|
|
413
|
+
test('validated metadata and empty views operation: ' + intent, () => {
|
|
414
|
+
const input = request('views: []\n', { intent });
|
|
415
|
+
const expected = intent === 'inspect' ?
|
|
416
|
+
{ ...complete(), static_checks_passed: false, issues: [diagnostic('no_views', 'caution', ['views'])] } :
|
|
417
|
+
{ exit_code: 2, structure: 'supported', static_checks_passed: false,
|
|
418
|
+
issues: [diagnostic('missing_required', 'format', ['views'])] };
|
|
419
|
+
compare(run(input), expectedReport(input, expected));
|
|
420
|
+
const supported = request(B, { intent });
|
|
421
|
+
compare(run(supported), expectedReport(supported, complete()));
|
|
422
|
+
compare(run({ ...supported, unknown: true }), earlyReport());
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
test('read-only/no-fetch execution tripwires (not an arbitrary-host sandbox)', () => {
|
|
427
|
+
fixture((entry, directory) => {
|
|
428
|
+
const preload = path.join(directory, 'tripwire.cjs');
|
|
429
|
+
fs.writeFileSync(preload, `
|
|
430
|
+
const fs = require('node:fs'), path = require('node:path');
|
|
431
|
+
const read = fs.readFileSync;
|
|
432
|
+
fs.readFileSync = function(file, ...args) {
|
|
433
|
+
const p = path.resolve(String(file));
|
|
434
|
+
if (!(p === ${JSON.stringify(entry)} || p.startsWith(${JSON.stringify(path.join(__dirname, 'node_modules') + path.sep)}) ||
|
|
435
|
+
p.startsWith(${JSON.stringify(path.join(directory, 'references') + path.sep)}))) throw Error('forbidden read');
|
|
436
|
+
return read.call(this, file, ...args);
|
|
437
|
+
};
|
|
438
|
+
const blocked = () => { throw Error('forbidden side effect'); };
|
|
439
|
+
for (const key of ['writeFile','writeFileSync','appendFile','appendFileSync','mkdir','mkdirSync','unlink','unlinkSync',
|
|
440
|
+
'rename','renameSync','rm','rmSync','createWriteStream']) fs[key] = blocked;
|
|
441
|
+
for (const name of ['node:net','node:http','node:https','node:dns','node:child_process']) {
|
|
442
|
+
const api = require(name);
|
|
443
|
+
for (const key of ['connect','createConnection','request','get','lookup','resolve','spawn','spawnSync','exec','execSync',
|
|
444
|
+
'execFile','execFileSync','fork']) if (key in api) api[key] = blocked;
|
|
445
|
+
}
|
|
446
|
+
global.fetch = blocked;
|
|
447
|
+
`);
|
|
448
|
+
const input = request(cases.documents.table, { intent: 'proofread' });
|
|
449
|
+
compare(run(input, ['--stdin'], entry, { env: { ...process.env, NODE_OPTIONS: '--require=' + preload } }),
|
|
450
|
+
expectedReport(input, complete()));
|
|
451
|
+
compare(run({ ...input, path: '/must-not-open' }, ['--stdin'], entry,
|
|
452
|
+
{ env: { ...process.env, NODE_OPTIONS: '--require=' + preload } }), earlyReport());
|
|
453
|
+
}, { dependencies: true });
|
|
454
|
+
});
|