@metaobjectsdev/sdk 0.9.0 → 0.10.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.
Files changed (96) hide show
  1. package/README.md +9 -0
  2. package/agent-context/README.md +14 -0
  3. package/agent-context/servers/csharp.meta.json +5 -0
  4. package/agent-context/servers/java.meta.json +5 -0
  5. package/agent-context/servers/kotlin.meta.json +5 -0
  6. package/agent-context/servers/python.meta.json +5 -0
  7. package/agent-context/servers/typescript.meta.json +5 -0
  8. package/agent-context/skills/metaobjects-authoring/SKILL.md +308 -0
  9. package/agent-context/skills/metaobjects-codegen/SKILL.md +99 -0
  10. package/agent-context/skills/metaobjects-codegen/references/csharp.md +87 -0
  11. package/agent-context/skills/metaobjects-codegen/references/java.md +94 -0
  12. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +110 -0
  13. package/agent-context/skills/metaobjects-codegen/references/typescript.md +135 -0
  14. package/agent-context/skills/metaobjects-prompts/SKILL.md +148 -0
  15. package/agent-context/skills/metaobjects-prompts/references/csharp.md +110 -0
  16. package/agent-context/skills/metaobjects-prompts/references/java.md +108 -0
  17. package/agent-context/skills/metaobjects-prompts/references/kotlin.md +130 -0
  18. package/agent-context/skills/metaobjects-prompts/references/python.md +116 -0
  19. package/agent-context/skills/metaobjects-prompts/references/typescript.md +150 -0
  20. package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  21. package/agent-context/skills/metaobjects-runtime-ui/references/java.md +96 -0
  22. package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  23. package/agent-context/skills/metaobjects-runtime-ui/references/react.md +86 -0
  24. package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  25. package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  26. package/agent-context/skills/metaobjects-verify/SKILL.md +107 -0
  27. package/agent-context/skills/metaobjects-verify/references/migration.md +72 -0
  28. package/agent-context/templates/always-on.md.mustache +27 -0
  29. package/dist/agent-context/assemble.d.ts +7 -0
  30. package/dist/agent-context/assemble.d.ts.map +1 -0
  31. package/dist/agent-context/assemble.js +61 -0
  32. package/dist/agent-context/assemble.js.map +1 -0
  33. package/dist/agent-context/content-root.d.ts +8 -0
  34. package/dist/agent-context/content-root.d.ts.map +1 -0
  35. package/dist/agent-context/content-root.js +35 -0
  36. package/dist/agent-context/content-root.js.map +1 -0
  37. package/dist/agent-context/index.d.ts +6 -0
  38. package/dist/agent-context/index.d.ts.map +1 -0
  39. package/dist/agent-context/index.js +6 -0
  40. package/dist/agent-context/index.js.map +1 -0
  41. package/dist/agent-context/resolve.d.ts +13 -0
  42. package/dist/agent-context/resolve.d.ts.map +1 -0
  43. package/dist/agent-context/resolve.js +30 -0
  44. package/dist/agent-context/resolve.js.map +1 -0
  45. package/dist/agent-context/scaffold.d.ts +60 -0
  46. package/dist/agent-context/scaffold.d.ts.map +1 -0
  47. package/dist/agent-context/scaffold.js +61 -0
  48. package/dist/agent-context/scaffold.js.map +1 -0
  49. package/dist/agent-context/types.d.ts +21 -0
  50. package/dist/agent-context/types.d.ts.map +1 -0
  51. package/dist/agent-context/types.js +12 -0
  52. package/dist/agent-context/types.js.map +1 -0
  53. package/dist/agent-docs/body.d.ts +5 -1
  54. package/dist/agent-docs/body.d.ts.map +1 -1
  55. package/dist/agent-docs/body.js +7 -3
  56. package/dist/agent-docs/body.js.map +1 -1
  57. package/dist/forge-types.js +2 -2
  58. package/dist/forge-types.js.map +1 -1
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +3 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/memory.d.ts.map +1 -1
  64. package/dist/memory.js +3 -1
  65. package/dist/memory.js.map +1 -1
  66. package/dist/storage/errors.d.ts +3 -3
  67. package/dist/storage/errors.d.ts.map +1 -1
  68. package/dist/storage/errors.js +6 -6
  69. package/dist/storage/errors.js.map +1 -1
  70. package/dist/storage/index.d.ts +1 -1
  71. package/dist/storage/index.d.ts.map +1 -1
  72. package/dist/storage/index.js +1 -1
  73. package/dist/storage/index.js.map +1 -1
  74. package/dist/storage/lifecycle.js +4 -4
  75. package/dist/storage/lifecycle.js.map +1 -1
  76. package/dist/storage/read.js +4 -4
  77. package/dist/storage/read.js.map +1 -1
  78. package/dist/storage/write.js +2 -2
  79. package/dist/storage/write.js.map +1 -1
  80. package/package.json +40 -22
  81. package/scripts/bundle-agent-context.mjs +19 -0
  82. package/src/agent-context/assemble.ts +68 -0
  83. package/src/agent-context/content-root.ts +35 -0
  84. package/src/agent-context/index.ts +5 -0
  85. package/src/agent-context/resolve.ts +33 -0
  86. package/src/agent-context/scaffold.ts +103 -0
  87. package/src/agent-context/types.ts +31 -0
  88. package/src/agent-docs/body.ts +7 -3
  89. package/src/forge-types.ts +2 -2
  90. package/src/index.ts +6 -3
  91. package/src/memory.ts +3 -1
  92. package/src/storage/errors.ts +6 -6
  93. package/src/storage/index.ts +3 -3
  94. package/src/storage/lifecycle.ts +4 -4
  95. package/src/storage/read.ts +4 -4
  96. package/src/storage/write.ts +2 -2
