@cavelang/canonical 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/README.md +100 -0
- package/dist/src/canonicalize.d.ts +56 -0
- package/dist/src/canonicalize.d.ts.map +1 -0
- package/dist/src/canonicalize.js +227 -0
- package/dist/src/canonicalize.js.map +1 -0
- package/dist/src/emit.d.ts +19 -0
- package/dist/src/emit.d.ts.map +1 -0
- package/dist/src/emit.js +112 -0
- package/dist/src/emit.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/prelude.d.ts +15 -0
- package/dist/src/prelude.d.ts.map +1 -0
- package/dist/src/prelude.js +38 -0
- package/dist/src/prelude.js.map +1 -0
- package/dist/src/registry.d.ts +62 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +86 -0
- package/dist/src/registry.js.map +1 -0
- package/package.json +43 -0
- package/src/canonicalize.ts +272 -0
- package/src/emit.ts +116 -0
- package/src/index.ts +22 -0
- package/src/prelude.ts +42 -0
- package/src/registry.ts +115 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared standard prelude (spec §5.5).
|
|
3
|
+
*
|
|
4
|
+
* Standard verbs SHOULD carry inverse declarations; emitters MAY prepend
|
|
5
|
+
* them to a document or keep them in a shared prelude. This module is that
|
|
6
|
+
* shared prelude: the spec's declaration block as CAVE text, plus a
|
|
7
|
+
* pre-built registry for callers that want inverse-aware reads without
|
|
8
|
+
* ingesting the text first.
|
|
9
|
+
*/
|
|
10
|
+
import * as Registry from "./registry.js";
|
|
11
|
+
/** The spec §5.5 declaration block, verbatim. */
|
|
12
|
+
export const standardPrelude = `REVERSE IS verb ; declares that two verbs name the same edge read in opposite directions
|
|
13
|
+
REVERSE HAS arity: 2
|
|
14
|
+
CONTAINS REVERSE PART-OF
|
|
15
|
+
CAUSE REVERSE CAUSED-BY
|
|
16
|
+
PRECEDES REVERSE FOLLOWS
|
|
17
|
+
USES REVERSE USED-BY
|
|
18
|
+
NEEDS REVERSE NEEDED-BY
|
|
19
|
+
ENABLES REVERSE ENABLED-BY
|
|
20
|
+
BLOCKS REVERSE BLOCKED-BY
|
|
21
|
+
EXTENDS REVERSE EXTENDED-BY
|
|
22
|
+
`;
|
|
23
|
+
const standardPairs = [
|
|
24
|
+
['CONTAINS', 'PART-OF'],
|
|
25
|
+
['CAUSE', 'CAUSED-BY'],
|
|
26
|
+
['PRECEDES', 'FOLLOWS'],
|
|
27
|
+
['USES', 'USED-BY'],
|
|
28
|
+
['NEEDS', 'NEEDED-BY'],
|
|
29
|
+
['ENABLES', 'ENABLED-BY'],
|
|
30
|
+
['BLOCKS', 'BLOCKED-BY'],
|
|
31
|
+
['EXTENDS', 'EXTENDED-BY']
|
|
32
|
+
];
|
|
33
|
+
/** Registry with the standard §5.5 inverse pairs declared. */
|
|
34
|
+
export const standardRegistry = standardPairs.reduce((registry, [primary, inverse]) => {
|
|
35
|
+
const declared = Registry.declareReverse(registry, primary, inverse);
|
|
36
|
+
return declared.registry;
|
|
37
|
+
}, Registry.declareVerb(Registry.empty, 'REVERSE'));
|
|
38
|
+
//# sourceMappingURL=prelude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prelude.js","sourceRoot":"","sources":["../../src/prelude.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC,iDAAiD;AACjD,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;CAU9B,CAAA;AAED,MAAM,aAAa,GAAgC;IACjD,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,OAAO,EAAE,WAAW,CAAC;IACtB,CAAC,UAAU,EAAE,SAAS,CAAC;IACvB,CAAC,MAAM,EAAE,SAAS,CAAC;IACnB,CAAC,OAAO,EAAE,WAAW,CAAC;IACtB,CAAC,SAAS,EAAE,YAAY,CAAC;IACzB,CAAC,QAAQ,EAAE,YAAY,CAAC;IACxB,CAAC,SAAS,EAAE,aAAa,CAAC;CAC3B,CAAA;AAED,8DAA8D;AAC9D,MAAM,CAAC,MAAM,gBAAgB,GAC3B,aAAa,CAAC,MAAM,CAAa,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE;IAChE,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACpE,OAAO,QAAQ,CAAC,QAAQ,CAAA;AAC1B,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verb registry (spec §5.4, §5.5).
|
|
3
|
+
*
|
|
4
|
+
* Tracks in-band verb knowledge: extension verb declarations (`MIGRATES IS
|
|
5
|
+
* verb`) and inverse pairs (`CONTAINS REVERSE PART-OF`). No verb is born
|
|
6
|
+
* with an inverse — a relation without a `REVERSE` declaration simply has
|
|
7
|
+
* no reverse name.
|
|
8
|
+
*
|
|
9
|
+
* The registry is an immutable value; declaring returns a new registry, so
|
|
10
|
+
* the canonicalization pipeline can thread it through a document and
|
|
11
|
+
* declarations take effect for subsequent lines only.
|
|
12
|
+
*/
|
|
13
|
+
/** An inverse pair. The primary is the left side of the first declaration (spec §5.5). */
|
|
14
|
+
export type Pair = {
|
|
15
|
+
readonly primary: string;
|
|
16
|
+
readonly inverse: string;
|
|
17
|
+
};
|
|
18
|
+
export type Registry = {
|
|
19
|
+
/** Every verb of every pair → its pair. */
|
|
20
|
+
readonly pairs: ReadonlyMap<string, Pair>;
|
|
21
|
+
/** Extension verbs declared via `X IS verb`. */
|
|
22
|
+
readonly declared: ReadonlySet<string>;
|
|
23
|
+
};
|
|
24
|
+
export type t = Registry;
|
|
25
|
+
/** Registry with no declarations. */
|
|
26
|
+
export declare const empty: Registry;
|
|
27
|
+
export type Declared = {
|
|
28
|
+
readonly ok: true;
|
|
29
|
+
readonly registry: Registry;
|
|
30
|
+
} | {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
readonly registry: Registry;
|
|
33
|
+
readonly problem: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Declares `a REVERSE b`: `a` is primary, `b` its inverse (spec §5.5).
|
|
37
|
+
* Redeclaring an existing pair in either direction is a no-op; a
|
|
38
|
+
* declaration that conflicts with an existing pair is rejected — the first
|
|
39
|
+
* declaration wins.
|
|
40
|
+
*/
|
|
41
|
+
export declare const declareReverse: (registry: Registry, a: string, b: string) => Declared;
|
|
42
|
+
/** Declares an extension verb (`X IS verb`, spec §5.4). */
|
|
43
|
+
export declare const declareVerb: (registry: Registry, verb: string) => Registry;
|
|
44
|
+
/**
|
|
45
|
+
* @returns the canonical primary of `verb` and whether `verb` is the
|
|
46
|
+
* inverse side. An unpaired verb is its own primary.
|
|
47
|
+
*/
|
|
48
|
+
export declare const primaryOf: (registry: Registry, verb: string) => {
|
|
49
|
+
primary: string;
|
|
50
|
+
isInverse: boolean;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* @returns the opposite name of `verb` — `CONTAINS` → `PART-OF`,
|
|
54
|
+
* `PART-OF` → `CONTAINS` — or `undefined` when no inverse is declared
|
|
55
|
+
* (reverse reads then fall back to an un-named object-side scan, spec §5.5).
|
|
56
|
+
*/
|
|
57
|
+
export declare const inverseOf: (registry: Registry, verb: string) => undefined | string;
|
|
58
|
+
/** @returns `true` if `verb` was declared as an extension verb. */
|
|
59
|
+
export declare const isDeclared: (registry: Registry, verb: string) => boolean;
|
|
60
|
+
/** All pairs, deduplicated, in insertion order. */
|
|
61
|
+
export declare const allPairs: (registry: Registry) => Pair[];
|
|
62
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,0FAA0F;AAC1F,MAAM,MAAM,IAAI,GAAG;IACjB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACzC,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,CAAC,GAAG,QAAQ,CAAA;AAExB,qCAAqC;AACrC,eAAO,MAAM,KAAK,EAAE,QAGnB,CAAA;AAED,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,UAAU,QAAQ,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,QAyBzE,CAAA;AAED,2DAA2D;AAC3D,eAAO,MAAM,WAAW,GAAI,UAAU,QAAQ,EAAE,MAAM,MAAM,KAAG,QAO9D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,UAAU,QAAQ,EAAE,MAAM,MAAM,KAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAMjG,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,UAAU,QAAQ,EAAE,MAAM,MAAM,KAAG,SAAS,GAAG,MAMxE,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,UAAU,GAAI,UAAU,QAAQ,EAAE,MAAM,MAAM,KAAG,OACjC,CAAA;AAE7B,mDAAmD;AACnD,eAAO,MAAM,QAAQ,GAAI,UAAU,QAAQ,KAAG,IAAI,EACX,CAAA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verb registry (spec §5.4, §5.5).
|
|
3
|
+
*
|
|
4
|
+
* Tracks in-band verb knowledge: extension verb declarations (`MIGRATES IS
|
|
5
|
+
* verb`) and inverse pairs (`CONTAINS REVERSE PART-OF`). No verb is born
|
|
6
|
+
* with an inverse — a relation without a `REVERSE` declaration simply has
|
|
7
|
+
* no reverse name.
|
|
8
|
+
*
|
|
9
|
+
* The registry is an immutable value; declaring returns a new registry, so
|
|
10
|
+
* the canonicalization pipeline can thread it through a document and
|
|
11
|
+
* declarations take effect for subsequent lines only.
|
|
12
|
+
*/
|
|
13
|
+
import { Verb } from '@cavelang/core';
|
|
14
|
+
/** Registry with no declarations. */
|
|
15
|
+
export const empty = {
|
|
16
|
+
pairs: new Map(),
|
|
17
|
+
declared: new Set()
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Declares `a REVERSE b`: `a` is primary, `b` its inverse (spec §5.5).
|
|
21
|
+
* Redeclaring an existing pair in either direction is a no-op; a
|
|
22
|
+
* declaration that conflicts with an existing pair is rejected — the first
|
|
23
|
+
* declaration wins.
|
|
24
|
+
*/
|
|
25
|
+
export const declareReverse = (registry, a, b) => {
|
|
26
|
+
if (!Verb.isVerbToken(a) || !Verb.isVerbToken(b)) {
|
|
27
|
+
return { ok: false, registry, problem: `REVERSE operands must be UPPERCASE verbs, got ${a} REVERSE ${b}` };
|
|
28
|
+
}
|
|
29
|
+
const existingA = registry.pairs.get(a);
|
|
30
|
+
const existingB = registry.pairs.get(b);
|
|
31
|
+
const existing = existingA ?? existingB;
|
|
32
|
+
if (existing !== undefined) {
|
|
33
|
+
const same = (existing.primary === a && existing.inverse === b) ||
|
|
34
|
+
(existing.primary === b && existing.inverse === a);
|
|
35
|
+
if (same && existingA === existingB) {
|
|
36
|
+
return { ok: true, registry };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
registry,
|
|
41
|
+
problem: `${a} REVERSE ${b} conflicts with existing ${existing.primary} REVERSE ${existing.inverse} — first declaration wins (spec §5.5)`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const pair = { primary: a, inverse: b };
|
|
45
|
+
const pairs = new Map(registry.pairs);
|
|
46
|
+
pairs.set(a, pair);
|
|
47
|
+
pairs.set(b, pair);
|
|
48
|
+
return { ok: true, registry: { pairs, declared: registry.declared } };
|
|
49
|
+
};
|
|
50
|
+
/** Declares an extension verb (`X IS verb`, spec §5.4). */
|
|
51
|
+
export const declareVerb = (registry, verb) => {
|
|
52
|
+
if (registry.declared.has(verb)) {
|
|
53
|
+
return registry;
|
|
54
|
+
}
|
|
55
|
+
const declared = new Set(registry.declared);
|
|
56
|
+
declared.add(verb);
|
|
57
|
+
return { pairs: registry.pairs, declared };
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* @returns the canonical primary of `verb` and whether `verb` is the
|
|
61
|
+
* inverse side. An unpaired verb is its own primary.
|
|
62
|
+
*/
|
|
63
|
+
export const primaryOf = (registry, verb) => {
|
|
64
|
+
const pair = registry.pairs.get(verb);
|
|
65
|
+
if (pair === undefined || pair.primary === verb) {
|
|
66
|
+
return { primary: verb, isInverse: false };
|
|
67
|
+
}
|
|
68
|
+
return { primary: pair.primary, isInverse: true };
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* @returns the opposite name of `verb` — `CONTAINS` → `PART-OF`,
|
|
72
|
+
* `PART-OF` → `CONTAINS` — or `undefined` when no inverse is declared
|
|
73
|
+
* (reverse reads then fall back to an un-named object-side scan, spec §5.5).
|
|
74
|
+
*/
|
|
75
|
+
export const inverseOf = (registry, verb) => {
|
|
76
|
+
const pair = registry.pairs.get(verb);
|
|
77
|
+
if (pair === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return pair.primary === verb ? pair.inverse : pair.primary;
|
|
81
|
+
};
|
|
82
|
+
/** @returns `true` if `verb` was declared as an extension verb. */
|
|
83
|
+
export const isDeclared = (registry, verb) => registry.declared.has(verb);
|
|
84
|
+
/** All pairs, deduplicated, in insertion order. */
|
|
85
|
+
export const allPairs = (registry) => [...new Set(registry.pairs.values())];
|
|
86
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cavelang/canonical",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CAVE canonicalization — verb registry, REVERSE inverse resolution, continuation expansion, canonical emitter.",
|
|
6
|
+
"license": "CC0-1.0",
|
|
7
|
+
"author": "Mirek Rusin (https://github.com/mirek)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mirek/cave.git",
|
|
11
|
+
"directory": "packages/canonical"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.18"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/src/index.d.ts",
|
|
19
|
+
"default": "./dist/src/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"!dist/test",
|
|
25
|
+
"!dist/tsconfig.tsbuildinfo",
|
|
26
|
+
"src",
|
|
27
|
+
"!src/**/*.test.ts",
|
|
28
|
+
"README.md",
|
|
29
|
+
"License.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@cavelang/core": "0.1.0",
|
|
36
|
+
"@cavelang/parser": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -b",
|
|
40
|
+
"test": "node --disable-warning=ExperimentalWarning --test 'test/*.test.ts'",
|
|
41
|
+
"typecheck": "tsc -b"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalization pipeline (spec §13.4).
|
|
3
|
+
*
|
|
4
|
+
* Turns a parsed document into canonical claims plus qualifier edges:
|
|
5
|
+
*
|
|
6
|
+
* 1. verbs are already uppercase (parser enforces the lexical shape);
|
|
7
|
+
* 2. inverse verbs swap subject/object and substitute the primary (§5.5);
|
|
8
|
+
* 3. continuation lines fill their inherited endpoint from the parent (§8.3);
|
|
9
|
+
* 4. entity whitespace normalizes to `-`, proper-noun casing preserved;
|
|
10
|
+
* 5. `raw` keeps the line exactly as written;
|
|
11
|
+
* 6–10. confidence, `~`, multipliers, tag splitting are handled by the
|
|
12
|
+
* parser and `@cavelang/core`;
|
|
13
|
+
* 11. claim keys are computed on the canonical form (`Key.of`);
|
|
14
|
+
* 12. contexts/tags ride on each claim for the store's side tables.
|
|
15
|
+
*
|
|
16
|
+
* Qualifier lines become claim nodes joined to their parent by edges
|
|
17
|
+
* (§8.1); `UNLESS x` normalizes to `WHEN` + negated condition (§8.2);
|
|
18
|
+
* grouped full claims link to their parent with the `QUALIFIES` role
|
|
19
|
+
* (§13.2). `REVERSE` and extension-verb declarations update the registry
|
|
20
|
+
* in-band, affecting subsequent lines (§5.4, §5.5).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Claim, Entity, Value, Verb } from '@cavelang/core'
|
|
24
|
+
import { parseDocument, type Ast } from '@cavelang/parser'
|
|
25
|
+
import * as Registry from './registry.ts'
|
|
26
|
+
|
|
27
|
+
/** Edge roles persisted in `cave_edge` (spec §13.2). */
|
|
28
|
+
export type EdgeRole = 'WHEN' | 'VIA' | 'BECAUSE' | 'QUALIFIES'
|
|
29
|
+
|
|
30
|
+
/** A canonical claim with its 1-based source line. */
|
|
31
|
+
export type Entry = {
|
|
32
|
+
readonly claim: Claim.t
|
|
33
|
+
readonly line: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Edge between claim indices (into `Result.claims`). */
|
|
37
|
+
export type Edge = {
|
|
38
|
+
readonly parent: number
|
|
39
|
+
readonly role: EdgeRole
|
|
40
|
+
readonly child: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type Problem = {
|
|
44
|
+
readonly line: number
|
|
45
|
+
readonly message: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type Result = {
|
|
49
|
+
readonly claims: readonly Entry[]
|
|
50
|
+
readonly edges: readonly Edge[]
|
|
51
|
+
/** Registry after processing — input registry plus in-band declarations. */
|
|
52
|
+
readonly registry: Registry.t
|
|
53
|
+
readonly problems: readonly Problem[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const roleOf: Record<Verb.Qualifier, EdgeRole> = {
|
|
57
|
+
WHEN: 'WHEN',
|
|
58
|
+
UNLESS: 'WHEN',
|
|
59
|
+
VIA: 'VIA',
|
|
60
|
+
BECAUSE: 'BECAUSE'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `>` maps to the standard `EXCEEDS`; other operators keep their symbol. */
|
|
64
|
+
const comparisonVerb = (op: Ast.ComparisonOp): string =>
|
|
65
|
+
op === '>' ? 'EXCEEDS' : op
|
|
66
|
+
|
|
67
|
+
const normalizeTerm = (term: Claim.Term): Claim.Term =>
|
|
68
|
+
term.kind === 'entity' ? Claim.entity(Entity.normalize(term.text)) : term
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Term ↔ payload conversion for the §5.5 inverse swap. The parser
|
|
72
|
+
* classifies a date/number endpoint as a metric payload but a subject is
|
|
73
|
+
* always a term; swapping must re-classify both directions the same way,
|
|
74
|
+
* or `deploy PRECEDES 2026-01-01` and `2026-01-01 FOLLOWS deploy` would
|
|
75
|
+
* key differently — two keys for one fact.
|
|
76
|
+
*/
|
|
77
|
+
const termOfPayload = (payload: Claim.Payload): undefined | Claim.Term => {
|
|
78
|
+
switch (payload.kind) {
|
|
79
|
+
case 'relation':
|
|
80
|
+
return payload.object
|
|
81
|
+
case 'metric':
|
|
82
|
+
return payload.value.kind === 'code' ? Claim.code(payload.value.raw) :
|
|
83
|
+
payload.value.kind === 'text' ? Claim.text(payload.value.raw) :
|
|
84
|
+
Claim.entity(Entity.normalize(payload.value.raw))
|
|
85
|
+
default:
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const payloadOfTerm = (term: Claim.Term): Claim.Payload => {
|
|
91
|
+
if (term.kind === 'entity') {
|
|
92
|
+
const value = Value.parse(term.text)
|
|
93
|
+
if (value.kind === 'number' || value.kind === 'date') {
|
|
94
|
+
return Claim.metric(value)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return Claim.relation(term)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const initOf = (meta: Ast.Meta): Partial<Claim.Init> => ({
|
|
101
|
+
contexts: meta.contexts,
|
|
102
|
+
tags: meta.tags,
|
|
103
|
+
importance: meta.importance,
|
|
104
|
+
...meta.conf !== undefined ? { conf: meta.conf } : {},
|
|
105
|
+
...meta.delta !== undefined ? { delta: meta.delta } : {},
|
|
106
|
+
...meta.sigmaLevel !== undefined ? { sigmaLevel: meta.sigmaLevel } : {},
|
|
107
|
+
...meta.comment !== undefined ? { comment: meta.comment } : {}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/** Canonicalizes a parsed document. */
|
|
111
|
+
export const canonicalize = (document: Ast.Document, registry: Registry.t = Registry.empty): Result => {
|
|
112
|
+
const claims: Entry[] = []
|
|
113
|
+
const edges: Edge[] = []
|
|
114
|
+
const problems: Problem[] = []
|
|
115
|
+
/** line index → claim index + the subject as (virtually) written, for §8.3 inheritance. */
|
|
116
|
+
const byLine = new Map<number, { index: number, writtenSubject: Claim.Term }>()
|
|
117
|
+
|
|
118
|
+
const problem = (line: number, message: string): void => {
|
|
119
|
+
problems.push({ line, message })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Canonicalizes one full claim body — §13.4 steps 2 and 4 plus assembly. */
|
|
123
|
+
const buildClaim = (full: Ast.Full, raw: string, line: number): { claim: Claim.t, writtenSubject: Claim.Term } => {
|
|
124
|
+
const writtenSubject = normalizeTerm(full.subject)
|
|
125
|
+
let subject = writtenSubject
|
|
126
|
+
let verb = full.verb
|
|
127
|
+
let payload: Claim.Payload = full.payload
|
|
128
|
+
if (payload.kind === 'relation') {
|
|
129
|
+
payload = Claim.relation(normalizeTerm(payload.object))
|
|
130
|
+
}
|
|
131
|
+
const { primary, isInverse } = Registry.primaryOf(registry, verb)
|
|
132
|
+
if (isInverse) {
|
|
133
|
+
const objectTerm = termOfPayload(payload)
|
|
134
|
+
if (objectTerm === undefined) {
|
|
135
|
+
problem(line, `inverse verb ${verb} needs an object to swap with — keeping the line as written (spec §5.5)`)
|
|
136
|
+
} else {
|
|
137
|
+
payload = payloadOfTerm(subject)
|
|
138
|
+
subject = objectTerm
|
|
139
|
+
verb = primary
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const claim = Claim.of({
|
|
143
|
+
subject,
|
|
144
|
+
verb,
|
|
145
|
+
negated: full.negated,
|
|
146
|
+
payload,
|
|
147
|
+
raw,
|
|
148
|
+
...initOf(full.meta)
|
|
149
|
+
})
|
|
150
|
+
return { claim, writtenSubject }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const append = (claim: Claim.t, line: number, writtenSubject: Claim.Term, lineIndex: number): number => {
|
|
154
|
+
const index = claims.length
|
|
155
|
+
claims.push({ claim, line })
|
|
156
|
+
byLine.set(lineIndex, { index, writtenSubject })
|
|
157
|
+
return index
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** In-band declarations take effect for subsequent lines (spec §5.4, §5.5). */
|
|
161
|
+
const applyDeclarations = (claim: Claim.t, line: number): void => {
|
|
162
|
+
if (claim.payload.kind !== 'relation' || claim.negated) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
const object = claim.payload.object
|
|
166
|
+
if (claim.verb === Verb.REVERSE && claim.subject.kind === 'entity' && object.kind === 'entity') {
|
|
167
|
+
const declared = Registry.declareReverse(registry, claim.subject.text, object.text)
|
|
168
|
+
registry = declared.registry
|
|
169
|
+
if (!declared.ok) {
|
|
170
|
+
problem(line, declared.problem)
|
|
171
|
+
}
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (claim.verb === 'IS' && object.kind === 'entity' && object.text === 'verb' &&
|
|
175
|
+
claim.subject.kind === 'entity' && Verb.isVerbToken(claim.subject.text)) {
|
|
176
|
+
registry = Registry.declareVerb(registry, claim.subject.text)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Builds the condition claim of a qualifier line (spec §8.2). */
|
|
181
|
+
const conditionOf = (payload: Ast.QualifierPayload, unless: boolean): Ast.Full => {
|
|
182
|
+
switch (payload.kind) {
|
|
183
|
+
case 'claim':
|
|
184
|
+
return {
|
|
185
|
+
...payload.claim,
|
|
186
|
+
negated: payload.claim.negated !== (payload.negated !== unless)
|
|
187
|
+
}
|
|
188
|
+
case 'entity':
|
|
189
|
+
return {
|
|
190
|
+
subject: payload.term,
|
|
191
|
+
verb: 'EXISTS',
|
|
192
|
+
negated: payload.negated !== unless,
|
|
193
|
+
payload: Claim.none,
|
|
194
|
+
meta: payload.meta
|
|
195
|
+
}
|
|
196
|
+
case 'comparison':
|
|
197
|
+
return {
|
|
198
|
+
subject: payload.left,
|
|
199
|
+
verb: comparisonVerb(payload.op),
|
|
200
|
+
negated: payload.negated !== unless,
|
|
201
|
+
payload: Claim.metric(payload.value),
|
|
202
|
+
meta: payload.meta
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
document.lines.forEach((line, lineIndex) => {
|
|
208
|
+
switch (line.kind) {
|
|
209
|
+
case 'claim': {
|
|
210
|
+
const { claim, writtenSubject } = buildClaim(line.claim, line.raw, line.line)
|
|
211
|
+
const index = append(claim, line.line, writtenSubject, lineIndex)
|
|
212
|
+
if (line.parent !== undefined) {
|
|
213
|
+
const parent = byLine.get(line.parent)
|
|
214
|
+
if (parent !== undefined) {
|
|
215
|
+
edges.push({ parent: parent.index, role: 'QUALIFIES', child: index })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
applyDeclarations(claim, line.line)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
case 'continuation': {
|
|
222
|
+
const parent = byLine.get(line.parent!)
|
|
223
|
+
if (parent === undefined) {
|
|
224
|
+
problem(line.line, 'continuation has no canonicalized parent')
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
const full: Ast.Full = { subject: parent.writtenSubject, ...line.body }
|
|
228
|
+
const { claim, writtenSubject } = buildClaim(full, line.raw, line.line)
|
|
229
|
+
append(claim, line.line, writtenSubject, lineIndex)
|
|
230
|
+
// §8.3: each continuation is an ordinary independent claim, so an
|
|
231
|
+
// in-band declaration works here exactly as on a full line (§5.4).
|
|
232
|
+
applyDeclarations(claim, line.line)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
case 'qualifier': {
|
|
236
|
+
const parent = byLine.get(line.parent!)
|
|
237
|
+
if (parent === undefined) {
|
|
238
|
+
problem(line.line, 'qualifier has no canonicalized parent')
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const full = conditionOf(line.payload, line.qualifier === 'UNLESS')
|
|
242
|
+
const { claim, writtenSubject } = buildClaim(full, line.raw, line.line)
|
|
243
|
+
const index = append(claim, line.line, writtenSubject, lineIndex)
|
|
244
|
+
edges.push({ parent: parent.index, role: roleOf[line.qualifier], child: index })
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
default:
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return { claims, edges, registry, problems }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parses and canonicalizes CAVE text in one step. Parser diagnostics merge
|
|
257
|
+
* into `problems`.
|
|
258
|
+
*/
|
|
259
|
+
export const canonicalizeText = (input: string, registry: Registry.t = Registry.empty): Result => {
|
|
260
|
+
const document = parseDocument(input)
|
|
261
|
+
const result = canonicalize(document, registry)
|
|
262
|
+
if (document.diagnostics.length === 0) {
|
|
263
|
+
return result
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
...result,
|
|
267
|
+
problems: [
|
|
268
|
+
...document.diagnostics.map(diagnostic => ({ line: diagnostic.line, message: diagnostic.message })),
|
|
269
|
+
...result.problems
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/emit.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits canonical CAVE text from canonical claims: colon attribute form
|
|
5
|
+
* (§3.4 — emitters MUST produce it), primary verb direction (§5.5),
|
|
6
|
+
* `WHEN NOT` rather than `UNLESS` (§8.2), metadata in the §3.2 anatomy
|
|
7
|
+
* order. Qualifier edges re-indent under their parent; grouped claims
|
|
8
|
+
* (`QUALIFIES` edges) re-indent as full lines.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Claim, Confidence, Tag, Value } from '@cavelang/core'
|
|
12
|
+
import type * as Canonicalize from './canonicalize.ts'
|
|
13
|
+
|
|
14
|
+
const payloadText = (payload: Claim.Payload): undefined | string => {
|
|
15
|
+
switch (payload.kind) {
|
|
16
|
+
case 'relation':
|
|
17
|
+
return Claim.formatTerm(payload.object)
|
|
18
|
+
case 'attribute':
|
|
19
|
+
return `${payload.attribute}: ${Value.format(payload.value)}`
|
|
20
|
+
case 'metric':
|
|
21
|
+
return Value.format(payload.value)
|
|
22
|
+
case 'none':
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metaText = (claim: Claim.t): string[] => {
|
|
28
|
+
const parts: string[] = []
|
|
29
|
+
if (claim.delta !== undefined) {
|
|
30
|
+
parts.push(`+/- ${Value.format(claim.delta)}`)
|
|
31
|
+
}
|
|
32
|
+
if (claim.sigmaLevel !== undefined) {
|
|
33
|
+
parts.push(`(${claim.sigmaLevel}σ)`)
|
|
34
|
+
}
|
|
35
|
+
for (const context of claim.contexts) {
|
|
36
|
+
parts.push(`@${context}`)
|
|
37
|
+
}
|
|
38
|
+
for (const tag of claim.tags) {
|
|
39
|
+
parts.push(Tag.format(tag))
|
|
40
|
+
}
|
|
41
|
+
if (claim.conf !== 1) {
|
|
42
|
+
parts.push(`@ ${Confidence.format(claim.conf)}`)
|
|
43
|
+
}
|
|
44
|
+
if (claim.importance) {
|
|
45
|
+
parts.push('!')
|
|
46
|
+
}
|
|
47
|
+
if (claim.comment !== undefined) {
|
|
48
|
+
parts.push(`; ${claim.comment}`)
|
|
49
|
+
}
|
|
50
|
+
return parts
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @returns one canonical line for a claim (no indentation). */
|
|
54
|
+
export const emitClaim = (claim: Claim.t): string => {
|
|
55
|
+
const parts = [Claim.formatTerm(claim.subject), claim.verb]
|
|
56
|
+
if (claim.negated) {
|
|
57
|
+
parts.push('NOT')
|
|
58
|
+
}
|
|
59
|
+
const payload = payloadText(claim.payload)
|
|
60
|
+
if (payload !== undefined) {
|
|
61
|
+
parts.push(payload)
|
|
62
|
+
}
|
|
63
|
+
parts.push(...metaText(claim))
|
|
64
|
+
return parts.join(' ')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @returns the qualifier-payload text of a condition claim. Negation always
|
|
69
|
+
* emits as a `NOT` *prefix* — the §8.2 canonical `WHEN NOT x` shape — never
|
|
70
|
+
* as the claim-internal `VERB NOT` form: a postfix `NOT` after a symbolic
|
|
71
|
+
* comparison verb (`WHEN cpu >= NOT 900`) would be unreadable to the
|
|
72
|
+
* parser and silently invert the condition on round trip.
|
|
73
|
+
*/
|
|
74
|
+
const conditionText = (claim: Claim.t): string => {
|
|
75
|
+
const body = claim.verb === 'EXISTS' && claim.payload.kind === 'none' ?
|
|
76
|
+
[Claim.formatTerm(claim.subject), ...metaText(claim)].join(' ') :
|
|
77
|
+
emitClaim({ ...claim, negated: false })
|
|
78
|
+
return claim.negated ? `NOT ${body}` : body
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Emits a whole canonicalization result as canonical CAVE text: top-level
|
|
83
|
+
* claims in claim order, children indented two spaces per level.
|
|
84
|
+
*/
|
|
85
|
+
export const emit = (result: Pick<Canonicalize.Result, 'claims' | 'edges'>): string => {
|
|
86
|
+
const childEdges = new Map<number, Canonicalize.Edge[]>()
|
|
87
|
+
const isChild = new Set<number>()
|
|
88
|
+
for (const edge of result.edges) {
|
|
89
|
+
isChild.add(edge.child)
|
|
90
|
+
const existing = childEdges.get(edge.parent)
|
|
91
|
+
if (existing === undefined) {
|
|
92
|
+
childEdges.set(edge.parent, [edge])
|
|
93
|
+
} else {
|
|
94
|
+
existing.push(edge)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const lines: string[] = []
|
|
98
|
+
const emitAt = (index: number, depth: number, role: undefined | Canonicalize.EdgeRole): void => {
|
|
99
|
+
const { claim } = result.claims[index]!
|
|
100
|
+
const indent = ' '.repeat(depth)
|
|
101
|
+
if (role === undefined || role === 'QUALIFIES') {
|
|
102
|
+
lines.push(`${indent}${emitClaim(claim)}`)
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(`${indent}${role} ${conditionText(claim)}`)
|
|
105
|
+
}
|
|
106
|
+
for (const edge of childEdges.get(index) ?? []) {
|
|
107
|
+
emitAt(edge.child, depth + 1, edge.role)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
result.claims.forEach((_, index) => {
|
|
111
|
+
if (!isChild.has(index)) {
|
|
112
|
+
emitAt(index, 0, undefined)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
return lines.length === 0 ? '' : `${lines.join('\n')}\n`
|
|
116
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavelang/canonical` — the CAVE semantic layer (spec §5.4, §5.5, §8, §13.4).
|
|
3
|
+
*
|
|
4
|
+
* Verb registry with in-band `REVERSE`/extension declarations, the
|
|
5
|
+
* canonicalization pipeline (inverse resolution, continuation expansion,
|
|
6
|
+
* qualifier edges, `UNLESS` normalization), the shared standard prelude and
|
|
7
|
+
* the canonical emitter.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { canonicalizeText, standardRegistry, emit } from '@cavelang/canonical'
|
|
11
|
+
*
|
|
12
|
+
* const result = canonicalizeText('packages/api PART-OF monorepo', standardRegistry)
|
|
13
|
+
* result.claims[0].claim // monorepo CONTAINS packages/api — one fact, two names
|
|
14
|
+
* emit(result) // canonical text, primary direction
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export * as Registry from './registry.ts'
|
|
19
|
+
export { canonicalize, canonicalizeText } from './canonicalize.ts'
|
|
20
|
+
export type { Edge, EdgeRole, Entry, Problem, Result } from './canonicalize.ts'
|
|
21
|
+
export { emit, emitClaim } from './emit.ts'
|
|
22
|
+
export { standardPrelude, standardRegistry } from './prelude.ts'
|