flowdular 0.3.2 → 0.4.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.
@@ -2478,6 +2478,17 @@ var module_spec_schema_default = {
2478
2478
  type: "array",
2479
2479
  items: { $ref: "#/$defs/decision" },
2480
2480
  uniqueItems: true
2481
+ },
2482
+ research: { $ref: "#/$defs/research" },
2483
+ adapters: {
2484
+ type: "array",
2485
+ items: { $ref: "#/$defs/adapter" },
2486
+ uniqueItems: true
2487
+ },
2488
+ templates: {
2489
+ type: "array",
2490
+ items: { $ref: "#/$defs/template" },
2491
+ uniqueItems: true
2481
2492
  }
2482
2493
  },
2483
2494
  allOf: [
@@ -2495,7 +2506,10 @@ var module_spec_schema_default = {
2495
2506
  settings: false,
2496
2507
  agentTools: false,
2497
2508
  outOfScope: false,
2498
- decisions: false
2509
+ decisions: false,
2510
+ research: false,
2511
+ adapters: false,
2512
+ templates: false
2499
2513
  }
2500
2514
  }
2501
2515
  }
@@ -2701,6 +2715,113 @@ var module_spec_schema_default = {
2701
2715
  answer: { type: "string", minLength: 1 },
2702
2716
  decidedBy: { enum: ["user", "default"] }
2703
2717
  }
2718
+ },
2719
+ domain: {
2720
+ type: "string",
2721
+ maxLength: 253,
2722
+ pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$"
2723
+ },
2724
+ research: {
2725
+ type: "object",
2726
+ additionalProperties: false,
2727
+ required: ["adapter", "evidenceOwner"],
2728
+ properties: {
2729
+ adapter: {
2730
+ enum: [
2731
+ "model-native",
2732
+ "searxng",
2733
+ "firecrawl",
2734
+ "connector",
2735
+ "recorded"
2736
+ ]
2737
+ },
2738
+ allowDomains: {
2739
+ type: "array",
2740
+ items: { $ref: "#/$defs/domain" },
2741
+ uniqueItems: true
2742
+ },
2743
+ denyDomains: {
2744
+ type: "array",
2745
+ items: { $ref: "#/$defs/domain" },
2746
+ uniqueItems: true
2747
+ },
2748
+ monthlyQueryBudget: { type: "integer", minimum: 1 },
2749
+ evidenceOwner: { $ref: "#/$defs/entityId" }
2750
+ }
2751
+ },
2752
+ adapter: {
2753
+ type: "object",
2754
+ additionalProperties: false,
2755
+ required: [
2756
+ "id",
2757
+ "direction",
2758
+ "connector",
2759
+ "operation",
2760
+ "port",
2761
+ "mapping"
2762
+ ],
2763
+ properties: {
2764
+ id: {
2765
+ type: "string",
2766
+ pattern: "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
2767
+ },
2768
+ direction: { enum: ["source", "sink"] },
2769
+ connector: { type: "string", pattern: "^[a-z][a-z0-9-]{0,95}$" },
2770
+ operation: { type: "string", pattern: "^[a-z][a-z0-9-]{0,63}$" },
2771
+ port: {
2772
+ type: "string",
2773
+ maxLength: 96,
2774
+ pattern: "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
2775
+ },
2776
+ schedule: {
2777
+ anyOf: [
2778
+ { type: "null" },
2779
+ {
2780
+ type: "string",
2781
+ maxLength: 100,
2782
+ pattern: "^[0-9a-z*,/-]+( [0-9a-z*,/-]+){4}$"
2783
+ }
2784
+ ]
2785
+ },
2786
+ mapping: {
2787
+ type: "array",
2788
+ minItems: 1,
2789
+ items: { $ref: "#/$defs/adapterMapping" },
2790
+ uniqueItems: true
2791
+ },
2792
+ recorded: {
2793
+ type: "string",
2794
+ pattern: "^adapters/[a-z0-9][a-z0-9-]*\\.recorded\\.json$"
2795
+ }
2796
+ }
2797
+ },
2798
+ adapterMapping: {
2799
+ type: "object",
2800
+ additionalProperties: false,
2801
+ required: ["to", "transform"],
2802
+ properties: {
2803
+ from: { type: "string", minLength: 1 },
2804
+ to: { type: "string", minLength: 1 },
2805
+ transform: { enum: ["rename", "constant", "format", "lookup"] },
2806
+ value: {
2807
+ anyOf: [{ type: "null" }, { type: "string", minLength: 1 }]
2808
+ }
2809
+ }
2810
+ },
2811
+ template: {
2812
+ type: "object",
2813
+ additionalProperties: false,
2814
+ required: ["id", "title", "inputEntity", "format", "body"],
2815
+ properties: {
2816
+ id: { $ref: "#/$defs/entityId" },
2817
+ title: { type: "string", minLength: 1 },
2818
+ inputEntity: { $ref: "#/$defs/entityId" },
2819
+ format: { enum: ["pdf", "docx"] },
2820
+ body: {
2821
+ type: "string",
2822
+ pattern: "^templates/[a-z][a-z0-9-]*\\.md$"
2823
+ }
2824
+ }
2704
2825
  }
2705
2826
  }
2706
2827
  };
@@ -2867,6 +2988,15 @@ var project_schema_default = {
2867
2988
  $ref: "#/$defs/githubAccount"
2868
2989
  },
2869
2990
  uniqueItems: true
2991
+ },
2992
+ labels: {
2993
+ type: "array",
2994
+ maxItems: 10,
2995
+ items: {
2996
+ type: "string",
2997
+ pattern: "^[A-Za-z0-9][A-Za-z0-9 ._:-]{0,49}$"
2998
+ },
2999
+ uniqueItems: true
2870
3000
  }
2871
3001
  }
2872
3002
  },
@@ -3114,8 +3244,79 @@ var projectSchema = project_schema_default;
3114
3244
  var moduleCatalogSchema = module_catalog_schema_default;
3115
3245
  var moduleArtifactSchema = module_artifact_schema_default;
3116
3246
 
3247
+ // ../contracts/src/translations.ts
3248
+ var PLURAL_CATEGORIES = [
3249
+ "zero",
3250
+ "one",
3251
+ "two",
3252
+ "few",
3253
+ "many",
3254
+ "other"
3255
+ ];
3256
+ var WHOLE_NUMBER_SAMPLES = [
3257
+ ...Array.from({ length: 200 }, (_, index) => index),
3258
+ 1e3,
3259
+ 1e4,
3260
+ 1e5,
3261
+ 1e6,
3262
+ 1e7,
3263
+ 1e8,
3264
+ 1e9
3265
+ ];
3266
+ function requiredPluralCategories(locale) {
3267
+ const rules = new Intl.PluralRules(locale);
3268
+ const selected = /* @__PURE__ */ new Set(["other"]);
3269
+ for (const sample of WHOLE_NUMBER_SAMPLES) selected.add(rules.select(sample));
3270
+ return PLURAL_CATEGORIES.filter((category) => selected.has(category));
3271
+ }
3272
+ function pluralMember(bundle, key) {
3273
+ const dot = key.lastIndexOf(".");
3274
+ if (dot <= 0) return null;
3275
+ const category = key.slice(dot + 1);
3276
+ if (!PLURAL_CATEGORIES.includes(category)) return null;
3277
+ const base = key.slice(0, dot);
3278
+ return Object.hasOwn(bundle, base + ".other") ? { base, category } : null;
3279
+ }
3280
+ function translationKeys(bundle) {
3281
+ const keys = /* @__PURE__ */ new Set();
3282
+ for (const key of Object.keys(bundle)) {
3283
+ keys.add(pluralMember(bundle, key)?.base ?? key);
3284
+ }
3285
+ return [...keys].sort();
3286
+ }
3287
+ function pluralFamilyIssues(bundle, locale) {
3288
+ const families = /* @__PURE__ */ new Map();
3289
+ for (const key of Object.keys(bundle)) {
3290
+ const member = pluralMember(bundle, key);
3291
+ if (member === null) continue;
3292
+ const categories = families.get(member.base) ?? /* @__PURE__ */ new Set();
3293
+ categories.add(member.category);
3294
+ families.set(member.base, categories);
3295
+ }
3296
+ const required = requiredPluralCategories(locale);
3297
+ const known = new Intl.PluralRules(
3298
+ locale
3299
+ ).resolvedOptions().pluralCategories;
3300
+ const issues = [];
3301
+ for (const base of [...families.keys()].sort()) {
3302
+ const categories = families.get(base);
3303
+ const missing = required.filter((category) => !categories.has(category));
3304
+ if (missing.length > 0) {
3305
+ issues.push(
3306
+ `${base} lacks ${missing.map((category) => base + "." + category).join(", ")} for ${locale}`
3307
+ );
3308
+ }
3309
+ for (const category of categories) {
3310
+ if (!known.includes(category)) {
3311
+ issues.push(`${base}.${category} is never selected for ${locale}`);
3312
+ }
3313
+ }
3314
+ }
3315
+ return issues;
3316
+ }
3317
+
3117
3318
  // ../contracts/src/index.ts
