@yakcc/cli 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +214 -0
- package/LICENSE-ATOMS +38 -0
- package/README.md +76 -0
- package/dist/bin.js +10884 -0
- package/dist/bin.js.map +1 -0
- package/dist/blocks/ascii-char/impl.ts +16 -0
- package/dist/blocks/ascii-char/proof/manifest.json +8 -0
- package/dist/blocks/ascii-char/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/ascii-char/spec.yak +80 -0
- package/dist/blocks/ascii-digit-set/impl.ts +9 -0
- package/dist/blocks/ascii-digit-set/proof/manifest.json +8 -0
- package/dist/blocks/ascii-digit-set/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/ascii-digit-set/spec.yak +66 -0
- package/dist/blocks/base64-alphabet/impl.ts +98 -0
- package/dist/blocks/base64-alphabet/proof/manifest.json +8 -0
- package/dist/blocks/base64-alphabet/proof/tests.fast-check.ts +18 -0
- package/dist/blocks/base64-alphabet/spec.yak +110 -0
- package/dist/blocks/bracket/impl.ts +20 -0
- package/dist/blocks/bracket/proof/manifest.json +8 -0
- package/dist/blocks/bracket/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/bracket/spec.yak +85 -0
- package/dist/blocks/char-code/impl.ts +15 -0
- package/dist/blocks/char-code/proof/manifest.json +8 -0
- package/dist/blocks/char-code/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/char-code/spec.yak +80 -0
- package/dist/blocks/comma/impl.ts +20 -0
- package/dist/blocks/comma/proof/manifest.json +8 -0
- package/dist/blocks/comma/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/comma/spec.yak +80 -0
- package/dist/blocks/comma-separated-integers/impl.ts +85 -0
- package/dist/blocks/comma-separated-integers/proof/manifest.json +8 -0
- package/dist/blocks/comma-separated-integers/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/comma-separated-integers/spec.yak +84 -0
- package/dist/blocks/digit/impl.ts +12 -0
- package/dist/blocks/digit/proof/manifest.json +8 -0
- package/dist/blocks/digit/proof/tests.fast-check.ts +21 -0
- package/dist/blocks/digit/spec.yak +79 -0
- package/dist/blocks/digit-or-throw/impl.ts +20 -0
- package/dist/blocks/digit-or-throw/proof/manifest.json +8 -0
- package/dist/blocks/digit-or-throw/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/digit-or-throw/spec.yak +80 -0
- package/dist/blocks/empty-list-content/impl.ts +20 -0
- package/dist/blocks/empty-list-content/proof/manifest.json +8 -0
- package/dist/blocks/empty-list-content/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/empty-list-content/spec.yak +80 -0
- package/dist/blocks/eof-check/impl.ts +16 -0
- package/dist/blocks/eof-check/proof/manifest.json +8 -0
- package/dist/blocks/eof-check/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/eof-check/spec.yak +76 -0
- package/dist/blocks/integer/impl.ts +29 -0
- package/dist/blocks/integer/proof/manifest.json +8 -0
- package/dist/blocks/integer/proof/tests.fast-check.ts +21 -0
- package/dist/blocks/integer/spec.yak +84 -0
- package/dist/blocks/list-of-ints/impl.ts +168 -0
- package/dist/blocks/list-of-ints/proof/manifest.json +8 -0
- package/dist/blocks/list-of-ints/proof/tests.fast-check.ts +23 -0
- package/dist/blocks/list-of-ints/spec.yak +103 -0
- package/dist/blocks/lru-node/impl.ts +50 -0
- package/dist/blocks/lru-node/proof/manifest.json +8 -0
- package/dist/blocks/lru-node/proof/tests.fast-check.ts +16 -0
- package/dist/blocks/lru-node/spec.yak +92 -0
- package/dist/blocks/memoize/impl.ts +60 -0
- package/dist/blocks/memoize/proof/manifest.json +8 -0
- package/dist/blocks/memoize/proof/tests.fast-check.ts +15 -0
- package/dist/blocks/memoize/spec.yak +91 -0
- package/dist/blocks/non-ascii-rejector/impl.ts +14 -0
- package/dist/blocks/non-ascii-rejector/proof/manifest.json +8 -0
- package/dist/blocks/non-ascii-rejector/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/non-ascii-rejector/spec.yak +67 -0
- package/dist/blocks/nonempty-list-content/impl.ts +116 -0
- package/dist/blocks/nonempty-list-content/proof/manifest.json +8 -0
- package/dist/blocks/nonempty-list-content/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/nonempty-list-content/spec.yak +88 -0
- package/dist/blocks/optional-whitespace/impl.ts +21 -0
- package/dist/blocks/optional-whitespace/proof/manifest.json +8 -0
- package/dist/blocks/optional-whitespace/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/optional-whitespace/spec.yak +76 -0
- package/dist/blocks/peek-char/impl.ts +15 -0
- package/dist/blocks/peek-char/proof/manifest.json +8 -0
- package/dist/blocks/peek-char/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/peek-char/spec.yak +76 -0
- package/dist/blocks/position-step/impl.ts +21 -0
- package/dist/blocks/position-step/proof/manifest.json +8 -0
- package/dist/blocks/position-step/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/position-step/spec.yak +85 -0
- package/dist/blocks/queue-drain/impl.ts +74 -0
- package/dist/blocks/queue-drain/proof/manifest.json +8 -0
- package/dist/blocks/queue-drain/proof/tests.fast-check.ts +16 -0
- package/dist/blocks/queue-drain/spec.yak +119 -0
- package/dist/blocks/semver-component-parser/impl.ts +115 -0
- package/dist/blocks/semver-component-parser/proof/manifest.json +8 -0
- package/dist/blocks/semver-component-parser/proof/tests.fast-check.ts +19 -0
- package/dist/blocks/semver-component-parser/spec.yak +109 -0
- package/dist/blocks/signed-integer/impl.ts +40 -0
- package/dist/blocks/signed-integer/proof/manifest.json +8 -0
- package/dist/blocks/signed-integer/proof/tests.fast-check.ts +21 -0
- package/dist/blocks/signed-integer/spec.yak +84 -0
- package/dist/blocks/string-from-position/impl.ts +18 -0
- package/dist/blocks/string-from-position/proof/manifest.json +8 -0
- package/dist/blocks/string-from-position/proof/tests.fast-check.ts +20 -0
- package/dist/blocks/string-from-position/spec.yak +85 -0
- package/dist/blocks/timer-handle/impl.ts +66 -0
- package/dist/blocks/timer-handle/proof/manifest.json +8 -0
- package/dist/blocks/timer-handle/proof/tests.fast-check.ts +18 -0
- package/dist/blocks/timer-handle/spec.yak +67 -0
- package/dist/blocks/whitespace/impl.ts +20 -0
- package/dist/blocks/whitespace/proof/manifest.json +8 -0
- package/dist/blocks/whitespace/proof/tests.fast-check.ts +21 -0
- package/dist/blocks/whitespace/spec.yak +80 -0
- package/dist/bootstrap/expected-failures.json +4 -0
- package/dist/bootstrap/expected-roots.json +49890 -0
- package/dist/bootstrap/yakcc.registry.sqlite +0 -0
- package/dist/index.js +10826 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// @decision DEC-V0-B4-SEED-queue-drain-001
|
|
4
|
+
// @title queue-drain: BFS drain primitive for Kahn's topological sort
|
|
5
|
+
// @status accepted
|
|
6
|
+
// @rationale
|
|
7
|
+
// The dependency-resolver B4 task requires a topological sort as its core
|
|
8
|
+
// algorithm. Kahn's algorithm (1962) is the standard BFS-based approach:
|
|
9
|
+
// maintain a queue of zero-in-degree nodes, drain it while decrementing
|
|
10
|
+
// successor in-degrees, enqueue newly-zero successors. This atom is the
|
|
11
|
+
// inner drain loop, extracted so the caller handles initialisation (computing
|
|
12
|
+
// initial in-degrees and seeding the queue with zero-in-degree nodes).
|
|
13
|
+
//
|
|
14
|
+
// Design decisions:
|
|
15
|
+
// (A) MUTATION CONTRACT: queue, inDegree are mutated in-place. The caller
|
|
16
|
+
// owns these data structures and passes them in. This avoids allocating
|
|
17
|
+
// a copy of potentially large structures inside the atom, keeping the
|
|
18
|
+
// space complexity O(1) additional beyond what the caller already holds.
|
|
19
|
+
//
|
|
20
|
+
// (B) CALLBACK VISITOR: onVisit is a callback rather than accumulating a
|
|
21
|
+
// result array. This lets callers choose their own output structure
|
|
22
|
+
// (array push, string concat, direct emit) without forcing an allocation
|
|
23
|
+
// inside the atom. It also makes the atom composable with streaming
|
|
24
|
+
// consumers.
|
|
25
|
+
//
|
|
26
|
+
// (C) VISIT COUNT RETURN: Returning visitCount rather than a boolean lets the
|
|
27
|
+
// caller determine whether a cycle exists (visitCount < |V|) AND how many
|
|
28
|
+
// nodes were processed, both of which may be useful.
|
|
29
|
+
//
|
|
30
|
+
// (D) ADJACENCY MAP OPTIONAL LOOKUP: adjacency.get(node) may return undefined
|
|
31
|
+
// if the node has no outgoing edges. The guard `?? []` handles this
|
|
32
|
+
// without requiring every leaf node to have an explicit empty-array entry.
|
|
33
|
+
//
|
|
34
|
+
// Reference: Kahn, A.B. (1962). "Topological sorting of large networks."
|
|
35
|
+
// Communications of the ACM, 5(11), 558-562.
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Drain a BFS queue using Kahn's topological sort algorithm.
|
|
39
|
+
*
|
|
40
|
+
* Processes every node currently in queue and any nodes whose in-degree drops
|
|
41
|
+
* to zero as a result. Mutates both queue and inDegree in-place.
|
|
42
|
+
*
|
|
43
|
+
* @param queue - Initial zero-in-degree nodes. Consumed and extended in-place.
|
|
44
|
+
* @param inDegree - In-degree for every node. Decremented as predecessors are visited.
|
|
45
|
+
* @param adjacency - Outgoing neighbour lists. Missing entries treated as empty.
|
|
46
|
+
* @param onVisit - Called once per visited node in topological order.
|
|
47
|
+
* @returns Number of nodes visited. Less than |V| iff the graph has a cycle.
|
|
48
|
+
*/
|
|
49
|
+
export function queueDrain(
|
|
50
|
+
queue: string[],
|
|
51
|
+
inDegree: Map<string, number>,
|
|
52
|
+
adjacency: Map<string, string[]>,
|
|
53
|
+
onVisit: (node: string) => void,
|
|
54
|
+
): number {
|
|
55
|
+
let visitCount = 0;
|
|
56
|
+
|
|
57
|
+
while (queue.length > 0) {
|
|
58
|
+
// shift() removes from the front -- FIFO BFS order
|
|
59
|
+
const node = queue.shift() as string;
|
|
60
|
+
onVisit(node);
|
|
61
|
+
visitCount++;
|
|
62
|
+
|
|
63
|
+
const neighbours = adjacency.get(node) ?? [];
|
|
64
|
+
for (const v of neighbours) {
|
|
65
|
+
const deg = (inDegree.get(v) ?? 0) - 1;
|
|
66
|
+
inDegree.set(v, deg);
|
|
67
|
+
if (deg === 0) {
|
|
68
|
+
queue.push(v);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return visitCount;
|
|
74
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Property tests for the queue-drain block.
|
|
3
|
+
// These tests exercise the contract declared in ../spec.yak against the
|
|
4
|
+
// implementation in ../impl.ts.
|
|
5
|
+
//
|
|
6
|
+
// Test IDs declared in spec.yak:
|
|
7
|
+
// queue-drain-linear
|
|
8
|
+
// queue-drain-empty
|
|
9
|
+
// queue-drain-single
|
|
10
|
+
// queue-drain-diamond
|
|
11
|
+
// queue-drain-cycle-detected
|
|
12
|
+
// queue-drain-parallel
|
|
13
|
+
|
|
14
|
+
// Re-export implementation so runners importing this artifact directly
|
|
15
|
+
// can access the block functions.
|
|
16
|
+
export * from "../impl.js";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "queue-drain",
|
|
3
|
+
"inputs": [
|
|
4
|
+
{
|
|
5
|
+
"name": "queue",
|
|
6
|
+
"type": "string[]",
|
|
7
|
+
"description": "Mutable queue of node IDs to process. Consumed (shifted) in-place during the drain."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "inDegree",
|
|
11
|
+
"type": "Map<string, number>",
|
|
12
|
+
"description": "Mutable in-degree map. Each processed node decrements successors; nodes reaching zero are enqueued."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "adjacency",
|
|
16
|
+
"type": "Map<string, string[]>",
|
|
17
|
+
"description": "Adjacency list: adjacency.get(u) is the list of nodes that u points to (u -> v edges)."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "onVisit",
|
|
21
|
+
"type": "(node: string) => void",
|
|
22
|
+
"description": "Called in topological order as each node is dequeued."
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"outputs": [
|
|
26
|
+
{
|
|
27
|
+
"name": "visitCount",
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Number of nodes visited. If visitCount < total nodes in graph, a cycle was detected."
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"preconditions": [
|
|
33
|
+
{
|
|
34
|
+
"description": "queue contains all nodes with initial in-degree 0."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"description": "inDegree maps every node in the graph to its current in-degree."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"description": "adjacency maps every node to its outgoing neighbour list (may be empty or absent)."
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"postconditions": [
|
|
44
|
+
{
|
|
45
|
+
"description": "queue is empty after the call."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"description": "onVisit was called once per visited node, in topological order."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"description": "visitCount equals the number of onVisit calls made."
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"invariants": [
|
|
55
|
+
{
|
|
56
|
+
"description": "A node is enqueued at most once (inDegree reaches 0 at most once per node)."
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"effects": [
|
|
60
|
+
{
|
|
61
|
+
"description": "Mutates queue (shifts elements, pushes newly-zero nodes)."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"description": "Mutates inDegree (decrements successor counts)."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"description": "Calls onVisit for each dequeued node."
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"level": "L0",
|
|
71
|
+
"behavior": "Drain the BFS queue using Kahn's algorithm for topological sort. While the queue is non-empty: dequeue the front node, call onVisit(node), then for each successor v in adjacency.get(node): decrement inDegree[v]; if inDegree[v] reaches 0, push v onto the queue. Return the total visit count. If the graph has a cycle, some nodes never reach in-degree 0 and visitCount will be less than the total node count.",
|
|
72
|
+
"guarantees": [
|
|
73
|
+
{
|
|
74
|
+
"id": "kahn-correct",
|
|
75
|
+
"description": "Produces a valid topological ordering for DAGs (Kahn 1962)."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "cycle-detectable",
|
|
79
|
+
"description": "visitCount < |V| iff the graph contains at least one cycle."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "each-node-once",
|
|
83
|
+
"description": "onVisit is called at most once per node."
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"errorConditions": [],
|
|
87
|
+
"nonFunctional": {
|
|
88
|
+
"time": "O(V + E) where V is vertices and E is edges",
|
|
89
|
+
"space": "O(1) additional (mutates caller-supplied structures)",
|
|
90
|
+
"purity": "impure (mutates queue, inDegree; calls onVisit)",
|
|
91
|
+
"threadSafety": "safe (single-threaded JS)"
|
|
92
|
+
},
|
|
93
|
+
"propertyTests": [
|
|
94
|
+
{
|
|
95
|
+
"id": "queue-drain-linear",
|
|
96
|
+
"description": "Linear chain A->B->C visits all 3 nodes in order A, B, C"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "queue-drain-empty",
|
|
100
|
+
"description": "Empty queue returns visitCount 0 and calls onVisit zero times"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "queue-drain-single",
|
|
104
|
+
"description": "Single node with no edges visits that node and returns 1"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "queue-drain-diamond",
|
|
108
|
+
"description": "Diamond DAG visits all 4 nodes, visitCount === 4"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "queue-drain-cycle-detected",
|
|
112
|
+
"description": "Graph with a cycle produces visitCount < total nodes"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "queue-drain-parallel",
|
|
116
|
+
"description": "Two independent chains each process correctly (visitCount === total)"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// @decision DEC-V0-B4-SEED-semver-component-parser-001
|
|
4
|
+
// @title semver-component-parser: version-string parser for MAJOR.MINOR.PATCH[-pre][+build]
|
|
5
|
+
// @status accepted
|
|
6
|
+
// @rationale
|
|
7
|
+
// The semver-range-satisfies B4 task requires parsing version strings into
|
|
8
|
+
// their numeric and string components before any range comparison. This atom
|
|
9
|
+
// implements the parsing primitive so the range-comparison logic can operate
|
|
10
|
+
// on structured data rather than raw strings.
|
|
11
|
+
//
|
|
12
|
+
// Design decisions:
|
|
13
|
+
// (A) SPLIT THEN PARSE: The implementation splits the input string rather
|
|
14
|
+
// than using a hand-rolled character-position parser. The split approach
|
|
15
|
+
// is more readable and the O(n) allocation cost is acceptable for version
|
|
16
|
+
// strings, which are short (typically < 30 chars). A position-based
|
|
17
|
+
// parser would save zero allocations in practice at the cost of
|
|
18
|
+
// significant code complexity.
|
|
19
|
+
//
|
|
20
|
+
// (B) BUILD BEFORE PRERELEASE: The '+' (build metadata) separator is handled
|
|
21
|
+
// AFTER splitting on '-' (prerelease). If the patch field contains '+',
|
|
22
|
+
// we split again. This correctly handles '1.0.0-beta+sha' where '-beta'
|
|
23
|
+
// is prerelease and 'sha' is build metadata appended to the patch field.
|
|
24
|
+
// Order matters: split on '-' first, then on '+'.
|
|
25
|
+
//
|
|
26
|
+
// (C) NO SEMANTIC VALIDATION OF IDENTIFIERS: Prerelease and build identifier
|
|
27
|
+
// characters are not validated (the spec only requires capturing them as
|
|
28
|
+
// strings). Full validation per semver.org spec section 9-10 would add
|
|
29
|
+
// complexity without benefit for the B4 task's comparator use-case.
|
|
30
|
+
//
|
|
31
|
+
// (D) EXPLICIT GUARD FOR ARRAY INDEXING: After verifying parts.length === 3
|
|
32
|
+
// via SyntaxError throw, TypeScript still types parts[n] as
|
|
33
|
+
// string | undefined. We use explicit string coercion ("" fallback via
|
|
34
|
+
// ?? "") to satisfy the type checker while maintaining the invariant
|
|
35
|
+
// that the value is always defined at this point in the code.
|
|
36
|
+
//
|
|
37
|
+
// Reference: Semantic Versioning 2.0.0 specification (semver.org),
|
|
38
|
+
// sections 2-10. Tom Preston-Werner, original author.
|
|
39
|
+
|
|
40
|
+
/** Parsed components of a semver version string. */
|
|
41
|
+
export interface SemverComponents {
|
|
42
|
+
readonly major: number;
|
|
43
|
+
readonly minor: number;
|
|
44
|
+
readonly patch: number;
|
|
45
|
+
readonly prerelease: string | null;
|
|
46
|
+
readonly build: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a semver version string into its numeric and string components.
|
|
51
|
+
*
|
|
52
|
+
* Handles the full MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD] grammar.
|
|
53
|
+
* Prerelease and build identifiers are captured as raw strings without
|
|
54
|
+
* further validation.
|
|
55
|
+
*
|
|
56
|
+
* @param input - Version string to parse.
|
|
57
|
+
* @returns Parsed SemverComponents.
|
|
58
|
+
* @throws SyntaxError if input does not match MAJOR.MINOR.PATCH structure.
|
|
59
|
+
*/
|
|
60
|
+
export function parseSemver(input: string): SemverComponents {
|
|
61
|
+
if (input.length === 0) {
|
|
62
|
+
throw new SyntaxError("parseSemver: empty input");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Separate prerelease suffix (everything after first '-')
|
|
66
|
+
let prerelease: string | null = null;
|
|
67
|
+
let build: string | null = null;
|
|
68
|
+
|
|
69
|
+
// Find build metadata first ('+' in the original string)
|
|
70
|
+
const plusIndex = input.indexOf("+");
|
|
71
|
+
let versionWithPre = input;
|
|
72
|
+
if (plusIndex !== -1) {
|
|
73
|
+
build = input.slice(plusIndex + 1);
|
|
74
|
+
versionWithPre = input.slice(0, plusIndex);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Find prerelease ('-' in the version-without-build string)
|
|
78
|
+
const dashIndex = versionWithPre.indexOf("-");
|
|
79
|
+
let coreVersion = versionWithPre;
|
|
80
|
+
if (dashIndex !== -1) {
|
|
81
|
+
prerelease = versionWithPre.slice(dashIndex + 1);
|
|
82
|
+
coreVersion = versionWithPre.slice(0, dashIndex);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parse MAJOR.MINOR.PATCH
|
|
86
|
+
const parts = coreVersion.split(".");
|
|
87
|
+
if (parts.length !== 3) {
|
|
88
|
+
throw new SyntaxError(
|
|
89
|
+
`parseSemver: expected MAJOR.MINOR.PATCH, got ${JSON.stringify(coreVersion)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// After the length guard above, parts[0..2] are always defined.
|
|
94
|
+
// The ?? "" fallback satisfies TypeScript's string | undefined narrowing
|
|
95
|
+
// while being unreachable in practice.
|
|
96
|
+
const majorStr = parts[0] ?? "";
|
|
97
|
+
const minorStr = parts[1] ?? "";
|
|
98
|
+
const patchStr = parts[2] ?? "";
|
|
99
|
+
|
|
100
|
+
const major = Number.parseInt(majorStr, 10);
|
|
101
|
+
const minor = Number.parseInt(minorStr, 10);
|
|
102
|
+
const patch = Number.parseInt(patchStr, 10);
|
|
103
|
+
|
|
104
|
+
if (Number.isNaN(major) || major < 0) {
|
|
105
|
+
throw new SyntaxError(`parseSemver: invalid MAJOR component ${JSON.stringify(majorStr)}`);
|
|
106
|
+
}
|
|
107
|
+
if (Number.isNaN(minor) || minor < 0) {
|
|
108
|
+
throw new SyntaxError(`parseSemver: invalid MINOR component ${JSON.stringify(minorStr)}`);
|
|
109
|
+
}
|
|
110
|
+
if (Number.isNaN(patch) || patch < 0) {
|
|
111
|
+
throw new SyntaxError(`parseSemver: invalid PATCH component ${JSON.stringify(patchStr)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { major, minor, patch, prerelease, build };
|
|
115
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Property tests for the semver-component-parser block.
|
|
3
|
+
// These tests exercise the contract declared in ../spec.yak against the
|
|
4
|
+
// implementation in ../impl.ts.
|
|
5
|
+
//
|
|
6
|
+
// Test IDs declared in spec.yak:
|
|
7
|
+
// semver-simple
|
|
8
|
+
// semver-prerelease
|
|
9
|
+
// semver-build
|
|
10
|
+
// semver-prerelease-and-build
|
|
11
|
+
// semver-zeros
|
|
12
|
+
// semver-large-numbers
|
|
13
|
+
// semver-invalid-no-dots
|
|
14
|
+
// semver-invalid-non-numeric
|
|
15
|
+
// semver-empty
|
|
16
|
+
|
|
17
|
+
// Re-export implementation so runners importing this artifact directly
|
|
18
|
+
// can access the block functions.
|
|
19
|
+
export * from "../impl.js";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "semver-component-parser",
|
|
3
|
+
"inputs": [
|
|
4
|
+
{
|
|
5
|
+
"name": "input",
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "A semver version string of the form MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]."
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"outputs": [
|
|
11
|
+
{
|
|
12
|
+
"name": "result",
|
|
13
|
+
"type": "{ major: number; minor: number; patch: number; prerelease: string | null; build: string | null }",
|
|
14
|
+
"description": "Parsed semver components. prerelease is null when no '-' suffix is present. build is null when no '+' suffix is present."
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"preconditions": [],
|
|
18
|
+
"postconditions": [
|
|
19
|
+
{
|
|
20
|
+
"description": "result.major >= 0 and is the integer parsed from the MAJOR field."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"description": "result.minor >= 0 and is the integer parsed from the MINOR field."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"description": "result.patch >= 0 and is the integer parsed from the PATCH field (before any pre-release or build suffix)."
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"invariants": [],
|
|
30
|
+
"effects": [],
|
|
31
|
+
"level": "L0",
|
|
32
|
+
"behavior": "Parse a semver version string into its numeric components (major, minor, patch) and optional string suffixes (prerelease, build). Parsing stops at the first '-' for prerelease and the first '+' for build metadata. No semantic validation of prerelease or build identifiers is performed -- they are captured as raw strings. Throws SyntaxError for strings that do not match the MAJOR.MINOR.PATCH structure.",
|
|
33
|
+
"guarantees": [
|
|
34
|
+
{
|
|
35
|
+
"id": "pure",
|
|
36
|
+
"description": "Referentially transparent; no side effects."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "numeric-components",
|
|
40
|
+
"description": "major, minor, patch are non-negative integers parsed from the first three dot-separated fields."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "prerelease-captured",
|
|
44
|
+
"description": "prerelease is the raw string after the first '-' and before any '+'; null if absent."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "build-captured",
|
|
48
|
+
"description": "build is the raw string after the first '+'; null if absent."
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"errorConditions": [
|
|
52
|
+
{
|
|
53
|
+
"description": "Input does not contain exactly two dots in the MAJOR.MINOR.PATCH prefix (e.g. '1.2' or '1.2.3.4').",
|
|
54
|
+
"errorType": "SyntaxError"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"description": "MAJOR, MINOR, or PATCH component is not a non-negative integer.",
|
|
58
|
+
"errorType": "SyntaxError"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"description": "Input is an empty string.",
|
|
62
|
+
"errorType": "SyntaxError"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"nonFunctional": {
|
|
66
|
+
"time": "O(n) where n is input.length",
|
|
67
|
+
"space": "O(n) for substrings",
|
|
68
|
+
"purity": "pure",
|
|
69
|
+
"threadSafety": "safe"
|
|
70
|
+
},
|
|
71
|
+
"propertyTests": [
|
|
72
|
+
{
|
|
73
|
+
"id": "semver-simple",
|
|
74
|
+
"description": "parseSemver('1.2.3') returns { major:1, minor:2, patch:3, prerelease:null, build:null }"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "semver-prerelease",
|
|
78
|
+
"description": "parseSemver('1.0.0-alpha.1') returns prerelease 'alpha.1'"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "semver-build",
|
|
82
|
+
"description": "parseSemver('1.0.0+build.42') returns build 'build.42'"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "semver-prerelease-and-build",
|
|
86
|
+
"description": "parseSemver('1.2.3-beta+exp.sha.5114f85') captures both prerelease and build"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "semver-zeros",
|
|
90
|
+
"description": "parseSemver('0.0.0') returns { major:0, minor:0, patch:0, prerelease:null, build:null }"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "semver-large-numbers",
|
|
94
|
+
"description": "parseSemver('100.200.300') returns major=100, minor=200, patch=300"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "semver-invalid-no-dots",
|
|
98
|
+
"description": "parseSemver('1') throws SyntaxError"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "semver-invalid-non-numeric",
|
|
102
|
+
"description": "parseSemver('a.b.c') throws SyntaxError"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "semver-empty",
|
|
106
|
+
"description": "parseSemver('') throws SyntaxError"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @decision DEC-SEEDS-SIGNEDINT-001: signed-integer extends integer with optional leading minus.
|
|
3
|
+
// Status: implemented (WI-006)
|
|
4
|
+
// Rationale: A common extension of unsigned integer parsing. Demonstrates contract refinement:
|
|
5
|
+
// signed-integer has a strictly wider input domain than integer but same output type contract.
|
|
6
|
+
|
|
7
|
+
export function signedInteger(input: string, position: number): readonly [number, number] {
|
|
8
|
+
if (position < 0) {
|
|
9
|
+
throw new RangeError(`Position ${position} is negative`);
|
|
10
|
+
}
|
|
11
|
+
if (position >= input.length) {
|
|
12
|
+
throw new SyntaxError(`Expected digit or '-' at position ${position} but reached end of input`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let pos = position;
|
|
16
|
+
let sign = 1;
|
|
17
|
+
|
|
18
|
+
if (input[pos] === "-") {
|
|
19
|
+
sign = -1;
|
|
20
|
+
pos++;
|
|
21
|
+
if (pos >= input.length) {
|
|
22
|
+
throw new SyntaxError(`Expected digit after '-' at position ${pos} but reached end of input`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const c = input[pos] as string;
|
|
27
|
+
if (c < "0" || c > "9") {
|
|
28
|
+
throw new SyntaxError(`Expected digit at position ${pos} but found ${JSON.stringify(c)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let value = 0;
|
|
32
|
+
while (pos < input.length) {
|
|
33
|
+
const ch = input[pos] as string;
|
|
34
|
+
if (ch < "0" || ch > "9") break;
|
|
35
|
+
value = value * 10 + (ch.charCodeAt(0) - 48);
|
|
36
|
+
pos++;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [sign * value, pos] as const;
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Property tests for the signed-integer block.
|
|
3
|
+
// These tests exercise the contract declared in ../spec.yak against the
|
|
4
|
+
// implementation in ../impl.ts.
|
|
5
|
+
//
|
|
6
|
+
// The definitive property-test corpus lives in the parent seed package's
|
|
7
|
+
// seed.test.ts (Suite 4: "property-test corpora"). This file satisfies the
|
|
8
|
+
// L0 proof/manifest.json "property_tests" artifact requirement and makes
|
|
9
|
+
// the test IDs declared in spec.yak traceable to this directory.
|
|
10
|
+
//
|
|
11
|
+
// Test IDs declared in spec.yak:
|
|
12
|
+
// signed-int-positive
|
|
13
|
+
// signed-int-negative
|
|
14
|
+
// signed-int-bare-minus
|
|
15
|
+
// signed-int-no-digit
|
|
16
|
+
// signed-int-eof
|
|
17
|
+
// signed-int-negative-pos
|
|
18
|
+
|
|
19
|
+
// Re-export the implementation so runners importing this artifact directly
|
|
20
|
+
// can access the block function.
|
|
21
|
+
export * from "../impl.js";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signed-integer",
|
|
3
|
+
"inputs": [
|
|
4
|
+
{
|
|
5
|
+
"name": "input",
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The full input string."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "position",
|
|
11
|
+
"type": "number",
|
|
12
|
+
"description": "Zero-based start position."
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"outputs": [
|
|
16
|
+
{
|
|
17
|
+
"name": "result",
|
|
18
|
+
"type": "readonly [number, number]",
|
|
19
|
+
"description": "[parsedValue, newPosition] where parsedValue may be negative."
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"preconditions": [],
|
|
23
|
+
"postconditions": [],
|
|
24
|
+
"invariants": [],
|
|
25
|
+
"effects": [],
|
|
26
|
+
"level": "L0",
|
|
27
|
+
"behavior": "Parse an optional '-' sign followed by one or more decimal digits. Returns [value, newPosition]. A bare '-' with no digits throws SyntaxError. Negative zero is represented as 0.",
|
|
28
|
+
"guarantees": [
|
|
29
|
+
{
|
|
30
|
+
"id": "pure",
|
|
31
|
+
"description": "Referentially transparent; no side effects."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "greedy",
|
|
35
|
+
"description": "Consumes as many digits as possible after the optional sign."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"errorConditions": [
|
|
39
|
+
{
|
|
40
|
+
"description": "No digit follows an optional '-' sign.",
|
|
41
|
+
"errorType": "SyntaxError"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"description": "No digit or '-' found at position.",
|
|
45
|
+
"errorType": "SyntaxError"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"description": "position < 0.",
|
|
49
|
+
"errorType": "RangeError"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"nonFunctional": {
|
|
53
|
+
"time": "O(n)",
|
|
54
|
+
"space": "O(1)",
|
|
55
|
+
"purity": "pure",
|
|
56
|
+
"threadSafety": "safe"
|
|
57
|
+
},
|
|
58
|
+
"propertyTests": [
|
|
59
|
+
{
|
|
60
|
+
"id": "signed-int-positive",
|
|
61
|
+
"description": "signedInteger('42', 0) returns [42, 2]"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "signed-int-negative",
|
|
65
|
+
"description": "signedInteger('-7', 0) returns [-7, 2]"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "signed-int-bare-minus",
|
|
69
|
+
"description": "signedInteger('-x', 0) throws SyntaxError"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "signed-int-no-digit",
|
|
73
|
+
"description": "signedInteger('abc', 0) throws SyntaxError"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "signed-int-eof",
|
|
77
|
+
"description": "signedInteger('', 0) throws SyntaxError"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "signed-int-negative-pos",
|
|
81
|
+
"description": "signedInteger('1', -1) throws RangeError"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @decision DEC-SEEDS-STRFROMPOS-001: substring extraction at a position is a reusable primitive.
|
|
3
|
+
// Status: implemented (WI-006)
|
|
4
|
+
// Rationale: Multiple blocks need to extract substrings starting at a position. Centralising
|
|
5
|
+
// bounds checking avoids each block duplicating the same guard logic.
|
|
6
|
+
|
|
7
|
+
export function stringFromPosition(input: string, start: number, end: number): string {
|
|
8
|
+
if (start < 0 || end < 0) {
|
|
9
|
+
throw new RangeError(`Negative index: start=${start}, end=${end}`);
|
|
10
|
+
}
|
|
11
|
+
if (start > end) {
|
|
12
|
+
throw new RangeError(`start ${start} > end ${end}`);
|
|
13
|
+
}
|
|
14
|
+
if (end > input.length) {
|
|
15
|
+
throw new RangeError(`end ${end} exceeds input length ${input.length}`);
|
|
16
|
+
}
|
|
17
|
+
return input.slice(start, end);
|
|
18
|
+
}
|