@metaobjectsdev/sdk 0.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +189 -0
- package/README.md +32 -0
- package/dist/agent-docs/body.d.ts +2 -0
- package/dist/agent-docs/body.d.ts.map +1 -0
- package/dist/agent-docs/body.js +563 -0
- package/dist/agent-docs/body.js.map +1 -0
- package/dist/agent-docs/content-hash.d.ts +8 -0
- package/dist/agent-docs/content-hash.d.ts.map +1 -0
- package/dist/agent-docs/content-hash.js +23 -0
- package/dist/agent-docs/content-hash.js.map +1 -0
- package/dist/agent-docs/index.d.ts +3 -0
- package/dist/agent-docs/index.d.ts.map +1 -0
- package/dist/agent-docs/index.js +4 -0
- package/dist/agent-docs/index.js.map +1 -0
- package/dist/config.d.ts +113 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +53 -0
- package/dist/config.js.map +1 -0
- package/dist/forge-types.d.ts +47 -0
- package/dist/forge-types.d.ts.map +1 -0
- package/dist/forge-types.js +133 -0
- package/dist/forge-types.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +30 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +105 -0
- package/dist/memory.js.map +1 -0
- package/dist/package.d.ts +65 -0
- package/dist/package.d.ts.map +1 -0
- package/dist/package.js +105 -0
- package/dist/package.js.map +1 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +26 -0
- package/dist/paths.js.map +1 -0
- package/dist/records/any.d.ts +467 -0
- package/dist/records/any.d.ts.map +1 -0
- package/dist/records/any.js +14 -0
- package/dist/records/any.js.map +1 -0
- package/dist/records/convention.d.ts +90 -0
- package/dist/records/convention.d.ts.map +1 -0
- package/dist/records/convention.js +9 -0
- package/dist/records/convention.js.map +1 -0
- package/dist/records/core.d.ts +84 -0
- package/dist/records/core.d.ts.map +1 -0
- package/dist/records/core.js +47 -0
- package/dist/records/core.js.map +1 -0
- package/dist/records/decision.d.ts +90 -0
- package/dist/records/decision.d.ts.map +1 -0
- package/dist/records/decision.js +9 -0
- package/dist/records/decision.js.map +1 -0
- package/dist/records/failure.d.ts +93 -0
- package/dist/records/failure.d.ts.map +1 -0
- package/dist/records/failure.js +10 -0
- package/dist/records/failure.js.map +1 -0
- package/dist/records/glossary.d.ts +111 -0
- package/dist/records/glossary.d.ts.map +1 -0
- package/dist/records/glossary.js +14 -0
- package/dist/records/glossary.js.map +1 -0
- package/dist/records/principle.d.ts +99 -0
- package/dist/records/principle.d.ts.map +1 -0
- package/dist/records/principle.js +12 -0
- package/dist/records/principle.js.map +1 -0
- package/dist/storage/errors.d.ts +14 -0
- package/dist/storage/errors.d.ts.map +1 -0
- package/dist/storage/errors.js +27 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/lifecycle.d.ts +5 -0
- package/dist/storage/lifecycle.d.ts.map +1 -0
- package/dist/storage/lifecycle.js +27 -0
- package/dist/storage/lifecycle.js.map +1 -0
- package/dist/storage/list.d.ts +8 -0
- package/dist/storage/list.d.ts.map +1 -0
- package/dist/storage/list.js +42 -0
- package/dist/storage/list.js.map +1 -0
- package/dist/storage/read.d.ts +9 -0
- package/dist/storage/read.d.ts.map +1 -0
- package/dist/storage/read.js +43 -0
- package/dist/storage/read.js.map +1 -0
- package/dist/storage/write.d.ts +8 -0
- package/dist/storage/write.d.ts.map +1 -0
- package/dist/storage/write.js +20 -0
- package/dist/storage/write.js.map +1 -0
- package/dist/workspace.d.ts +49 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +280 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +48 -0
- package/src/agent-docs/body.ts +562 -0
- package/src/agent-docs/content-hash.ts +25 -0
- package/src/agent-docs/index.ts +8 -0
- package/src/config.ts +69 -0
- package/src/forge-types.ts +167 -0
- package/src/index.ts +98 -0
- package/src/memory.ts +116 -0
- package/src/package.ts +120 -0
- package/src/paths.ts +30 -0
- package/src/records/any.ts +15 -0
- package/src/records/convention.ts +10 -0
- package/src/records/core.ts +55 -0
- package/src/records/decision.ts +10 -0
- package/src/records/failure.ts +11 -0
- package/src/records/glossary.ts +15 -0
- package/src/records/principle.ts +13 -0
- package/src/storage/errors.ts +23 -0
- package/src/storage/index.ts +10 -0
- package/src/storage/lifecycle.ts +38 -0
- package/src/storage/list.ts +53 -0
- package/src/storage/read.ts +54 -0
- package/src/storage/write.ts +32 -0
- package/src/workspace.ts +342 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Meta Forge type + attribute name constants. Centralized for typo-safety
|
|
2
|
+
// per CLAUDE.md ("Named constants for metamodel strings — always") and so
|
|
3
|
+
// downstream readers (codegen prompts, MCP exposers) have one source of
|
|
4
|
+
// truth. See SP5 §4 for the design rationale.
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// New top-level types registered by registerForgeTypes()
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const FORGE_TYPE_DECISION = "decision";
|
|
11
|
+
export const FORGE_TYPE_PRINCIPLE = "principle";
|
|
12
|
+
export const FORGE_TYPE_CONVENTION = "convention";
|
|
13
|
+
export const FORGE_TYPE_GLOSSARY = "glossary";
|
|
14
|
+
export const FORGE_TYPE_FAILURE = "failure";
|
|
15
|
+
|
|
16
|
+
export const FORGE_TYPES = [
|
|
17
|
+
FORGE_TYPE_DECISION,
|
|
18
|
+
FORGE_TYPE_PRINCIPLE,
|
|
19
|
+
FORGE_TYPE_CONVENTION,
|
|
20
|
+
FORGE_TYPE_GLOSSARY,
|
|
21
|
+
FORGE_TYPE_FAILURE,
|
|
22
|
+
] as const;
|
|
23
|
+
export type ForgeType = (typeof FORGE_TYPES)[number];
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Subtypes per type (open lists; "base" is always valid)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export const FORGE_DECISION_SUBTYPES = ["base", "global", "scoped"] as const;
|
|
30
|
+
export const FORGE_PRINCIPLE_SUBTYPES = ["base", "advisory", "enforced"] as const;
|
|
31
|
+
export const FORGE_CONVENTION_SUBTYPES = ["base"] as const;
|
|
32
|
+
export const FORGE_GLOSSARY_SUBTYPES = ["base"] as const;
|
|
33
|
+
export const FORGE_FAILURE_SUBTYPES = ["base"] as const;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// @forge* attribute names (camelCase, no separator)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
// Universal (any forge-related node)
|
|
40
|
+
export const FORGE_ATTR_CONFIDENCE = "forgeConfidence";
|
|
41
|
+
export const FORGE_ATTR_SOURCE = "forgeSource";
|
|
42
|
+
export const FORGE_ATTR_CAPTURED_AT = "forgeCapturedAt";
|
|
43
|
+
export const FORGE_ATTR_LAST_VALIDATED_COMMIT = "forgeLastValidatedCommit";
|
|
44
|
+
|
|
45
|
+
// Object/entity
|
|
46
|
+
export const FORGE_ATTR_PRIMARY_LOCATION = "forgePrimaryLocation";
|
|
47
|
+
export const FORGE_ATTR_OCCURRENCES = "forgeOccurrences";
|
|
48
|
+
|
|
49
|
+
// Decision
|
|
50
|
+
export const FORGE_ATTR_RATIONALE = "forgeRationale";
|
|
51
|
+
export const FORGE_ATTR_ALTERNATIVES = "forgeAlternatives";
|
|
52
|
+
export const FORGE_ATTR_SCOPE = "forgeScope";
|
|
53
|
+
|
|
54
|
+
// Principle
|
|
55
|
+
export const FORGE_ATTR_STATEMENT = "forgeStatement";
|
|
56
|
+
export const FORGE_ATTR_ENFORCEMENT = "forgeEnforcement";
|
|
57
|
+
|
|
58
|
+
// Convention
|
|
59
|
+
export const FORGE_ATTR_PATTERN_DESCRIPTION = "forgePatternDescription";
|
|
60
|
+
export const FORGE_ATTR_EXAMPLES = "forgeExamples";
|
|
61
|
+
export const FORGE_ATTR_COUNTER_EXAMPLES = "forgeCounterExamples";
|
|
62
|
+
export const FORGE_ATTR_APPLIES_TO = "forgeAppliesTo";
|
|
63
|
+
|
|
64
|
+
// Glossary
|
|
65
|
+
export const FORGE_ATTR_TERM = "forgeTerm";
|
|
66
|
+
export const FORGE_ATTR_SYNONYMS = "forgeSynonyms";
|
|
67
|
+
export const FORGE_ATTR_DEFINITION = "forgeDefinition";
|
|
68
|
+
export const FORGE_ATTR_CODE_ANCHORS = "forgeCodeAnchors";
|
|
69
|
+
export const FORGE_ATTR_SEE_ALSO = "forgeSeeAlso";
|
|
70
|
+
|
|
71
|
+
// Failure
|
|
72
|
+
export const FORGE_ATTR_WHAT_WAS_TRIED = "forgeWhatWasTried";
|
|
73
|
+
export const FORGE_ATTR_WHY_IT_FAILED = "forgeWhyItFailed";
|
|
74
|
+
|
|
75
|
+
export const FORGE_ATTRS = [
|
|
76
|
+
FORGE_ATTR_CONFIDENCE,
|
|
77
|
+
FORGE_ATTR_SOURCE,
|
|
78
|
+
FORGE_ATTR_CAPTURED_AT,
|
|
79
|
+
FORGE_ATTR_LAST_VALIDATED_COMMIT,
|
|
80
|
+
FORGE_ATTR_PRIMARY_LOCATION,
|
|
81
|
+
FORGE_ATTR_OCCURRENCES,
|
|
82
|
+
FORGE_ATTR_RATIONALE,
|
|
83
|
+
FORGE_ATTR_ALTERNATIVES,
|
|
84
|
+
FORGE_ATTR_SCOPE,
|
|
85
|
+
FORGE_ATTR_STATEMENT,
|
|
86
|
+
FORGE_ATTR_ENFORCEMENT,
|
|
87
|
+
FORGE_ATTR_PATTERN_DESCRIPTION,
|
|
88
|
+
FORGE_ATTR_EXAMPLES,
|
|
89
|
+
FORGE_ATTR_COUNTER_EXAMPLES,
|
|
90
|
+
FORGE_ATTR_APPLIES_TO,
|
|
91
|
+
FORGE_ATTR_TERM,
|
|
92
|
+
FORGE_ATTR_SYNONYMS,
|
|
93
|
+
FORGE_ATTR_DEFINITION,
|
|
94
|
+
FORGE_ATTR_CODE_ANCHORS,
|
|
95
|
+
FORGE_ATTR_SEE_ALSO,
|
|
96
|
+
FORGE_ATTR_WHAT_WAS_TRIED,
|
|
97
|
+
FORGE_ATTR_WHY_IT_FAILED,
|
|
98
|
+
] as const;
|
|
99
|
+
export type ForgeAttr = (typeof FORGE_ATTRS)[number];
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// registerForgeTypes
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
import {
|
|
106
|
+
type ChildRule,
|
|
107
|
+
type TypeDefinition,
|
|
108
|
+
TypeId,
|
|
109
|
+
TypeRegistry,
|
|
110
|
+
MetaData,
|
|
111
|
+
TYPE_ATTR,
|
|
112
|
+
CHILD_RULE_WILDCARD,
|
|
113
|
+
} from "@metaobjectsdev/metadata";
|
|
114
|
+
|
|
115
|
+
/** Minimal concrete MetaData subclass used for all forge descriptive nodes. */
|
|
116
|
+
class MetaForgeNode extends MetaData {}
|
|
117
|
+
|
|
118
|
+
function wildcardOf(childType: string): ChildRule {
|
|
119
|
+
return {
|
|
120
|
+
childType,
|
|
121
|
+
childSubType: CHILD_RULE_WILDCARD,
|
|
122
|
+
childName: CHILD_RULE_WILDCARD,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function def(
|
|
127
|
+
type: string,
|
|
128
|
+
subType: string,
|
|
129
|
+
description: string,
|
|
130
|
+
childRules: ChildRule[],
|
|
131
|
+
): TypeDefinition {
|
|
132
|
+
return {
|
|
133
|
+
typeId: new TypeId(type, subType),
|
|
134
|
+
description,
|
|
135
|
+
factory: (typeId, name) => new MetaForgeNode(typeId, name),
|
|
136
|
+
childRules,
|
|
137
|
+
attributes: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register Meta Forge's five descriptive top-level types into the given
|
|
143
|
+
* registry. Must be called AFTER `registerCoreTypes()`.
|
|
144
|
+
*
|
|
145
|
+
* Each type accepts `attr` children for expanded-form @forge* attributes.
|
|
146
|
+
* Loader's default permissive mode tolerates these new types as children
|
|
147
|
+
* of the metadata wrapper (unknowns are warned, not errored).
|
|
148
|
+
*/
|
|
149
|
+
export function registerForgeTypes(registry: TypeRegistry): void {
|
|
150
|
+
const forgeChildRules = [wildcardOf(TYPE_ATTR)];
|
|
151
|
+
|
|
152
|
+
for (const subType of FORGE_DECISION_SUBTYPES) {
|
|
153
|
+
registry.register(def(FORGE_TYPE_DECISION, subType, `Forge decision (${subType})`, forgeChildRules));
|
|
154
|
+
}
|
|
155
|
+
for (const subType of FORGE_PRINCIPLE_SUBTYPES) {
|
|
156
|
+
registry.register(def(FORGE_TYPE_PRINCIPLE, subType, `Forge principle (${subType})`, forgeChildRules));
|
|
157
|
+
}
|
|
158
|
+
for (const subType of FORGE_CONVENTION_SUBTYPES) {
|
|
159
|
+
registry.register(def(FORGE_TYPE_CONVENTION, subType, `Forge convention (${subType})`, forgeChildRules));
|
|
160
|
+
}
|
|
161
|
+
for (const subType of FORGE_GLOSSARY_SUBTYPES) {
|
|
162
|
+
registry.register(def(FORGE_TYPE_GLOSSARY, subType, `Forge glossary entry (${subType})`, forgeChildRules));
|
|
163
|
+
}
|
|
164
|
+
for (const subType of FORGE_FAILURE_SUBTYPES) {
|
|
165
|
+
registry.register(def(FORGE_TYPE_FAILURE, subType, `Forge failure record (${subType})`, forgeChildRules));
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Records
|
|
2
|
+
export { RecordCore, RecordType, RecordSource } from "./records/core.js";
|
|
3
|
+
export { ConventionRecord } from "./records/convention.js";
|
|
4
|
+
export { DecisionRecord } from "./records/decision.js";
|
|
5
|
+
export { PrincipleRecord } from "./records/principle.js";
|
|
6
|
+
export { GlossaryRecord } from "./records/glossary.js";
|
|
7
|
+
export { FailureRecord } from "./records/failure.js";
|
|
8
|
+
export { AnyRecord } from "./records/any.js";
|
|
9
|
+
|
|
10
|
+
// Storage
|
|
11
|
+
export {
|
|
12
|
+
readRecord,
|
|
13
|
+
recordExists,
|
|
14
|
+
writeRecord,
|
|
15
|
+
removeRecord,
|
|
16
|
+
listRecords,
|
|
17
|
+
promoteRecord,
|
|
18
|
+
supersede,
|
|
19
|
+
MetaForgeRecordNotFoundError,
|
|
20
|
+
MetaForgeAlreadyPromotedError,
|
|
21
|
+
MetaForgeRecordParseError,
|
|
22
|
+
} from "./storage/index.js";
|
|
23
|
+
export type { ListOptions } from "./storage/index.js";
|
|
24
|
+
|
|
25
|
+
// Paths
|
|
26
|
+
export { recordPath, resolveMetaRoot } from "./paths.js";
|
|
27
|
+
|
|
28
|
+
// Config
|
|
29
|
+
export { ConfigSchema, DEFAULT_CONFIG, loadConfig, saveConfig } from "./config.js";
|
|
30
|
+
export type { Config } from "./config.js";
|
|
31
|
+
|
|
32
|
+
// Meta Forge metadata types + attribute name constants (registered into a
|
|
33
|
+
// TypeRegistry to let Loader parse decision/principle/etc. children + the
|
|
34
|
+
// @forge* attribute namespace).
|
|
35
|
+
export {
|
|
36
|
+
// Type names
|
|
37
|
+
FORGE_TYPE_DECISION,
|
|
38
|
+
FORGE_TYPE_PRINCIPLE,
|
|
39
|
+
FORGE_TYPE_CONVENTION,
|
|
40
|
+
FORGE_TYPE_GLOSSARY,
|
|
41
|
+
FORGE_TYPE_FAILURE,
|
|
42
|
+
FORGE_TYPES,
|
|
43
|
+
// Subtypes
|
|
44
|
+
FORGE_DECISION_SUBTYPES,
|
|
45
|
+
FORGE_PRINCIPLE_SUBTYPES,
|
|
46
|
+
FORGE_CONVENTION_SUBTYPES,
|
|
47
|
+
FORGE_GLOSSARY_SUBTYPES,
|
|
48
|
+
FORGE_FAILURE_SUBTYPES,
|
|
49
|
+
// Attribute names
|
|
50
|
+
FORGE_ATTR_CONFIDENCE,
|
|
51
|
+
FORGE_ATTR_SOURCE,
|
|
52
|
+
FORGE_ATTR_CAPTURED_AT,
|
|
53
|
+
FORGE_ATTR_LAST_VALIDATED_COMMIT,
|
|
54
|
+
FORGE_ATTR_PRIMARY_LOCATION,
|
|
55
|
+
FORGE_ATTR_OCCURRENCES,
|
|
56
|
+
FORGE_ATTR_RATIONALE,
|
|
57
|
+
FORGE_ATTR_ALTERNATIVES,
|
|
58
|
+
FORGE_ATTR_SCOPE,
|
|
59
|
+
FORGE_ATTR_STATEMENT,
|
|
60
|
+
FORGE_ATTR_ENFORCEMENT,
|
|
61
|
+
FORGE_ATTR_PATTERN_DESCRIPTION,
|
|
62
|
+
FORGE_ATTR_EXAMPLES,
|
|
63
|
+
FORGE_ATTR_COUNTER_EXAMPLES,
|
|
64
|
+
FORGE_ATTR_APPLIES_TO,
|
|
65
|
+
FORGE_ATTR_TERM,
|
|
66
|
+
FORGE_ATTR_SYNONYMS,
|
|
67
|
+
FORGE_ATTR_DEFINITION,
|
|
68
|
+
FORGE_ATTR_CODE_ANCHORS,
|
|
69
|
+
FORGE_ATTR_SEE_ALSO,
|
|
70
|
+
FORGE_ATTR_WHAT_WAS_TRIED,
|
|
71
|
+
FORGE_ATTR_WHY_IT_FAILED,
|
|
72
|
+
FORGE_ATTRS,
|
|
73
|
+
// Registration helper
|
|
74
|
+
registerForgeTypes,
|
|
75
|
+
} from "./forge-types.js";
|
|
76
|
+
export type { ForgeType, ForgeAttr } from "./forge-types.js";
|
|
77
|
+
|
|
78
|
+
// Memory loader — read metaobjects/ into a MetaData tree
|
|
79
|
+
// (workspace-aware: walks extends: deps via pnpm-workspace.yaml or
|
|
80
|
+
// package.json workspaces field if present)
|
|
81
|
+
export { loadMemory, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR } from "./memory.js";
|
|
82
|
+
|
|
83
|
+
// Workspace discovery — finds peer metadata packages in a monorepo
|
|
84
|
+
export { discoverWorkspace, resolveExtendsOrder, packageLabel } from "./workspace.js";
|
|
85
|
+
export type { Workspace, WorkspacePackage } from "./workspace.js";
|
|
86
|
+
|
|
87
|
+
// Package manifest — the v0.3 package.meta.json model. Three-field manifest
|
|
88
|
+
// (name, version, extends) that defines a metadata package's identity and
|
|
89
|
+
// upstream dependencies. The package's metadata tree IS its public API;
|
|
90
|
+
// there is no exports field. See docs/strategy/2026-05-12-v0.3-ai-first-
|
|
91
|
+
// metadata-loading.md.
|
|
92
|
+
export {
|
|
93
|
+
PackageManifestSchema,
|
|
94
|
+
readPackageManifest,
|
|
95
|
+
resolveMetaobjectsPackage,
|
|
96
|
+
PACKAGE_MANIFEST_FILE,
|
|
97
|
+
} from "./package.js";
|
|
98
|
+
export type { PackageManifest } from "./package.js";
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { FileMetaDataLoader } from "@metaobjectsdev/metadata/core";
|
|
4
|
+
import {
|
|
5
|
+
TypeRegistry,
|
|
6
|
+
registerCoreTypes,
|
|
7
|
+
type MetaRoot,
|
|
8
|
+
} from "@metaobjectsdev/metadata";
|
|
9
|
+
import { registerForgeTypes } from "./forge-types.js";
|
|
10
|
+
import { discoverWorkspace, resolveExtendsOrder } from "./workspace.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default directory name (relative to project root) where metadata JSON files
|
|
14
|
+
* are scanned. Scaffold via `meta init`; the directory is committed to git.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_METADATA_DIR = "metaobjects";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default directory name (relative to project root) for MetaObjects' own
|
|
20
|
+
* runtime state: config.json, .gen-state/, package.meta.json, agent docs.
|
|
21
|
+
* Scaffold via `meta init`; most contents are committed to git.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_METAOBJECTS_DIR = ".metaobjects";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load all metadata files from `<repoRoot>/metaobjects/` into a single
|
|
27
|
+
* MetaData. If `<repoRoot>/.meta/package.meta.json` declares `extends:` deps
|
|
28
|
+
* and a workspace can be discovered (pnpm-workspace.yaml or package.json
|
|
29
|
+
* workspaces), peer packages are loaded too in topological dep-first order.
|
|
30
|
+
*
|
|
31
|
+
* Excludes `_pending/`. Registers metaobjects core types plus Meta Forge's
|
|
32
|
+
* descriptive top-level types (decision, principle, etc.) so mixed content
|
|
33
|
+
* parses without warnings.
|
|
34
|
+
*
|
|
35
|
+
* Throws if `metaobjects/` doesn't exist (callers should run `meta init`).
|
|
36
|
+
*
|
|
37
|
+
* @param repoRoot The project's working-directory root (e.g. process.cwd()).
|
|
38
|
+
* `loadMemory` resolves `metaobjects/` and (if workspace-aware) the
|
|
39
|
+
* transitive `extends:` graph automatically.
|
|
40
|
+
*/
|
|
41
|
+
export async function loadMemory(repoRoot: string): Promise<MetaRoot> {
|
|
42
|
+
const registry = new TypeRegistry();
|
|
43
|
+
registerCoreTypes(registry);
|
|
44
|
+
registerForgeTypes(registry);
|
|
45
|
+
|
|
46
|
+
// Collect all metadata file paths to load. Order matters for the parser's
|
|
47
|
+
// deferred-resolution pass (it parses in array order, then resolves supers
|
|
48
|
+
// against the merged tree afterwards) — dep packages first, current last.
|
|
49
|
+
const paths = await collectMetadataPaths(repoRoot);
|
|
50
|
+
|
|
51
|
+
const loader = new FileMetaDataLoader({ registry });
|
|
52
|
+
const result = await loader.loadFiles(paths);
|
|
53
|
+
|
|
54
|
+
if (result.errors.length > 0) {
|
|
55
|
+
const first = result.errors[0]!;
|
|
56
|
+
throw first;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.root;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Dep packages' metaobjects/ files first (topological order), then current.
|
|
63
|
+
async function collectMetadataPaths(repoRoot: string): Promise<string[]> {
|
|
64
|
+
const currentMetaDir = join(repoRoot, ".meta");
|
|
65
|
+
const ws = await discoverWorkspace(repoRoot);
|
|
66
|
+
|
|
67
|
+
// Workspace path: walk extends, load dep metaobjects/ dirs first
|
|
68
|
+
if (ws !== undefined) {
|
|
69
|
+
const currentPkg = ws.packages.find((p) => p.metaDir === currentMetaDir);
|
|
70
|
+
if (currentPkg !== undefined && currentPkg.manifest.extends.length > 0) {
|
|
71
|
+
const ordered = resolveExtendsOrder(ws, currentMetaDir);
|
|
72
|
+
const paths: string[] = [];
|
|
73
|
+
for (const pkg of ordered) {
|
|
74
|
+
// Each workspace package's metadata lives alongside its .meta/ dir
|
|
75
|
+
const pkgRoot = join(pkg.metaDir, "..");
|
|
76
|
+
paths.push(...(await listJsonFiles(join(pkgRoot, DEFAULT_METADATA_DIR))));
|
|
77
|
+
}
|
|
78
|
+
return paths;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Single-package path: scan metaobjects/ at the project root
|
|
83
|
+
return listJsonFiles(join(repoRoot, DEFAULT_METADATA_DIR));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Recursively list *.json files under a directory, excluding _pending/ at
|
|
88
|
+
* any level. Subdirectories (e.g. projections/) are walked depth-first.
|
|
89
|
+
* Files within a directory are sorted alphabetically for deterministic load
|
|
90
|
+
* order; subdirectories are visited after files at the same level.
|
|
91
|
+
*/
|
|
92
|
+
async function listJsonFiles(dir: string): Promise<string[]> {
|
|
93
|
+
let entries: string[];
|
|
94
|
+
try {
|
|
95
|
+
entries = await readdir(dir);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new Error(`loadMemory: cannot read ${dir}: ${(err as Error).message}`);
|
|
98
|
+
}
|
|
99
|
+
const paths: string[] = [];
|
|
100
|
+
const subdirs: string[] = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry === "_pending") continue;
|
|
103
|
+
const full = join(dir, entry);
|
|
104
|
+
const s = await stat(full);
|
|
105
|
+
if (s.isDirectory()) {
|
|
106
|
+
subdirs.push(full);
|
|
107
|
+
} else if (s.isFile() && entry.endsWith(".json")) {
|
|
108
|
+
paths.push(full);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Recurse into subdirectories after collecting files at this level
|
|
112
|
+
for (const sub of subdirs.sort()) {
|
|
113
|
+
paths.push(...(await listJsonFiles(sub)));
|
|
114
|
+
}
|
|
115
|
+
return paths;
|
|
116
|
+
}
|
package/src/package.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Per the v0.3 AI-first metadata loading strategy: a package.meta.json is the
|
|
6
|
+
// boundary between metadata bundles. Three fields total — name, version,
|
|
7
|
+
// extends. No exports list; no @private. The package's metadata tree IS its
|
|
8
|
+
// public API. See docs/strategy/2026-05-12-v0.3-ai-first-metadata-loading.md
|
|
9
|
+
// §4.3.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates a package version string. npm-compatible: semver basics
|
|
13
|
+
* (major.minor.patch with optional prerelease/build). Not a full semver
|
|
14
|
+
* parser — strict-enough for now.
|
|
15
|
+
*/
|
|
16
|
+
const VersionStringSchema = z.string().regex(
|
|
17
|
+
/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/,
|
|
18
|
+
"package version must follow semver (e.g. 1.2.0 or 1.2.0-alpha.1)",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates a package extends entry. Either a bare package name or
|
|
23
|
+
* a name with a semver range — npm-style. Range parsing is shallow
|
|
24
|
+
* for v0.3 v0.1; full range resolution lands when multi-package
|
|
25
|
+
* loading actually ships.
|
|
26
|
+
*/
|
|
27
|
+
const ExtendsEntrySchema = z.string().min(1);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates a canonical metaobjects package ref: snake_case segments
|
|
31
|
+
* separated by `::`. Examples: `trainer_website`, `acme::common::user_mgmt`.
|
|
32
|
+
* Used in metadata file refs (extends:, super:) cross-language.
|
|
33
|
+
*/
|
|
34
|
+
const MetaobjectsPackageSchema = z.string().regex(
|
|
35
|
+
/^[a-z][a-z0-9_]*(::[a-z][a-z0-9_]*)*$/,
|
|
36
|
+
"metaobjectsPackage must be snake_case segments separated by :: (e.g. acme::common::user_mgmt)",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The package.meta.json schema. Authoring this is the one-time cost of
|
|
41
|
+
* defining a metadata package; subsequent edits should be rare.
|
|
42
|
+
*/
|
|
43
|
+
export const PackageManifestSchema = z.object({
|
|
44
|
+
/**
|
|
45
|
+
* Canonical name of the package in the language's native package manager.
|
|
46
|
+
* Conventionally `@scope/name` (npm-style) for TS, but bare names also
|
|
47
|
+
* work. Each language's package config (pom.xml, .csproj, pyproject.toml)
|
|
48
|
+
* uses its own native field for this — only TS uses `name` here.
|
|
49
|
+
*/
|
|
50
|
+
name: z.string().min(1),
|
|
51
|
+
|
|
52
|
+
/** Semver version of this package. */
|
|
53
|
+
version: VersionStringSchema,
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional cross-language canonical ref for this package, used inside
|
|
57
|
+
* metadata files (extends:, super:) and for cross-language consumption.
|
|
58
|
+
* If omitted, derived from `name` via a simple rule: `@scope/name` →
|
|
59
|
+
* `scope::name` (strip @, replace `/` with `::`). Hyphens stay as-is in
|
|
60
|
+
* the auto-derived form — for hierarchical naming users declare this
|
|
61
|
+
* field explicitly.
|
|
62
|
+
*
|
|
63
|
+
* Per v0.3 strategy doc §8.2.
|
|
64
|
+
*/
|
|
65
|
+
metaobjectsPackage: MetaobjectsPackageSchema.optional(),
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Other packages whose metadata trees this package extends. Each entry is
|
|
69
|
+
* a package name; the loader resolves it to the matching peer package.
|
|
70
|
+
* Empty/omitted means this package has no upstream dependencies.
|
|
71
|
+
*/
|
|
72
|
+
extends: z.array(ExtendsEntrySchema).default([]),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export type PackageManifest = z.infer<typeof PackageManifestSchema>;
|
|
76
|
+
|
|
77
|
+
export const PACKAGE_MANIFEST_FILE = "package.meta.json";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read a package.meta.json from a given .meta/ root, if present.
|
|
81
|
+
* Returns undefined when the file isn't there — packages are optional in
|
|
82
|
+
* v0.3 v0.1 (loadMemory still works for single-package usage without one,
|
|
83
|
+
* and forge init scaffolds one but doesn't enforce its presence).
|
|
84
|
+
*/
|
|
85
|
+
export async function readPackageManifest(metaDir: string): Promise<PackageManifest | undefined> {
|
|
86
|
+
const path = join(metaDir, PACKAGE_MANIFEST_FILE);
|
|
87
|
+
try {
|
|
88
|
+
const s = await stat(path);
|
|
89
|
+
if (!s.isFile()) return undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const raw = await readFile(path, "utf8");
|
|
94
|
+
return PackageManifestSchema.parse(JSON.parse(raw));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Return the canonical metaobjects package ref for a manifest. Uses the
|
|
99
|
+
* explicit `metaobjectsPackage` field if set; otherwise derives from `name`
|
|
100
|
+
* via the simple rule: `@scope/foo-bar` → `scope::foo-bar` (strip @,
|
|
101
|
+
* replace `/` with `::`, leave hyphens). For hierarchical or non-trivial
|
|
102
|
+
* naming users declare `metaobjectsPackage` explicitly.
|
|
103
|
+
*
|
|
104
|
+
* Returns undefined if `name` itself isn't a valid bare name (e.g. has
|
|
105
|
+
* uppercase chars) and no explicit ref is set — the caller should treat
|
|
106
|
+
* this as an authoring error.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveMetaobjectsPackage(manifest: PackageManifest): string | undefined {
|
|
109
|
+
if (manifest.metaobjectsPackage !== undefined) return manifest.metaobjectsPackage;
|
|
110
|
+
// Auto-derive from name: strip leading @, replace / with ::, lowercase
|
|
111
|
+
// remains untouched — npm names are already lowercase per registry rules.
|
|
112
|
+
const stripped = manifest.name.replace(/^@/, "");
|
|
113
|
+
const derived = stripped.replace(/\//g, "::");
|
|
114
|
+
// Validate that the derived form is a legal canonical ref. If not,
|
|
115
|
+
// return undefined so the caller can surface a clear error.
|
|
116
|
+
if (!/^[a-z][a-z0-9_-]*(::[a-z][a-z0-9_-]*)*$/.test(derived)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return derived;
|
|
120
|
+
}
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { join, dirname, resolve, parse } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function recordPath(
|
|
5
|
+
metaRoot: string,
|
|
6
|
+
type: string,
|
|
7
|
+
id: string,
|
|
8
|
+
opts: { pending?: boolean } = {},
|
|
9
|
+
): string {
|
|
10
|
+
const segments = opts.pending
|
|
11
|
+
? ["memory", "_pending", type, `${id}.json`]
|
|
12
|
+
: ["memory", type, `${id}.json`];
|
|
13
|
+
return join(metaRoot, ...segments);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveMetaRoot(startDir: string): Promise<string> {
|
|
17
|
+
let current = resolve(startDir);
|
|
18
|
+
const root = parse(current).root;
|
|
19
|
+
while (current !== root) {
|
|
20
|
+
const candidate = join(current, ".meta");
|
|
21
|
+
try {
|
|
22
|
+
const s = await stat(candidate);
|
|
23
|
+
if (s.isDirectory()) return candidate;
|
|
24
|
+
} catch {
|
|
25
|
+
// not present at this level
|
|
26
|
+
}
|
|
27
|
+
current = dirname(current);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`no .meta directory found walking up from ${startDir}`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ConventionRecord } from "./convention.js";
|
|
3
|
+
import { DecisionRecord } from "./decision.js";
|
|
4
|
+
import { PrincipleRecord } from "./principle.js";
|
|
5
|
+
import { GlossaryRecord } from "./glossary.js";
|
|
6
|
+
import { FailureRecord } from "./failure.js";
|
|
7
|
+
|
|
8
|
+
export const AnyRecord = z.discriminatedUnion("type", [
|
|
9
|
+
ConventionRecord,
|
|
10
|
+
DecisionRecord,
|
|
11
|
+
PrincipleRecord,
|
|
12
|
+
GlossaryRecord,
|
|
13
|
+
FailureRecord,
|
|
14
|
+
]);
|
|
15
|
+
export type AnyRecord = z.infer<typeof AnyRecord>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RecordCore } from "./core.js";
|
|
3
|
+
|
|
4
|
+
export const ConventionRecord = RecordCore.extend({
|
|
5
|
+
type: z.literal("convention"),
|
|
6
|
+
pattern_description: z.string(),
|
|
7
|
+
examples: z.array(z.string()),
|
|
8
|
+
applies_to: z.array(z.string()),
|
|
9
|
+
});
|
|
10
|
+
export type ConventionRecord = z.infer<typeof ConventionRecord>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const RecordType = z.enum([
|
|
4
|
+
"convention",
|
|
5
|
+
"decision",
|
|
6
|
+
"principle",
|
|
7
|
+
"glossary",
|
|
8
|
+
"failure",
|
|
9
|
+
]);
|
|
10
|
+
export type RecordType = z.infer<typeof RecordType>;
|
|
11
|
+
|
|
12
|
+
export const RecordSource = z.enum([
|
|
13
|
+
"ts-ast",
|
|
14
|
+
"drizzle",
|
|
15
|
+
"prisma",
|
|
16
|
+
"openapi",
|
|
17
|
+
"human",
|
|
18
|
+
"claude",
|
|
19
|
+
"llm-from-commits",
|
|
20
|
+
"llm-from-prs",
|
|
21
|
+
"ingest:ts-ast",
|
|
22
|
+
"ingest:drizzle",
|
|
23
|
+
"ingest:zod",
|
|
24
|
+
]);
|
|
25
|
+
export type RecordSource = z.infer<typeof RecordSource>;
|
|
26
|
+
|
|
27
|
+
export const RecordCore = z.object({
|
|
28
|
+
schema_version: z.literal(1),
|
|
29
|
+
type: RecordType,
|
|
30
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
31
|
+
title: z.string(),
|
|
32
|
+
confidence: z.number().min(0).max(1),
|
|
33
|
+
source: RecordSource,
|
|
34
|
+
captured_at: z.string().datetime(),
|
|
35
|
+
last_validated_against_commit: z.string(),
|
|
36
|
+
superseded_by: z.string().optional(),
|
|
37
|
+
evidence: z
|
|
38
|
+
.object({
|
|
39
|
+
commits: z.array(z.string()).optional(),
|
|
40
|
+
prs: z.array(z.string()).optional(),
|
|
41
|
+
conversation_id: z.string().optional(),
|
|
42
|
+
})
|
|
43
|
+
.optional(),
|
|
44
|
+
deviations: z
|
|
45
|
+
.array(
|
|
46
|
+
z.object({
|
|
47
|
+
when: z.string().datetime(),
|
|
48
|
+
why: z.string(),
|
|
49
|
+
conversation_id: z.string().optional(),
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
.default([]),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type RecordCore = z.infer<typeof RecordCore>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RecordCore } from "./core.js";
|
|
3
|
+
|
|
4
|
+
export const DecisionRecord = RecordCore.extend({
|
|
5
|
+
type: z.literal("decision"),
|
|
6
|
+
rationale: z.string(),
|
|
7
|
+
alternatives_considered: z.array(z.string()),
|
|
8
|
+
scope: z.union([z.literal("global"), z.array(z.string())]),
|
|
9
|
+
});
|
|
10
|
+
export type DecisionRecord = z.infer<typeof DecisionRecord>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RecordCore } from "./core.js";
|
|
3
|
+
|
|
4
|
+
export const FailureRecord = RecordCore.extend({
|
|
5
|
+
type: z.literal("failure"),
|
|
6
|
+
what_was_tried: z.string(),
|
|
7
|
+
why_it_failed: z.string(),
|
|
8
|
+
do_not_repeat_in: z.array(z.string()).optional(),
|
|
9
|
+
related_decisions: z.array(z.string()).optional(),
|
|
10
|
+
});
|
|
11
|
+
export type FailureRecord = z.infer<typeof FailureRecord>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RecordCore } from "./core.js";
|
|
3
|
+
|
|
4
|
+
export const GlossaryRecord = RecordCore.extend({
|
|
5
|
+
type: z.literal("glossary"),
|
|
6
|
+
term: z.string(),
|
|
7
|
+
synonyms: z.array(z.string()),
|
|
8
|
+
definition: z.string(),
|
|
9
|
+
code_anchors: z.object({
|
|
10
|
+
entity: z.string().optional(),
|
|
11
|
+
files: z.array(z.string()).optional(),
|
|
12
|
+
}),
|
|
13
|
+
see_also: z.array(z.string()),
|
|
14
|
+
});
|
|
15
|
+
export type GlossaryRecord = z.infer<typeof GlossaryRecord>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RecordCore } from "./core.js";
|
|
3
|
+
|
|
4
|
+
export const PrincipleRecord = RecordCore.extend({
|
|
5
|
+
type: z.literal("principle"),
|
|
6
|
+
statement: z.string(),
|
|
7
|
+
rationale: z.string(),
|
|
8
|
+
scope: z.array(z.string()),
|
|
9
|
+
examples: z.array(z.string()),
|
|
10
|
+
counter_examples: z.array(z.string()),
|
|
11
|
+
enforcement: z.enum(["advisory", "block"]).default("advisory"),
|
|
12
|
+
});
|
|
13
|
+
export type PrincipleRecord = z.infer<typeof PrincipleRecord>;
|