3118
- var PLATFORM_API_VERSION = "0.1.9";
3319
+ var PLATFORM_API_VERSION = "0.1.21";
3119
3320
 
3120
3321
  // ../kernel/src/module-compatibility.ts
3121
3322
  function satisfiesModuleVersion(version, range) {
@@ -3298,6 +3499,10 @@ var validators = {
3298
3499
  cliExtension: ajv.compile(cliExtensionSchema),
3299
3500
  platformSpec: ajv.compile(platformSpecSchema)
3300
3501
  };
3502
+ var MONTH_NAMES = "jan feb mar apr may jun jul aug sep oct nov dec".split(
3503
+ " "
3504
+ );
3505
+ var WEEKDAY_NAMES = "sun mon tue wed thu fri sat".split(" ");
3301
3506
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
3302
3507
  ".git",
3303
3508
  ".flowdular",
@@ -3796,7 +4001,7 @@ import { dirname as dirname6, join as join6, relative as relative5 } from "node:
3796
4001
  import { createRequire } from "node:module";
3797
4002
  import { readFile as readFile5 } from "node:fs/promises";
3798
4003
  import { dirname as dirname4, join as join3 } from "node:path";
3799
- var SDK_VERSION = "0.3.2";
4004
+ var SDK_VERSION = "0.4.0";
3800
4005
  function npmPackage(specifier) {
3801
4006
  return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
3802
4007
  }
@@ -3900,6 +4105,115 @@ async function findModuleFiles(workspace) {
3900
4105
  import { access as access2, readFile as readFile7, readdir as readdir3 } from "node:fs/promises";
3901
4106
  import { dirname as dirname5, join as join5, relative as relative4 } from "node:path";
3902
4107
  import { parse as parseYaml3 } from "yaml";
4108
+
4109
+ // src/dom-properties.ts
4110
+ var DOM_PROPERTY_SPELLINGS = [
4111
+ "acceptCharset",
4112
+ "allowFullScreen",
4113
+ "autoCapitalize",
4114
+ "autoComplete",
4115
+ "autoCorrect",
4116
+ "autoFocus",
4117
+ "autoPlay",
4118
+ "charSet",
4119
+ "contentEditable",
4120
+ "crossOrigin",
4121
+ "defaultChecked",
4122
+ "defaultValue",
4123
+ "disablePictureInPicture",
4124
+ "disableRemotePlayback",
4125
+ "encType",
4126
+ "fetchPriority",
4127
+ "formAction",
4128
+ "formEncType",
4129
+ "formMethod",
4130
+ "formNoValidate",
4131
+ "formTarget",
4132
+ "httpEquiv",
4133
+ "imageSizes",
4134
+ "imageSrcSet",
4135
+ "inputMode",
4136
+ "itemID",
4137
+ "itemProp",
4138
+ "itemRef",
4139
+ "itemScope",
4140
+ "itemType",
4141
+ "maxLength",
4142
+ "noModule",
4143
+ "noValidate",
4144
+ "playsInline",
4145
+ "readOnly",
4146
+ "referrerPolicy",
4147
+ "spellCheck",
4148
+ "srcDoc",
4149
+ "srcLang",
4150
+ "srcSet",
4151
+ "tabIndex",
4152
+ "viewBox"
4153
+ ];
4154
+ var SPELLINGS = new Map(
4155
+ DOM_PROPERTY_SPELLINGS.map((name) => [name.toLowerCase(), name])
4156
+ );
4157
+ function hostTagAttributes(source) {
4158
+ const tags = [];
4159
+ for (const match of source.matchAll(/<[a-z][a-z0-9-]*/g)) {
4160
+ const start = match.index + match[0].length;
4161
+ let depth = 0;
4162
+ let quote = "";
4163
+ let text = "";
4164
+ let end = start;
4165
+ for (; end < source.length; end += 1) {
4166
+ const character = source[end];
4167
+ if (quote !== "") {
4168
+ if (character === quote) quote = "";
4169
+ text += " ";
4170
+ continue;
4171
+ }
4172
+ if (depth > 0) {
4173
+ if (character === "{") depth += 1;
4174
+ else if (character === "}") depth -= 1;
4175
+ text += depth === 0 ? character : " ";
4176
+ continue;
4177
+ }
4178
+ if (character === "{") {
4179
+ depth = 1;
4180
+ text += character;
4181
+ continue;
4182
+ }
4183
+ if (character === '"' || character === "'") {
4184
+ quote = character;
4185
+ text += character;
4186
+ continue;
4187
+ }
4188
+ if (character === ">") break;
4189
+ text += character;
4190
+ }
4191
+ tags.push({ start, text });
4192
+ }
4193
+ return tags;
4194
+ }
4195
+ function domPropertyMisspellings(source) {
4196
+ const found = [];
4197
+ for (const tag of hostTagAttributes(source)) {
4198
+ for (const attribute of tag.text.matchAll(
4199
+ /(?<=\s)([A-Za-z][A-Za-z-]*)=/g
4200
+ )) {
4201
+ const name = attribute[1];
4202
+ const expected = SPELLINGS.get(name.toLowerCase());
4203
+ if (expected === void 0 || expected === name) continue;
4204
+ const index = tag.start + attribute.index;
4205
+ found.push({
4206
+ name,
4207
+ expected,
4208
+ index,
4209
+ line: source.slice(0, index).split("\n").length
4210
+ });
4211
+ }
4212
+ }
4213
+ return found.sort((left, right) => left.index - right.index);
4214
+ }
4215
+
4216
+ // src/module-validate.ts
3903
4217
  function issue(code, message, path, severity = "error") {
3904
4218
  return { code, message, path, severity };
3905
4219
  }
@@ -4088,6 +4402,7 @@ async function translationIssues(moduleRoot, manifest, projectLocales) {
4088
4402
  }
4089
4403
  if (!manifest.capabilities.includes("translations")) return issues;
4090
4404
  const keySets = /* @__PURE__ */ new Map();
4405
+ const referenceBundleKeys = /* @__PURE__ */ new Set();
4091
4406
  for (const locale of manifest.locales) {
4092
4407
  const path = `translations/${locale}.json`;
4093
4408
  if (!await exists(join5(moduleRoot, path))) {
@@ -4105,7 +4420,21 @@ async function translationIssues(moduleRoot, manifest, projectLocales) {
4105
4420
  if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
4106
4421
  throw new Error("Expected a JSON object.");
4107
4422
  }
4108
- keySets.set(locale, Object.keys(bundle).sort());
4423
+ const strings = bundle;
4424
+ keySets.set(locale, translationKeys(strings));
4425
+ if (keySets.size === 1) {
4426
+ for (const key of Object.keys(strings)) referenceBundleKeys.add(key);
4427
+ }
4428
+ const plurals = pluralFamilyIssues(strings, locale);
4429
+ if (plurals.length > 0) {
4430
+ issues.push(
4431
+ issue(
4432
+ "TRANSLATION_PLURAL_INCOMPLETE",
4433
+ `${path} plural families do not match the ${locale} plural rules: ${plurals.join("; ")}.`,
4434
+ path
4435
+ )
4436
+ );
4437
+ }
4109
4438
  } catch (error) {
4110
4439
  issues.push(
4111
4440
  issue(
@@ -4134,7 +4463,7 @@ async function translationIssues(moduleRoot, manifest, projectLocales) {
4134
4463
  )
4135
4464
  );
4136
4465
  }
4137
- const referenceKeys = new Set(reference[1]);
4466
+ const referenceKeys = /* @__PURE__ */ new Set([...reference[1], ...referenceBundleKeys]);
4138
4467
  const files = await clientSourceFiles(moduleRoot);
4139
4468
  if (files.length > 0) {
4140
4469
  const namespace = manifest.id.split(".")[0] ?? manifest.id;
@@ -4166,6 +4495,21 @@ async function translationIssues(moduleRoot, manifest, projectLocales) {
4166
4495
  }
4167
4496
  return issues;
4168
4497
  }
4498
+ function filtersTags(source) {
4499
+ const tags = [];
4500
+ for (const match of source.matchAll(/<Filters\b/g)) {
4501
+ let depth = 0;
4502
+ let end = match.index + match[0].length;
4503
+ for (; end < source.length; end += 1) {
4504
+ const character = source[end];
4505
+ if (character === "{") depth += 1;
4506
+ else if (character === "}") depth -= 1;
4507
+ else if (character === ">" && depth === 0) break;
4508
+ }
4509
+ tags.push(source.slice(match.index, end));
4510
+ }
4511
+ return tags;
4512
+ }
4169
4513
  async function userInterfaceIssues(moduleRoot) {
4170
4514
  const issues = [];
4171
4515
  for (const file of await clientSourceFiles(moduleRoot)) {
@@ -4189,6 +4533,27 @@ async function userInterfaceIssues(moduleRoot) {
4189
4533
  )
4190
4534
  );
4191
4535
  }
4536
+ const misspelled = domPropertyMisspellings(source);
4537
+ if (misspelled.length > 0) {
4538
+ issues.push(
4539
+ issue(
4540
+ "DOM_PROPERTY_CASING",
4541
+ "Host elements use Octane's DOM property spelling: " + misspelled.map(
4542
+ (found) => `line ${found.line} ${found.name} as ${found.expected}`
4543
+ ).join(", ") + ".",
4544
+ path
4545
+ )
4546
+ );
4547
+ }
4548
+ if (filtersTags(source).some((tag) => !/\slabel=/.test(tag))) {
4549
+ issues.push(
4550
+ issue(
4551
+ "FILTERS_LABEL_MISSING",
4552
+ 'Filters from @flowdular/ui needs a translated label; without one it shows the English word "Filters" in every locale.',
4553
+ path
4554
+ )
4555
+ );
4556
+ }
4192
4557
  }
4193
4558
  return issues;
4194
4559
  }
package/dist/index.js CHANGED
@@ -2650,6 +2650,17 @@ var module_spec_schema_default = {
2650
2650
  type: "array",
2651
2651
  items: { $ref: "#/$defs/decision" },
2652
2652
  uniqueItems: true
2653
+ },
2654
+ research: { $ref: "#/$defs/research" },
2655
+ adapters: {
2656
+ type: "array",
2657
+ items: { $ref: "#/$defs/adapter" },
2658
+ uniqueItems: true
2659
+ },
2660
+ templates: {
2661
+ type: "array",
2662
+ items: { $ref: "#/$defs/template" },
2663
+ uniqueItems: true
2653
2664
  }
2654
2665
  },
2655
2666
  allOf: [
@@ -2667,7 +2678,10 @@ var module_spec_schema_default = {
2667
2678
  settings: false,
2668
2679
  agentTools: false,
2669
2680
  outOfScope: false,
2670
- decisions: false
2681
+ decisions: false,
2682
+ research: false,
2683
+ adapters: false,
2684
+ templates: false
2671
2685
  }
2672
2686
  }
2673
2687
  }
