@prestack/thread 0.0.1 → 0.0.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/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { cwd } from 'node:process'
|
|
4
|
+
|
|
5
|
+
const NAMES = ['_threads', 'threads'] as const
|
|
6
|
+
const ROOT = 'thread.ts'
|
|
7
|
+
|
|
8
|
+
export interface Tree {
|
|
9
|
+
readonly at: string
|
|
10
|
+
readonly dir: string
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly root: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class UnthreadedRepositoryError extends Error {
|
|
16
|
+
constructor(from: string) {
|
|
17
|
+
super(
|
|
18
|
+
`no tree above ${from} — looked for a ${NAMES.map((one) => `${one}/`).join(' or ')} directory in every directory up to the filesystem root. ` +
|
|
19
|
+
'Run `thread init` to write one.',
|
|
20
|
+
)
|
|
21
|
+
this.name = 'UnthreadedRepositoryError'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function findTree(from: string = cwd()): Tree {
|
|
26
|
+
let at = resolve(from)
|
|
27
|
+
for (;;) {
|
|
28
|
+
for (const name of NAMES) {
|
|
29
|
+
const dir = join(at, name)
|
|
30
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) return { at, dir, name, root: join(dir, ROOT) }
|
|
31
|
+
}
|
|
32
|
+
const up = dirname(at)
|
|
33
|
+
if (up === at) throw new UnthreadedRepositoryError(from)
|
|
34
|
+
at = up
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function strayFiles(root: string, prefix: string, ends: string): readonly string[] {
|
|
39
|
+
const skip = new Set(['node_modules', '.git', '.next', 'dist', '_generated', 'coverage'])
|
|
40
|
+
const found: string[] = []
|
|
41
|
+
const walk = (dir: string): void => {
|
|
42
|
+
|
|
43
|
+
let held
|
|
44
|
+
try {
|
|
45
|
+
held = readdirSync(dir, { withFileTypes: true })
|
|
46
|
+
} catch {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
for (const one of held) {
|
|
50
|
+
if (one.name.startsWith('.') || skip.has(one.name)) continue
|
|
51
|
+
const path = join(dir, one.name)
|
|
52
|
+
if (one.isDirectory()) {
|
|
53
|
+
walk(path)
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
if (one.name.startsWith(prefix) && one.name.endsWith(ends)) {
|
|
57
|
+
found.push(relative(root, path).split('\\').join('/'))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
walk(root)
|
|
62
|
+
return found.sort()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function inTree(tree: Tree, file: string): string {
|
|
66
|
+
return join(tree.dir, file)
|
|
67
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
export const VALUE_TYPES = ['text', 'number', 'boolean', 'date', 'timestamp', 'json'] as const
|
|
2
|
+
|
|
3
|
+
export type ValueType = (typeof VALUE_TYPES)[number]
|
|
4
|
+
|
|
5
|
+
export const KEYBOARDS = ['text', 'numeric', 'decimal', 'tel', 'email', 'url', 'search'] as const
|
|
6
|
+
|
|
7
|
+
export type Keyboard = (typeof KEYBOARDS)[number]
|
|
8
|
+
|
|
9
|
+
export const ENTRY_MODES = ['plain', 'calculator', 'masked'] as const
|
|
10
|
+
|
|
11
|
+
export type EntryMode = (typeof ENTRY_MODES)[number]
|
|
12
|
+
|
|
13
|
+
export type Checked = { readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly because: string }
|
|
14
|
+
|
|
15
|
+
export const held = (value: unknown): Checked => ({ ok: true, value })
|
|
16
|
+
|
|
17
|
+
export const refused = (because: string): Checked => ({ ok: false, because })
|
|
18
|
+
|
|
19
|
+
export interface Facts {
|
|
20
|
+
readonly locale?: string
|
|
21
|
+
readonly currency?: string
|
|
22
|
+
readonly min?: number
|
|
23
|
+
readonly max?: number
|
|
24
|
+
readonly maxLen?: number
|
|
25
|
+
readonly maxBytes?: number
|
|
26
|
+
readonly maxDepth?: number
|
|
27
|
+
readonly options?: readonly string[]
|
|
28
|
+
readonly now?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Format {
|
|
32
|
+
readonly name: string
|
|
33
|
+
readonly type: ValueType
|
|
34
|
+
readonly check: (raw: unknown, facts: Facts) => Checked
|
|
35
|
+
readonly shown?: (value: unknown, facts: Facts) => string
|
|
36
|
+
readonly keyboard?: Keyboard
|
|
37
|
+
readonly accepts?: RegExp
|
|
38
|
+
readonly maxChars?: number
|
|
39
|
+
readonly entry?: EntryMode
|
|
40
|
+
readonly mask?: string
|
|
41
|
+
readonly secret?: boolean
|
|
42
|
+
readonly autocomplete?: string
|
|
43
|
+
readonly requires?: readonly string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function stripControl(text: string): string {
|
|
47
|
+
let out = ''
|
|
48
|
+
for (const character of text) {
|
|
49
|
+
const point = character.codePointAt(0) ?? 0
|
|
50
|
+
const control = point <= 0x08 || point === 0x0b || point === 0x0c || (point >= 0x0e && point <= 0x1f) || (point >= 0x7f && point <= 0x9f)
|
|
51
|
+
if (!control) out += character
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const numeric = (raw: unknown): number => (typeof raw === 'number' ? raw : Number(raw))
|
|
57
|
+
|
|
58
|
+
const SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:'])
|
|
59
|
+
|
|
60
|
+
export const BOUNDED: Readonly<Partial<Record<ValueType, string>>> = {
|
|
61
|
+
text: 'number of characters',
|
|
62
|
+
number: 'value',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function bounded(one: number, facts: Facts): Checked {
|
|
66
|
+
if (facts.min !== undefined && one < facts.min) return refused('format.number.below_min')
|
|
67
|
+
if (facts.max !== undefined && one > facts.max) return refused('format.number.above_max')
|
|
68
|
+
return held(one)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function depthWithin(value: unknown, limit: number, depth = 0): boolean {
|
|
72
|
+
if (depth > limit) return false
|
|
73
|
+
if (Array.isArray(value)) return value.every((one) => depthWithin(one, limit, depth + 1))
|
|
74
|
+
if (value !== null && typeof value === 'object') return Object.values(value).every((one) => depthWithin(one, limit, depth + 1))
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const instant = (raw: unknown): Checked =>
|
|
79
|
+
typeof raw === 'string' && !Number.isNaN(Date.parse(raw))
|
|
80
|
+
? held(new Date(raw).toISOString())
|
|
81
|
+
: refused('format.timestamp.unreadable')
|
|
82
|
+
|
|
83
|
+
const dated = (value: unknown, facts: Facts, options: Intl.DateTimeFormatOptions): string =>
|
|
84
|
+
new Intl.DateTimeFormat(facts.locale, options).format(new Date(String(value)))
|
|
85
|
+
|
|
86
|
+
export const FORMATS: readonly Format[] = [
|
|
87
|
+
{
|
|
88
|
+
name: 'text',
|
|
89
|
+
type: 'text',
|
|
90
|
+
keyboard: 'text',
|
|
91
|
+
check: (raw, facts) => {
|
|
92
|
+
if (typeof raw !== 'string') return refused('format.text.not_text')
|
|
93
|
+
const clean = stripControl(raw).trim()
|
|
94
|
+
return facts.maxLen !== undefined && clean.length > facts.maxLen ? refused('format.text.too_long') : held(clean)
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'email',
|
|
99
|
+
type: 'text',
|
|
100
|
+
keyboard: 'email',
|
|
101
|
+
autocomplete: 'email',
|
|
102
|
+
check: (raw) => {
|
|
103
|
+
if (typeof raw !== 'string') return refused('format.email.invalid')
|
|
104
|
+
const one = raw.trim().toLowerCase()
|
|
105
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(one) ? held(one) : refused('format.email.invalid')
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'tel',
|
|
110
|
+
type: 'text',
|
|
111
|
+
keyboard: 'tel',
|
|
112
|
+
accepts: /[0-9+]/,
|
|
113
|
+
maxChars: 16,
|
|
114
|
+
autocomplete: 'tel',
|
|
115
|
+
check: (raw) => {
|
|
116
|
+
if (typeof raw !== 'string') return refused('format.tel.invalid')
|
|
117
|
+
const digits = raw.replace(/[^\d+]/g, '')
|
|
118
|
+
return /^\+?\d{6,15}$/.test(digits) ? held(digits) : refused('format.tel.invalid')
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'url',
|
|
123
|
+
type: 'text',
|
|
124
|
+
keyboard: 'url',
|
|
125
|
+
check: (raw) => {
|
|
126
|
+
if (typeof raw !== 'string') return refused('format.url.invalid')
|
|
127
|
+
let parsed: URL
|
|
128
|
+
try {
|
|
129
|
+
parsed = new URL(raw.trim())
|
|
130
|
+
} catch {
|
|
131
|
+
return refused('format.url.invalid')
|
|
132
|
+
}
|
|
133
|
+
return SCHEMES.has(parsed.protocol) ? held(parsed.toString()) : refused('format.url.scheme')
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'password',
|
|
138
|
+
type: 'text',
|
|
139
|
+
secret: true,
|
|
140
|
+
maxChars: 256,
|
|
141
|
+
autocomplete: 'current-password',
|
|
142
|
+
check: (raw) => (typeof raw === 'string' && raw.length >= 8 ? held(raw) : refused('format.password.too_short')),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'otp',
|
|
146
|
+
type: 'text',
|
|
147
|
+
keyboard: 'numeric',
|
|
148
|
+
accepts: /\d/,
|
|
149
|
+
maxChars: 6,
|
|
150
|
+
autocomplete: 'one-time-code',
|
|
151
|
+
check: (raw) => (typeof raw === 'string' && /^\d{6}$/.test(raw) ? held(raw) : refused('format.otp.invalid')),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'ref',
|
|
155
|
+
type: 'text',
|
|
156
|
+
check: (raw) => (typeof raw === 'string' && /^ref:[a-z0-9/_-]+$/i.test(raw) ? held(raw) : refused('format.ref.invalid')),
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
name: 'choice',
|
|
161
|
+
type: 'text',
|
|
162
|
+
requires: ['options'],
|
|
163
|
+
check: (raw, facts) => {
|
|
164
|
+
if (facts.options === undefined) return refused('format.choice.undeclared_options')
|
|
165
|
+
return typeof raw === 'string' && facts.options.includes(raw) ? held(raw) : refused('format.choice.undeclared')
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'number',
|
|
170
|
+
type: 'number',
|
|
171
|
+
keyboard: 'decimal',
|
|
172
|
+
accepts: /[0-9.,-]/,
|
|
173
|
+
check: (raw, facts) => {
|
|
174
|
+
const one = numeric(raw)
|
|
175
|
+
return Number.isFinite(one) ? bounded(one, facts) : refused('format.number.invalid')
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'integer',
|
|
180
|
+
type: 'number',
|
|
181
|
+
keyboard: 'numeric',
|
|
182
|
+
accepts: /[0-9-]/,
|
|
183
|
+
check: (raw, facts) => {
|
|
184
|
+
const one = numeric(raw)
|
|
185
|
+
return Number.isInteger(one) ? bounded(one, facts) : refused('format.integer.invalid')
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'money',
|
|
190
|
+
type: 'number',
|
|
191
|
+
keyboard: 'decimal',
|
|
192
|
+
accepts: /[0-9]/,
|
|
193
|
+
entry: 'calculator',
|
|
194
|
+
requires: ['currency'],
|
|
195
|
+
check: (raw, facts) => {
|
|
196
|
+
const one = numeric(raw)
|
|
197
|
+
return Number.isFinite(one) ? bounded(one, facts) : refused('format.money.invalid')
|
|
198
|
+
},
|
|
199
|
+
shown: (value, facts) =>
|
|
200
|
+
facts.currency === undefined
|
|
201
|
+
? String(value)
|
|
202
|
+
: new Intl.NumberFormat(facts.locale, { style: 'currency', currency: facts.currency }).format(Number(value)),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'percent',
|
|
206
|
+
type: 'number',
|
|
207
|
+
keyboard: 'decimal',
|
|
208
|
+
accepts: /[0-9.,]/,
|
|
209
|
+
check: (raw) => {
|
|
210
|
+
const one = numeric(raw)
|
|
211
|
+
return Number.isFinite(one) && one >= 0 && one <= 100 ? held(one) : refused('format.percent.range')
|
|
212
|
+
},
|
|
213
|
+
shown: (value, facts) =>
|
|
214
|
+
new Intl.NumberFormat(facts.locale, { style: 'percent', maximumFractionDigits: 1 }).format(Number(value) / 100),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'duration',
|
|
218
|
+
type: 'number',
|
|
219
|
+
keyboard: 'numeric',
|
|
220
|
+
accepts: /[0-9]/,
|
|
221
|
+
check: (raw) => {
|
|
222
|
+
const one = numeric(raw)
|
|
223
|
+
if (!Number.isFinite(one)) return refused('format.number.invalid')
|
|
224
|
+
return one < 0 ? refused('format.duration.negative') : held(one)
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'rating',
|
|
229
|
+
type: 'number',
|
|
230
|
+
keyboard: 'numeric',
|
|
231
|
+
accepts: /[0-9]/,
|
|
232
|
+
check: (raw, facts) => {
|
|
233
|
+
const one = numeric(raw)
|
|
234
|
+
const ceiling = facts.max ?? 5
|
|
235
|
+
return Number.isInteger(one) && one >= 1 && one <= ceiling ? held(one) : refused('format.rating.range')
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'boolean',
|
|
240
|
+
type: 'boolean',
|
|
241
|
+
check: (raw) => {
|
|
242
|
+
if (typeof raw === 'boolean') return held(raw)
|
|
243
|
+
if (raw === 'true' || raw === 'false') return held(raw === 'true')
|
|
244
|
+
return refused('format.boolean.invalid')
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'date',
|
|
249
|
+
type: 'date',
|
|
250
|
+
keyboard: 'numeric',
|
|
251
|
+
accepts: /[0-9-]/,
|
|
252
|
+
maxChars: 10,
|
|
253
|
+
mask: '0000-00-00',
|
|
254
|
+
entry: 'masked',
|
|
255
|
+
check: (raw) => {
|
|
256
|
+
if (typeof raw !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(raw)) return refused('format.date.shape')
|
|
257
|
+
return Number.isNaN(Date.parse(raw)) ? refused('format.date.invalid') : held(raw)
|
|
258
|
+
},
|
|
259
|
+
shown: (value, facts) => dated(`${String(value)}T00:00:00Z`, facts, { dateStyle: 'medium', timeZone: 'UTC' }),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'timestamp',
|
|
263
|
+
type: 'timestamp',
|
|
264
|
+
check: instant,
|
|
265
|
+
shown: (value, facts) => dated(value, facts, { dateStyle: 'medium', timeStyle: 'short' }),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'elapsed',
|
|
269
|
+
type: 'timestamp',
|
|
270
|
+
check: instant,
|
|
271
|
+
requires: ['now'],
|
|
272
|
+
shown: (value, facts) => {
|
|
273
|
+
const now = facts.now ?? Date.parse(new Date().toISOString())
|
|
274
|
+
const gap = Date.parse(String(value)) - now
|
|
275
|
+
const UNITS: readonly (readonly [Intl.RelativeTimeFormatUnit, number])[] = [
|
|
276
|
+
['year', 31_536_000_000],
|
|
277
|
+
['month', 2_592_000_000],
|
|
278
|
+
['week', 604_800_000],
|
|
279
|
+
['day', 86_400_000],
|
|
280
|
+
['hour', 3_600_000],
|
|
281
|
+
['minute', 60_000],
|
|
282
|
+
['second', 1000],
|
|
283
|
+
]
|
|
284
|
+
const spoken = new Intl.RelativeTimeFormat(facts.locale, { numeric: 'auto' })
|
|
285
|
+
for (const [unit, span] of UNITS) {
|
|
286
|
+
if (Math.abs(gap) >= span) return spoken.format(Math.trunc(gap / span), unit)
|
|
287
|
+
}
|
|
288
|
+
return spoken.format(0, 'second')
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'json',
|
|
293
|
+
type: 'json',
|
|
294
|
+
check: (raw, facts) => {
|
|
295
|
+
if (facts.maxDepth !== undefined && !depthWithin(raw, facts.maxDepth)) return refused('format.json.too_deep')
|
|
296
|
+
const size = JSON.stringify(raw)?.length ?? 0
|
|
297
|
+
return facts.maxBytes !== undefined && size > facts.maxBytes ? refused('format.json.too_large') : held(raw)
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'geo',
|
|
302
|
+
type: 'json',
|
|
303
|
+
check: (raw) => {
|
|
304
|
+
if (raw === null || typeof raw !== 'object') return refused('format.geo.invalid')
|
|
305
|
+
const { lat, lng } = raw as { lat?: unknown; lng?: unknown }
|
|
306
|
+
const within = typeof lat === 'number' && lat >= -90 && lat <= 90 && typeof lng === 'number' && lng >= -180 && lng <= 180
|
|
307
|
+
return within ? held({ lat, lng }) : refused('format.geo.invalid')
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'richtext',
|
|
312
|
+
type: 'json',
|
|
313
|
+
check: (raw) => {
|
|
314
|
+
if (raw === null || typeof raw !== 'object') return refused('format.richtext.invalid')
|
|
315
|
+
const blocks = (raw as { blocks?: unknown }).blocks
|
|
316
|
+
return Array.isArray(blocks) ? held({ blocks }) : refused('format.richtext.invalid')
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
const BY_NAME = new Map(FORMATS.map((one) => [one.name, one]))
|
|
322
|
+
|
|
323
|
+
export function formatsOf(type: ValueType): readonly Format[] {
|
|
324
|
+
return FORMATS.filter((one) => one.type === type)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function formatNamed(name: string): Format | undefined {
|
|
328
|
+
return BY_NAME.get(name)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function plainFormat(type: ValueType): Format {
|
|
332
|
+
const one = BY_NAME.get(type)
|
|
333
|
+
if (one === undefined) throw new Error(`no plain format is declared for value type "${type}"`)
|
|
334
|
+
return one
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function formatFor(type: ValueType, name: string | undefined): Format | undefined {
|
|
338
|
+
const one = name === undefined ? plainFormat(type) : BY_NAME.get(name)
|
|
339
|
+
return one === undefined || one.type !== type ? undefined : one
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function checkValue(type: ValueType, name: string | undefined, raw: unknown, facts: Facts = {}): Checked {
|
|
343
|
+
const one = formatFor(type, name)
|
|
344
|
+
return one === undefined ? refused(`format.unusable.${name ?? type}`) : one.check(raw, facts)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function showValue(type: ValueType, name: string | undefined, value: unknown, facts: Facts = {}): string {
|
|
348
|
+
const one = formatFor(type, name)
|
|
349
|
+
return one?.shown === undefined ? String(value) : one.shown(value, facts)
|
|
350
|
+
}
|
|
@@ -37,7 +37,7 @@ const SHIPPED = [
|
|
|
37
37
|
into: 'derivation.ts',
|
|
38
38
|
list: 'GENERATORS',
|
|
39
39
|
id: 'release-generator',
|
|
40
|
-
source: " {\n id: 'release-generator',\n at: 'node_modules/@prestack/thread/dist/derivation/generators/release.js',\n describes: 'The writer of what a host reads to run this app — it derives the config from what the app already declares, its own build and start commands and the names of every secret it says it must be given, and refuses
|
|
40
|
+
source: " {\n id: 'release-generator',\n at: 'node_modules/@prestack/thread/dist/derivation/generators/release.js',\n describes: 'The writer of what a host reads to run this app — it derives the config from what the app already declares, its own build and start commands and the names of every secret it says it must be given, and refuses to write while this app installs a package of this framework as a dependency, because what a host runs may hold nothing of the scaffolding that assembled it.',\n version: 1,\n by: 'thread release',\n runs: 'thread release --check',\n },",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
into: 'derivation.ts',
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Attaching } from '../core/attach.ts'
|
|
2
|
+
|
|
3
|
+
import { quoted } from '../core/rows.ts'
|
|
4
|
+
|
|
5
|
+
const DESCRIBES =
|
|
6
|
+
'What turns a declaration into a file: the rows that state something is to be made, and the generators that make it. They are kept apart so you can read what an app declares without opening the machinery that builds it.'
|
|
7
|
+
|
|
8
|
+
const DERIVATION_SOURCE = [
|
|
9
|
+
"import type { Threaded } from '../core/thread.ts'",
|
|
10
|
+
'',
|
|
11
|
+
'export const DERIVATION: Threaded = {',
|
|
12
|
+
" id: 'derivation',",
|
|
13
|
+
" at: '_threads/derivation.ts',",
|
|
14
|
+
` describes: ${quoted(DESCRIBES)},`,
|
|
15
|
+
' version: 1,',
|
|
16
|
+
' derives: [],',
|
|
17
|
+
'}',
|
|
18
|
+
'',
|
|
19
|
+
'export const GENERATORS: readonly Threaded[] = []',
|
|
20
|
+
'',
|
|
21
|
+
].join('\n')
|
|
22
|
+
|
|
23
|
+
const SEEDED = {
|
|
24
|
+
attaches: [] as const,
|
|
25
|
+
id: 'derivation',
|
|
26
|
+
declares: [
|
|
27
|
+
' {',
|
|
28
|
+
' id: \'derivation\',',
|
|
29
|
+
' at: \'_threads/derivation.ts\',',
|
|
30
|
+
' describes: \'What turns a declaration into a file: the rows that state something is to be made, and the generators that make it. They are kept apart so you can read what an app declares without opening the machinery that builds it.\',',
|
|
31
|
+
' version: 1,',
|
|
32
|
+
' },',
|
|
33
|
+
].join('\n'),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SHIPPED: readonly { readonly into: string; readonly list: string; readonly id: string; readonly source: string }[] = [
|
|
37
|
+
{
|
|
38
|
+
into: 'derivation.ts',
|
|
39
|
+
list: 'GENERATORS',
|
|
40
|
+
id: 'build-generator',
|
|
41
|
+
source: " {\n id: 'build-generator',\n at: 'node_modules/@prestack/thread/dist/derivation/generators/build.js',\n describes: 'The builder of what this app ships — it reads the row that derives an artifact to learn the entry it comes from, and refuses to resolve any file inside the app the descent never reaches, so a file nothing declares has no way into the thing that runs.',\n version: 1,\n by: 'thread build',\n runs: 'thread build --check',\n },",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
into: 'derivation.ts',
|
|
45
|
+
list: 'GENERATORS',
|
|
46
|
+
id: 'release-generator',
|
|
47
|
+
source: " {\n id: 'release-generator',\n at: 'node_modules/@prestack/thread/dist/derivation/generators/release.js',\n describes: 'The writer of what a host reads to run this app — it derives the config from what the app already declares, its own build and start commands and the names of every secret it says it must be given, and refuses to write while this app installs a package of this framework as a dependency, because what a host runs may hold nothing of the scaffolding that assembled it.',\n version: 1,\n by: 'thread release',\n runs: 'thread release --check',\n },",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
into: 'derivation.ts',
|
|
51
|
+
list: 'GENERATORS',
|
|
52
|
+
id: 'release-noun',
|
|
53
|
+
source: " {\n id: 'release-noun',\n at: 'node_modules/@prestack/thread/dist/derivation/makers/release.js',\n describes: 'The command that declares where this app goes when it leaves your machine. It writes the row saying where this app goes and nothing else, because everything the config needs is already declared somewhere and asking for it again would be the app’s own facts typed a second time.',\n version: 1,\n by: 'thread create release',\n },",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
into: 'derivation.ts',
|
|
57
|
+
list: 'GENERATORS',
|
|
58
|
+
id: 'generator-noun',
|
|
59
|
+
source: " {\n id: 'generator-noun',\n at: 'node_modules/@prestack/thread/dist/derivation/generators/derivation.js',\n describes: 'Declares a generator this app derives a file with. `thread create generator` asks what it is, where its file goes and the command that writes it, then writes the row and seeds the file beside it — so a generator arrives through the same wizard as everything else, and the command it names exists from that moment because the CLI reads every `by:` the tree states.',\n version: 1,\n by: 'thread create generator',\n },",
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
export const ATTACH: Attaching = {
|
|
63
|
+
attaches: SEEDED.attaches,
|
|
64
|
+
id: SEEDED.id,
|
|
65
|
+
declares: SEEDED.declares,
|
|
66
|
+
writes: [{ file: 'derivation.ts', source: DERIVATION_SOURCE }],
|
|
67
|
+
rows: SHIPPED,
|
|
68
|
+
}
|