eyeling 1.19.4 → 1.19.6
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/HANDBOOK.md +48 -89
- package/examples/deck/extra.md +169 -0
- package/examples/extra/collatz-1000.js +138 -0
- package/examples/extra/control-system.js +68 -0
- package/examples/extra/deep-taxonomy-100000.js +95 -0
- package/examples/extra/delfour.js +110 -0
- package/examples/extra/euler-identity.js +41 -0
- package/examples/extra/fibonacci.js +81 -0
- package/examples/extra/goldbach-1000.js +112 -0
- package/examples/extra/gps.js +274 -0
- package/examples/extra/kaprekar-6174.js +112 -0
- package/examples/extra/matrix-mechanics.js +69 -0
- package/examples/extra/odrl-dpv-ehds-risk-ranked.js +255 -0
- package/examples/extra/output/collatz-1000.txt +18 -0
- package/examples/extra/output/control-system.txt +14 -0
- package/examples/extra/output/deep-taxonomy-100000.txt +15 -0
- package/examples/extra/output/delfour.txt +20 -0
- package/examples/extra/output/euler-identity.txt +12 -0
- package/examples/extra/output/fibonacci.txt +21 -0
- package/examples/extra/output/goldbach-1000.txt +17 -0
- package/examples/extra/output/gps.txt +33 -0
- package/examples/extra/output/kaprekar-6174.txt +17 -0
- package/examples/extra/output/matrix-mechanics.txt +14 -0
- package/examples/extra/output/odrl-dpv-ehds-risk-ranked.txt +48 -0
- package/examples/extra/output/path-discovery.txt +28 -0
- package/examples/extra/output/pn-junction-tunneling.txt +15 -0
- package/examples/extra/output/polynomial.txt +20 -0
- package/examples/extra/output/sudoku.txt +47 -0
- package/examples/extra/output/transistor-switch.txt +16 -0
- package/examples/extra/path-discovery.js +45114 -0
- package/examples/extra/pn-junction-tunneling.js +69 -0
- package/examples/extra/polynomial.js +181 -0
- package/examples/extra/sudoku.js +330 -0
- package/examples/extra/transistor-switch.js +93 -0
- package/examples/fibonacci.n3 +2 -0
- package/examples/output/fibonacci.n3 +1 -0
- package/eyeling.js +49 -45
- package/lib/engine.js +49 -45
- package/package.json +3 -2
- package/test/extra.test.js +100 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Standalone retail-insight envelope demo with fixed policy, payload, and catalog data.
|
|
6
|
+
* The checks verify integrity, authorization, minimization, and the recommendation outcome.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
|
|
11
|
+
const SECRET = 'neutral-insight-demo-shared-secret';
|
|
12
|
+
const PHONE_CREATED_AT = '2025-10-05T20:33:48.907163+00:00';
|
|
13
|
+
const PHONE_EXPIRES_AT = '2025-10-05T22:33:48.907185+00:00';
|
|
14
|
+
const SCANNER_AUTH_AT = '2025-10-05T20:35:48.907163+00:00';
|
|
15
|
+
|
|
16
|
+
// Fixed product catalog used by the recommendation step.
|
|
17
|
+
const CATALOG = [
|
|
18
|
+
{ id: 'prod:BIS_001', name: 'Classic Tea Biscuits', sugarTenths: 120 },
|
|
19
|
+
{ id: 'prod:BIS_101', name: 'Low-Sugar Tea Biscuits', sugarTenths: 30 },
|
|
20
|
+
{ id: 'prod:CHOC_050', name: 'Milk Chocolate Bar', sugarTenths: 150 },
|
|
21
|
+
{ id: 'prod:CHOC_150', name: '85% Dark Chocolate', sugarTenths: 60 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function sha256Hex(text) {
|
|
25
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hmacSha256Hex(secret, text) {
|
|
29
|
+
return crypto.createHmac('sha256', secret).update(text).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runDemo() {
|
|
33
|
+
const insightId = 'https://example.org/insight/delfour';
|
|
34
|
+
const insightJson = `{"createdAt":"${PHONE_CREATED_AT}","expiresAt":"${PHONE_EXPIRES_AT}","id":"${insightId}","metric":"sugar_g_per_serving","retailer":"Delfour","scopeDevice":"self-scanner","scopeEvent":"pick_up_scanner","suggestionPolicy":"lower_metric_first_higher_price_ok","threshold":10.0,"type":"ins:Insight"}`;
|
|
35
|
+
const policyJson = `{"duty":{"action":"odrl:delete","constraint":{"leftOperand":"odrl:dateTime","operator":"odrl:eq","rightOperand":"${PHONE_EXPIRES_AT}"}},"permission":{"action":"odrl:use","constraint":{"leftOperand":"odrl:purpose","operator":"odrl:eq","rightOperand":"shopping_assist"},"target":"${insightId}"},"profile":"Delfour-Insight-Policy","prohibition":{"action":"odrl:distribute","constraint":{"leftOperand":"odrl:purpose","operator":"odrl:eq","rightOperand":"marketing"},"target":"${insightId}"},"type":"odrl:Policy"}`;
|
|
36
|
+
const envelopeJson = `{"insight":${insightJson},"policy":${policyJson}}`;
|
|
37
|
+
|
|
38
|
+
const payloadHashHex = sha256Hex(envelopeJson);
|
|
39
|
+
const hmacHex = hmacSha256Hex(SECRET, envelopeJson);
|
|
40
|
+
const checkHash = sha256Hex(envelopeJson);
|
|
41
|
+
const checkHmac = hmacSha256Hex(SECRET, envelopeJson);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
insightJson,
|
|
45
|
+
policyJson,
|
|
46
|
+
payloadHashHex,
|
|
47
|
+
hmacHex,
|
|
48
|
+
signatureVerified: checkHmac === hmacHex,
|
|
49
|
+
payloadHashMatches: checkHash === payloadHashHex,
|
|
50
|
+
minimizationOk: !insightJson.includes('Diabetes') && !insightJson.includes('medical'),
|
|
51
|
+
authorizationAllowed: SCANNER_AUTH_AT < PHONE_EXPIRES_AT && policyJson.includes('shopping_assist'),
|
|
52
|
+
dutyTimingOk: true,
|
|
53
|
+
scanned: CATALOG[0],
|
|
54
|
+
alternative: CATALOG[1],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Report the policy outcome, recommendation, and integrity checks.
|
|
59
|
+
// Build the final ARC-style report and exit non-zero if a check fails.
|
|
60
|
+
function main() {
|
|
61
|
+
const s = runDemo();
|
|
62
|
+
const bannerFlagsHighSugar = s.scanned.sugarTenths >= 100;
|
|
63
|
+
const alternativeIsLower = s.alternative.sugarTenths < s.scanned.sugarTenths;
|
|
64
|
+
const marketingProhibited = s.policyJson.includes('marketing') && s.policyJson.includes('odrl:distribute');
|
|
65
|
+
const scopeComplete =
|
|
66
|
+
s.insightJson.includes('scopeDevice') &&
|
|
67
|
+
s.insightJson.includes('scopeEvent') &&
|
|
68
|
+
s.insightJson.includes('expiresAt');
|
|
69
|
+
const ok =
|
|
70
|
+
s.signatureVerified &&
|
|
71
|
+
s.payloadHashMatches &&
|
|
72
|
+
s.minimizationOk &&
|
|
73
|
+
scopeComplete &&
|
|
74
|
+
s.authorizationAllowed &&
|
|
75
|
+
bannerFlagsHighSugar &&
|
|
76
|
+
alternativeIsLower &&
|
|
77
|
+
s.dutyTimingOk &&
|
|
78
|
+
marketingProhibited;
|
|
79
|
+
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push('=== Answer ===');
|
|
82
|
+
lines.push(
|
|
83
|
+
'The scanner is allowed to use a neutral shopping insight and recommends Low-Sugar Tea Biscuits instead of Classic Tea Biscuits.',
|
|
84
|
+
);
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('=== Reason Why ===');
|
|
87
|
+
lines.push(
|
|
88
|
+
'The phone desensitizes a diabetes-related household condition into a scoped low-sugar need, wraps it in an expiring Insight+Policy envelope, and signs it.',
|
|
89
|
+
);
|
|
90
|
+
lines.push(`scanned product : ${s.scanned.name}`);
|
|
91
|
+
lines.push(`suggested alternative: ${s.alternative.name}`);
|
|
92
|
+
lines.push(`payload SHA-256 : ${s.payloadHashHex}`);
|
|
93
|
+
lines.push(`HMAC-SHA256 : ${s.hmacHex}`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('=== Check ===');
|
|
96
|
+
lines.push(`signature verifies : ${s.signatureVerified ? 'yes' : 'no'}`);
|
|
97
|
+
lines.push(`payload hash matches : ${s.payloadHashMatches ? 'yes' : 'no'}`);
|
|
98
|
+
lines.push(`minimization strips sensitive terms: ${s.minimizationOk ? 'yes' : 'no'}`);
|
|
99
|
+
lines.push(`scope complete : ${scopeComplete ? 'yes' : 'no'}`);
|
|
100
|
+
lines.push(`authorization allowed : ${s.authorizationAllowed ? 'yes' : 'no'}`);
|
|
101
|
+
lines.push(`high-sugar banner : ${bannerFlagsHighSugar ? 'yes' : 'no'}`);
|
|
102
|
+
lines.push(`alternative lowers sugar : ${alternativeIsLower ? 'yes' : 'no'}`);
|
|
103
|
+
lines.push(`duty timing consistent : ${s.dutyTimingOk ? 'yes' : 'no'}`);
|
|
104
|
+
lines.push(`marketing prohibited : ${marketingProhibited ? 'yes' : 'no'}`);
|
|
105
|
+
|
|
106
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
107
|
+
process.exit(ok ? 0 : 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal exact-arithmetic witness for Euler's identity.
|
|
6
|
+
* Using an exact representation keeps the check section purely logical instead of approximate.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function add(a, b) {
|
|
10
|
+
return { re: a.re + b.re, im: a.im + b.im };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Build the exact witness and print the three ARC sections.
|
|
14
|
+
function main() {
|
|
15
|
+
const expIpi = { re: -1, im: 0 };
|
|
16
|
+
const one = { re: 1, im: 0 };
|
|
17
|
+
const result = add(expIpi, one);
|
|
18
|
+
const modulusSq = expIpi.re * expIpi.re + expIpi.im * expIpi.im;
|
|
19
|
+
const identityOk = result.re === 0 && result.im === 0;
|
|
20
|
+
const unitCircleOk = modulusSq === 1;
|
|
21
|
+
const ok = identityOk && unitCircleOk;
|
|
22
|
+
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push('=== Answer ===');
|
|
25
|
+
lines.push("Euler's identity holds exactly in this exact-arithmetic model: exp(i*pi) + 1 = 0.");
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push('=== Reason Why ===');
|
|
28
|
+
lines.push('exp(i*pi) is represented as (-1, 0) and adding (1, 0) gives the exact zero complex number.');
|
|
29
|
+
lines.push(`exp(i*pi) : (${expIpi.re}, ${expIpi.im})`);
|
|
30
|
+
lines.push(`exp(i*pi)+1 : (${result.re}, ${result.im})`);
|
|
31
|
+
lines.push(`|exp(i*pi)|^2: ${modulusSq}`);
|
|
32
|
+
lines.push('');
|
|
33
|
+
lines.push('=== Check ===');
|
|
34
|
+
lines.push(`identity exact: ${identityOk ? 'yes' : 'no'}`);
|
|
35
|
+
lines.push(`unit circle : ${unitCircleOk ? 'yes' : 'no'}`);
|
|
36
|
+
|
|
37
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
38
|
+
process.exit(ok ? 0 : 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
main();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Exact Fibonacci benchmark using BigInt throughout.
|
|
6
|
+
* The main path is iterative, while fast doubling is used as an independent cross-check.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function fibonacciIterative(n) {
|
|
10
|
+
let a = 0n;
|
|
11
|
+
let b = 1n;
|
|
12
|
+
for (let i = 0; i < n; i += 1) {
|
|
13
|
+
const t = a + b;
|
|
14
|
+
a = b;
|
|
15
|
+
b = t;
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Independent cross-check based on the fast-doubling identities.
|
|
21
|
+
function fastDoubling(n) {
|
|
22
|
+
if (n === 0) return [0n, 1n];
|
|
23
|
+
const [a, b] = fastDoubling(Math.floor(n / 2));
|
|
24
|
+
const c = a * (2n * b - a);
|
|
25
|
+
const d = a * a + b * b;
|
|
26
|
+
if (n % 2 === 0) return [c, d];
|
|
27
|
+
return [d, c + d];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Compute the requested values, then verify a few known identities and size facts.
|
|
31
|
+
function main() {
|
|
32
|
+
const targets = [0, 1, 10, 100, 1000, 10000];
|
|
33
|
+
const vals = targets.map((n) => fibonacciIterative(n));
|
|
34
|
+
|
|
35
|
+
const f10Ok = vals[2] === 55n;
|
|
36
|
+
const f1000Str = vals[4].toString();
|
|
37
|
+
const f10000Str = vals[5].toString();
|
|
38
|
+
const f1000Digits = f1000Str.length;
|
|
39
|
+
const f10000Digits = f10000Str.length;
|
|
40
|
+
|
|
41
|
+
let fastOk = true;
|
|
42
|
+
for (let i = 0; i < targets.length; i += 1) {
|
|
43
|
+
const [a] = fastDoubling(targets[i]);
|
|
44
|
+
if (a !== vals[i]) fastOk = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const f99 = fibonacciIterative(99);
|
|
48
|
+
const f100 = fibonacciIterative(100);
|
|
49
|
+
const f101 = fibonacciIterative(101);
|
|
50
|
+
const cassiniOk = f101 * f99 === f100 * f100 + 1n;
|
|
51
|
+
const f10000Last3Ok = f10000Str.endsWith('875');
|
|
52
|
+
|
|
53
|
+
const ok = f10Ok && fastOk && cassiniOk && f1000Digits === 209 && f10000Digits === 2090 && f10000Last3Ok;
|
|
54
|
+
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push('=== Answer ===');
|
|
57
|
+
lines.push('The requested Fibonacci values are computed exactly, up to F(10000).');
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push('=== Reason Why ===');
|
|
60
|
+
lines.push(
|
|
61
|
+
'The main computation uses the defining recurrence F(n+1)=F(n)+F(n-1), and the results are cross-checked with fast doubling.',
|
|
62
|
+
);
|
|
63
|
+
for (let i = 0; i < targets.length; i += 1) {
|
|
64
|
+
lines.push(`value[${i}] : F(${targets[i]}) = ${vals[i].toString()}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(`digits in F(1000) : ${f1000Digits}`);
|
|
67
|
+
lines.push(`digits in F(10000) : ${f10000Digits}`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push('=== Check ===');
|
|
70
|
+
lines.push(`F(10) = 55 : ${f10Ok ? 'yes' : 'no'}`);
|
|
71
|
+
lines.push(`fast doubling agrees : ${fastOk ? 'yes' : 'no'}`);
|
|
72
|
+
lines.push(`Cassini at n=100 : ${cassiniOk ? 'yes' : 'no'}`);
|
|
73
|
+
lines.push(`F(1000) has 209 digits: ${f1000Digits === 209 ? 'yes' : 'no'}`);
|
|
74
|
+
lines.push(`F(10000) has 2090 digits: ${f10000Digits === 2090 ? 'yes' : 'no'}`);
|
|
75
|
+
lines.push(`F(10000) ends in 875 : ${f10000Last3Ok ? 'yes' : 'no'}`);
|
|
76
|
+
|
|
77
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
78
|
+
process.exit(ok ? 0 : 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Goldbach sweep for all even targets up to 1000.
|
|
6
|
+
* A sieve provides the prime table; the report summarizes sparse, rich, and balanced decompositions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const LIMIT = 1000;
|
|
10
|
+
|
|
11
|
+
function sieve(limit) {
|
|
12
|
+
const prime = new Array(limit + 1).fill(true);
|
|
13
|
+
prime[0] = false;
|
|
14
|
+
prime[1] = false;
|
|
15
|
+
for (let p = 2; p * p <= limit; p += 1) {
|
|
16
|
+
if (!prime[p]) continue;
|
|
17
|
+
for (let m = p * p; m <= limit; m += p) prime[m] = false;
|
|
18
|
+
}
|
|
19
|
+
return prime;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectPrimes(prime, limit) {
|
|
23
|
+
const out = [];
|
|
24
|
+
for (let i = 2; i <= limit; i += 1) if (prime[i]) out.push(i);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function goldbachPairs(target, primes, prime) {
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const p of primes) {
|
|
31
|
+
if (p > Math.floor(target / 2)) break;
|
|
32
|
+
const q = target - p;
|
|
33
|
+
if (prime[q]) count += 1;
|
|
34
|
+
}
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Sweep the entire range and summarize the decomposition counts.
|
|
39
|
+
function main() {
|
|
40
|
+
const prime = sieve(LIMIT);
|
|
41
|
+
const primes = collectPrimes(prime, LIMIT);
|
|
42
|
+
|
|
43
|
+
let totalDecompositions = 0;
|
|
44
|
+
let fewest = Number.MAX_SAFE_INTEGER;
|
|
45
|
+
let most = 0;
|
|
46
|
+
let richestTarget = 4;
|
|
47
|
+
const hardest = [];
|
|
48
|
+
let allRepresented = true;
|
|
49
|
+
|
|
50
|
+
for (let target = 4; target <= LIMIT; target += 2) {
|
|
51
|
+
const count = goldbachPairs(target, primes, prime);
|
|
52
|
+
totalDecompositions += count;
|
|
53
|
+
if (count === 0) allRepresented = false;
|
|
54
|
+
if (count < fewest) {
|
|
55
|
+
fewest = count;
|
|
56
|
+
hardest.length = 0;
|
|
57
|
+
hardest.push(target);
|
|
58
|
+
} else if (count === fewest) {
|
|
59
|
+
hardest.push(target);
|
|
60
|
+
}
|
|
61
|
+
if (count > most) {
|
|
62
|
+
most = count;
|
|
63
|
+
richestTarget = target;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let bestA = 0;
|
|
68
|
+
let bestB = 0;
|
|
69
|
+
let bestDiff = Number.MAX_SAFE_INTEGER;
|
|
70
|
+
for (const p of primes) {
|
|
71
|
+
if (p > LIMIT / 2) break;
|
|
72
|
+
const q = LIMIT - p;
|
|
73
|
+
if (prime[q] && q >= p) {
|
|
74
|
+
const diff = q - p;
|
|
75
|
+
if (diff < bestDiff) {
|
|
76
|
+
bestDiff = diff;
|
|
77
|
+
bestA = p;
|
|
78
|
+
bestB = q;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const primeCountOk = primes.length === 168;
|
|
84
|
+
const balancedPairOk = bestA + bestB === LIMIT && prime[bestA] && prime[bestB];
|
|
85
|
+
const ok = allRepresented && primeCountOk && balancedPairOk;
|
|
86
|
+
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push('=== Answer ===');
|
|
89
|
+
lines.push('Every even integer from 4 through 1000 has at least one Goldbach decomposition in the tested range.');
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('=== Reason Why ===');
|
|
92
|
+
lines.push(
|
|
93
|
+
'The program builds a prime table, enumerates unordered pairs p+q=n for each even target, and summarizes sparse and rich cases.',
|
|
94
|
+
);
|
|
95
|
+
lines.push(`even targets checked : ${(LIMIT - 4) / 2 + 1}`);
|
|
96
|
+
lines.push(`total decompositions : ${totalDecompositions}`);
|
|
97
|
+
lines.push(`fewest decompositions: ${fewest}`);
|
|
98
|
+
lines.push(`hardest targets : ${hardest.join(', ')}`);
|
|
99
|
+
lines.push(`most decompositions : ${most}`);
|
|
100
|
+
lines.push(`richest target : ${richestTarget}`);
|
|
101
|
+
lines.push(`balanced pair(1000) : ${bestA} + ${bestB}`);
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('=== Check ===');
|
|
104
|
+
lines.push(`all represented : ${allRepresented ? 'yes' : 'no'}`);
|
|
105
|
+
lines.push(`prime count known : ${primeCountOk ? 'yes' : 'no'}`);
|
|
106
|
+
lines.push(`balanced pair valid : ${balancedPairOk ? 'yes' : 'no'}`);
|
|
107
|
+
|
|
108
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
109
|
+
process.exit(ok ? 0 : 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main();
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Small route-planning case with fixed legs, costs, and trust-style metrics.
|
|
6
|
+
* Routes are composed forward from the primitive descriptions and then filtered by the goal constraints.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const CITY = {
|
|
10
|
+
GENT: 0,
|
|
11
|
+
BRUGGE: 1,
|
|
12
|
+
KORTRIJK: 2,
|
|
13
|
+
OOSTENDE: 3,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ACTION = {
|
|
17
|
+
DRIVE_GENT_BRUGGE: 0,
|
|
18
|
+
DRIVE_GENT_KORTRIJK: 1,
|
|
19
|
+
DRIVE_KORTRIJK_BRUGGE: 2,
|
|
20
|
+
DRIVE_BRUGGE_OOSTENDE: 3,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Primitive route descriptions: each one is a direct edge with its accumulated metrics.
|
|
24
|
+
const DESCRIPTIONS = [
|
|
25
|
+
{
|
|
26
|
+
from: CITY.GENT,
|
|
27
|
+
to: CITY.BRUGGE,
|
|
28
|
+
action: ACTION.DRIVE_GENT_BRUGGE,
|
|
29
|
+
durationSeconds: 1500,
|
|
30
|
+
costMilli: 6,
|
|
31
|
+
beliefPpm: 960000,
|
|
32
|
+
comfortPpm: 990000,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
from: CITY.GENT,
|
|
36
|
+
to: CITY.KORTRIJK,
|
|
37
|
+
action: ACTION.DRIVE_GENT_KORTRIJK,
|
|
38
|
+
durationSeconds: 1600,
|
|
39
|
+
costMilli: 7,
|
|
40
|
+
beliefPpm: 960000,
|
|
41
|
+
comfortPpm: 990000,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
from: CITY.KORTRIJK,
|
|
45
|
+
to: CITY.BRUGGE,
|
|
46
|
+
action: ACTION.DRIVE_KORTRIJK_BRUGGE,
|
|
47
|
+
durationSeconds: 1600,
|
|
48
|
+
costMilli: 7,
|
|
49
|
+
beliefPpm: 960000,
|
|
50
|
+
comfortPpm: 990000,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
from: CITY.BRUGGE,
|
|
54
|
+
to: CITY.OOSTENDE,
|
|
55
|
+
action: ACTION.DRIVE_BRUGGE_OOSTENDE,
|
|
56
|
+
durationSeconds: 900,
|
|
57
|
+
costMilli: 4,
|
|
58
|
+
beliefPpm: 980000,
|
|
59
|
+
comfortPpm: 1000000,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const GOAL = {
|
|
64
|
+
maxDurationSeconds: 5000,
|
|
65
|
+
maxCostMilli: 5000,
|
|
66
|
+
minBeliefPpm: 200000,
|
|
67
|
+
minComfortPpm: 400000,
|
|
68
|
+
maxStages: 1,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function multiplyPpm(left, right) {
|
|
72
|
+
return Math.floor((left * right) / 1000000);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function actionName(action) {
|
|
76
|
+
switch (action) {
|
|
77
|
+
case ACTION.DRIVE_GENT_BRUGGE:
|
|
78
|
+
return 'drive_gent_brugge';
|
|
79
|
+
case ACTION.DRIVE_GENT_KORTRIJK:
|
|
80
|
+
return 'drive_gent_kortrijk';
|
|
81
|
+
case ACTION.DRIVE_KORTRIJK_BRUGGE:
|
|
82
|
+
return 'drive_kortrijk_brugge';
|
|
83
|
+
case ACTION.DRIVE_BRUGGE_OOSTENDE:
|
|
84
|
+
return 'drive_brugge_oostende';
|
|
85
|
+
default:
|
|
86
|
+
return '?';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stageCount(route) {
|
|
91
|
+
return route.actions.length > 0 ? 1 : 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function routeSatisfies(route, c) {
|
|
95
|
+
return (
|
|
96
|
+
route.durationSeconds <= c.maxDurationSeconds &&
|
|
97
|
+
route.costMilli <= c.maxCostMilli &&
|
|
98
|
+
route.beliefPpm >= c.minBeliefPpm &&
|
|
99
|
+
route.comfortPpm >= c.minComfortPpm &&
|
|
100
|
+
stageCount(route) <= c.maxStages
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function routeEquals(a, b) {
|
|
105
|
+
return (
|
|
106
|
+
a.from === b.from &&
|
|
107
|
+
a.to === b.to &&
|
|
108
|
+
a.actions.length === b.actions.length &&
|
|
109
|
+
a.durationSeconds === b.durationSeconds &&
|
|
110
|
+
a.costMilli === b.costMilli &&
|
|
111
|
+
a.beliefPpm === b.beliefPpm &&
|
|
112
|
+
a.comfortPpm === b.comfortPpm &&
|
|
113
|
+
a.actions.every((action, i) => action === b.actions[i])
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compareRoutes(left, right) {
|
|
118
|
+
if (left.actions.length < right.actions.length) return -1;
|
|
119
|
+
if (left.actions.length > right.actions.length) return 1;
|
|
120
|
+
for (let i = 0; i < left.actions.length && i < right.actions.length; i += 1) {
|
|
121
|
+
if (left.actions[i] < right.actions[i]) return -1;
|
|
122
|
+
if (left.actions[i] > right.actions[i]) return 1;
|
|
123
|
+
}
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function routeMatchesDescriptions(route) {
|
|
128
|
+
let current = route.from;
|
|
129
|
+
let durationSeconds = 0;
|
|
130
|
+
let costMilli = 0;
|
|
131
|
+
let beliefPpm = 1000000;
|
|
132
|
+
let comfortPpm = 1000000;
|
|
133
|
+
|
|
134
|
+
for (const action of route.actions) {
|
|
135
|
+
let found = false;
|
|
136
|
+
for (const d of DESCRIPTIONS) {
|
|
137
|
+
if (d.from === current && d.action === action) {
|
|
138
|
+
current = d.to;
|
|
139
|
+
durationSeconds += d.durationSeconds;
|
|
140
|
+
costMilli += d.costMilli;
|
|
141
|
+
beliefPpm = multiplyPpm(beliefPpm, d.beliefPpm);
|
|
142
|
+
comfortPpm = multiplyPpm(comfortPpm, d.comfortPpm);
|
|
143
|
+
found = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!found) return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
current === route.to &&
|
|
152
|
+
durationSeconds === route.durationSeconds &&
|
|
153
|
+
costMilli === route.costMilli &&
|
|
154
|
+
beliefPpm === route.beliefPpm &&
|
|
155
|
+
comfortPpm === route.comfortPpm
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Compose longer routes from the primitive descriptions until no new route appears.
|
|
160
|
+
function inferGoalRoutes() {
|
|
161
|
+
const known = [];
|
|
162
|
+
let agendaHead = 0;
|
|
163
|
+
|
|
164
|
+
for (const d of DESCRIPTIONS) {
|
|
165
|
+
known.push({
|
|
166
|
+
from: d.from,
|
|
167
|
+
to: d.to,
|
|
168
|
+
actions: [d.action],
|
|
169
|
+
durationSeconds: d.durationSeconds,
|
|
170
|
+
costMilli: d.costMilli,
|
|
171
|
+
beliefPpm: d.beliefPpm,
|
|
172
|
+
comfortPpm: d.comfortPpm,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while (agendaHead < known.length) {
|
|
177
|
+
const rest = known[agendaHead++];
|
|
178
|
+
for (const d of DESCRIPTIONS) {
|
|
179
|
+
if (d.to === rest.from) {
|
|
180
|
+
const route = {
|
|
181
|
+
from: d.from,
|
|
182
|
+
to: rest.to,
|
|
183
|
+
actions: [d.action, ...rest.actions],
|
|
184
|
+
durationSeconds: d.durationSeconds + rest.durationSeconds,
|
|
185
|
+
costMilli: d.costMilli + rest.costMilli,
|
|
186
|
+
beliefPpm: multiplyPpm(d.beliefPpm, rest.beliefPpm),
|
|
187
|
+
comfortPpm: multiplyPpm(d.comfortPpm, rest.comfortPpm),
|
|
188
|
+
};
|
|
189
|
+
let duplicate = false;
|
|
190
|
+
for (const knownRoute of known) {
|
|
191
|
+
if (routeEquals(knownRoute, route)) {
|
|
192
|
+
duplicate = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!duplicate) known.push(route);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return known
|
|
202
|
+
.filter((route) => route.from === CITY.GENT && route.to === CITY.OOSTENDE && routeSatisfies(route, GOAL))
|
|
203
|
+
.sort(compareRoutes);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatDecimal(value, scale, digits) {
|
|
207
|
+
const fractionalScale = 10 ** digits;
|
|
208
|
+
const scaled = value * fractionalScale;
|
|
209
|
+
const rounded = Math.floor((scaled + Math.floor(scale / 2)) / scale);
|
|
210
|
+
const whole = Math.floor(rounded / fractionalScale);
|
|
211
|
+
const fractional = rounded % fractionalScale;
|
|
212
|
+
return `${whole}.${String(fractional).padStart(digits, '0')}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function routeLines(index, route) {
|
|
216
|
+
const lines = [];
|
|
217
|
+
lines.push(`Route #${index}`);
|
|
218
|
+
lines.push(` Steps : ${route.actions.length}`);
|
|
219
|
+
lines.push(` Duration : ${route.durationSeconds} s (≤ ${GOAL.maxDurationSeconds})`);
|
|
220
|
+
lines.push(` Cost : ${formatDecimal(route.costMilli, 1000, 3)} (≤ ${formatDecimal(GOAL.maxCostMilli, 1000, 1)})`);
|
|
221
|
+
lines.push(
|
|
222
|
+
` Belief : ${formatDecimal(route.beliefPpm, 1000000, 3)} (≥ ${formatDecimal(GOAL.minBeliefPpm, 1000000, 1)})`,
|
|
223
|
+
);
|
|
224
|
+
lines.push(
|
|
225
|
+
` Comfort : ${formatDecimal(route.comfortPpm, 1000000, 3)} (≥ ${formatDecimal(GOAL.minComfortPpm, 1000000, 1)})`,
|
|
226
|
+
);
|
|
227
|
+
lines.push(` Stages : ${stageCount(route)} (≤ ${GOAL.maxStages})`);
|
|
228
|
+
for (let i = 0; i < route.actions.length; i += 1) {
|
|
229
|
+
lines.push(` ${i + 1}. ${actionName(route.actions[i])}`);
|
|
230
|
+
}
|
|
231
|
+
return lines;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Emit every route that satisfies the fixed Gent -> Oostende goal.
|
|
235
|
+
function main() {
|
|
236
|
+
const routes = inferGoalRoutes();
|
|
237
|
+
|
|
238
|
+
let allRoutesSatisfyConstraints = true;
|
|
239
|
+
let allRoutesHitGoalEndpoints = true;
|
|
240
|
+
let allMetricsRecompute = true;
|
|
241
|
+
for (const route of routes) {
|
|
242
|
+
allRoutesSatisfyConstraints &&= routeSatisfies(route, GOAL);
|
|
243
|
+
allRoutesHitGoalEndpoints &&= route.from === CITY.GENT && route.to === CITY.OOSTENDE;
|
|
244
|
+
allMetricsRecompute &&= routeMatchesDescriptions(route);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const ok = routes.length === 2 && allRoutesSatisfyConstraints && allRoutesHitGoalEndpoints && allMetricsRecompute;
|
|
248
|
+
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push('=== Answer ===');
|
|
251
|
+
lines.push('The GPS case finds all goal routes from Gent to Oostende that satisfy the route constraints.');
|
|
252
|
+
lines.push('case : gps');
|
|
253
|
+
lines.push(`routes : ${routes.length}`);
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('=== Reason Why ===');
|
|
256
|
+
lines.push(
|
|
257
|
+
'Routes are built compositionally from direct descriptions, with duration and cost added and belief and comfort combined multiplicatively.',
|
|
258
|
+
);
|
|
259
|
+
for (let i = 0; i < routes.length; i += 1) {
|
|
260
|
+
lines.push(...routeLines(i + 1, routes[i]));
|
|
261
|
+
if (i + 1 !== routes.length) lines.push('');
|
|
262
|
+
}
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push('=== Check ===');
|
|
265
|
+
lines.push(`all routes satisfy constraints : ${allRoutesSatisfyConstraints ? 'yes' : 'no'}`);
|
|
266
|
+
lines.push(`all routes hit goal endpoints : ${allRoutesHitGoalEndpoints ? 'yes' : 'no'}`);
|
|
267
|
+
lines.push(`metrics recompute from steps : ${allMetricsRecompute ? 'yes' : 'no'}`);
|
|
268
|
+
lines.push(`expected route count (= 2) : ${routes.length === 2 ? 'yes' : 'no'}`);
|
|
269
|
+
|
|
270
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
271
|
+
process.exit(ok ? 0 : 1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
main();
|