@@ -2873,6 +2887,113 @@ var module_spec_schema_default = {
2873
2887
  answer: { type: "string", minLength: 1 },
2874
2888
  decidedBy: { enum: ["user", "default"] }
2875
2889
  }
2890
+ },
2891
+ domain: {
2892
+ type: "string",
2893
+ maxLength: 253,
2894
+ pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$"
2895
+ },
2896
+ research: {
2897
+ type: "object",
2898
+ additionalProperties: false,
2899
+ required: ["adapter", "evidenceOwner"],
2900
+ properties: {
2901
+ adapter: {
2902
+ enum: [
2903
+ "model-native",
2904
+ "searxng",
2905
+ "firecrawl",
2906
+ "connector",
2907
+ "recorded"
2908
+ ]
2909
+ },
2910
+ allowDomains: {
2911
+ type: "array",
2912
+ items: { $ref: "#/$defs/domain" },
2913
+ uniqueItems: true
2914
+ },
2915
+ denyDomains: {
2916
+ type: "array",
2917
+ items: { $ref: "#/$defs/domain" },
2918
+ uniqueItems: true
2919
+ },
2920
+ monthlyQueryBudget: { type: "integer", minimum: 1 },
2921
+ evidenceOwner: { $ref: "#/$defs/entityId" }
2922
+ }
2923
+ },
2924
+ adapter: {
2925
+ type: "object",
2926
+ additionalProperties: false,
2927
+ required: [
2928
+ "id",
2929
+ "direction",
2930
+ "connector",
2931
+ "operation",
2932
+ "port",
2933
+ "mapping"
2934
+ ],
2935
+ properties: {
2936
+ id: {
2937
+ type: "string",
2938
+ pattern: "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
2939
+ },
2940
+ direction: { enum: ["source", "sink"] },
2941
+ connector: { type: "string", pattern: "^[a-z][a-z0-9-]{0,95}$" },
2942
+ operation: { type: "string", pattern: "^[a-z][a-z0-9-]{0,63}$" },
2943
+ port: {
2944
+ type: "string",
2945
+ maxLength: 96,
2946
+ pattern: "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)+$"
2947
+ },
2948
+ schedule: {
2949
+ anyOf: [
2950
+ { type: "null" },
2951
+ {
2952
+ type: "string",
2953
+ maxLength: 100,
2954
+ pattern: "^[0-9a-z*,/-]+( [0-9a-z*,/-]+){4}$"
2955
+ }
2956
+ ]
2957
+ },
2958
+ mapping: {
2959
+ type: "array",
2960
+ minItems: 1,
2961
+ items: { $ref: "#/$defs/adapterMapping" },
2962
+ uniqueItems: true
2963
+ },
2964
+ recorded: {
2965
+ type: "string",
2966
+ pattern: "^adapters/[a-z0-9][a-z0-9-]*\\.recorded\\.json$"
2967
+ }
2968
+ }
2969
+ },
2970
+ adapterMapping: {
2971
+ type: "object",
2972
+ additionalProperties: false,
2973
+ required: ["to", "transform"],
2974
+ properties: {
2975
+ from: { type: "string", minLength: 1 },
2976
+ to: { type: "string", minLength: 1 },
2977
+ transform: { enum: ["rename", "constant", "format", "lookup"] },
2978
+ value: {
2979
+ anyOf: [{ type: "null" }, { type: "string", minLength: 1 }]
2980
+ }
2981
+ }
2982
+ },
2983
+ template: {
2984
+ type: "object",
2985
+ additionalProperties: false,
2986
+ required: ["id", "title", "inputEntity", "format", "body"],
2987
+ properties: {
2988
+ id: { $ref: "#/$defs/entityId" },
2989
+ title: { type: "string", minLength: 1 },
2990
+ inputEntity: { $ref: "#/$defs/entityId" },
2991
+ format: { enum: ["pdf", "docx"] },
2992
+ body: {
2993
+ type: "string",
2994
+ pattern: "^templates/[a-z][a-z0-9-]*\\.md$"
2995
+ }
2996
+ }
2876
2997
  }