@@ -0,0 +1,103 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { AssembledFile, Stack } from "./types.js";
3
+
4
+ /** Consumer-relative path of the sidecar manifest that tracks scaffolded files. */
5
+ export const AGENT_CONTEXT_MANIFEST_PATH = ".metaobjects/.agent-context.json";
6
+
7
+ export interface Manifest {
8
+ version: 1;
9
+ /**
10
+ * The MetaObjects version that last scaffolded this agent context. Used to nudge
11
+ * a re-scaffold when the installed version moves ahead (the skills/docs ship with
12
+ * the package, so an upgrade can leave the copied-in context stale). Optional for
13
+ * back-compat with manifests written before version tracking existed.
14
+ */
15
+ generatedBy?: string;
16
+ servers: string[];
17
+ clients: string[];
18
+ /** consumer-relative path → sha256 of the contents as last scaffolded. */
19
+ files: Record<string, string>;
20
+ }
21
+
22
+ export function hashContents(s: string): string {
23
+ return createHash("sha256").update(s, "utf8").digest("hex");
24
+ }
25
+
26
+ export interface ScaffoldDecision {
27
+ /** files to (over)write at their own path: new, or unmodified-since-last-scaffold. */
28
+ writes: { path: string; contents: string }[];
29
+ /** hand-edited files: write the fresh contents to `<path>.new`, leave the original. */
30
+ conflicts: { path: string; newPath: string; contents: string }[];
31
+ /** the manifest to persist after writing. */
32
+ manifest: Manifest;
33
+ /** paths the prior manifest tracked that are no longer assembled (e.g. stack shrank) — reported, never auto-deleted. */
34
+ removed: string[];
35
+ }
36
+
37
+ /**
38
+ * Decide what to write for a (re-)scaffold. Pure: filesystem access is via the
39
+ * `readCurrent` callback (returns the on-disk contents, or undefined if absent).
40
+ * A file is safe to overwrite iff it is absent, or its on-disk hash still equals
41
+ * the hash the prior manifest recorded (i.e. the user hasn't hand-edited it).
42
+ */
43
+ export function planScaffold(opts: {
44
+ stack: Stack;
45
+ assembled: AssembledFile[];
46
+ prior: Manifest | undefined;
47
+ readCurrent: (path: string) => string | undefined;
48
+ /** The MetaObjects version doing the scaffold — stamped into the manifest. */
49
+ generatedBy: string;
50
+ }): ScaffoldDecision {
51
+ const { stack, assembled, prior, readCurrent, generatedBy } = opts;
52
+ const writes: ScaffoldDecision["writes"] = [];
53
+ const conflicts: ScaffoldDecision["conflicts"] = [];
54
+ const files: Record<string, string> = {};
55
+
56
+ for (const f of assembled) {
57
+ files[f.path] = hashContents(f.contents);
58
+ const current = readCurrent(f.path);
59
+ if (current === undefined) {
60
+ writes.push({ path: f.path, contents: f.contents });
61
+ continue;
62
+ }
63
+ const priorHash = prior?.files[f.path];
64
+ if (priorHash !== undefined && hashContents(current) === priorHash) {
65
+ writes.push({ path: f.path, contents: f.contents }); // unmodified → refresh to latest
66
+ } else {
67
+ conflicts.push({ path: f.path, newPath: `${f.path}.new`, contents: f.contents });
68
+ }
69
+ }
70
+
71
+ const assembledPaths = new Set(assembled.map((f) => f.path));
72
+ const removed = prior ? Object.keys(prior.files).filter((p) => !assembledPaths.has(p)) : [];
73
+
74
+ return {
75
+ writes,
76
+ conflicts,
77
+ manifest: { version: 1, generatedBy, servers: stack.servers, clients: stack.clients, files },
78
+ removed,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * A one-line nudge if the scaffolded agent context predates the installed MetaObjects
84
+ * (so `gen`/`verify` can remind the user to refresh the skills after an upgrade), or
85
+ * `null` when there is nothing to say — no agent context scaffolded, or it is in sync.
86
+ * Advisory only: never throws, never blocks, never writes.
87
+ */
88
+ export function agentContextStaleness(opts: {
89
+ manifest: Manifest | undefined;
90
+ currentVersion: string;
91
+ }): string | null {
92
+ const { manifest, currentVersion } = opts;
93
+ if (manifest === undefined) return null; // no agent context here → nothing to nudge
94
+ // Exact-equality on purpose: ANY drift nudges (a re-scaffold is cheap + idempotent).
95
+ // Don't "fix" this into a semver compare — a prerelease/build-metadata difference is
96
+ // still a reason to refresh, and the nudge is advisory, never a gate.
97
+ if (manifest.generatedBy === currentVersion) return null; // in sync
98
+ const from = manifest.generatedBy ?? "an older MetaObjects";
99
+ return (
100
+ `MetaObjects agent context was generated by ${from}; you're on ${currentVersion}. ` +
101
+ `Re-run 'meta init --docs-only --refresh-docs' to refresh the .claude/skills docs.`
102
+ );
103
+ }
@@ -0,0 +1,31 @@
1
+ export const SERVER_LANGS = ["typescript", "java", "kotlin", "csharp", "python"] as const;
2
+ export type ServerLang = (typeof SERVER_LANGS)[number];
3
+
4
+ export const CLIENT_FRAMEWORKS = ["react", "tanstack", "angular"] as const;
5
+ export type ClientFramework = (typeof CLIENT_FRAMEWORKS)[number];
6
+
7
+ /** Always-present token: schema migrations are TS-owned for every port (ADR-0015). */
8
+ export const MIGRATION_TOKEN = "migration";
9
+
10
+ export const SKILL_NAMES = [
11
+ "metaobjects-authoring",
12
+ "metaobjects-codegen",
13
+ "metaobjects-runtime-ui",
14
+ "metaobjects-prompts",
15
+ "metaobjects-verify",
16
+ ] as const;
17
+ export type SkillName = (typeof SKILL_NAMES)[number];
18
+
19
+ /** The resolved tech-stack of a consumer project. */
20
+ export interface Stack {
21
+ servers: ServerLang[]; // deduped, in SERVER_LANGS order
22
+ clients: ClientFramework[]; // deduped, in CLIENT_FRAMEWORKS order
23
+ /** servers ∪ clients ∪ {"migration"} — the install-selection set for reference fragments. */
24
+ tokens: ReadonlySet<string>;
25
+ }
26
+
27
+ /** A file the assembler emits, path relative to the consumer project root. */
28
+ export interface AssembledFile {
29
+ path: string; // e.g. ".metaobjects/AGENTS.md", ".claude/skills/metaobjects-codegen/references/java.md"
30
+ contents: string;
31
+ }
@@ -1,4 +1,8 @@
1
1
  // Agent reference docs body. Scaffolded into .metaobjects/AGENTS.md and CLAUDE.md by `meta init`.
2
+ /**
3
+ * @deprecated The single-blob agent doc is replaced by the assembled agent-context
4
+ * (see `@metaobjectsdev/sdk/agent-context`). Kept only for back-compat; not scaffolded by `meta init`.
5
+ */
2
6
  export const AGENT_DOCS_BODY = `# Meta Forge — agent reference
3
7
 
4
8
  This file is scaffolded by \`meta init\` and lives alongside your \`metaobjects/\` records. It teaches AI coding assistants (Claude Code, Codex, etc.) how to read and modify MetaObjects metadata correctly. Refresh after CLI updates with \`meta init --refresh-docs\`.
@@ -148,7 +152,7 @@ The v0.2 keys (\`super\`, \`overlay\`, \`override\`, \`isInterface\`, \`implemen
148
152
  ### Package paths and inheritance
149
153
 
150
154
  - Package segments separated by \`::\` — \`acme::common::id\`
151
- - Relative references in \`extends:\` — \`..::common::id\` means "go up to parent package, descend into \`common::id\`"
155
+ - Relative references in \`extends:\` — \`..::common::id\` means "go up to parent package, descend into \`common::id\`". Relative forms (\`..::\` parent-relative, leading \`::\` root-absolute) are a **YAML-authoring affordance only**; canonical JSON must be fully-qualified (a relative ref in JSON is rejected with \`ERR_RELATIVE_REF_IN_CANONICAL\`).
152
156
  - Cross-file resolution works as long as all files are passed to Loader (or live in the same \`metaobjects/\` directory)
153
157
 
154
158
  ### Two special intercepted attrs (parser-routed)
@@ -318,7 +322,7 @@ When a list needs computed columns (counts, sums, joined fields), create a **pro
318
322
  { "origin": { "subType": "aggregate",
319
323
  "@agg": "count", "@of": "Week.id", "@via": "Program.weeks" }}
320
324
  ]}},
321
- { "identity": { "subType": "primary", "@fields": "id" } }
325
+ { "identity": { "subType": "primary", "name": "id", "@fields": "id" } }
322
326
  ]
323
327
  }
324
328
  }
@@ -517,7 +521,7 @@ metaobjects.config.ts generator wiring (committed)
517
521
  "@forgeSource": "human",
518
522
  "@forgePrimaryLocation": "src/db/users.schema.ts",
519
523
  "children": [
520
- {"field": {"name": "id", "extends": "..::common::id"}},
524
+ {"field": {"name": "id", "extends": "common::id"}},
521
525
  {"field": {"name": "email", "subType": "string",
522
526
  "@column": "email_address",
523
527
  "children": [{"validator": {"subType": "required"}}]
@@ -114,7 +114,7 @@ import {
114
114
  } from "@metaobjectsdev/metadata";
115
115
 
116
116
  /** Minimal concrete MetaData subclass used for all forge descriptive nodes. */
117
- class MetaForgeNode extends MetaData {}
117
+ class ForgeNode extends MetaData {}
118
118
 
119
119
  function wildcardOf(childType: string): ChildRule {
120
120
  return {
@@ -133,7 +133,7 @@ function def(
133
133
  return {
134
134
  typeId: new TypeId(type, subType),
135
135
  description,
136
- factory: (typeId, name) => new MetaForgeNode(typeId, name),
136
+ factory: (typeId, name) => new ForgeNode(typeId, name),
137
137
  childRules,
138
138
  attributes: [],
139
139
  };
package/src/index.ts CHANGED
@@ -16,9 +16,9 @@ export {
16
16
  listRecords,
17
17
  promoteRecord,
18
18
  supersede,
19
- MetaForgeRecordNotFoundError,
20
- MetaForgeAlreadyPromotedError,
21
- MetaForgeRecordParseError,
19
+ ForgeRecordNotFoundError,
20
+ ForgeAlreadyPromotedError,
21
+ ForgeRecordParseError,
22
22
  } from "./storage/index.js";
23
23
  export type { ListOptions } from "./storage/index.js";
24
24
 
@@ -103,3 +103,6 @@ export {
103
103
  PACKAGE_MANIFEST_FILE,
104
104
  } from "./package.js";
105
105
  export type { PackageManifest } from "./package.js";
106
+
107
+ // Agent context — stack resolver, file assembler, and vocabulary types
108
+ export * from "./agent-context/index.js";
package/src/memory.ts CHANGED
@@ -97,7 +97,9 @@ export async function loadMemory(
97
97
  // against the merged tree afterwards) — dep packages first, current last.
98
98
  const paths = await collectMetadataPaths(repoRoot);
99
99
 
100
- const loader = new MetaDataLoader({ registry });
100
+ const loader = new MetaDataLoader({
101
+ registry,
102
+ });
101
103
  const result = await loader.load(paths.map((p) => new FileSource(p)));
102
104
 
103
105
  if (result.errors.length > 0) {
@@ -1,23 +1,23 @@
1
- export class MetaForgeRecordNotFoundError extends Error {
1
+ export class ForgeRecordNotFoundError extends Error {
2
2
  constructor(public readonly path: string) {
3
3
  super(`Record not found: ${path}`);
4
- this.name = "MetaForgeRecordNotFoundError";
4
+ this.name = "ForgeRecordNotFoundError";
5
5
  }
6
6
  }
7
7
 
8
- export class MetaForgeAlreadyPromotedError extends Error {
8
+ export class ForgeAlreadyPromotedError extends Error {
9
9
  constructor(public readonly path: string) {
10
10
  super(`A canonical record already exists at: ${path}`);
11
- this.name = "MetaForgeAlreadyPromotedError";
11
+ this.name = "ForgeAlreadyPromotedError";
12
12
  }
13
13
  }
14
14
 
15
- export class MetaForgeRecordParseError extends Error {
15
+ export class ForgeRecordParseError extends Error {
16
16
  constructor(
17
17
  public readonly path: string,
18
18
  public override readonly cause: unknown,
19
19
  ) {
20
20
  super(`Failed to parse record at ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
21
- this.name = "MetaForgeRecordParseError";
21
+ this.name = "ForgeRecordParseError";
22
22
  }
23
23
  }
