@intentius/chant 0.84.0 → 0.85.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 (66) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/mcp/resource-handlers.d.ts +2 -1
  3. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  4. package/dist/cli/mcp/server.d.ts +1 -0
  5. package/dist/cli/mcp/server.d.ts.map +1 -1
  6. package/dist/cli/mcp/tools/composites.d.ts +44 -0
  7. package/dist/cli/mcp/tools/composites.d.ts.map +1 -0
  8. package/dist/cli/mcp/tools/search.d.ts.map +1 -1
  9. package/dist/cli/registry.d.ts +2 -0
  10. package/dist/cli/registry.d.ts.map +1 -1
  11. package/dist/components/cli-support.d.ts +4 -0
  12. package/dist/components/cli-support.d.ts.map +1 -1
  13. package/dist/composite.d.ts +6 -0
  14. package/dist/composite.d.ts.map +1 -1
  15. package/dist/lexicon.d.ts +44 -0
  16. package/dist/lexicon.d.ts.map +1 -1
  17. package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
  18. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  19. package/dist/workspace/composites.d.ts +139 -0
  20. package/dist/workspace/composites.d.ts.map +1 -0
  21. package/dist/workspace/graph-cli.d.ts +13 -2
  22. package/dist/workspace/graph-cli.d.ts.map +1 -1
  23. package/dist/workspace/intent-cli.d.ts +5 -1
  24. package/dist/workspace/intent-cli.d.ts.map +1 -1
  25. package/dist/workspace/intent-joins.d.ts +31 -3
  26. package/dist/workspace/intent-joins.d.ts.map +1 -1
  27. package/dist/workspace/intent.d.ts +27 -2
  28. package/dist/workspace/intent.d.ts.map +1 -1
  29. package/dist/workspace/member-commands.d.ts +7 -2
  30. package/dist/workspace/member-commands.d.ts.map +1 -1
  31. package/dist/workspace/reason-codes.d.ts +14 -0
  32. package/dist/workspace/reason-codes.d.ts.map +1 -1
  33. package/dist/workspace/records.d.ts +1 -1
  34. package/dist/workspace/records.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/cli/handlers/graph.ts +4 -0
  37. package/src/cli/main.test.ts +9 -0
  38. package/src/cli/main.ts +8 -0
  39. package/src/cli/mcp/resource-handlers.ts +17 -0
  40. package/src/cli/mcp/server.test.ts +140 -4
  41. package/src/cli/mcp/server.ts +5 -1
  42. package/src/cli/mcp/tools/composites.ts +98 -0
  43. package/src/cli/mcp/tools/search.ts +47 -5
  44. package/src/cli/registry.ts +2 -0
  45. package/src/components/cli-support.test.ts +16 -0
  46. package/src/components/cli-support.ts +8 -2
  47. package/src/composite.ts +9 -0
  48. package/src/lexicon.ts +47 -0
  49. package/src/workspace/__fixtures__/contract-repo.ts +17 -0
  50. package/src/workspace/composites.schema.json +471 -0
  51. package/src/workspace/composites.test.ts +244 -0
  52. package/src/workspace/composites.ts +295 -0
  53. package/src/workspace/graph-cli.ts +32 -4
  54. package/src/workspace/intent-cli.ts +26 -2
  55. package/src/workspace/intent-joins.ts +48 -3
  56. package/src/workspace/intent.schema.json +80 -17
  57. package/src/workspace/intent.test.ts +138 -20
  58. package/src/workspace/intent.ts +72 -14
  59. package/src/workspace/member-commands.ts +11 -5
  60. package/src/workspace/read-contract.test.ts +31 -3
  61. package/src/workspace/reason-codes.test.ts +34 -1
  62. package/src/workspace/reason-codes.ts +22 -0
  63. package/src/workspace/records-contract.test.ts +22 -2
  64. package/src/workspace/records.schema.json +2 -2
  65. package/src/workspace/records.test.ts +93 -0
  66. package/src/workspace/records.ts +54 -10
@@ -62,6 +62,12 @@ export const RECORD_WARNING_CODES = [
62
62
  "asset-stale",
63
63
  /** A supersedes link from a record whose state is weaker than the one it names, so it has no effect yet (#2524 D4). */
64
64
  "record-supersedes-pending",
65
+ /**
66
+ * The kind's pins field is an empty list: the record cites no evidence and
67
+ * pins no file. Information for a reviewer, such as a decision made in a
68
+ * product's own design flow with nothing to cite (#2654).
69
+ */
70
+ "record-no-evidence",
65
71
  ] as const satisfies readonly ReasonCode[];
66
72
  export type RecordWarningCode = (typeof RECORD_WARNING_CODES)[number];
67
73
 
