@meantjs/profile 0.0.0-stage → 0.1.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/CHANGELOG.md +37 -0
- package/LICENSE +202 -0
- package/README.md +248 -2
- package/bin/candid-profile.mjs +6 -0
- package/data/packs/icrc1.profile.json +275 -0
- package/data/packs/icrc2.profile.json +515 -0
- package/data/profile-v0.schema.json +966 -0
- package/data/profile-v0.validate.mjs +9 -0
- package/package.json +70 -4
- package/src/address.d.mts +30 -0
- package/src/address.mjs +62 -0
- package/src/analyze.mjs +298 -0
- package/src/browser.d.mts +208 -0
- package/src/browser.mjs +50 -0
- package/src/checks.mjs +81 -0
- package/src/cli.mjs +114 -0
- package/src/compile.mjs +304 -0
- package/src/doc-annotations.mjs +394 -0
- package/src/idl.mjs +10 -0
- package/src/iface.mjs +123 -0
- package/src/index.d.mts +70 -0
- package/src/index.mjs +15 -0
- package/src/pack-match.mjs +19 -0
- package/src/packs.mjs +25 -0
- package/src/resolve.mjs +114 -0
- package/src/resolved.mjs +60 -0
- package/src/schema-core.mjs +45 -0
- package/src/schema.mjs +23 -0
- package/src/served.mjs +59 -0
- package/src/sha256.mjs +59 -0
- package/src/subtype.mjs +271 -0
- package/src/tags.mjs +90 -0
- package/src/util.mjs +16 -0
- package/src/validate.mjs +49 -0
package/src/tags.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag storage and position enumeration. TagIndex, `positions` are moved verbatim from scripts/validate.mjs;
|
|
3
|
+
* LayeredTags is the multi-profile merge of spec section 8.
|
|
4
|
+
*/
|
|
5
|
+
import { edgeKey } from "./address.mjs";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------- tag index
|
|
8
|
+
|
|
9
|
+
/** Tags reachable at a position: node tags < edge tags < method-position tags (spec §8). */
|
|
10
|
+
export class TagIndex {
|
|
11
|
+
constructor() { this.byKey = new Map(); }
|
|
12
|
+
add(key, tag, entry) {
|
|
13
|
+
if (!this.byKey.has(key)) this.byKey.set(key, {});
|
|
14
|
+
this.byKey.get(key)[tag] = entry;
|
|
15
|
+
}
|
|
16
|
+
at({ key, parent, ref }, iface) {
|
|
17
|
+
const nodeTags = this.byKey.get(`n:${iface.unwrapOpt(ref).ref}`);
|
|
18
|
+
const edgeTags = parent ? this.byKey.get(edgeKey(parent)) : undefined;
|
|
19
|
+
const posTags = this.byKey.get(key);
|
|
20
|
+
if (!nodeTags && !edgeTags && !posTags) return undefined;
|
|
21
|
+
return { ...(nodeTags ?? {}), ...(edgeTags ?? {}), ...(posTags ?? {}) };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Every value position of a method side, with canonical key, parent edge and display address. */
|
|
26
|
+
export function* positions(iface, methodName, side, maxDepth = 8) {
|
|
27
|
+
const m = iface.methods.get(methodName);
|
|
28
|
+
const tuple = side === "args" ? m.args : m.results;
|
|
29
|
+
const sideKey = side === "args" ? "arg" : "res";
|
|
30
|
+
function* rec(ref, parent, ids, display, index, depth, seen) {
|
|
31
|
+
const key = `m:${methodName}|${sideKey}|${index}|${ids.join("/")}`;
|
|
32
|
+
const { ref: r } = iface.unwrapOpt(ref);
|
|
33
|
+
const cyclic = seen.has(r) || depth >= maxDepth;
|
|
34
|
+
yield { ref, parent, key, display, cyclic };
|
|
35
|
+
if (cyclic) return;
|
|
36
|
+
const n = iface.node(r);
|
|
37
|
+
const next = new Set(seen).add(r);
|
|
38
|
+
if (n.kind === "record" || n.kind === "variant") {
|
|
39
|
+
for (const f of n.fields) {
|
|
40
|
+
yield* rec(f.type, { container: r, kind: n.kind, id: f.id }, [...ids, String(f.id)], `${display}.${iface.fieldName(r, f.id)}`, index, depth + 1, next);
|
|
41
|
+
}
|
|
42
|
+
} else if (n.kind === "vec" && !iface.isBlob(r)) {
|
|
43
|
+
yield* rec(n.inner, { container: r, kind: "vec" }, [...ids, "[]"], `${display}[]`, index, depth + 1, next);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (let i = 0; i < tuple.length; i++) {
|
|
47
|
+
yield* rec(tuple[i], null, [], `${methodName}${side === "args" ? "" : "->"}(${i})`, i, 0, new Set());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Spec section 8, merging. One TagIndex per layer, highest precedence first (author, third_party, pack, inferred).
|
|
53
|
+
* - Within a layer: a method-position tag beats a `type:` edge tag beats a whole-declaration tag (TagIndex.at).
|
|
54
|
+
* - Across layers: the first layer that has the tag at the position wins; the same tag in lower layers is overridden.
|
|
55
|
+
* Merging is per tag per position: other tags on the position are kept.
|
|
56
|
+
*/
|
|
57
|
+
export class LayeredTags {
|
|
58
|
+
/** @param layers Array<{ name: string, index: TagIndex }> */
|
|
59
|
+
constructor(layers) { this.layers = layers; }
|
|
60
|
+
|
|
61
|
+
static #first(parts) {
|
|
62
|
+
let out;
|
|
63
|
+
for (const t of parts) {
|
|
64
|
+
if (!t) continue;
|
|
65
|
+
out ??= {};
|
|
66
|
+
for (const k of Object.keys(t)) if (!(k in out)) out[k] = t[k];
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Effective tags at a value position (undefined when none). */
|
|
72
|
+
at(pos, iface) { return LayeredTags.#first(this.layers.map((l) => l.index.at(pos, iface))); }
|
|
73
|
+
|
|
74
|
+
/** Effective tags on a method (always an object). */
|
|
75
|
+
methodTags(name) { return LayeredTags.#first(this.layers.map((l) => l.index.byKey.get(`m:${name}`))) ?? {}; }
|
|
76
|
+
|
|
77
|
+
/** Every candidate per tag at a value position, highest layer first: { tag -> [entry, ...] }. */
|
|
78
|
+
candidates(pos, iface) { return this.#collect(this.layers.map((l) => l.index.at(pos, iface))); }
|
|
79
|
+
|
|
80
|
+
candidatesAtMethod(name) { return this.#collect(this.layers.map((l) => l.index.byKey.get(`m:${name}`))); }
|
|
81
|
+
|
|
82
|
+
#collect(parts) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const t of parts) {
|
|
85
|
+
if (!t) continue;
|
|
86
|
+
for (const [k, v] of Object.entries(t)) (out[k] ??= []).push(v);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/util.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sha256Hex } from "./sha256.mjs";
|
|
2
|
+
|
|
3
|
+
// The pure label helpers live in idl.mjs, so that the address entry loads nothing else.
|
|
4
|
+
export { U32_MAX, idlHash } from "./idl.mjs";
|
|
5
|
+
|
|
6
|
+
/** RFC 8785 (JCS) canonical JSON for the value shapes profiles use. */
|
|
7
|
+
export function jcs(v) {
|
|
8
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
9
|
+
if (Array.isArray(v)) return `[${v.map(jcs).join(",")}]`;
|
|
10
|
+
const keys = Object.keys(v).sort();
|
|
11
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${jcs(v[k])}`).join(",")}}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function profileId(doc) {
|
|
15
|
+
return `candid-profile:v0:sha256:${sha256Hex(jcs(doc))}`;
|
|
16
|
+
}
|
package/src/validate.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-profile validation: the behavior of the original scripts/validate.mjs, as a library call.
|
|
3
|
+
* A pack is checked against its own `binds.requires` fragment; a canister profile against the given interface.
|
|
4
|
+
*/
|
|
5
|
+
import { profileId } from "./util.mjs";
|
|
6
|
+
import { compileInterface } from "./compile.mjs";
|
|
7
|
+
import { analyzeProfiles } from "./analyze.mjs";
|
|
8
|
+
import { resolvedForm } from "./resolved.mjs";
|
|
9
|
+
import { schemaDiagnostics } from "./schema-core.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param doc the parsed profile
|
|
13
|
+
* @param opts.validateSchema from createSchemaValidator()
|
|
14
|
+
* @param opts.did Candid source text of the interface (canister profiles; packs use binds.requires)
|
|
15
|
+
* @param opts.didLabel how E_CANDID names the .did (a path)
|
|
16
|
+
* @param opts.resolved also return the resolved form
|
|
17
|
+
* @returns { ok, profile_id?, interface_id?, methods?, annotations?, diagnostics, resolved? }
|
|
18
|
+
*/
|
|
19
|
+
export async function validateProfile(doc, opts) {
|
|
20
|
+
const diagnostics = [];
|
|
21
|
+
const diag = (severity, code, at, message) => diagnostics.push({ severity, code, at, message });
|
|
22
|
+
|
|
23
|
+
if (!opts.validateSchema(doc)) {
|
|
24
|
+
diagnostics.push(...schemaDiagnostics(opts.validateSchema));
|
|
25
|
+
return { ok: false, diagnostics };
|
|
26
|
+
}
|
|
27
|
+
const pid = profileId(doc);
|
|
28
|
+
|
|
29
|
+
let didSource;
|
|
30
|
+
if (doc.kind === "pack") didSource = doc.binds.requires;
|
|
31
|
+
else if (opts.did !== undefined && opts.did !== null) didSource = opts.did;
|
|
32
|
+
else {
|
|
33
|
+
diag("warning", "W_NO_INTERFACE", "", "canister profile without --did: semantic checks skipped");
|
|
34
|
+
return { ok: true, profile_id: pid, diagnostics };
|
|
35
|
+
}
|
|
36
|
+
const compiled = await compileInterface(didSource);
|
|
37
|
+
if (compiled.failure) {
|
|
38
|
+
for (const d of compiled.failure) diag("error", "E_CANDID", doc.kind === "pack" ? "binds.requires" : opts.didLabel, `${d.code}: ${d.message}`);
|
|
39
|
+
return { ok: false, profile_id: pid, diagnostics };
|
|
40
|
+
}
|
|
41
|
+
const { iface } = compiled;
|
|
42
|
+
const rebinding = doc.kind === "canister" && doc.binds.interface_id !== iface.interfaceId;
|
|
43
|
+
const analysis = analyzeProfiles(iface, [{ doc, pid, rebinding, apply: false }], { attribute: false });
|
|
44
|
+
|
|
45
|
+
const ok = !analysis.diagnostics.some((d) => d.severity === "error");
|
|
46
|
+
const result = { ok, profile_id: pid, interface_id: iface.interfaceId, methods: iface.methods.size, annotations: doc.annotations.length, diagnostics: analysis.diagnostics };
|
|
47
|
+
if (opts.resolved) result.resolved = resolvedForm(iface, [{ doc, pid }], analysis.layered, analysis.diagnostics);
|
|
48
|
+
return result;
|
|
49
|
+
}
|