@starci/hfs 4.7.34 → 4.7.35

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 (48) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +8 -11
  3. package/lint/run.mjs +70 -29
  4. package/package.json +2 -2
  5. package/runtime/knowledge/grammars/index.yaml +102 -0
  6. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +302 -0
  7. package/runtime/knowledge/grammars/starci/playbook.yaml +1159 -0
  8. package/runtime/knowledge/hfs/canon-pins.yaml +22 -8
  9. package/runtime/knowledge/hfs/fe-stack.yaml +7 -0
  10. package/runtime/knowledge/hfs/rules.yaml +133 -83
  11. package/runtime/knowledge/hfs/slots.yaml +3 -3
  12. package/runtime/knowledge/ui/presentation/font.yaml +101 -0
  13. package/runtime/knowledge/ui/presentation/padding.yaml +216 -0
  14. package/runtime/modules/kernel/failure-codes.yaml +40 -20
  15. package/runtime/modules/kernel/removed-vocabulary.yaml +29 -0
  16. package/runtime/packages/cli/src/catalog.generated.mjs +7 -7
  17. package/runtime/scripts/gates/canon-pins.mjs +103 -0
  18. package/runtime/scripts/gates/gate.mjs +3 -36
  19. package/runtime/scripts/gates/output.mjs +40 -0
  20. package/runtime/scripts/gates/test-world-run.mjs +2 -4
  21. package/runtime/scripts/hfs/architecture/config.mjs +8 -8
  22. package/runtime/scripts/hfs/check.mjs +1 -1
  23. package/runtime/scripts/hfs/db-restamp-verb.mjs +2 -2
  24. package/runtime/scripts/hfs/declaration-shape.mjs +8 -2
  25. package/runtime/scripts/hfs/grammar-presentation.mjs +152 -0
  26. package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
  27. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -0
  28. package/runtime/scripts/hfs/rules/fe-stack.mjs +44 -1
  29. package/runtime/scripts/hfs/slots.mjs +1 -0
  30. package/runtime/scripts/hfs/view.mjs +1 -0
  31. package/runtime/scripts/lib/canon-binding-text.mjs +129 -0
  32. package/runtime/scripts/lib/example-refs.mjs +1 -1
  33. package/runtime/scripts/lib/grammar-lint.mjs +48 -0
  34. package/runtime/scripts/lib/grammar-vendor-parity.mjs +31 -111
  35. package/runtime/scripts/lib/grammar-vendor-source.mjs +345 -0
  36. package/runtime/scripts/lib/numeric-match.mjs +5 -0
  37. package/runtime/scripts/lib/source-package-imports.mjs +111 -0
  38. package/runtime/scripts/work/draw/draw-layer.mjs +5 -4
  39. package/scaffold/app.mjs +7 -5
  40. package/src/main.mjs +26 -13
  41. package/stack/config.mjs +2 -1
  42. package/stack/render.mjs +24 -3
  43. package/sync/index.mjs +8 -2
  44. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +0 -1
  45. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +0 -1
  46. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -1
  47. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +0 -1
  48. package/templates/fe/tool-config/eslint.config.mjs +4 -1
package/stack/config.mjs CHANGED
@@ -9,7 +9,8 @@ import { refusal } from './errors.mjs';
9
9
  export const STACKS_DIR = '.starcistacks';
10
10
  export const STACKS_FILE = 'application-stacks.yaml';
11
11
 
