@x12i/memorix-format 1.0.0 → 2.0.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 +15 -0
- package/README.md +25 -63
- package/dist/associated.d.ts +25 -12
- package/dist/associated.d.ts.map +1 -1
- package/dist/associated.js +35 -32
- package/dist/associated.js.map +1 -1
- package/dist/cli.js +129 -77
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/profiles.d.ts +12 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +52 -0
- package/dist/profiles.js.map +1 -0
- package/dist/types.d.ts +8 -80
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +17 -9
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +43 -184
- package/dist/validate.js.map +1 -1
- package/docs/memorix-format-requirements.md +52 -0
- package/docs/memorix-format.md +3 -1
- package/examples/valid-snapshot.json +3 -12
- package/package.json +14 -6
- package/profiles/snapshots.json +27 -0
- package/dist/concept.d.ts +0 -16
- package/dist/concept.d.ts.map +0 -1
- package/dist/concept.js +0 -75
- package/dist/concept.js.map +0 -1
- package/dist/constants.d.ts +0 -21
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -49
- package/dist/constants.js.map +0 -1
- package/dist/inspect.d.ts +0 -4
- package/dist/inspect.d.ts.map +0 -1
- package/dist/inspect.js +0 -51
- package/dist/inspect.js.map +0 -1
- package/dist/utils.d.ts +0 -6
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -23
- package/dist/utils.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
Breaking relative to 1.0.0:
|
|
6
|
+
|
|
7
|
+
- Depends on **`@x12i/memorix-base-format@^1.0.0`** for the fixed skeleton (`recordId`, `concept`, `data`, `_*`) and unnamed open buckets.
|
|
8
|
+
- Product rules (`associated*`, `analytics` / `enrichment` / `insights` meanings, SCOPE) live only in this package as profiles / layered checks.
|
|
9
|
+
- `classifyRootKey` comes from base — `associatedData` / `analytics` are simply **open**, not special base categories.
|
|
10
|
+
- CLI adds `--profile`, directory validation; prefers requirement IDs (`ASSOC-2`, `SCOPE-2`, …).
|
|
11
|
+
- Re-exports base APIs (`validateMemorixBaseRecord`, `parseContentTypeMeta`, …).
|
|
12
|
+
|
|
13
|
+
## 1.0.0
|
|
14
|
+
|
|
15
|
+
Initial release (monolithic validator with forced product naming mixed into envelope checks).
|
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# @x12i/memorix-format
|
|
2
2
|
|
|
3
|
-
Memorix record format
|
|
3
|
+
Memorix **product** record format — profiles and conventions on top of
|
|
4
|
+
[`@x12i/memorix-base-format`](https://www.npmjs.com/package/@x12i/memorix-base-format).
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
| Package | Owns |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `@x12i/memorix-base-format` | Fixed skeleton: `recordId`, `concept`, `data`, `_…`; unnamed open buckets; ContentTypeMeta |
|
|
9
|
+
| `@x12i/memorix-format` | Memorix open-bucket meanings, `associated*` rules, snapshot SCOPE, built-in profiles |
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
14
|
npm install @x12i/memorix-format
|
|
15
|
+
# pulls @x12i/memorix-base-format
|
|
11
16
|
```
|
|
12
17
|
|
|
13
18
|
## SDK
|
|
@@ -15,76 +20,33 @@ npm install @x12i/memorix-format
|
|
|
15
20
|
```ts
|
|
16
21
|
import {
|
|
17
22
|
validateMemorixRecord,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
validateMemorixBaseRecord,
|
|
24
|
+
classifyRootKey,
|
|
25
|
+
snapshotsContentTypeMeta,
|
|
21
26
|
} from "@x12i/memorix-format";
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const id = buildConceptIdentifier("ipAddress", "10.100.18.64");
|
|
29
|
-
const prop = buildAssociatedPropertyName({
|
|
30
|
-
form: "content",
|
|
28
|
+
// Product validation (base + ASSOC/SCOPE + optional profile)
|
|
29
|
+
validateMemorixRecord(record, {
|
|
30
|
+
profile: "snapshots",
|
|
31
31
|
objectType: "vulnerabilities",
|
|
32
|
-
contentType: "
|
|
32
|
+
contentType: "snapshots",
|
|
33
33
|
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Exports
|
|
37
|
-
|
|
38
|
-
| Import | Purpose |
|
|
39
|
-
|---|---|
|
|
40
|
-
| `@x12i/memorix-format` | Types, validate, inspect, concept + associated helpers |
|
|
41
|
-
| `@x12i/memorix-format/validate` | Validator only |
|
|
42
|
-
| `@x12i/memorix-format/types` | Type-only surface |
|
|
43
|
-
|
|
44
|
-
### Validation rules (clean envelope)
|
|
45
|
-
|
|
46
|
-
- Required: `recordId`, `concept` (with `identifier` + `title`), `data` (object)
|
|
47
|
-
- `concept.identifier` requires `kind`, kind-named value, and `identifierKey`
|
|
48
|
-
- `analytics` / `enrichment` / `insights` must be objects when present
|
|
49
|
-
- `associated*` properties must be arrays of objects (omit when empty)
|
|
50
|
-
- Legacy roots (`identity`, root `lifecycle`, `sourceFacts`, …) are errors unless `--allow-legacy`
|
|
51
|
-
- With `--content-type` other than `snapshots`, associated*/analytics/enrichment/insights are rejected
|
|
52
|
-
|
|
53
|
-
## CLI
|
|
54
34
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
npx memorix-format inspect ./record.json
|
|
59
|
-
npx memorix-format docs
|
|
60
|
-
npx memorix-format docs memorix-format.md
|
|
35
|
+
// Base only (no product vocabulary)
|
|
36
|
+
validateMemorixBaseRecord(record);
|
|
37
|
+
classifyRootKey("associatedData"); // "open"
|
|
61
38
|
```
|
|
62
39
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
## Packaged documentation
|
|
66
|
-
|
|
67
|
-
| Doc | Topic |
|
|
68
|
-
|---|---|
|
|
69
|
-
| [memorix-format.md](./docs/memorix-format.md) | Canonical clean record envelope |
|
|
70
|
-
| [memorix-format-additions.md](./docs/memorix-format-additions.md) | `data` / `associatedData` / `analytics` / `insights` semantics |
|
|
71
|
-
| [association-format.md](./docs/association-format.md) | `associated*` naming and materialization |
|
|
72
|
-
| [memorix-workflow-format.md](./docs/memorix-workflow-format.md) | `memorix-workflow/1` |
|
|
73
|
-
| [memorix-format-for-dummies-association-corrections.md](./docs/memorix-format-for-dummies-association-corrections.md) | Association corrections |
|
|
74
|
-
|
|
75
|
-
## Develop
|
|
40
|
+
## CLI
|
|
76
41
|
|
|
77
42
|
```bash
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
npx memorix-format validate ./record.json --profile=snapshots --object-type=assets
|
|
44
|
+
npx memorix-format validate ./dir/ --content-type=events
|
|
45
|
+
npx memorix-format profiles
|
|
46
|
+
npx memorix-format docs memorix-format-requirements.md
|
|
82
47
|
```
|
|
83
48
|
|
|
84
|
-
##
|
|
49
|
+
## Migration from 1.0.0
|
|
85
50
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
npm run build && npm test
|
|
89
|
-
npm publish --access public
|
|
90
|
-
```
|
|
51
|
+
See [CHANGELOG.md](./CHANGELOG.md). Envelope validation moved to base-format;
|
|
52
|
+
this package no longer pretends product bucket names are part of the base law.
|
package/dist/associated.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memorix product open-bucket helpers.
|
|
3
|
+
* Base envelope rules live in @x12i/memorix-base-format — do not reimplement them here.
|
|
4
|
+
*/
|
|
5
|
+
export declare const LEGACY_ASSOCIATED_NAMES: Set<string>;
|
|
6
|
+
export declare const KNOWN_OPEN_BUCKETS: {
|
|
7
|
+
analytics: {
|
|
8
|
+
kind: "rollup";
|
|
9
|
+
shape: "object";
|
|
10
|
+
};
|
|
11
|
+
enrichment: {
|
|
12
|
+
kind: "sourced";
|
|
13
|
+
shape: "object";
|
|
14
|
+
};
|
|
15
|
+
insights: {
|
|
16
|
+
kind: "interpretive";
|
|
17
|
+
shape: "object";
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare const DEFAULT_TOP_ASSOCIATED_ITEMS = 5;
|
|
21
|
+
export declare function isAssociatedPropertyName(name: string): boolean;
|
|
22
|
+
export declare function isLegacyAssociatedPropertyName(name: string): boolean;
|
|
23
|
+
export declare function isEnrichmentAssociatedPropertyName(name: string): boolean;
|
|
24
|
+
export declare function isTopAssociatedPropertyName(name: string): boolean;
|
|
25
|
+
export declare function pascalCaseObjectType(value: string): string;
|
|
1
26
|
export type AssociatedPropertyNameInput = {
|
|
2
27
|
form: "content";
|
|
3
28
|
objectType: string;
|
|
@@ -21,18 +46,6 @@ export type AssociatedPropertyNameInput = {
|
|
|
21
46
|
objectType: string;
|
|
22
47
|
top?: boolean;
|
|
23
48
|
};
|
|
24
|
-
/** True when a root key is an associated* property. */
|
|
25
|
-
export declare function isAssociatedPropertyName(name: string): boolean;
|
|
26
|
-
/** True when the property name follows the enrichment association convention. */
|
|
27
|
-
export declare function isEnrichmentAssociatedPropertyName(name: string): boolean;
|
|
28
|
-
/** True when the property name follows the ranked top association convention. */
|
|
29
|
-
export declare function isTopAssociatedPropertyName(name: string): boolean;
|
|
30
|
-
/** True for known legacy short associated names. */
|
|
31
|
-
export declare function isLegacyAssociatedPropertyName(name: string): boolean;
|
|
32
|
-
/** Build canonical v2 associated property name. */
|
|
33
49
|
export declare function buildAssociatedPropertyName(input: AssociatedPropertyNameInput): string;
|
|
34
|
-
/** Validate v2 associated property name conventions (throws on hard errors). */
|
|
35
|
-
export declare function assertAssociatedPropertyName(name: string): void;
|
|
36
|
-
/** List associated* keys present on a record root. */
|
|
37
50
|
export declare function listAssociatedProperties(record: Record<string, unknown>): string[];
|
|
38
51
|
//# sourceMappingURL=associated.d.ts.map
|
package/dist/associated.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"associated.d.ts","sourceRoot":"","sources":["../src/associated.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"associated.d.ts","sourceRoot":"","sources":["../src/associated.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,eAAO,MAAM,uBAAuB,aAQlC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;CAI9B,CAAC;AAEF,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpE;AAED,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,MAAM,MAAM,2BAA2B,GACnC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GAC1F;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7D,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,2BAA2B,GAAG,MAAM,CAmBtF;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAElF"}
|
package/dist/associated.js
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Memorix product open-bucket helpers.
|
|
3
|
+
* Base envelope rules live in @x12i/memorix-base-format — do not reimplement them here.
|
|
4
|
+
*/
|
|
5
|
+
export const LEGACY_ASSOCIATED_NAMES = new Set([
|
|
6
|
+
"associatedData",
|
|
7
|
+
"associatedDiscovery",
|
|
8
|
+
"associatedAnalysis",
|
|
9
|
+
"associatedSiblings",
|
|
10
|
+
"associatedChildren",
|
|
11
|
+
"associatedAssets",
|
|
12
|
+
"associatedSubnets",
|
|
13
|
+
]);
|
|
14
|
+
export const KNOWN_OPEN_BUCKETS = {
|
|
15
|
+
analytics: { kind: "rollup", shape: "object" },
|
|
16
|
+
enrichment: { kind: "sourced", shape: "object" },
|
|
17
|
+
insights: { kind: "interpretive", shape: "object" },
|
|
18
|
+
};
|
|
19
|
+
export const DEFAULT_TOP_ASSOCIATED_ITEMS = 5;
|
|
4
20
|
export function isAssociatedPropertyName(name) {
|
|
5
21
|
return name.startsWith("associated");
|
|
6
22
|
}
|
|
7
|
-
|
|
23
|
+
export function isLegacyAssociatedPropertyName(name) {
|
|
24
|
+
return LEGACY_ASSOCIATED_NAMES.has(name);
|
|
25
|
+
}
|
|
8
26
|
export function isEnrichmentAssociatedPropertyName(name) {
|
|
9
27
|
return /^associated(?:Top)?[A-Z][A-Za-z0-9]*Enrichment$/.test(name);
|
|
10
28
|
}
|
|
11
|
-
/** True when the property name follows the ranked top association convention. */
|
|
12
29
|
export function isTopAssociatedPropertyName(name) {
|
|
13
30
|
return name.startsWith("associatedTop");
|
|
14
31
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
export function pascalCaseObjectType(value) {
|
|
33
|
+
return value
|
|
34
|
+
.split(/[-_]/)
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
37
|
+
.join("");
|
|
18
38
|
}
|
|
19
|
-
/** Build canonical v2 associated property name. */
|
|
20
39
|
export function buildAssociatedPropertyName(input) {
|
|
21
40
|
const objectSegment = pascalCaseObjectType(input.objectType);
|
|
22
41
|
const topPrefix = input.top ? "Top" : "";
|
|
23
42
|
switch (input.form) {
|
|
24
|
-
case "content":
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case "snapshotsRootProperty": {
|
|
29
|
-
const rootSegment = pascalCaseObjectType(input.rootProperty);
|
|
30
|
-
return `associated${topPrefix}${objectSegment}Snapshots${rootSegment}`;
|
|
31
|
-
}
|
|
43
|
+
case "content":
|
|
44
|
+
return `associated${topPrefix}${objectSegment}${pascalCaseObjectType(input.contentType)}`;
|
|
45
|
+
case "snapshotsRootProperty":
|
|
46
|
+
return `associated${topPrefix}${objectSegment}Snapshots${pascalCaseObjectType(input.rootProperty)}`;
|
|
32
47
|
case "enrichment":
|
|
33
48
|
return `associated${topPrefix}${objectSegment}Enrichment`;
|
|
34
49
|
case "concepts":
|
|
@@ -36,24 +51,12 @@ export function buildAssociatedPropertyName(input) {
|
|
|
36
51
|
case "recordIds":
|
|
37
52
|
return `associated${topPrefix}${objectSegment}RecordIds`;
|
|
38
53
|
default: {
|
|
39
|
-
const
|
|
40
|
-
throw new Error(`Unsupported
|
|
54
|
+
const _e = input;
|
|
55
|
+
throw new Error(`Unsupported input: ${JSON.stringify(_e)}`);
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
|
-
/** Validate v2 associated property name conventions (throws on hard errors). */
|
|
45
|
-
export function assertAssociatedPropertyName(name) {
|
|
46
|
-
if (!name.startsWith("associated")) {
|
|
47
|
-
throw new Error(`Associated property must start with "associated": ${name}`);
|
|
48
|
-
}
|
|
49
|
-
if (name.includes("RecordsIds")) {
|
|
50
|
-
throw new Error(`Use "RecordIds" not "RecordsIds": ${name}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** List associated* keys present on a record root. */
|
|
54
59
|
export function listAssociatedProperties(record) {
|
|
55
|
-
return Object.keys(record)
|
|
56
|
-
.filter(isAssociatedPropertyName)
|
|
57
|
-
.sort();
|
|
60
|
+
return Object.keys(record).filter(isAssociatedPropertyName).sort();
|
|
58
61
|
}
|
|
59
62
|
//# sourceMappingURL=associated.js.map
|
package/dist/associated.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"associated.js","sourceRoot":"","sources":["../src/associated.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"associated.js","sourceRoot":"","sources":["../src/associated.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC;IAC7C,gBAAgB;IAChB,qBAAqB;IACrB,oBAAoB;IACpB,oBAAoB;IACpB,oBAAoB;IACpB,kBAAkB;IAClB,mBAAmB;CACpB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE,KAAK,EAAE,QAAiB,EAAE;IAChE,UAAU,EAAE,EAAE,IAAI,EAAE,SAAkB,EAAE,KAAK,EAAE,QAAiB,EAAE;IAClE,QAAQ,EAAE,EAAE,IAAI,EAAE,cAAuB,EAAE,KAAK,EAAE,QAAiB,EAAE;CACtE,CAAC;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAE9C,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,OAAO,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,IAAY;IACzD,OAAO,uBAAuB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,IAAY;IAC7D,OAAO,iDAAiD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,IAAY;IACtD,OAAO,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAO,KAAK;SACT,KAAK,CAAC,MAAM,CAAC;SACb,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAC3D,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AASD,MAAM,UAAU,2BAA2B,CAAC,KAAkC;IAC5E,MAAM,aAAa,GAAG,oBAAoB,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,aAAa,SAAS,GAAG,aAAa,GAAG,oBAAoB,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;QAC5F,KAAK,uBAAuB;YAC1B,OAAO,aAAa,SAAS,GAAG,aAAa,YAAY,oBAAoB,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC;QACtG,KAAK,YAAY;YACf,OAAO,aAAa,SAAS,GAAG,aAAa,YAAY,CAAC;QAC5D,KAAK,UAAU;YACb,OAAO,aAAa,SAAS,GAAG,aAAa,UAAU,CAAC;QAC1D,KAAK,WAAW;YACd,OAAO,aAAa,SAAS,GAAG,aAAa,WAAW,CAAC;QAC3D,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,EAAE,GAAU,KAAK,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,MAA+B;IACtE,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,wBAAwB,CAAC,CAAC,IAAI,EAAE,CAAC;AACrE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { inspectMemorixBaseRecord, parseContentTypeMeta, parseRecordsJson, } from "@x12i/memorix-base-format";
|
|
7
|
+
import { BUILTIN_PROFILES } from "./profiles.js";
|
|
8
|
+
import { validateMemorixRecord } from "./validate.js";
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
10
11
|
function packageVersion() {
|
|
@@ -17,10 +18,7 @@ function packageVersion() {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
function docsDir() {
|
|
20
|
-
const candidates = [
|
|
21
|
-
join(__dirname, "..", "docs"),
|
|
22
|
-
join(process.cwd(), "docs"),
|
|
23
|
-
];
|
|
21
|
+
const candidates = [join(__dirname, "..", "docs"), join(process.cwd(), "docs")];
|
|
24
22
|
for (const dir of candidates) {
|
|
25
23
|
if (existsSync(dir))
|
|
26
24
|
return dir;
|
|
@@ -30,26 +28,24 @@ function docsDir() {
|
|
|
30
28
|
function printHelp() {
|
|
31
29
|
console.log(`memorix-format <command> [options]
|
|
32
30
|
|
|
33
|
-
Memorix
|
|
31
|
+
Memorix product format (layers on @x12i/memorix-base-format).
|
|
34
32
|
|
|
35
33
|
Commands:
|
|
36
|
-
validate [
|
|
37
|
-
inspect [
|
|
38
|
-
docs [name] List
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
validate [path|-] Validate with base + Memorix profile rules
|
|
35
|
+
inspect [path|-] Summarize root categories (via base)
|
|
36
|
+
docs [name] List or print packaged docs
|
|
37
|
+
profiles List built-in Memorix profiles
|
|
38
|
+
version
|
|
39
|
+
help
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
memorix-format validate - --content-type=snapshots < records.ndjson
|
|
51
|
-
memorix-format inspect ./record.json --json
|
|
52
|
-
memorix-format docs memorix-format.md
|
|
41
|
+
Options:
|
|
42
|
+
--profile=<name> Built-in profile: ${BUILTIN_PROFILES.join(" | ")}
|
|
43
|
+
--object-type=<name> Object type for profile resolution
|
|
44
|
+
--content-type=<ct> SCOPE hint (non-snapshots reject known buckets / associated*)
|
|
45
|
+
--meta=<file> Custom ContentTypeMeta JSON (overrides --profile)
|
|
46
|
+
--allow-legacy Warn on retired roots
|
|
47
|
+
--strict
|
|
48
|
+
--json
|
|
53
49
|
`);
|
|
54
50
|
}
|
|
55
51
|
function readFlag(args, name) {
|
|
@@ -60,15 +56,58 @@ function readFlag(args, name) {
|
|
|
60
56
|
function hasFlag(args, name) {
|
|
61
57
|
return args.includes(`--${name}`);
|
|
62
58
|
}
|
|
63
|
-
function
|
|
64
|
-
const path =
|
|
65
|
-
if (path)
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
function loadMeta(args) {
|
|
60
|
+
const path = readFlag(args, "meta");
|
|
61
|
+
if (!path)
|
|
62
|
+
return undefined;
|
|
63
|
+
const parsed = parseContentTypeMeta(JSON.parse(readFileSync(resolve(path), "utf8")));
|
|
64
|
+
if (!parsed.ok || !parsed.meta) {
|
|
65
|
+
throw new Error(`Invalid --meta: ${parsed.issues.map((i) => i.message).join("; ")}`);
|
|
66
|
+
}
|
|
67
|
+
return parsed.meta;
|
|
69
68
|
}
|
|
70
|
-
function
|
|
69
|
+
function collectInputFiles(pathArg) {
|
|
70
|
+
if (!pathArg || pathArg === "-")
|
|
71
|
+
return ["-"];
|
|
72
|
+
const abs = resolve(pathArg);
|
|
73
|
+
if (!existsSync(abs))
|
|
74
|
+
throw new Error(`Path not found: ${abs}`);
|
|
75
|
+
const st = statSync(abs);
|
|
76
|
+
if (st.isFile())
|
|
77
|
+
return [abs];
|
|
78
|
+
if (!st.isDirectory())
|
|
79
|
+
throw new Error(`Not a file or directory: ${abs}`);
|
|
80
|
+
const out = [];
|
|
81
|
+
const walk = (dir) => {
|
|
82
|
+
for (const name of readdirSync(dir)) {
|
|
83
|
+
const p = join(dir, name);
|
|
84
|
+
const s = statSync(p);
|
|
85
|
+
if (s.isDirectory())
|
|
86
|
+
walk(p);
|
|
87
|
+
else if (name.endsWith(".json") || name.endsWith(".ndjson"))
|
|
88
|
+
out.push(p);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(abs);
|
|
92
|
+
return out.sort();
|
|
93
|
+
}
|
|
94
|
+
function readFileOrStdin(path) {
|
|
95
|
+
if (path === "-")
|
|
96
|
+
return readFileSync(0, "utf8");
|
|
97
|
+
return readFileSync(path, "utf8");
|
|
98
|
+
}
|
|
99
|
+
function buildOptions(args) {
|
|
100
|
+
const profileRaw = readFlag(args, "profile");
|
|
101
|
+
const profile = profileRaw && BUILTIN_PROFILES.includes(profileRaw)
|
|
102
|
+
? profileRaw
|
|
103
|
+
: undefined;
|
|
104
|
+
if (profileRaw && !profile) {
|
|
105
|
+
throw new Error(`Unknown --profile=${profileRaw}; use ${BUILTIN_PROFILES.join("|")}`);
|
|
106
|
+
}
|
|
71
107
|
return {
|
|
108
|
+
meta: loadMeta(args),
|
|
109
|
+
profile,
|
|
110
|
+
objectType: readFlag(args, "object-type"),
|
|
72
111
|
contentType: readFlag(args, "content-type"),
|
|
73
112
|
allowLegacyEnvelope: hasFlag(args, "allow-legacy"),
|
|
74
113
|
strict: hasFlag(args, "strict"),
|
|
@@ -76,76 +115,87 @@ function buildValidateOptions(args) {
|
|
|
76
115
|
}
|
|
77
116
|
function runValidate(args) {
|
|
78
117
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
79
|
-
const file = positional[0];
|
|
80
|
-
const options = buildValidateOptions(args);
|
|
81
118
|
const asJson = hasFlag(args, "json");
|
|
82
|
-
let
|
|
119
|
+
let options;
|
|
83
120
|
try {
|
|
84
|
-
|
|
121
|
+
options = buildOptions(args);
|
|
85
122
|
}
|
|
86
123
|
catch (err) {
|
|
87
|
-
|
|
88
|
-
console.error(`Failed to parse input: ${message}`);
|
|
124
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
89
125
|
return 2;
|
|
90
126
|
}
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
let files;
|
|
128
|
+
try {
|
|
129
|
+
files = collectInputFiles(positional[0]);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
93
133
|
return 2;
|
|
94
134
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
const
|
|
135
|
+
if (files.length === 0) {
|
|
136
|
+
console.error("No .json / .ndjson files found");
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
const rows = [];
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
let records;
|
|
142
|
+
try {
|
|
143
|
+
records = parseRecordsJson(readFileOrStdin(file));
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.error(`Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < records.length; i++) {
|
|
150
|
+
const r = validateMemorixRecord(records[i], options);
|
|
151
|
+
rows.push({ file, index: i, ok: r.ok, recordId: r.recordId, issues: r.issues });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (rows.length === 0) {
|
|
155
|
+
console.error("No records found");
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
const ok = rows.every((r) => r.ok);
|
|
100
159
|
if (asJson) {
|
|
101
|
-
console.log(JSON.stringify({ ok, count:
|
|
160
|
+
console.log(JSON.stringify({ ok, count: rows.length, results: rows }, null, 2));
|
|
102
161
|
}
|
|
103
162
|
else {
|
|
104
|
-
for (const r of
|
|
105
|
-
|
|
106
|
-
if (r.ok) {
|
|
107
|
-
console.log(`OK ${label}`);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
console.log(`FAIL ${label}`);
|
|
111
|
-
}
|
|
163
|
+
for (const r of rows) {
|
|
164
|
+
console.log(`${r.ok ? "OK" : "FAIL"} ${r.file}#${r.index} ${r.recordId ?? ""}`.trimEnd());
|
|
112
165
|
for (const issue of r.issues) {
|
|
113
166
|
console.log(` [${issue.severity}] ${issue.path}: ${issue.message} (${issue.code})`);
|
|
114
167
|
}
|
|
115
168
|
}
|
|
116
|
-
console.log(`${ok ? "Passed" : "Failed"}: ${
|
|
169
|
+
console.log(`${ok ? "Passed" : "Failed"}: ${rows.filter((r) => r.ok).length}/${rows.length}`);
|
|
117
170
|
}
|
|
118
171
|
return ok ? 0 : 1;
|
|
119
172
|
}
|
|
120
173
|
function runInspect(args) {
|
|
121
174
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
122
|
-
const file = positional[0];
|
|
123
175
|
const asJson = hasFlag(args, "json");
|
|
124
|
-
let
|
|
176
|
+
let files;
|
|
125
177
|
try {
|
|
126
|
-
|
|
178
|
+
files = collectInputFiles(positional[0]);
|
|
127
179
|
}
|
|
128
180
|
catch (err) {
|
|
129
|
-
|
|
130
|
-
console.error(`Failed to parse input: ${message}`);
|
|
181
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
131
182
|
return 2;
|
|
132
183
|
}
|
|
133
|
-
const inspections =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
184
|
+
const inspections = [];
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
const records = parseRecordsJson(readFileOrStdin(file));
|
|
187
|
+
for (let i = 0; i < records.length; i++) {
|
|
188
|
+
inspections.push({ file, index: i, ...inspectMemorixBaseRecord(records[i]) });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
137
191
|
if (asJson) {
|
|
138
192
|
console.log(JSON.stringify({ count: inspections.length, records: inspections }, null, 2));
|
|
139
|
-
return 0;
|
|
140
193
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log(` legacy roots: ${row.legacyRoots.join(", ")}`);
|
|
147
|
-
if (row.unknownRoots.length)
|
|
148
|
-
console.log(` unknown roots: ${row.unknownRoots.join(", ")}`);
|
|
194
|
+
else {
|
|
195
|
+
for (const row of inspections) {
|
|
196
|
+
console.log(`#${row.file}:${row.index} recordId=${row.recordId ?? "(missing)"}`);
|
|
197
|
+
console.log(` open: ${row.categories.open.join(", ") || "(none)"}`);
|
|
198
|
+
}
|
|
149
199
|
}
|
|
150
200
|
return 0;
|
|
151
201
|
}
|
|
@@ -153,11 +203,11 @@ function runDocs(args) {
|
|
|
153
203
|
const dir = docsDir();
|
|
154
204
|
const name = args.find((a) => !a.startsWith("--"));
|
|
155
205
|
if (!existsSync(dir)) {
|
|
156
|
-
console.error(`Docs
|
|
206
|
+
console.error(`Docs not found: ${dir}`);
|
|
157
207
|
return 1;
|
|
158
208
|
}
|
|
159
209
|
if (!name) {
|
|
160
|
-
console.log(`Packaged
|
|
210
|
+
console.log(`Packaged memorix-format docs (${dir}):`);
|
|
161
211
|
for (const file of readdirSync(dir).sort()) {
|
|
162
212
|
if (file.endsWith(".md"))
|
|
163
213
|
console.log(` ${file}`);
|
|
@@ -168,7 +218,6 @@ function runDocs(args) {
|
|
|
168
218
|
const path = join(dir, base);
|
|
169
219
|
if (!existsSync(path)) {
|
|
170
220
|
console.error(`Doc not found: ${base}`);
|
|
171
|
-
console.error(`Available: ${readdirSync(dir).filter((f) => f.endsWith(".md")).join(", ")}`);
|
|
172
221
|
return 1;
|
|
173
222
|
}
|
|
174
223
|
process.stdout.write(readFileSync(path, "utf8"));
|
|
@@ -184,6 +233,10 @@ async function main(argv) {
|
|
|
184
233
|
console.log(packageVersion());
|
|
185
234
|
return 0;
|
|
186
235
|
}
|
|
236
|
+
if (cmd === "profiles") {
|
|
237
|
+
console.log(BUILTIN_PROFILES.join("\n"));
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
187
240
|
if (cmd === "validate")
|
|
188
241
|
return runValidate(args);
|
|
189
242
|
if (cmd === "inspect")
|
|
@@ -194,6 +247,5 @@ async function main(argv) {
|
|
|
194
247
|
printHelp();
|
|
195
248
|
return 2;
|
|
196
249
|
}
|
|
197
|
-
|
|
198
|
-
process.exit(exitCode);
|
|
250
|
+
process.exit(await main(process.argv.slice(2)));
|
|
199
251
|
//# sourceMappingURL=cli.js.map
|