@@ -4,7 +4,7 @@ export { listRecords } from "./list.js";
4
4
  export type { ListOptions } from "./list.js";
5
5
  export { promoteRecord, supersede } from "./lifecycle.js";
6
6
  export {
7
- MetaForgeRecordNotFoundError,
8
- MetaForgeAlreadyPromotedError,
9
- MetaForgeRecordParseError,
7
+ ForgeRecordNotFoundError,
8
+ ForgeAlreadyPromotedError,
9
+ ForgeRecordParseError,
10
10
  } from "./errors.js";
@@ -2,7 +2,7 @@ import type { AnyRecord } from "../records/any.js";
2
2
  import type { RecordType } from "../records/core.js";
3
3
  import { readRecord, recordExists } from "./read.js";
4
4
  import { writeRecord, removeRecord } from "./write.js";
5
- import { MetaForgeAlreadyPromotedError, MetaForgeRecordNotFoundError } from "./errors.js";
5
+ import { ForgeAlreadyPromotedError, ForgeRecordNotFoundError } from "./errors.js";
6
6
  import { recordPath } from "../paths.js";
7
7
 
8
8
  export async function promoteRecord(
@@ -11,10 +11,10 @@ export async function promoteRecord(
11
11
  id: string,
12
12
  ): Promise<void> {
13
13
  if (!(await recordExists(metaRoot, type, id, { pending: true }))) {
14
- throw new MetaForgeRecordNotFoundError(recordPath(metaRoot, type, id, { pending: true }));
14
+ throw new ForgeRecordNotFoundError(recordPath(metaRoot, type, id, { pending: true }));
15
15
  }
16
16
  if (await recordExists(metaRoot, type, id)) {
17
- throw new MetaForgeAlreadyPromotedError(recordPath(metaRoot, type, id));
17
+ throw new ForgeAlreadyPromotedError(recordPath(metaRoot, type, id));
18
18
  }
19
19
  const record = await readRecord(metaRoot, type, id, { pending: true });
20
20
  await writeRecord(metaRoot, record);
@@ -27,7 +27,7 @@ export async function supersede(
27
27
  newRecord: AnyRecord,
28
28
  ): Promise<void> {
29
29
  if (!(await recordExists(metaRoot, newRecord.type, oldId))) {
30
- throw new MetaForgeRecordNotFoundError(recordPath(metaRoot, newRecord.type, oldId));
30
+ throw new ForgeRecordNotFoundError(recordPath(metaRoot, newRecord.type, oldId));
31
31
  }
32
32
  // Write the new record first, so a failure leaves the old record intact.
33
33
  await writeRecord(metaRoot, newRecord);
@@ -3,7 +3,7 @@ import type { AnyRecord } from "../records/any.js";
3
3
  import { AnyRecord as AnyRecordSchema } from "../records/any.js";
4
4
  import type { RecordType } from "../records/core.js";
5
5
  import { recordPath } from "../paths.js";
6
- import { MetaForgeRecordNotFoundError, MetaForgeRecordParseError } from "./errors.js";
6
+ import { ForgeRecordNotFoundError, ForgeRecordParseError } from "./errors.js";
7
7
 
8
8
  export async function readRecord(
9
9
  metaRoot: string,
@@ -16,17 +16,17 @@ export async function readRecord(
16
16
  try {
17
17
  raw = await readFile(path, "utf8");
18
18
  } catch (err) {
19
- if (isNoEntError(err)) throw new MetaForgeRecordNotFoundError(path);
19
+ if (isNoEntError(err)) throw new ForgeRecordNotFoundError(path);
20
20
  throw err;
21
21
  }
22
22
  let parsedJson: unknown;
23
23
  try {
24
24
  parsedJson = JSON.parse(raw);
25
25
  } catch (err) {
26
- throw new MetaForgeRecordParseError(path, err);
26
+ throw new ForgeRecordParseError(path, err);
27
27
  }
28
28
  const result = AnyRecordSchema.safeParse(parsedJson);
29
- if (!result.success) throw new MetaForgeRecordParseError(path, result.error);
29
+ if (!result.success) throw new ForgeRecordParseError(path, result.error);
30
30
  return result.data;
31
31
  }
32
32
 
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
3
3
  import type { AnyRecord } from "../records/any.js";
4
4
  import { AnyRecord as AnyRecordSchema } from "../records/any.js";
5
5
  import { recordPath } from "../paths.js";
6
- import { MetaForgeRecordNotFoundError } from "./errors.js";
6
+ import { ForgeRecordNotFoundError } from "./errors.js";
7
7
  import { recordExists } from "./read.js";
8
8
 
9
9
  export async function writeRecord(
@@ -26,7 +26,7 @@ export async function removeRecord(
26
26
  opts: { pending?: boolean } = {},
27
27
  ): Promise<void> {
28
28
  if (!(await recordExists(metaRoot, type, id, opts))) {
29
- throw new MetaForgeRecordNotFoundError(recordPath(metaRoot, type, id, opts));
29
+ throw new ForgeRecordNotFoundError(recordPath(metaRoot, type, id, opts));
30
30
  }
31
31
  await unlink(recordPath(metaRoot, type, id, opts));
32
32
  }