12
- const readYaml = (file, what) => {
12
+ /** Read a YAML input, refusing with a stack error naming its purpose when it is missing or invalid. */
13
+ export const readYaml = (file, what) => {
13
14
  let source;
14
15
  try { source = fs.readFileSync(file, 'utf8'); } catch { throw refusal('STACKS_DEPLOY_FILE_MISSING', `${what} (${file}) is not readable`); }
15
16
  try { return parseYaml(source); } catch { throw refusal('STACKS_DEPLOY_FILE_INVALID', `${what} is not valid YAML`); }
package/stack/render.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  // render.mjs - renders a release: the committed stack source plus a generated override (image references, plain environment, the
2
2
  // `<KEY>_FILE` pointers and the versioned secret grants) resolved by `docker stack config`. Writes `stack.generated.yaml`, `stack.yaml`
3
3
  // and `release.json` under the output directory, and nothing at all when a guard refuses. Output carries names and counts only; the
4
- // docker child gets a minimal environment, never the age identity or DOCKER_HOST.
4
+ // docker child gets a minimal environment, never the age identity or DOCKER_HOST. The override mirrors the source's compose version:
5
+ // Docker's legacy stack loader refuses mixed versions across its input files.
5
6
  import fs from 'node:fs';
6
7
  import os from 'node:os';
7
8
  import path from 'node:path';
9
+ import { readYaml } from './config.mjs';
8
10
  import { tool } from './context.mjs';
9
11
  import { hasPlaceholderDigest, isImageReference, isReleaseId } from './digest.mjs';
10
12
  import { failure, refusal } from './errors.mjs';
@@ -35,6 +37,22 @@ function checkInputs(config, options) {
35
37
  if (options.previous !== null && options.previous !== undefined && !isReleaseId(options.previous)) throw refusal('STACK_PREVIOUS_INVALID', 'previous must be 16 lowercase hex characters');
36
38
  }
37
39
 
40
+ /** Docker reads an absent, empty or 3 version as its current 3.x default; every other version must match exactly. */
41
+ const versionKey = (version) => {
42
+ const text = version === undefined ? '' : String(version);
43
+ return text === '' || text === '3' ? null : text;
44
+ };
45
+
46
+ function checkComposeVersions(sourceVersion, extraCompose) {
47
+ for (const file of extraCompose) {
48
+ const version = readYaml(file, `the extra compose file ${path.basename(file)}`)?.version;
49
+ if (versionKey(version) !== versionKey(sourceVersion)) {
50
+ throw refusal('STACK_COMPOSE_VERSION_MISMATCH',
51
+ `${path.basename(file)} declares compose version ${version ?? '(none)'} but the stack source declares ${sourceVersion ?? '(none)'}; docker stack config refuses files whose versions differ`);
52
+ }
53
+ }
54
+ }
55
+
38
56
  /** Compose interpolation treats `$` specially; the override doubles it so a value reaches the service unchanged. */
39
57
  const escapeDollar = (value) => value.replaceAll('$', '$$$$');
40
58
 
@@ -70,9 +88,10 @@ function serviceOverride(config, sealed, images, service) {
70
88
  return out;
71
89
  }
72
90
 
73
- function buildOverride(config, sealed, images) {
91
+ function buildOverride(config, sealed, images, version) {
74
92
  const services = new Set([...Object.keys(config.images), ...Object.keys(config.environment), ...Object.keys(sealed.grants)]);
75
93
  return {
94
+ ...(version === undefined ? {} : { version }),
76
95
  services: Object.fromEntries([...services].sort((a, b) => a.localeCompare(b)).map((service) => [service, serviceOverride(config, sealed, images, service)])),
77
96
  secrets: Object.fromEntries(Object.entries(sealed.secrets).map(([key, entry]) => [key, { external: true, name: entry.runtimeName }])),
78
97
  };
@@ -124,9 +143,11 @@ function writeOutput(out, override, stackBytes, release) {
124
143
  /** Renders a release into `options.out` (a fresh temp directory when null). Effects stay local; nothing leaves the machine. */
125
144
  export async function renderRelease(ctx, config, options) {
126
145
  checkInputs(config, options);
146
+ const sourceVersion = readYaml(config.stackFile, 'the stack source')?.version;
147
+ checkComposeVersions(sourceVersion, options.extraCompose ?? []);
127
148
  const sealed = loadSealed(ctx, config);
128
149
  const configs = configObjects(config);
129
- const override = buildOverride(config, sealed, options.images);
150
+ const override = buildOverride(config, sealed, options.images, sourceVersion);
130
151
  const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'stack-override-'));
131
152
  try {
132
153
  const overrideFile = path.join(scratch, 'stack.generated.yaml');
package/sync/index.mjs CHANGED
@@ -35,6 +35,7 @@ import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs'
35
35
  import { ciWorkflowCommands } from './ci-commands.mjs';
36
36
  import { dependabotUpdates, hasDeclaredServices, readDeclaredQuality, readDeclaredBadges, sonarHostLine } from './quality-services.mjs';
37
37
  import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
38
+ import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
38
39
 
39
40
  export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
40
41
  const NODE_MAJOR = 22;
@@ -133,11 +134,13 @@ function readBundled(name) {
133
134
 
134
135
  /**
135
136
  * Fill `{{name}}` from vars and expand `{{> partial}}` lines. A partial line may be written as a line comment (`// {{> partial}}`)
136
- * so a JavaScript template stays valid source before rendering. Empty variable-only lines disappear; an unknown name is an error.
137
+ * so a JavaScript template stays valid source before rendering. Variable-only lines may also use a line comment;
138
+ * empty variable-only lines disappear and an unknown name is an error.
137
139
  */
138
140
  export function render(text, vars, readTemplate = readBundled) {
139
141
  const withPartials = text.replace(/^(?:\/\/ )?\{\{> ([\w./-]+)\}\}\n/gm, (_, name) => trailingNewline(render(readTemplate(name), vars, readTemplate)));
140
- const presentLines = withPartials.replace(/^[ \t]*\{\{([A-Za-z]\w*)\}\}[ \t]*(?:\n|$)/gm, (line, key) => vars[key] === '' ? '' : line);
142
+ const presentLines = withPartials.replace(/^[ \t]*(?:\/\/ )?\{\{([A-Za-z]\w*)\}\}[ \t]*(?:\n|$)/gm,
143
+ (line, key) => vars[key] === '' ? '' : line.replace(/^(\s*)\/\/ /, '$1'));
141
144
  return presentLines.replace(/\{\{([A-Za-z]\w*)\}\}/g, (_, key) => {
142
145
  if (!Object.hasOwn(vars, key)) throw new SyncError('HFS_SYNC_TEMPLATE_VARIABLE', `template names {{${key}}}, which sync does not provide`);
143
146
  return vars[key];
@@ -341,6 +344,7 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
341
344
  const lite = app.edition === 'lite';
342
345
  const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
343
346
  const quality = source.quality ?? qualityServices({});
347
+ const grammarLint = scope === 'fe' ? grammarLintFor(fe.grammar) : null;
344
348
  return {
345
349
  header: HEADER(scope),
346
350
  readmeBadges: source.badges ?? '',
@@ -375,6 +379,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
375
379
  dependabotUpdates: dependabotUpdates(quality.dependabot),
376
380
  tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])].join(','),
377
381
  styleGlob: STYLE_GLOB,
382
+ grammarLintImport: grammarLint?.package ? `import { ${grammarLint.factory} } from "${grammarLint.package}"` : '',
383
+ grammarLintExport: grammarLint?.package ? `export default [...starciFeConfig({ hfs }), ...${grammarLint.factory}({ hfs })]` : 'export default starciFeConfig({ hfs })',
378
384
  imageMatrix: imageMatrix(app, source),
379
385
  imageProject: app.project,
380
386
  ...stackWorkflowVariables(stack, app.project, name => stackDockerfile(app, name)),
@@ -1,5 +1,4 @@
1
1
  # Rendered development model. App images and owner infrastructure must be provisioned before use.
2
- version: "3.8"
3
2
  services:
4
3
  core:
5
4
  image: "{{project}}/core:v0.0.0"
@@ -1,5 +1,4 @@
1
1
  # Rendered production model. All-zero app digests are illustrative; publish verified images before use.
2
- version: "3.8"
3
2
  services:
4
3
  core:
5
4
  image: "{{project}}/core:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
@@ -1,5 +1,4 @@
1
1
  # Illustrative development model. Replace all-zero application digests with verified published release digests before use.
2
- version: "3.8"
3
2
  services:
4
3
  api:
5
4
  image: "{{project}}/api:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
@@ -1,5 +1,4 @@
1
1
  # Illustrative production model. Replace all-zero application digests with verified published release digests before use.
2
- version: "3.8"
3
2
  services:
4
3
  api:
5
4
  image: "{{project}}/api:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
@@ -1,3 +1,6 @@
1
1
  import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
2
+ // {{grammarLintImport}}
2
3
 
3
- export default starciFeConfig({ hfs: loadHfs(import.meta.url) })
4
+ const hfs = loadHfs(import.meta.url)
5
+
6
+ // {{grammarLintExport}}