@@ -344,22 +350,54 @@ export interface ReadRecordsResult {
344
350
 
345
351
  type Validator = (data: unknown) => { ok: true } | { ok: false; errors: string[] };
346
352
 
353
+ /** One ajv error, compiled with `verbose` so the failing schema and data come with it. */
354
+ interface SchemaError {
355
+ instancePath: string;
356
+ schemaPath: string;
357
+ keyword: string;
358
+ message?: string;
359
+ params?: { failingKeyword?: string };
360
+ parentSchema?: Record<string, unknown>;
361
+ data?: unknown;
362
+ }
363
+
364
+ /**
365
+ * ajv's errors as `<path> <message>` lines. A failed `if` whose `then` or
366
+ * `else` branch has a `description` is reported by that description alone, in
367
+ * place of the branch's own errors, with each `{field}` filled from the value
368
+ * the branch checked. The decision schema words its dissent rule this way, so
369
+ * the message names the reviewer (#2652). The schema stays plain JSON Schema,
370
+ * with no keyword a strict validator would refuse.
371
+ */
372
+ function renderSchemaErrors(errors: readonly SchemaError[]): string[] {
373
+ const replaced: string[] = [];
374
+ const described = new Map<SchemaError, string>();
375
+ for (const e of errors) {
376
+ const branch = e.keyword === "if" ? e.params?.failingKeyword : undefined;
377
+ const text = branch ? (e.parentSchema?.[branch] as { description?: unknown } | undefined)?.description : undefined;
378
+ if (!branch || typeof text !== "string") continue;
379
+ const at = e.data !== null && typeof e.data === "object" ? (e.data as Record<string, unknown>) : {};
380
+ described.set(e, text.replace(/\{([A-Za-z0-9_]+)\}/g, (all, key: string) => (typeof at[key] === "string" ? (at[key] as string) : all)));
381
+ replaced.push(`${e.schemaPath.replace(/\/if$/, "")}/${branch}/`);
382
+ }
383
+ return errors
384
+ .filter((e) => described.has(e) || !replaced.some((prefix) => e.schemaPath.startsWith(prefix)))
385
+ .map((e) => `${e.instancePath || "/"} ${described.get(e) ?? e.message ?? "is invalid"}`);
386
+ }
387
+
347
388
  async function compileSchema(schema: Record<string, unknown>): Promise<Validator> {
348
389
  const mod = (await import("ajv")) as unknown as { default: unknown };
349
390
  // ajv is CommonJS; its class is the default export, or that export's own default.
350
391
  const Ajv = ((mod.default as { default?: unknown }).default ?? mod.default) as new (opts: object) => {
351
- compile(s: object): ((d: unknown) => boolean) & { errors?: Array<{ instancePath: string; message?: string }> | null };
392
+ compile(s: object): ((d: unknown) => boolean) & { errors?: SchemaError[] | null };
352
393
  };
353
394
  let validate: ReturnType<InstanceType<typeof Ajv>["compile"]>;
354
395
  try {
355
- validate = new Ajv({ allErrors: true, strict: false }).compile(schema);
396
+ validate = new Ajv({ allErrors: true, strict: false, verbose: true }).compile(schema);
356
397
  } catch (err) {
357
398
  throw new RecordReadError("schema-invalid", `the kind's schema does not compile: ${message(err)}`);
358
399
  }
359
- return (data) =>
360
- validate(data)
361
- ? { ok: true }
362
- : { ok: false, errors: (validate.errors ?? []).map((e) => `${e.instancePath || "/"} ${e.message ?? "is invalid"}`) };
400
+ return (data) => (validate(data) ? { ok: true } : { ok: false, errors: renderSchemaErrors(validate.errors ?? []) });
363
401
  }
364
402
 
365
403
  /** Read every record `loaded` locates, through `options.source`. */
@@ -392,10 +430,16 @@ export async function readRecords(loaded: LoadedRecordKind, options: ReadRecords
392
430
  if (!result.ok) {
393
431
  entry.reasons.push({ code: "record-schema-invalid", message: result.errors.join("; ") });
394
432
  }
395
- if (kind.pins && options.assets) {
396
- const checked = checkPins(pinEntries(fm.value, kind.pins.field), options.assets);
397
- entry.assets = checked.assets;
398
- entry.warnings = checked.warnings;
433
+ if (kind.pins) {
434
+ const cited = fm.value[kind.pins.field];
435
+ if (Array.isArray(cited) && cited.length === 0) {
436
+ entry.warnings.push({ code: "record-no-evidence", message: `${kind.pins.field} is empty: the record cites nothing and pins no file` });
437
+ }
438
+ if (options.assets) {
439
+ const checked = checkPins(pinEntries(fm.value, kind.pins.field), options.assets);
440
+ entry.assets = checked.assets;
441
+ entry.warnings.push(...checked.warnings);
442
+ }
399
443
  }
400
444
  }
401
445