2877
2998
  }
2878
2999
  };
@@ -3039,6 +3160,15 @@ var project_schema_default = {
3039
3160
  $ref: "#/$defs/githubAccount"
3040
3161
  },
3041
3162
  uniqueItems: true
3163
+ },
3164
+ labels: {
3165
+ type: "array",
3166
+ maxItems: 10,
3167
+ items: {
3168
+ type: "string",
3169
+ pattern: "^[A-Za-z0-9][A-Za-z0-9 ._:-]{0,49}$"
3170
+ },
3171
+ uniqueItems: true
3042
3172
  }
3043
3173
  }
3044
3174
  },
@@ -3286,8 +3416,79 @@ var projectSchema = project_schema_default;
3286
3416
  var moduleCatalogSchema = module_catalog_schema_default;
3287
3417
  var moduleArtifactSchema = module_artifact_schema_default;
3288
3418
 
3419
+ // ../contracts/src/translations.ts
3420
+ var PLURAL_CATEGORIES = [
3421
+ "zero",
3422
+ "one",
3423
+ "two",
3424
+ "few",
3425
+ "many",
3426
+ "other"
3427
+ ];
3428
+ var WHOLE_NUMBER_SAMPLES = [
3429
+ ...Array.from({ length: 200 }, (_, index) => index),
3430
+ 1e3,
3431
+ 1e4,
3432
+ 1e5,
3433
+ 1e6,
3434
+ 1e7,
3435
+ 1e8,
3436
+ 1e9
3437
+ ];
3438
+ function requiredPluralCategories(locale) {
3439
+ const rules = new Intl.PluralRules(locale);
3440
+ const selected = /* @__PURE__ */ new Set(["other"]);
3441
+ for (const sample of WHOLE_NUMBER_SAMPLES) selected.add(rules.select(sample));
3442
+ return PLURAL_CATEGORIES.filter((category) => selected.has(category));
3443
+ }
3444
+ function pluralMember(bundle, key) {
3445
+ const dot = key.lastIndexOf(".");
3446
+ if (dot <= 0) return null;
3447
+ const category = key.slice(dot + 1);
3448
+ if (!PLURAL_CATEGORIES.includes(category)) return null;
3449
+ const base = key.slice(0, dot);
3450
+ return Object.hasOwn(bundle, base + ".other") ? { base, category } : null;
3451
+ }
3452
+ function translationKeys(bundle) {
3453
+ const keys = /* @__PURE__ */ new Set();
3454
+ for (const key of Object.keys(bundle)) {
3455
+ keys.add(pluralMember(bundle, key)?.base ?? key);
3456
+ }
3457
+ return [...keys].sort();
3458
+ }
3459
+ function pluralFamilyIssues(bundle, locale) {
3460
+ const families = /* @__PURE__ */ new Map();
3461
+ for (const key of Object.keys(bundle)) {
3462
+ const member = pluralMember(bundle, key);
3463
+ if (member === null) continue;
3464
+ const categories = families.get(member.base) ?? /* @__PURE__ */ new Set();
3465
+ categories.add(member.category);
3466
+ families.set(member.base, categories);
3467
+ }
3468
+ const required = requiredPluralCategories(locale);
3469
+ const known = new Intl.PluralRules(
3470
+ locale
3471
+ ).resolvedOptions().pluralCategories;
3472
+ const issues = [];
3473
+ for (const base of [...families.keys()].sort()) {
3474
+ const categories = families.get(base);
3475
+ const missing = required.filter((category) => !categories.has(category));
3476
+ if (missing.length > 0) {
3477
+ issues.push(
3478
+ `${base} lacks ${missing.map((category) => base + "." + category).join(", ")} for ${locale}`
3479
+ );
3480
+ }
3481
+ for (const category of categories) {
3482
+ if (!known.includes(category)) {
3483
+ issues.push(`${base}.${category} is never selected for ${locale}`);
3484
+ }
3485
+ }
3486
+ }
3487
+ return issues;
3488
+ }
3489
+
3289
3490
  // ../contracts/src/index.ts
3290
- var PLATFORM_API_VERSION = "0.1.9";
3491
+ var PLATFORM_API_VERSION = "0.1.21";
3291
3492
 
3292
3493
  // src/module-templates.ts
