@gregiteen/ssss-cli 0.14.1 → 0.14.3
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 +43 -0
- package/README.md +19 -11
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +19 -32
- package/docs/help/bundle.md +1 -1
- package/docs/help/overview.md +5 -8
- package/docs/help/schema.md +40 -0
- package/docs/ssss-spec.md +6 -3
- package/package.json +8 -1
- package/registry/core.json +2 -2
- package/schemas/bundle.schema.json +300 -0
- package/schemas/core-registry.schema.json +553 -0
- package/scripts/build-reference-bundle.mjs +9 -10
- package/scripts/check-commit-durability.mjs +1 -0
- package/scripts/check-docs-parity.mjs +38 -0
- package/scripts/cmd-new.mjs +79 -18
- package/scripts/cmd-schema.mjs +152 -0
- package/scripts/conformance-09.mjs +41 -0
- package/scripts/conformance.mjs +40 -2
- package/scripts/ssss.mjs +2 -0
- package/scripts/sync-docs.mjs +56 -0
- package/skills/ssss/SKILL.md +1 -1
- package/skills/ssss/core/SKILL.md +2 -2
- package/skills/ssss/core/references/core-registry.json +2 -2
- package/skills/ssss/core/references/ssss-spec.md +6 -3
- package/src/bundle.mjs +1 -0
- package/src/registry.mjs +9 -2
- package/src/schema-validator.mjs +315 -0
- package/types/kernel.d.ts +1 -0
- package/types/schema-validator.d.ts +24 -0
- package/registry/extensions/defaults.json +0 -5
- package/registry/extensions/festech.json +0 -1115
- package/registry/extensions/field-crm.json +0 -1018
package/scripts/cmd-new.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const HELP = usage({
|
|
|
28
28
|
usage: 'ssss new <dir> [options]',
|
|
29
29
|
options: [
|
|
30
30
|
['--name <s>', 'Package name (default: the target directory name).'],
|
|
31
|
+
['--extension <s>', 'Scaffold a custom app extension registry under registry/extensions/<name>.json.'],
|
|
31
32
|
['--with-total-recall', 'Also wire the Total Recall memory OS (total-recall-brain).'],
|
|
32
33
|
['--install', 'Run `npm install` after scaffolding (and Total Recall init if requested).'],
|
|
33
34
|
['--no-git', 'Do not run `git init`.'],
|
|
@@ -36,6 +37,7 @@ const HELP = usage({
|
|
|
36
37
|
],
|
|
37
38
|
examples: [
|
|
38
39
|
'ssss new my-app',
|
|
40
|
+
'ssss new my-app --extension myapp',
|
|
39
41
|
'ssss new my-app --with-total-recall --install',
|
|
40
42
|
],
|
|
41
43
|
seeAlso: ['ssss help scaffold', 'ssss export', 'ssss conformance'],
|
|
@@ -65,6 +67,15 @@ export async function run(argv) {
|
|
|
65
67
|
const name = flags.name || path.basename(root);
|
|
66
68
|
const ref = flags.ref || `v${VERSION}`;
|
|
67
69
|
const withTR = !!flags['with-total-recall'];
|
|
70
|
+
const extensionName = flags.extension === undefined ? null : String(flags.extension);
|
|
71
|
+
if (extensionName !== null) {
|
|
72
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(extensionName)) {
|
|
73
|
+
die(`--extension '${extensionName}' is not a valid registry name (lowercase letter first, then a-z, 0-9, _ or -; max 64).`);
|
|
74
|
+
}
|
|
75
|
+
if (extensionName === 'core' || extensionName === 'ssss') {
|
|
76
|
+
die(`--extension '${extensionName}' is reserved for the SSSS core registry.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
68
79
|
|
|
69
80
|
if (fs.existsSync(root) && fs.readdirSync(root).length && !flags.force) {
|
|
70
81
|
die(`${root} is not empty. Use --force to scaffold anyway.`);
|
|
@@ -79,7 +90,9 @@ export async function run(argv) {
|
|
|
79
90
|
ssss: 'ssss',
|
|
80
91
|
test: 'node --test',
|
|
81
92
|
export: 'ssss export vault --profile sale --out dist/bundle.ucw.json',
|
|
82
|
-
validate:
|
|
93
|
+
validate: extensionName
|
|
94
|
+
? `ssss schema validate registry/extensions/${extensionName}.json && ssss conformance --engine`
|
|
95
|
+
: 'ssss conformance --engine',
|
|
83
96
|
},
|
|
84
97
|
dependencies: { '@gregiteen/ssss-cli': `github:gregiteen/ssss#${ref}` },
|
|
85
98
|
...(withTR ? { devDependencies: { 'total-recall-brain': 'latest' } } : {}),
|
|
@@ -96,14 +109,42 @@ export async function run(argv) {
|
|
|
96
109
|
W(root, 'vault/tasks/first-task.md',
|
|
97
110
|
'---\ntype: task\ntitle: First Task\ndescription: Starter private setup task that demonstrates tenant_private filtering.\ntimestamp: 2026-07-02T00:00:00Z\npriority: normal\ncategory: setup\nstatus: pending\n---\n\n# First task\n\nThis is `tenant_private` (§5.5) — it is **dropped** from `template`/`sale` exports.\n');
|
|
98
111
|
|
|
112
|
+
// ── optional app extension registry ─────────────────────────────────────
|
|
113
|
+
if (extensionName) {
|
|
114
|
+
const extRegistry = {
|
|
115
|
+
$schema: 'https://ssss.dev/schemas/core-registry.schema.json',
|
|
116
|
+
registry: extensionName,
|
|
117
|
+
extends: 'core',
|
|
118
|
+
version: '0.1.0',
|
|
119
|
+
description: `Custom SSSS primitive extensions for ${name}.`,
|
|
120
|
+
document_primitives: {
|
|
121
|
+
[`${extensionName}_item`]: {
|
|
122
|
+
family: 'content',
|
|
123
|
+
canonical_filename: null,
|
|
124
|
+
append_only: false,
|
|
125
|
+
portability: 'structural',
|
|
126
|
+
required_fields: ['type', 'name', 'title'],
|
|
127
|
+
optional_fields: ['description', 'status'],
|
|
128
|
+
enums: {
|
|
129
|
+
status: ['active', 'archived', 'draft'],
|
|
130
|
+
},
|
|
131
|
+
notes: `Custom primitive for ${name} application.`,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
W(root, `registry/extensions/${extensionName}.json`, JSON.stringify(extRegistry, null, 2) + '\n');
|
|
136
|
+
W(root, `vault/${extensionName}_items/first-${extensionName}-item.md`,
|
|
137
|
+
`---\ntype: ${extensionName}_item\ntitle: Starter Item\ndescription: Starter ${extensionName} item.\ntimestamp: 2026-07-02T00:00:00Z\nname: Starter Item\nstatus: active\n---\n\n# Starter Item\n\nCustom primitive defined in \`registry/extensions/${extensionName}.json\`.\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
99
140
|
// ── conformance test (zero-dependency: node --test) ─────────────────────
|
|
100
|
-
W(root, 'test/ssss-conformance.test.mjs',
|
|
141
|
+
W(root, 'test/ssss-conformance.test.mjs', conformanceTest(extensionName));
|
|
101
142
|
|
|
102
143
|
// ── repo meta ───────────────────────────────────────────────────────────
|
|
103
144
|
W(root, '.gitignore', 'node_modules/\ndist/\n*.ucw.json\n.DS_Store\n');
|
|
104
|
-
W(root, 'README.md', readme(name, withTR));
|
|
105
|
-
W(root, 'CLAUDE.md', claudeMd(name, withTR));
|
|
106
|
-
W(root, 'docs/design.md', designMd(name));
|
|
145
|
+
W(root, 'README.md', readme(name, withTR, extensionName));
|
|
146
|
+
W(root, 'CLAUDE.md', claudeMd(name, withTR, extensionName));
|
|
147
|
+
W(root, 'docs/design.md', designMd(name, extensionName));
|
|
107
148
|
|
|
108
149
|
// ── git + install (best-effort, flag-gated, no shell) ───────────────────
|
|
109
150
|
const log = (m) => console.error(m);
|
|
@@ -145,7 +186,8 @@ export async function run(argv) {
|
|
|
145
186
|
console.log(' npx ssss help # explore the standard');
|
|
146
187
|
}
|
|
147
188
|
|
|
148
|
-
|
|
189
|
+
function conformanceTest(extensionName) {
|
|
190
|
+
return `/**
|
|
149
191
|
* SSSS conformance — proves this repo's toolchain matches the published standard.
|
|
150
192
|
* Zero test dependencies: run with \`node --test\` (Node 18+).
|
|
151
193
|
*/
|
|
@@ -161,7 +203,18 @@ import { exportBundle, validateBundle, provisionBundle, importBundle } from '@gr
|
|
|
161
203
|
|
|
162
204
|
const require = createRequire(import.meta.url);
|
|
163
205
|
const { fixtures } = require('@gregiteen/ssss-cli/conformance/fixtures.json');
|
|
164
|
-
const
|
|
206
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
207
|
+
const VAULT = path.join(ROOT, 'vault');
|
|
208
|
+
${extensionName
|
|
209
|
+
? `// App-specific primitives live in this repo's registry/extensions/; the core comes from the package.
|
|
210
|
+
const REGISTRY = path.join(ROOT, 'registry');
|
|
211
|
+
const EXTENSIONS = [${JSON.stringify(extensionName)}];
|
|
212
|
+
const EXPORT_OPTIONS = { registryDir: REGISTRY, requiredExtensions: EXTENSIONS };
|
|
213
|
+
const REGISTRY_OPTIONS = { registryDir: REGISTRY };
|
|
214
|
+
const ENGINE_OPTIONS = { registryDir: REGISTRY, registryNames: EXTENSIONS };`
|
|
215
|
+
: `const EXPORT_OPTIONS = {};
|
|
216
|
+
const REGISTRY_OPTIONS = {};
|
|
217
|
+
const ENGINE_OPTIONS = {};`}
|
|
165
218
|
|
|
166
219
|
test('engine implements the canonical Operation Contract (spec §6)', async () => {
|
|
167
220
|
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-conf-'));
|
|
@@ -179,17 +232,17 @@ test('engine implements the canonical Operation Contract (spec §6)', async () =
|
|
|
179
232
|
});
|
|
180
233
|
|
|
181
234
|
test('this vault exports, validates, and round-trips as a sale bundle (spec §16/§17)', async () => {
|
|
182
|
-
const bundle = exportBundle(VAULT, { profile: 'sale', name: 'starter' });
|
|
183
|
-
const { valid, errors } = validateBundle(bundle);
|
|
235
|
+
const bundle = exportBundle(VAULT, { profile: 'sale', name: 'starter', ...EXPORT_OPTIONS });
|
|
236
|
+
const { valid, errors } = validateBundle(bundle, REGISTRY_OPTIONS);
|
|
184
237
|
assert.ok(valid, 'bundle invalid: ' + errors.join('; '));
|
|
185
238
|
assert.ok(!bundle.files.some((f) => f.path.startsWith('tasks/')), 'tenant_private leaked into sale export');
|
|
186
239
|
|
|
187
|
-
const plan = provisionBundle(bundle, { workspaceId: 'ws-test' });
|
|
188
|
-
assert.ok(plan.ok, 'provision failed: ' + JSON.stringify(plan.unresolved) + ' ' + JSON.stringify(plan.danglingLinks));
|
|
240
|
+
const plan = provisionBundle(bundle, { workspaceId: 'ws-test', ...REGISTRY_OPTIONS });
|
|
241
|
+
assert.ok(plan.ok, 'provision failed: ' + JSON.stringify(plan.errors) + ' ' + JSON.stringify(plan.unresolved) + ' ' + JSON.stringify(plan.danglingLinks));
|
|
189
242
|
|
|
190
243
|
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-import-'));
|
|
191
244
|
try {
|
|
192
|
-
const engine = createEngine();
|
|
245
|
+
const engine = createEngine(ENGINE_OPTIONS);
|
|
193
246
|
const r1 = await importBundle(plan.plan, target, engine);
|
|
194
247
|
assert.ok(r1.ok && r1.committed === bundle.files.length, 'import did not commit every file');
|
|
195
248
|
const r2 = await importBundle(plan.plan, target, engine);
|
|
@@ -197,8 +250,9 @@ test('this vault exports, validates, and round-trips as a sale bundle (spec §16
|
|
|
197
250
|
} finally { fs.rmSync(target, { recursive: true, force: true }); }
|
|
198
251
|
});
|
|
199
252
|
`;
|
|
253
|
+
}
|
|
200
254
|
|
|
201
|
-
function readme(name, withTR) {
|
|
255
|
+
function readme(name, withTR, extensionName) {
|
|
202
256
|
return `# ${name}
|
|
203
257
|
|
|
204
258
|
An [SSSS](https://github.com/gregiteen/ssss)-native project: a Markdown **vault** is
|
|
@@ -208,7 +262,7 @@ the source of truth, validated and packaged by the dependency-free \`@gregiteen/
|
|
|
208
262
|
|
|
209
263
|
\`\`\`
|
|
210
264
|
vault/ Your SSSS documents (structural + tenant_private primitives).
|
|
211
|
-
test/ Conformance test — replays the canonical fixtures + round-trips the vault.
|
|
265
|
+
${extensionName ? `registry/ Custom primitive extension schema (registry/extensions/${extensionName}.json).\n` : ''}test/ Conformance test — replays the canonical fixtures + round-trips the vault.
|
|
212
266
|
\`\`\`
|
|
213
267
|
|
|
214
268
|
## Use
|
|
@@ -236,18 +290,23 @@ npx total-recall recall "..." # semantic recall
|
|
|
236
290
|
` : ''}`;
|
|
237
291
|
}
|
|
238
292
|
|
|
239
|
-
function claudeMd(name, withTR) {
|
|
293
|
+
function claudeMd(name, withTR, extensionName) {
|
|
240
294
|
return `# ${name} — agent guide
|
|
241
295
|
|
|
242
296
|
This repo uses **SSSS** (Structured Semantic Syntax System) as its state contract.
|
|
243
297
|
|
|
244
298
|
## Source of truth
|
|
245
299
|
- \`vault/\` is the only source of truth — typed Markdown documents with YAML
|
|
246
|
-
frontmatter. Every file declares a \`type\` defined in the \`@gregiteen/ssss-cli\` core registry.
|
|
300
|
+
frontmatter. Every file declares a \`type\` defined in the \`@gregiteen/ssss-cli\` core registry${extensionName ? ` or local \`registry/extensions/${extensionName}.json\`` : ''}.
|
|
247
301
|
- Mutate the vault through the standard, not by hand-writing files in bulk:
|
|
248
302
|
\`npx ssss\` for bundle/tenant tooling, or the \`@gregiteen/ssss-cli/engine\` Operation Contract
|
|
249
303
|
in code.
|
|
250
304
|
|
|
305
|
+
## Extension & Registry Isolation Invariant
|
|
306
|
+
- App-specific primitives belong exclusively in \`registry/extensions/\` inside this application repository.
|
|
307
|
+
- Never modify or fork \`@gregiteen/ssss-cli\` to add app-specific schemas.
|
|
308
|
+
- Validate local extension schemas with: \`npx ssss schema validate registry/extensions/<name>.json\`.
|
|
309
|
+
|
|
251
310
|
## Portability (§5.5) — the rule that matters
|
|
252
311
|
- \`structural\` documents (rules, workflows, pages, assistants) are the sellable model.
|
|
253
312
|
- \`tenant_private\` documents (tasks, customer data) are **never** shipped in a
|
|
@@ -264,7 +323,7 @@ ${withTR ? `
|
|
|
264
323
|
` : ''}`;
|
|
265
324
|
}
|
|
266
325
|
|
|
267
|
-
function designMd(name) {
|
|
326
|
+
function designMd(name, extensionName) {
|
|
268
327
|
return `# ${name} — System Architecture & Design
|
|
269
328
|
|
|
270
329
|
> Technical design and architecture specification for ${name}.
|
|
@@ -278,7 +337,7 @@ function designMd(name) {
|
|
|
278
337
|
- **Vault Structure**: Typed Markdown documents under \`vault/\` serving as the source of truth.
|
|
279
338
|
- **Workflows & Execution**: Automated procedures defined in \`vault/workflows/\` triggered by operational events.
|
|
280
339
|
- **Capabilities & Resources**: Tools and external service integrations required for execution.
|
|
281
|
-
|
|
340
|
+
${extensionName ? `- **Extension Registry**: Custom primitive definitions located in \`registry/extensions/${extensionName}.json\` extending core SSSS.\n` : ''}
|
|
282
341
|
## Data & Schema Contracts
|
|
283
342
|
|
|
284
343
|
<Core entities, external dependencies, and integration interfaces.>
|
|
@@ -287,5 +346,7 @@ function designMd(name) {
|
|
|
287
346
|
|
|
288
347
|
- SSSS VFS is the single source of truth for application state.
|
|
289
348
|
- Structural business logic is strictly decoupled from private tenant operational data.
|
|
349
|
+
- App-specific primitives reside in \`registry/extensions/\` within this repository and extend \`core\`.
|
|
350
|
+
- Core SSSS library (\`@gregiteen/ssss-cli\`) remains vendor-neutral and dependency-free.
|
|
290
351
|
`;
|
|
291
352
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ssss schema` — inspect, export, and validate against the canonical Draft 2020-12
|
|
3
|
+
* JSON Schemas for SSSS registries (`schemas/core-registry.schema.json`) and
|
|
4
|
+
* `.ucw` transport bundles (`schemas/bundle.schema.json`).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import {
|
|
10
|
+
getCoreRegistrySchema,
|
|
11
|
+
getBundleSchema,
|
|
12
|
+
validateJsonSchema,
|
|
13
|
+
} from '../src/schema-validator.mjs';
|
|
14
|
+
import { parseArgs, usage, die, wantsHelp } from './lib/cli.mjs';
|
|
15
|
+
|
|
16
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(HERE, '..');
|
|
18
|
+
const SCHEMAS_DIR = path.resolve(ROOT, 'schemas');
|
|
19
|
+
|
|
20
|
+
const HELP = usage({
|
|
21
|
+
summary: 'Output, export, or validate against the canonical SSSS Draft 2020-12 JSON Schemas.',
|
|
22
|
+
usage: 'ssss schema <core|bundle|export|validate> [target] [options]',
|
|
23
|
+
options: [
|
|
24
|
+
['--out <path>', 'Destination file (core/bundle) or directory (export; default: ./schemas).'],
|
|
25
|
+
['--type <core|bundle>', 'Target schema kind for `validate` (auto-detected when omitted).'],
|
|
26
|
+
['--schema <file>', 'Custom JSON Schema file path for `validate`.'],
|
|
27
|
+
['--json', 'Emit machine-readable JSON output for `export` or `validate`.'],
|
|
28
|
+
['--quiet', 'Suppress human-readable output on valid `validate` check.'],
|
|
29
|
+
],
|
|
30
|
+
examples: [
|
|
31
|
+
'ssss schema core',
|
|
32
|
+
'ssss schema bundle --out ./bundle.schema.json',
|
|
33
|
+
'ssss schema export --out ./schemas',
|
|
34
|
+
'ssss schema validate registry/core.json',
|
|
35
|
+
'ssss schema validate conformance/reference-bundle.ucw.json --type bundle',
|
|
36
|
+
],
|
|
37
|
+
seeAlso: ['ssss registry', 'ssss validate', 'ssss help schema'],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function emit(value, output) {
|
|
41
|
+
const text = typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`;
|
|
42
|
+
if (output) {
|
|
43
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.resolve(output), text, { encoding: 'utf8' });
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write(text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectSchemaType(data, explicitType) {
|
|
51
|
+
if (explicitType === 'core' || explicitType === 'registry') return 'core';
|
|
52
|
+
if (explicitType === 'bundle') return 'bundle';
|
|
53
|
+
if (explicitType) {
|
|
54
|
+
die(`Unknown --type '${explicitType}'. Use 'core' or 'bundle'.`);
|
|
55
|
+
}
|
|
56
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
57
|
+
if (typeof data.$schema === 'string') {
|
|
58
|
+
if (data.$schema.includes('bundle.schema.json')) return 'bundle';
|
|
59
|
+
if (data.$schema.includes('core-registry.schema.json') || data.$schema.includes('registry')) return 'core';
|
|
60
|
+
}
|
|
61
|
+
if ('manifest' in data || 'files' in data) return 'bundle';
|
|
62
|
+
if ('registry' in data || 'document_primitives' in data) return 'core';
|
|
63
|
+
}
|
|
64
|
+
return 'core';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function run(argv) {
|
|
68
|
+
if (wantsHelp(argv)) { console.log(HELP); return; }
|
|
69
|
+
const verb = argv[0];
|
|
70
|
+
const { positionals, flags } = parseArgs(argv.slice(1), {
|
|
71
|
+
booleans: ['json', 'quiet'],
|
|
72
|
+
alias: { j: 'json', q: 'quiet' },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const coreSchemaPath = path.join(SCHEMAS_DIR, 'core-registry.schema.json');
|
|
77
|
+
const bundleSchemaPath = path.join(SCHEMAS_DIR, 'bundle.schema.json');
|
|
78
|
+
|
|
79
|
+
if (verb === 'core') {
|
|
80
|
+
const content = fs.readFileSync(coreSchemaPath, 'utf8');
|
|
81
|
+
emit(content, flags.out);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (verb === 'bundle') {
|
|
85
|
+
const content = fs.readFileSync(bundleSchemaPath, 'utf8');
|
|
86
|
+
emit(content, flags.out);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (verb === 'export') {
|
|
90
|
+
const targetDir = path.resolve(flags.out || './schemas');
|
|
91
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
92
|
+
const written = [
|
|
93
|
+
path.join(targetDir, 'core-registry.schema.json'),
|
|
94
|
+
path.join(targetDir, 'bundle.schema.json'),
|
|
95
|
+
];
|
|
96
|
+
fs.copyFileSync(coreSchemaPath, written[0]);
|
|
97
|
+
fs.copyFileSync(bundleSchemaPath, written[1]);
|
|
98
|
+
if (flags.json) {
|
|
99
|
+
console.log(JSON.stringify({ exported: written, directory: targetDir }, null, 2));
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`Exported canonical schemas to ${targetDir}`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (verb === 'validate') {
|
|
106
|
+
const file = positionals[0];
|
|
107
|
+
if (!file) die(`validate requires a target JSON file.\n\n${HELP}`);
|
|
108
|
+
const resolvedFile = path.resolve(file);
|
|
109
|
+
if (!fs.existsSync(resolvedFile)) die(`no such file: ${file}`);
|
|
110
|
+
let data;
|
|
111
|
+
try {
|
|
112
|
+
data = JSON.parse(fs.readFileSync(resolvedFile, 'utf8'));
|
|
113
|
+
} catch (err) {
|
|
114
|
+
die(`not valid JSON (${file}): ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let schema;
|
|
118
|
+
let schemaLabel;
|
|
119
|
+
if (flags.schema) {
|
|
120
|
+
const customSchemaPath = path.resolve(flags.schema);
|
|
121
|
+
if (!fs.existsSync(customSchemaPath)) die(`no such schema file: ${flags.schema}`);
|
|
122
|
+
schema = JSON.parse(fs.readFileSync(customSchemaPath, 'utf8'));
|
|
123
|
+
schemaLabel = flags.schema;
|
|
124
|
+
} else {
|
|
125
|
+
const kind = detectSchemaType(data, flags.type);
|
|
126
|
+
schema = kind === 'bundle' ? getBundleSchema() : getCoreRegistrySchema();
|
|
127
|
+
schemaLabel = kind;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { valid, errors } = validateJsonSchema(data, schema);
|
|
131
|
+
if (flags.json) {
|
|
132
|
+
console.log(JSON.stringify({ valid, schema: schemaLabel, file, errors }, null, 2));
|
|
133
|
+
if (!valid) process.exit(1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (valid) {
|
|
137
|
+
if (!flags.quiet) {
|
|
138
|
+
console.log(`✓ valid — ${file} (${schemaLabel} schema)`);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.error(`✗ invalid — ${errors.length} error(s) in ${file}:`);
|
|
143
|
+
for (const e of errors) {
|
|
144
|
+
console.error(` - ${e.path ? `${e.path}: ` : ''}${e.message}`);
|
|
145
|
+
}
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
die(`Unknown schema verb '${verb || ''}'.\n\n${HELP}`);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
die(error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -33,6 +33,7 @@ import { MemoryIdempotencyStore, FileIdempotencyStore, runIdempotencyContract }
|
|
|
33
33
|
import { capabilityCovers, createCapabilityAuthorizer } from '../src/authorization.mjs';
|
|
34
34
|
import { createEngine } from '../src/engine.mjs';
|
|
35
35
|
import { validateVerifiedPrincipal } from '../src/authorization.mjs';
|
|
36
|
+
import { validateJsonSchema } from '../src/schema-validator.mjs';
|
|
36
37
|
|
|
37
38
|
async function rejects(promise, pattern) {
|
|
38
39
|
try { await promise; return false; } catch (error) { return pattern.test(error.message); }
|
|
@@ -438,6 +439,45 @@ async function checkCapabilities(root, check) {
|
|
|
438
439
|
(await writeScoped('ps-2', 'rules/engineering/lint.md', doc('rule', 'Lint'))).error?.code === 'forbidden');
|
|
439
440
|
}
|
|
440
441
|
|
|
442
|
+
function checkJsonSchemas(check) {
|
|
443
|
+
const coreRegistry = JSON.parse(fs.readFileSync(path.resolve('registry/core.json'), 'utf8'));
|
|
444
|
+
const coreSchema = JSON.parse(fs.readFileSync(path.resolve('schemas/core-registry.schema.json'), 'utf8'));
|
|
445
|
+
const coreResult = validateJsonSchema(coreRegistry, coreSchema);
|
|
446
|
+
check('schema: registry/core.json validates against schemas/core-registry.schema.json', coreResult.valid, coreResult.errors?.[0]?.message);
|
|
447
|
+
|
|
448
|
+
const bundle = JSON.parse(fs.readFileSync(path.resolve('conformance/reference-bundle.ucw.json'), 'utf8'));
|
|
449
|
+
const bundleSchema = JSON.parse(fs.readFileSync(path.resolve('schemas/bundle.schema.json'), 'utf8'));
|
|
450
|
+
const bundleResult = validateJsonSchema(bundle, bundleSchema);
|
|
451
|
+
check('schema: reference-bundle.ucw.json validates against schemas/bundle.schema.json', bundleResult.valid, bundleResult.errors?.[0]?.message);
|
|
452
|
+
|
|
453
|
+
const badCore = { ...coreRegistry };
|
|
454
|
+
delete badCore.spec_version;
|
|
455
|
+
const badCoreResult = validateJsonSchema(badCore, coreSchema);
|
|
456
|
+
check('schema: core registry schema rejects missing spec_version', !badCoreResult.valid && badCoreResult.errors.some((e) => e.path.includes('spec_version')));
|
|
457
|
+
|
|
458
|
+
const badPortability = JSON.parse(JSON.stringify(coreRegistry));
|
|
459
|
+
badPortability.document_primitives.memory.portability = 'illegal_class';
|
|
460
|
+
const badPortResult = validateJsonSchema(badPortability, coreSchema);
|
|
461
|
+
check('schema: core registry schema rejects invalid portability enum', !badPortResult.valid && badPortResult.errors.some((e) => e.path.includes('portability')));
|
|
462
|
+
|
|
463
|
+
const badBundle = { files: [] };
|
|
464
|
+
const badBundleResult = validateJsonSchema(badBundle, bundleSchema);
|
|
465
|
+
check('schema: bundle schema rejects missing manifest', !badBundleResult.valid && badBundleResult.errors.some((e) => e.path.includes('manifest')));
|
|
466
|
+
|
|
467
|
+
// Extension registries need no spec_version/description (those are core-only, A-009).
|
|
468
|
+
const extDef = definePrimitive({ namespace: 'acme', name: 'Widget', fields: [{ id: 'title', name: 'Title', kind: 'string', required: true }] });
|
|
469
|
+
const extResult = validateJsonSchema(definitionsToExtensionRegistry('acme', [extDef]), coreSchema);
|
|
470
|
+
check('schema: definitionsToExtensionRegistry() output validates against the registry schema', extResult.valid, extResult.errors?.[0] && `${extResult.errors[0].path}: ${extResult.errors[0].message}`);
|
|
471
|
+
|
|
472
|
+
const strayKey = validateJsonSchema({ ...coreRegistry, unexpected_root_key: true }, coreSchema);
|
|
473
|
+
check('schema: registry schema rejects unknown root keys (fail-closed)', !strayKey.valid && strayKey.errors.some((e) => e.path === 'unexpected_root_key'));
|
|
474
|
+
|
|
475
|
+
const dupFields = JSON.parse(JSON.stringify(coreRegistry));
|
|
476
|
+
dupFields.document_primitives.rule.required_fields = ['type', 'type'];
|
|
477
|
+
const dupResult = validateJsonSchema(dupFields, coreSchema);
|
|
478
|
+
check('schema: registry schema rejects duplicate required_fields (uniqueItems)', !dupResult.valid && dupResult.errors.some((e) => e.message.includes('uniqueItems')));
|
|
479
|
+
}
|
|
480
|
+
|
|
441
481
|
// Resource hooks (spec §6.3): every prepare ends in exactly one finalize or
|
|
442
482
|
// reconcile, and nothing after the event append may undo the commit.
|
|
443
483
|
async function checkResourceLifecycle(check) {
|
|
@@ -785,6 +825,7 @@ export async function runKernel09Conformance() {
|
|
|
785
825
|
checkFrontmatterSubset(check);
|
|
786
826
|
checkRequiredPresence(check);
|
|
787
827
|
await checkCapabilities(temp, check);
|
|
828
|
+
checkJsonSchemas(check);
|
|
788
829
|
} finally {
|
|
789
830
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
790
831
|
}
|
package/scripts/conformance.mjs
CHANGED
|
@@ -430,13 +430,13 @@ async function runBundleConformance() {
|
|
|
430
430
|
// D4: Resource bindings emitted in result
|
|
431
431
|
check('provisionBundle emits resource bindings with relations and modes (D4)',
|
|
432
432
|
provAlpha.bindings.length > 0 &&
|
|
433
|
-
provAlpha.bindings.some((b) => b.type === '
|
|
433
|
+
provAlpha.bindings.some((b) => (b.type === 'resource' || b.type === 'domain') && b.value === 'alpha.example'));
|
|
434
434
|
|
|
435
435
|
// D4: Unbound required resource fails when not dryRun
|
|
436
436
|
const unboundBundle = JSON.parse(JSON.stringify(bundle));
|
|
437
437
|
const provUnbound = provisionBundle(unboundBundle, {
|
|
438
438
|
workspaceId: 'ws-unbound',
|
|
439
|
-
parameters: { business_name: 'Unbound Fest' }, // domain omitted!
|
|
439
|
+
parameters: { business_name: 'Unbound Fest' }, // domain / resource binding omitted!
|
|
440
440
|
dryRun: false,
|
|
441
441
|
});
|
|
442
442
|
check('unbound required resource fails provisioning when dryRun is false (D4)',
|
|
@@ -1135,6 +1135,44 @@ async function runCliSmokeConformance() {
|
|
|
1135
1135
|
}
|
|
1136
1136
|
check('ssss new starter vault files validate against the registry', files.length > 0 && failures.length === 0, JSON.stringify(failures));
|
|
1137
1137
|
|
|
1138
|
+
// A-008: an app-local extension scaffold must pass its own generated test.
|
|
1139
|
+
const extApp = path.join(tmp, 'ext-app');
|
|
1140
|
+
execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'new', extApp, '--extension', 'acme', '--no-git'], { stdio: 'ignore' });
|
|
1141
|
+
fs.mkdirSync(path.join(extApp, 'node_modules', '@gregiteen'), { recursive: true });
|
|
1142
|
+
fs.symlinkSync(path.resolve(__dirname, '..'), path.join(extApp, 'node_modules', '@gregiteen', 'ssss-cli'), 'dir');
|
|
1143
|
+
let extTestOk = true;
|
|
1144
|
+
let extTestDetail = '';
|
|
1145
|
+
try { execFileSync('node', ['--test'], { cwd: extApp, stdio: 'pipe' }); }
|
|
1146
|
+
catch (err) { extTestOk = false; extTestDetail = String(err.stdout || err.message).slice(-600); }
|
|
1147
|
+
check('ssss new --extension scaffolds an app whose own node --test passes', extTestOk, extTestDetail);
|
|
1148
|
+
|
|
1149
|
+
const badExt = (() => {
|
|
1150
|
+
try { execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'new', path.join(tmp, 'bad-ext'), '--extension', 'core', '--no-git'], { stdio: 'pipe' }); return false; }
|
|
1151
|
+
catch { return !fs.existsSync(path.join(tmp, 'bad-ext')); }
|
|
1152
|
+
})();
|
|
1153
|
+
check('ssss new --extension rejects reserved registry names before writing', badExt);
|
|
1154
|
+
|
|
1155
|
+
// ssss schema validate: core, scaffolded extension, bundle, and a failing input.
|
|
1156
|
+
const schemaValidate = (target, extra = []) => {
|
|
1157
|
+
try {
|
|
1158
|
+
return { code: 0, out: JSON.parse(execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'schema', 'validate', target, '--json', ...extra], { encoding: 'utf8' })) };
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
return { code: err.status, out: JSON.parse(err.stdout || '{}') };
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const coreValid = schemaValidate(path.join(REGISTRY_DIR, 'core.json'));
|
|
1164
|
+
const extValid = schemaValidate(path.join(extApp, 'registry', 'extensions', 'acme.json'));
|
|
1165
|
+
const bundleValid = schemaValidate(path.resolve(__dirname, '..', 'conformance', 'reference-bundle.ucw.json'));
|
|
1166
|
+
check('ssss schema validate accepts core, extension, and bundle documents',
|
|
1167
|
+
coreValid.code === 0 && coreValid.out.valid && extValid.code === 0 && extValid.out.valid &&
|
|
1168
|
+
bundleValid.code === 0 && bundleValid.out.valid && bundleValid.out.schema === 'bundle',
|
|
1169
|
+
JSON.stringify({ coreValid, extValid, bundleValid }).slice(0, 600));
|
|
1170
|
+
const badRegistryPath = path.join(tmp, 'bad-registry.json');
|
|
1171
|
+
fs.writeFileSync(badRegistryPath, JSON.stringify({ registry: 'acme', document_primitives: {}, typo_key: 1 }));
|
|
1172
|
+
const badValid = schemaValidate(badRegistryPath, ['--type', 'core']);
|
|
1173
|
+
check('ssss schema validate exits 1 with structured errors on an invalid registry',
|
|
1174
|
+
badValid.code === 1 && badValid.out.valid === false && badValid.out.errors.some((e) => e.path === 'typo_key'));
|
|
1175
|
+
|
|
1138
1176
|
const semanticOutput = JSON.parse(execFileSync('node', [
|
|
1139
1177
|
path.join(__dirname, 'ssss.mjs'),
|
|
1140
1178
|
'semantic',
|
package/scripts/ssss.mjs
CHANGED
|
@@ -41,6 +41,7 @@ Authoring, wire protocol & conformance:
|
|
|
41
41
|
bridge [options] Execute operations over stdin/stdout JSON (spec §6.8).
|
|
42
42
|
primitive <verb> Create, validate, inspect, migrate, or emit types.
|
|
43
43
|
registry <verb> Compose, lock, or verify an effective registry.
|
|
44
|
+
schema <verb> Output or export canonical JSON Schemas (core, bundle).
|
|
44
45
|
adapter conformance Run machine-readable adapter contract suites.
|
|
45
46
|
migrate 0.8-to-0.9 Diagnose and plan a non-destructive 0.9 migration.
|
|
46
47
|
autolink [dir] Generate OKF wiki links across a vault.
|
|
@@ -72,6 +73,7 @@ const MODULES = {
|
|
|
72
73
|
semantic: './cmd-semantic.mjs',
|
|
73
74
|
primitive: './cmd-primitive.mjs',
|
|
74
75
|
registry: './cmd-registry.mjs',
|
|
76
|
+
schema: './cmd-schema.mjs',
|
|
75
77
|
adapter: './cmd-adapter.mjs',
|
|
76
78
|
migrate: './cmd-migrate.mjs',
|
|
77
79
|
serve: './cmd-serve.mjs',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Automatically synchronize and verify documentation after OpenWiki updates:
|
|
4
|
+
* 1. Updates overview.md command lists, topics, and primitive counts.
|
|
5
|
+
* 2. Ingests updated docs into OpenWiki knowledge graph.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const SCRIPTS_DIR = path.join(ROOT, 'scripts');
|
|
13
|
+
const HELP_DIR = path.join(ROOT, 'docs', 'help');
|
|
14
|
+
const OVERVIEW_FILE = path.join(HELP_DIR, 'overview.md');
|
|
15
|
+
const CORE_REGISTRY = path.join(ROOT, 'registry', 'core.json');
|
|
16
|
+
|
|
17
|
+
const commands = fs.readdirSync(SCRIPTS_DIR)
|
|
18
|
+
.filter((f) => f.startsWith('cmd-') && f.endsWith('.mjs'))
|
|
19
|
+
.map((f) => f.replace(/^cmd-/, '').replace(/\.mjs$/, ''))
|
|
20
|
+
.sort();
|
|
21
|
+
|
|
22
|
+
const helpTopics = fs.existsSync(HELP_DIR)
|
|
23
|
+
? fs.readdirSync(HELP_DIR)
|
|
24
|
+
.filter((f) => f.endsWith('.md'))
|
|
25
|
+
.map((f) => f.replace(/\.md$/, ''))
|
|
26
|
+
.sort()
|
|
27
|
+
: [];
|
|
28
|
+
|
|
29
|
+
let corePrimitivesCount = 20;
|
|
30
|
+
let contractPrimitivesCount = 5;
|
|
31
|
+
if (fs.existsSync(CORE_REGISTRY)) {
|
|
32
|
+
const core = JSON.parse(fs.readFileSync(CORE_REGISTRY, 'utf8'));
|
|
33
|
+
corePrimitivesCount = Object.keys(core.document_primitives || {}).length;
|
|
34
|
+
contractPrimitivesCount = Object.keys(core.contract_primitives || {}).length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(OVERVIEW_FILE)) {
|
|
38
|
+
let content = fs.readFileSync(OVERVIEW_FILE, 'utf8');
|
|
39
|
+
|
|
40
|
+
// Update primitive counts
|
|
41
|
+
const regPattern = /\|\s*Core registry\s*\|\s*`registry\/core\.json`\s*\|\s*[^|]+\|/;
|
|
42
|
+
const regReplacement = `| Core registry | \`registry/core.json\` | ${corePrimitivesCount} document primitives + ${contractPrimitivesCount} contract primitives; semantic (§11.9), bundle (§16), and provisioning (§17) rules. |`;
|
|
43
|
+
content = content.replace(regPattern, regReplacement);
|
|
44
|
+
|
|
45
|
+
// Update help topic list
|
|
46
|
+
const helpSection = `Run \`ssss help <topic>\` for any of:\n\`${helpTopics.join('`, `')}\`.`;
|
|
47
|
+
content = content.replace(/Run `ssss help <topic>` for any of:\n[\s\S]*?(?=\n\nRun `ssss <command>)/, helpSection);
|
|
48
|
+
|
|
49
|
+
// Update command list
|
|
50
|
+
const cmdSection = `Run \`ssss <command> --help\` for: \`${commands.join('`, `')}\`.`;
|
|
51
|
+
content = content.replace(/Run `ssss <command> --help` for:[\s\S]*?(?=\n*$)/, cmdSection);
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(OVERVIEW_FILE, content.trim() + '\n', 'utf8');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`✓ Synchronized docs/help/overview.md: ${commands.length} commands, ${helpTopics.length} help topics, ${corePrimitivesCount} primitives.`);
|
package/skills/ssss/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SSSS portable core — 0.14.
|
|
1
|
+
# SSSS portable core — 0.14.2
|
|
2
2
|
|
|
3
3
|
Verified against the SSSS owner package, registry and spec on 2026-11-02. Establish ground truth from the installed validator, composed registry plus lock, and actual host mutation path. Schema support does not prove a configured runner or provider integration.
|
|
4
4
|
|
|
@@ -15,4 +15,4 @@ Preserve portability classes: structural definitions, tenant-private data and re
|
|
|
15
15
|
## Verification
|
|
16
16
|
Run relevant schema/registry/lock and operation conformance, with valid and invalid examples. Cover duplicate/conflicting idempotency (including retries answered from the event log), unsafe paths, unauthorized/expired actors, leases, append semantics, rollback and commit-intent crash recovery where changed. Use `ssss --help`, then operation-specific help; commands differ by installed version. Report exact package version, registry/lock identity and actual results. Keep secrets and private runtime data out of skill content, bundles and logs.
|
|
17
17
|
|
|
18
|
-
Normative sources are bundled below from the owner release tag v0.14.
|
|
18
|
+
Normative sources are bundled below from the owner release tag v0.14.2 (`docs/ssss-spec.md` and `registry/core.json` at that tag). Retain authored local wrappers/references; this managed core is the portable update boundary.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://ssss.dev/registry
|
|
2
|
+
"$schema": "https://ssss.dev/schemas/core-registry.schema.json",
|
|
3
3
|
"registry": "core",
|
|
4
4
|
"spec_version": "0.9",
|
|
5
5
|
"description": "The canonical SSSS core registry and semantic application-kernel contract. Core primitives are qualified as ssss:<name>; applications define namespaced primitives through layered extension registries without copying or redefining core behavior.",
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
"classes": {
|
|
270
270
|
"structural": "The reusable business model — sellable and templatable. Carries no operator- or customer-specific data. Examples: workflow, assistant, page, skill, model, rule, memory, migration, release.",
|
|
271
271
|
"tenant_private": "The operator's private operational data — customers, transcripts, runtime work. NEVER leaves the workspace in a template or sale bundle; included only in an encrypted backup. Examples: conversation, run, task, conflict, and (via extension) profile / CRM contact / deal.",
|
|
272
|
-
"resource_bound": "Requires a real external resource bound at provision time (a domain, phone number, mailbox, API connection). NEVER ships the seller's resource value; ships only a requirement declaration the buyer satisfies. Defined by extension registries (e.g.
|
|
272
|
+
"resource_bound": "Requires a real external resource bound at provision time (a domain, phone number, mailbox, API connection). NEVER ships the seller's resource value; ships only a requirement declaration the buyer satisfies. Defined by extension registries (e.g. a domain, phone_number, or integration_connection primitive)."
|
|
273
273
|
},
|
|
274
274
|
"export_profiles": {
|
|
275
275
|
"backup": { "includes": ["structural", "tenant_private", "resource_bound"], "note": "Complete fidelity copy. tenant_private MUST be encrypted at rest." },
|
|
@@ -261,8 +261,9 @@ primitive — it implements the subset its product requires — but any primitiv
|
|
|
261
261
|
| `primitive` | meta | structural | A governed, versioned definition for a namespaced SSSS primitive. |
|
|
262
262
|
|
|
263
263
|
The **Portability** column is the primitive's default portability class (§5.5); a host
|
|
264
|
-
resolves it from `registry/core.json`.
|
|
265
|
-
|
|
264
|
+
resolves it from `registry/core.json`. The canonical machine-readable registry conforms to the
|
|
265
|
+
JSON Schema (Draft 2020-12) published in `schemas/core-registry.schema.json`. Extension registries
|
|
266
|
+
assign their own primitives a class — notably `resource_bound` for things like domains and phone numbers.
|
|
266
267
|
|
|
267
268
|
### 5.2 Contract Primitives
|
|
268
269
|
|
|
@@ -1869,7 +1870,9 @@ A bundle is a JSON object with three top-level members:
|
|
|
1869
1870
|
```
|
|
1870
1871
|
|
|
1871
1872
|
`files` MUST be sorted by `path` (§16.3 order) so that two exports of the same vault state
|
|
1872
|
-
are identical and content-hashable (§16.3, `provenance.content_hash`).
|
|
1873
|
+
are identical and content-hashable (§16.3, `provenance.content_hash`). The normative JSON Schema
|
|
1874
|
+
(Draft 2020-12) for `.ucw` bundle containers is published in `schemas/bundle.schema.json` and
|
|
1875
|
+
exportable via the CLI command `ssss schema bundle`.
|
|
1873
1876
|
|
|
1874
1877
|
### 16.2 The Manifest
|
|
1875
1878
|
|
package/src/bundle.mjs
CHANGED
|
@@ -63,6 +63,7 @@ function requirementParameterKey(type, field) {
|
|
|
63
63
|
function resourceFieldParameterAliases(type, field) {
|
|
64
64
|
const aliases = [requirementParameterKey(type, field), field];
|
|
65
65
|
if (type === 'domain' && field === 'domain_name') aliases.push('domain');
|
|
66
|
+
if (type === 'resource' && (field === 'binds' || field === 'name')) aliases.push('domain', 'resource_domain');
|
|
66
67
|
if (type === 'phone_number' && field === 'phone_number') aliases.push('phone');
|
|
67
68
|
return aliases;
|
|
68
69
|
}
|