3293
3494
  function packageSuffix(id) {
@@ -3687,6 +3888,7 @@ function packageJson(model) {
3687
3888
  "migrations",
3688
3889
  "translations",
3689
3890
  "spec",
3891
+ ...spec.templates?.length ? ["templates"] : [],
3690
3892
  "README.md"
3691
3893
  ],
3692
3894
  scripts: {
@@ -4530,34 +4732,76 @@ export function create${names.pascal}ClientState() {
4530
4732
  }
4531
4733
  `;
4532
4734
  }
4533
- function columnWidth(index, count) {
4534
- if (count < 2) return "100%";
4535
- const rest = Math.max(1, Math.floor(35 / (count - 1)));
4536
- return index === 0 ? `${Math.max(1, 100 - rest * (count - 1))}%` : `${rest}%`;
4537
- }
4538
- function cellValue(field, namespace) {
4735
+ function scaffoldCell(field, namespace, index) {
4539
4736
  const access7 = `record.${field.id}`;
4540
- if (field.type === "enum") {
4541
- const label2 = `t('${namespace}.${field.id}.' + ${access7})`;
4542
- return field.required ? label2 : `${access7} === null ? '' : ${label2}`;
4543
- }
4544
- if (field.type === "json") return `JSON.stringify(${access7})`;
4545
- if (field.type === "integer" || field.type === "boolean") {
4546
- return field.required ? `String(${access7})` : `${access7} === null ? '' : String(${access7})`;
4737
+ const identity = index === 0;
4738
+ const priority = identity || field.state ? 1 : 2;
4739
+ const cell = (component, props, width, fieldPriority = priority, numeric = false) => ({
4740
+ component,
4741
+ markup: `<${component} ${props} />`,
4742
+ width: identity ? "auto" : width,
4743
+ priority: identity ? 1 : fieldPriority,
4744
+ numeric
4745
+ });
4746
+ switch (field.type) {
4747
+ case "enum":
4748
+ return cell(
4749
+ "CellTag",
4750
+ `label={t('${namespace}.${field.id}.' + ${access7})}` + (field.state ? " dot" : ""),
4751
+ "140px"
4752
+ );
4753
+ case "integer":
4754
+ case "decimal":
4755
+ return cell("CellNumber", `value={${access7}}`, "120px", priority, true);
4756
+ case "boolean":
4757
+ return cell("CellText", `value={String(${access7})}`, "100px");
4758
+ case "date":
4759
+ return cell("CellTime", `value={${access7}} kind="date"`, "140px");
4760
+ case "datetime":
4761
+ return cell("CellTime", `value={${access7}}`, "170px");
4762
+ case "reference":
4763
+ return cell("CellCode", `value={${access7}}`, "160px", 3);
4764
+ case "json":
4765
+ return cell(
4766
+ "CellText",
4767
+ `value={JSON.stringify(${access7})} mono`,
4768
+ "200px",
4769
+ 3
4770
+ );
4771
+ case "text":
4772
+ return cell("CellText", `value={${access7}} wrap muted`, "240px", 3);
4773
+ default:
4774
+ return cell(
4775
+ "CellText",
4776
+ `value={${access7}}` + (identity ? " strong" : ""),
4777
+ "200px"
4778
+ );
4547
4779
  }
4548
- return field.required ? access7 : `${access7} ?? ''`;
4780
+ }
4781
+ function scaffoldCells(model) {
4782
+ return model.columns.map(
4783
+ (field, index) => scaffoldCell(field, model.names.namespace, index)
4784
+ );
4785
+ }
4786
+ function cellImports(model) {
4787
+ const names = new Set(scaffoldCells(model).map((cell) => cell.component));
4788
+ if (model.columns.some((field) => !field.required)) names.add("CellMuted");
4789
+ return [...names].sort();
4549
4790
  }
4550
4791
  function tableColumns(model) {
4551
4792
  const { columns, names } = model;
4793
+ const cells = scaffoldCells(model);
4552
4794
  return columns.map((field, index) => {
4553
- const value = cellValue(field, names.namespace);
4554
- const cell = index === 0 ? `<b>{${value}}</b>` : value;
4555
- const numeric = field.type === "integer" || field.type === "decimal" ? " numeric: true,\n" : "";
4795
+ const cell = cells[index];
4796
+ const markup = field.required ? cell.markup : `record.${field.id} === null
4797
+ ? <CellMuted>{t('${names.namespace}.value.none')}</CellMuted>
4798
+ : ${cell.markup}`;
4556
4799
  return ` {
4557
4800
  key: '${field.id}',
4558
4801
  header: t('${names.namespace}.table.column.${field.id}'),
4559
- width: '${columnWidth(index, columns.length)}',
4560
- ${numeric} cell: (record) => ${cell},
4802
+ width: '${cell.width}',
4803
+ ${cell.priority > 1 ? ` priority: ${cell.priority},
4804
+ ` : ""}${cell.numeric ? " numeric: true,\n" : ""} cell: (record) => ${markup},
4561
4805
  },
4562
4806
  `;
4563
4807
  }).join("");
@@ -4592,7 +4836,8 @@ import { t } from '@flowdular/client';
4592
4836
  import {
4593
4837
  Alert,
4594
4838
  Button,
4595
- Icon,
4839
+ ${cellImports(model).map((name) => ` ${name},
4840
+ `).join("")} Icon,
4596
4841
  PageHeader,
4597
4842
  TableCard,
4598
4843
  type TableColumn,
@@ -4880,6 +5125,74 @@ export default defineCliExtension({
4880
5125
  });
4881
5126
  `;
4882
5127
  }
5128
+ function tsLiteral(value, depth) {
5129
+ if (typeof value === "string") return stringLiteral(value);
5130
+ if (value === null || typeof value !== "object") return String(value);
5131
+ const list = Array.isArray(value);
5132
+ const entries = list ? value.map((item) => tsLiteral(item, depth + 1)) : Object.entries(value).map(
5133
+ ([key, item]) => `${key}: ${tsLiteral(item, depth + 1)}`
5134
+ );
5135
+ if (entries.length === 0) return list ? "[]" : "{}";
5136
+ const indent = " ".repeat(depth + 1);
5137
+ return `${list ? "[" : "{"}
5138
+ ${entries.map((entry) => `${indent}${entry},
5139
+ `).join("")}${" ".repeat(depth)}${list ? "]" : "}"}`;
5140
+ }
5141
+ function researchFile(model, research) {
5142
+ return `import type { ModuleSpecResearch } from '@flowdular/contracts';
5143
+
5144
+ export const RESEARCH_CAPABILITIES = {
5145
+ search: 'research.search.v1',
5146
+ fetch: 'research.fetch.v1',
5147
+ evidence: 'research.evidence.v1',
5148
+ } as const;
5149
+
5150
+ export const ${model.names.constant}_RESEARCH = ${tsLiteral(research, 0)} as const satisfies ModuleSpecResearch;
5151
+ `;
5152
+ }
5153
+ function researchFixtures(research) {
5154
+ const host = research.allowDomains?.[0] ?? "example.com";
5155
+ const url = `https://${host}/example`;
5156
+ return json({
5157
+ queries: {
5158
+ example: [
5159
+ {
5160
+ url,
5161
+ title: "Example result",
5162
+ snippet: "Replace with a recorded search result.",
5163
+ source: host
5164
+ }
5165
+ ]
5166
+ },
5167
+ pages: {
5168
+ [url]: {
5169
+ title: "Example result",
5170
+ text: "Replace with the text of the recorded page."
5171
+ }
5172
+ }
5173
+ });
5174
+ }
5175
+ function recordedFixture(adapter) {
5176
+ return json({
5177
+ adapter: adapter.id,
5178
+ operation: adapter.operation,
5179
+ calls: [{ input: {}, body: {} }]
5180
+ });
5181
+ }
5182
+ function adapterFile(name, adapter) {
5183
+ const constant = name.replace(/-/g, "_").toUpperCase();
5184
+ return `import type { ModuleSpecAdapter } from '@flowdular/contracts';
5185
+
5186
+ export const ${constant}_ADAPTER = ${tsLiteral(adapter, 0)} as const satisfies ModuleSpecAdapter;
5187
+ `;
5188
+ }
5189
+ function templateBody(spec, template) {
5190
+ const fields = (spec.entities ?? []).find((entity) => entity.id === template.inputEntity)?.fields.map((field) => field.id).join(", ");
5191
+ return `# ${template.title}
5192
+
5193
+ <!-- Rendered as ${template.format} from one ${template.inputEntity} record. Fields: ${fields}. -->
5194
+ `;
5195
+ }
4883
5196
  var POLISH_LABELS = {
4884
5197
  name: "Nazwa",
4885
5198
  status: "Status",
@@ -4914,6 +5227,7 @@ function valueLabels(model, locale) {
4914
5227
  function translation(locale, name, description, model) {
4915
5228
  const columns = columnLabels(model, locale);
4916
5229
  const values2 = valueLabels(model, locale);
5230
+ const optional = model.columns.some((field) => !field.required);
4917
5231
  if (locale === "pl") {
4918
5232
  return json({
4919
5233
  "module.name": `Modu\u0142 ${name}`,
@@ -4925,12 +5239,16 @@ function translation(locale, name, description, model) {
4925
5239
  "action.refresh": "Od\u015Bwie\u017C",
4926
5240
  "table.title": "Rekordy",
4927
5241
  "table.caption": `Rekordy modu\u0142u ${name}`,
4928
- "table.count": "Liczba rekord\xF3w: {count}",
5242
+ "table.count.one": "{count} rekord",
5243
+ "table.count.few": "{count} rekordy",
5244
+ "table.count.many": "{count} rekord\xF3w",
5245
+ "table.count.other": "{count} rekordu",
4929
5246
  ...columns,
4930
5247
  "table.loading": "Wczytywanie rekord\xF3w\u2026",
4931
5248
  "table.emptyTitle": "Brak rekord\xF3w",
4932
5249
  "table.emptyHint": "Utworzone rekordy pojawi\u0105 si\u0119 w tym miejscu.",
4933
5250
  ...values2,
5251
+ ...optional ? { "value.none": "Brak" } : {},
4934
5252
  "error.load": "Nie uda\u0142o si\u0119 wczyta\u0107 rekord\xF3w.",
4935
5253
  "error.request": "Operacja na rekordach nie powiod\u0142a si\u0119."
4936
5254
  });
@@ -4945,12 +5263,18 @@ function translation(locale, name, description, model) {
4945
5263
  "action.refresh": "Refresh",
4946
5264
  "table.title": "Records",
4947
5265
  "table.caption": `${name} records`,
4948
- "table.count": "{count} records",
5266
+ ...Object.fromEntries(
5267
+ requiredPluralCategories(locale).map((category) => [
5268
+ `table.count.${category}`,
5269
+ category === "one" ? "{count} record" : "{count} records"
5270
+ ])
5271
+ ),
4949
5272
  ...columns,
4950
5273
  "table.loading": "Loading records\u2026",
4951
5274
  "table.emptyTitle": "No records yet",
4952
5275
  "table.emptyHint": "Records created in this workspace appear here.",
4953
5276
  ...values2,
5277
+ ...optional ? { "value.none": "None" } : {},
4954
5278
  "error.load": "Could not load records.",
4955
5279
  "error.request": "The records operation failed."
4956
5280
  });
@@ -5012,6 +5336,31 @@ function planScaffold(spec, specSource) {
5012
5336
  files.set("src/cli/commands.json", cliCatalog(model));
5013
5337
  files.set("src/cli/index.ts", cliEntry(model));
5014
5338
  }
5339
+ if (spec.research) {
5340
+ files.set("src/research.ts", researchFile(model, spec.research));
5341
+ files.set("research-fixtures.json", researchFixtures(spec.research));
5342
+ }
5343
+ for (const adapter of spec.adapters ?? []) {
5344
+ const name = adapter.id.slice(spec.id.length + 1).replace(/\./g, "-");
5345
+ const paths = [
5346
+ `src/adapters/${name}.ts`,
5347
+ adapter.recorded ?? `adapters/${name}.recorded.json`
5348
+ ];
5349
+ for (const path of paths) {
5350
+ if (files.has(path)) {
5351
+ throw new Error(
5352
+ `Adapter "${adapter.id}" and another adapter both map to ${path}.`
5353
+ );
5354
+ }
5355
+ }
5356
+ files.set(paths[0], adapterFile(name, adapter));
5357
+ files.set(paths[1], recordedFixture(adapter));
5358
+ }
5359
+ for (const template of spec.templates ?? []) {
5360
+ if (!files.has(template.body)) {
5361
+ files.set(template.body, templateBody(spec, template));
5362
+ }
5363
+ }
5015
5364
  files.set("tests/module.test.ts", testFile(model));
5016
5365
  for (const locale of /* @__PURE__ */ new Set(["en", ...spec.locales])) {
5017
5366
  files.set(
@@ -5068,6 +5417,51 @@ function duplicateIssues(items, key, section) {
5068
5417
  });
5069
5418
  return issues;
5070
5419
  }
5420
+ var MONTH_NAMES = "jan feb mar apr may jun jul aug sep oct nov dec".split(
5421
+ " "
5422
+ );
5423
+ var WEEKDAY_NAMES = "sun mon tue wed thu fri sat".split(" ");
5424
+ var CRON_FIELDS = [
5425
+ { name: "minute", min: 0, max: 59 },
5426
+ { name: "hour", min: 0, max: 23 },
5427
+ { name: "day of month", min: 1, max: 31 },
5428
+ { name: "month", min: 1, max: 12, names: MONTH_NAMES },
5429
+ { name: "day of week", min: 0, max: 7, names: WEEKDAY_NAMES }
5430
+ ];
5431
+ function cronIssue(expression) {
5432
+ const fields = expression.split(" ");
5433
+ if (fields.length !== 5) return "it needs five fields";
5434
+ for (const [index, field] of fields.entries()) {
5435
+ const spec = CRON_FIELDS[index];
5436
+ const names = "names" in spec ? spec.names : [];
5437
+ const value = (raw) => {
5438
+ const named = names.indexOf(raw);
5439
+ if (named >= 0) return named + spec.min;
5440
+ const number = /^\d{1,2}$/.test(raw) ? Number(raw) : Number.NaN;
5441
+ return number >= spec.min && number <= spec.max ? number : void 0;
5442
+ };
5443
+ const items = field.split(",");
5444
+ if (items.length > 32) return `${spec.name} lists too many values`;
5445
+ for (const item of items) {
5446
+ const [base = "", step, extra] = item.split("/");
5447
+ if (extra !== void 0 || base === "")
5448
+ return `${spec.name} has an unsupported value`;
5449
+ if (step !== void 0 && (!/^\d{1,2}$/.test(step) || Number(step) < 1 || Number(step) > spec.max - spec.min + 1))
5450
+ return `${spec.name} has an unsupported step`;
5451
+ if (base === "*") continue;
5452
+ const bounds = base.split("-");
5453
+ if (bounds.length > 2) return `${spec.name} has an unsupported range`;
5454
+ const from = value(bounds[0]);
5455
+ const to = bounds.length === 2 ? value(bounds[1]) : from;
5456
+ if (from === void 0 || to === void 0)
5457
+ return `${spec.name} must be between ${spec.min} and ${spec.max}`;
5458
+ if (bounds.length === 1 && step !== void 0)
5459
+ return `${spec.name} needs a range or * before a step`;
5460
+ if (to < from) return `${spec.name} range runs backwards`;
5461
+ }
5462
+ }
5463
+ return void 0;
5464
+ }
5071
5465
  function moduleSpecIssues(value) {
5072
5466
  const spec = value;
5073
5467
  if (!spec || typeof spec !== "object" || spec.schemaVersion !== 2) return [];
@@ -5091,7 +5485,9 @@ function moduleSpecIssues(value) {
5091
5485
  ...duplicateIssues(spec.widgets, "id", "widgets"),
5092
5486
  ...duplicateIssues(spec.settings, "key", "settings"),
5093
5487
  ...duplicateIssues(spec.agentTools, "id", "agentTools"),
5094
- ...duplicateIssues(spec.decisions, "id", "decisions")
5488
+ ...duplicateIssues(spec.decisions, "id", "decisions"),
5489
+ ...duplicateIssues(spec.adapters, "id", "adapters"),
5490
+ ...duplicateIssues(spec.templates, "id", "templates")
5095
5491
  ];
5096
5492
  if (capabilities2.has("database") && entities.length === 0) {
5097
5493
  issues.push(
@@ -5182,12 +5578,12 @@ function moduleSpecIssues(value) {
5182
5578
  });
5183
5579
  }
5184
5580
  });
5185
- const entityIssue = (entity, path) => {
5581
+ const entityIssue = (entity, path, key = "entity") => {
5186
5582
  if (entity === void 0 || fieldsByEntity.has(entity)) return void 0;
5187
5583
  return specIssue(
5188
5584
  "SPEC_ENTITY_UNKNOWN",
5189
5585
  `"${entity}" is not an entity of this specification.`,
5190
- `${path}/entity`
5586
+ `${path}/${key}`
5191
5587
  );
5192
5588
  };
5193
5589
  (spec.screens ?? []).forEach((screen, index) => {
@@ -5250,6 +5646,62 @@ function moduleSpecIssues(value) {
5250
5646
  )
5251
5647
  );
5252
5648
  });
5649
+ const evidenceOwner = entityIssue(
5650
+ spec.research?.evidenceOwner,
5651
+ "/research",
5652
+ "evidenceOwner"
5653
+ );
5654
+ if (evidenceOwner) issues.push(evidenceOwner);
5655
+ (spec.templates ?? []).forEach((template, index) => {
5656
+ const unknownEntity = entityIssue(
5657
+ template.inputEntity,
5658
+ `/templates/${index}`,
5659
+ "inputEntity"
5660
+ );
5661
+ if (unknownEntity) issues.push(unknownEntity);
5662
+ });
5663
+ (spec.adapters ?? []).forEach((adapter, index) => {
5664
+ const path = `/adapters/${index}`;
5665
+ if (!adapter.id.startsWith(`${spec.id}.`)) {
5666
+ issues.push(
5667
+ specIssue(
5668
+ "SPEC_ADAPTER_ID_NAMESPACE",
5669
+ `Adapter "${adapter.id}" must start with the module id "${spec.id}.".`,
5670
+ `${path}/id`
5671
+ )
5672
+ );
5673
+ }
5674
+ if (adapter.direction === "source" && ![...modules].some((owner) => adapter.port.startsWith(`${owner}.`))) {
5675
+ issues.push(
5676
+ specIssue(
5677
+ "SPEC_ADAPTER_PORT_UNKNOWN",
5678
+ `Source adapter "${adapter.id}" writes through port "${adapter.port}", which belongs neither to this module nor to a declared dependency.`,
5679
+ `${path}/port`
5680
+ )
5681
+ );
5682
+ }
5683
+ const schedule = typeof adapter.schedule === "string" ? cronIssue(adapter.schedule) : void 0;
5684
+ if (schedule) {
5685
+ issues.push(
5686
+ specIssue(
5687
+ "SPEC_ADAPTER_SCHEDULE_INVALID",
5688
+ `Schedule of adapter "${adapter.id}" is not a five-field cron: ${schedule}.`,
5689
+ `${path}/schedule`
5690
+ )
5691
+ );
5692
+ }
5693
+ adapter.mapping.forEach((entry, entryIndex) => {
5694
+ const missing = entry.transform !== "constant" && entry.from === void 0 ? "from" : entry.transform !== "rename" && typeof entry.value !== "string" ? "value" : void 0;
5695
+ if (!missing) return;
5696
+ issues.push(
5697
+ specIssue(
5698
+ "SPEC_ADAPTER_MAPPING_INVALID",
5699
+ `A ${entry.transform} mapping of adapter "${adapter.id}" needs "${missing}".`,
5700
+ `${path}/mapping/${entryIndex}/${missing}`
5701
+ )
5702
+ );
5703
+ });
5704
+ });
5253
5705
  if (capabilities2.has("client") && !(spec.screens ?? []).some(
5254
5706
  (screen) => RENDERABLE_SCREEN_KINDS.has(screen.kind)
5255
5707
  )) {
@@ -5391,19 +5843,14 @@ var ALLOWED = Object.freeze({ allowed: true });
5391
5843
  var NO_POLICIES = Object.freeze([]);
5392
5844
 
5393
5845
  // ../kernel/src/approval-grant.ts
5394
- import { createHash as createHash2, createHmac, timingSafeEqual } from "node:crypto";
5846
+ import * as nodeCrypto2 from "node:crypto";
5395
5847
 
5396
5848
  // ../kernel/src/keyring.ts
5397
- import {
5398
- createCipheriv,
5399
- createDecipheriv,
5400
- createHash,
5401
- randomBytes
5402
- } from "node:crypto";
5849
+ import * as nodeCrypto from "node:crypto";
5403
5850
  var KEYRING_KEY_BYTES = 32;
5404
5851
  var KEYRING_MAX_PREVIOUS_KEYS = 8;
5405
5852
  function keyFingerprint(key) {
5406
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
5853
+ return nodeCrypto.createHash("sha256").update(key).digest("hex").slice(0, 16);
5407
5854
  }
5408
5855
  function parsePreviousKeys(value, decode) {
5409
5856
  if (!value) return [];
@@ -5475,7 +5922,7 @@ function canonical(value) {
5475
5922
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`).join(",")}}`;
5476
5923
  }
5477
5924
  function approvalInputDigest(input) {
5478
- return createHash2("sha256").update(canonical(input), "utf8").digest("hex");
5925
+ return nodeCrypto2.createHash("sha256").update(canonical(input), "utf8").digest("hex");
5479
5926
  }
5480
5927
  function claimString(value, maximum = MAX_CLAIM_LENGTH) {
5481
5928
  return typeof value === "string" && value.length >= 1 && value.length <= maximum && !/[\s\u0000]/.test(value) ? value : void 0;
@@ -5513,7 +5960,7 @@ function readClaims(value) {
5513
5960
  };
5514
5961
  }
5515
5962
  function signature(key, signed) {
5516
- return createHmac("sha256", key).update(signed, "utf8").digest();
5963
+ return nodeCrypto2.createHmac("sha256", key).update(signed, "utf8").digest();
5517
5964
  }
5518
5965
  function invalid(message) {
5519
5966
  return { ok: false, reason: "APPROVAL_GRANT_INVALID", message };
@@ -5534,7 +5981,7 @@ function verifyApprovalGrant(keyring, token, expected, now = Date.now()) {
5534
5981
  }
5535
5982
  const expectedSignature = signature(key, `${prefix}.${keyId}.${payload}`);
5536
5983
  const suppliedSignature = Buffer.from(supplied, "base64url");
5537
- if (suppliedSignature.byteLength !== expectedSignature.byteLength || !timingSafeEqual(suppliedSignature, expectedSignature)) {
5984
+ if (suppliedSignature.byteLength !== expectedSignature.byteLength || !nodeCrypto2.timingSafeEqual(suppliedSignature, expectedSignature)) {
5538
5985
  return invalid("The approval grant signature does not verify.");
5539
5986
  }
5540
5987
  let parsed;
@@ -6137,7 +6584,7 @@ import { dirname as dirname6, join as join5, relative as relative5 } from "node:
6137
6584
  import { createRequire } from "node:module";
6138
6585
  import { readFile as readFile4 } from "node:fs/promises";
6139
6586
  import { dirname as dirname4, join as join2 } from "node:path";
6140
- var SDK_VERSION = "0.3.2";
6587
+ var SDK_VERSION = "0.4.0";
6141
6588
  var LIBRARIES = /* @__PURE__ */ new Set([
6142
6589
  "ai-provider",
6143
6590
  "cli-protocol",
@@ -6317,6 +6764,113 @@ function failure(code, message, details) {
6317
6764
  };
6318
6765
  }
6319
6766
 
6767
+ // src/dom-properties.ts
6768
+ var DOM_PROPERTY_SPELLINGS = [
6769
+ "acceptCharset",
6770
+ "allowFullScreen",
6771
+ "autoCapitalize",
6772
+ "autoComplete",
6773
+ "autoCorrect",
6774
+ "autoFocus",
6775
+ "autoPlay",
6776
+ "charSet",
6777
+ "contentEditable",
6778
+ "crossOrigin",
6779
+ "defaultChecked",
6780
+ "defaultValue",
6781
+ "disablePictureInPicture",
6782
+ "disableRemotePlayback",
6783
+ "encType",
6784
+ "fetchPriority",
6785
+ "formAction",
6786
+ "formEncType",
6787
+ "formMethod",
6788
+ "formNoValidate",
6789
+ "formTarget",
6790
+ "httpEquiv",
6791
+ "imageSizes",
6792
+ "imageSrcSet",
6793
+ "inputMode",
6794
+ "itemID",
6795
+ "itemProp",
6796
+ "itemRef",
6797
+ "itemScope",
6798
+ "itemType",
6799
+ "maxLength",
6800
+ "noModule",
6801
+ "noValidate",
6802
+ "playsInline",
6803
+ "readOnly",
6804
+ "referrerPolicy",
6805
+ "spellCheck",
6806
+ "srcDoc",
6807
+ "srcLang",
6808
+ "srcSet",
6809
+ "tabIndex",
6810
+ "viewBox"
6811
+ ];
6812
+ var SPELLINGS = new Map(
6813
+ DOM_PROPERTY_SPELLINGS.map((name) => [name.toLowerCase(), name])
6814
+ );
6815
+ function hostTagAttributes(source) {
6816
+ const tags = [];
6817
+ for (const match of source.matchAll(/<[a-z][a-z0-9-]*/g)) {
6818
+ const start = match.index + match[0].length;
6819
+ let depth = 0;
6820
+ let quote = "";
6821
+ let text = "";
6822
+ let end = start;
6823
+ for (; end < source.length; end += 1) {
6824
+ const character = source[end];
6825
+ if (quote !== "") {
6826
+ if (character === quote) quote = "";
6827
+ text += " ";
6828
+ continue;
6829
+ }
6830
+ if (depth > 0) {
6831
+ if (character === "{") depth += 1;
6832
+ else if (character === "}") depth -= 1;
6833
+ text += depth === 0 ? character : " ";
6834
+ continue;
6835
+ }
6836
+ if (character === "{") {
6837
+ depth = 1;
6838
+ text += character;
6839
+ continue;
6840
+ }
6841
+ if (character === '"' || character === "'") {
6842
+ quote = character;
6843
+ text += character;
6844
+ continue;
6845
+ }
6846
+ if (character === ">") break;
6847
+ text += character;
6848
+ }
6849
+ tags.push({ start, text });
6850
+ }
6851
+ return tags;
6852
+ }
6853
+ function domPropertyMisspellings(source) {
6854
+ const found = [];
6855
+ for (const tag of hostTagAttributes(source)) {
6856
+ for (const attribute of tag.text.matchAll(
6857
+ /(?<=\s)([A-Za-z][A-Za-z-]*)=/g
6858
+ )) {
6859
+ const name = attribute[1];
6860
+ const expected = SPELLINGS.get(name.toLowerCase());
6861
+ if (expected === void 0 || expected === name) continue;
6862
+ const index = tag.start + attribute.index;
6863
+ found.push({
6864
+ name,
6865
+ expected,
6866
+ index,
6867
+ line: source.slice(0, index).split("\n").length
6868
+ });
6869
+ }
6870
+ }
6871
+ return found.sort((left, right) => left.index - right.index);
6872
+ }
6873
+
6320
6874
  // src/module-validate.ts
6321
6875
  function issue(code, message, path, severity = "error") {
6322
6876
  return { code, message, path, severity };
@@ -6506,6 +7060,7 @@ async function translationIssues(moduleRoot, manifest2, projectLocales) {
6506
7060
  }
6507
7061
  if (!manifest2.capabilities.includes("translations")) return issues;
6508
7062
  const keySets = /* @__PURE__ */ new Map();
7063
+ const referenceBundleKeys = /* @__PURE__ */ new Set();
6509
7064
  for (const locale of manifest2.locales) {
6510
7065
  const path = `translations/${locale}.json`;
6511
7066
  if (!await exists(join4(moduleRoot, path))) {
@@ -6523,7 +7078,21 @@ async function translationIssues(moduleRoot, manifest2, projectLocales) {
6523
7078
  if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
6524
7079
  throw new Error("Expected a JSON object.");
6525
7080
  }
6526
- keySets.set(locale, Object.keys(bundle).sort());
7081
+ const strings = bundle;
7082
+ keySets.set(locale, translationKeys(strings));
7083
+ if (keySets.size === 1) {
7084
+ for (const key of Object.keys(strings)) referenceBundleKeys.add(key);
7085
+ }
7086
+ const plurals = pluralFamilyIssues(strings, locale);
7087
+ if (plurals.length > 0) {
7088
+ issues.push(
7089
+ issue(
7090
+ "TRANSLATION_PLURAL_INCOMPLETE",
7091
+ `${path} plural families do not match the ${locale} plural rules: ${plurals.join("; ")}.`,
7092
+ path
7093
+ )
7094
+ );
7095
+ }
6527
7096
  } catch (error) {
6528
7097
  issues.push(
6529
7098
  issue(
@@ -6552,7 +7121,7 @@ async function translationIssues(moduleRoot, manifest2, projectLocales) {
6552
7121
  )
6553
7122
  );
6554
7123
  }
6555
- const referenceKeys = new Set(reference[1]);
7124
+ const referenceKeys = /* @__PURE__ */ new Set([...reference[1], ...referenceBundleKeys]);
6556
7125
  const files = await clientSourceFiles(moduleRoot);
6557
7126
  if (files.length > 0) {
6558
7127
  const namespace = manifest2.id.split(".")[0] ?? manifest2.id;
@@ -6584,6 +7153,21 @@ async function translationIssues(moduleRoot, manifest2, projectLocales) {
6584
7153
  }
6585
7154
  return issues;
6586
7155
  }
7156
+ function filtersTags(source) {
7157
+ const tags = [];
7158
+ for (const match of source.matchAll(/<Filters\b/g)) {
7159
+ let depth = 0;
7160
+ let end = match.index + match[0].length;
7161
+ for (; end < source.length; end += 1) {
7162
+ const character = source[end];
7163
+ if (character === "{") depth += 1;
7164
+ else if (character === "}") depth -= 1;
7165
+ else if (character === ">" && depth === 0) break;
7166
+ }
7167
+ tags.push(source.slice(match.index, end));
7168
+ }
7169
+ return tags;
7170
+ }
6587
7171
  async function userInterfaceIssues(moduleRoot) {
6588
7172
  const issues = [];
6589
7173
  for (const file of await clientSourceFiles(moduleRoot)) {
@@ -6607,6 +7191,27 @@ async function userInterfaceIssues(moduleRoot) {
6607
7191
  )
6608
7192
  );
6609
7193
  }
7194
+ const misspelled = domPropertyMisspellings(source);
7195
+ if (misspelled.length > 0) {
7196
+ issues.push(
7197
+ issue(
7198
+ "DOM_PROPERTY_CASING",
7199
+ "Host elements use Octane's DOM property spelling: " + misspelled.map(
7200
+ (found) => `line ${found.line} ${found.name} as ${found.expected}`
7201
+ ).join(", ") + ".",
7202
+ path
7203
+ )
7204
+ );
7205
+ }
7206
+ if (filtersTags(source).some((tag) => !/\slabel=/.test(tag))) {
7207
+ issues.push(
7208
+ issue(
7209
+ "FILTERS_LABEL_MISSING",
7210
+ 'Filters from @flowdular/ui needs a translated label; without one it shows the English word "Filters" in every locale.',
7211
+ path
7212
+ )
7213
+ );
7214
+ }
6610
7215
  }
6611
7216
  return issues;
6612
7217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowdular",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "flowdular": "./dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "ajv": "8.20.0",
17
17
  "pg": "8.23.0",
18
18
  "yaml": "2.9.0",
19
- "@flowdular/sdk": "0.3.2"
19
+ "@flowdular/sdk": "0.4.0"
20
20
  },
21
21
  "license": "MIT",
22
22
  "repository": {