@starci/hfs 1.0.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 (74) hide show
  1. package/README.md +38 -0
  2. package/bin/hfs.mjs +100 -0
  3. package/package.json +28 -0
  4. package/runtime/engine/runtime-root.mjs +32 -0
  5. package/runtime/engine/yaml.mjs +161 -0
  6. package/runtime/knowledge/hfs/canon-pins.yaml +212 -0
  7. package/runtime/knowledge/hfs/slots.yaml +842 -0
  8. package/runtime/modules/kernel/failure-codes.yaml +169 -0
  9. package/runtime/scripts/lib/glob.mjs +23 -0
  10. package/runtime/scripts/lib/hfs-check.mjs +305 -0
  11. package/runtime/scripts/lib/hfs-slots.mjs +675 -0
  12. package/runtime/scripts/lib/path-key.mjs +15 -0
  13. package/sync/cli.mjs +15 -0
  14. package/sync/hygiene.mjs +92 -0
  15. package/sync/index.mjs +224 -0
  16. package/sync/skeleton.mjs +54 -0
  17. package/sync/sonar-key.mjs +45 -0
  18. package/templates/be/e2e.yml +21 -0
  19. package/templates/be/gitignore +2 -0
  20. package/templates/be/pre-commit +8 -0
  21. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +31 -0
  22. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +7 -0
  23. package/templates/be/skeleton/apps/__app__/src/app.module.ts +23 -0
  24. package/templates/be/skeleton/apps/__app__/src/main.ts +20 -0
  25. package/templates/be/skeleton/src/features/system-health/index.ts +1 -0
  26. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +6 -0
  27. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +13 -0
  28. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +18 -0
  29. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +10 -0
  30. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +36 -0
  31. package/templates/be/skeleton/src/modules/platform/config/env-source.ts +40 -0
  32. package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +21 -0
  33. package/templates/be/skeleton/src/modules/platform/config/index.ts +5 -0
  34. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +19 -0
  35. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +15 -0
  36. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +8 -0
  37. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +15 -0
  38. package/templates/be/skeleton/src/modules/platform/errors/domain-error.ts +11 -0
  39. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +38 -0
  40. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +24 -0
  41. package/templates/be/skeleton/src/modules/platform/errors/index.ts +2 -0
  42. package/templates/be/skeleton/src/modules/platform/logging/index.ts +5 -0
  43. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +33 -0
  44. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +35 -0
  45. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +9 -0
  46. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +19 -0
  47. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +11 -0
  48. package/templates/be/sonar-project.properties +10 -0
  49. package/templates/be/starciwork.gitignore +39 -0
  50. package/templates/common/ci.yml +50 -0
  51. package/templates/common/codecov.yml +13 -0
  52. package/templates/common/gitignore.base +33 -0
  53. package/templates/common/pre-push +5 -0
  54. package/templates/fe/e2e.yml +22 -0
  55. package/templates/fe/gitignore +3 -0
  56. package/templates/fe/pre-commit +7 -0
  57. package/templates/fe/skeleton/apps/__app__/next.config.ts +11 -0
  58. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +22 -0
  59. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +31 -0
  60. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +15 -0
  61. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +27 -0
  62. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +24 -0
  63. package/templates/fe/skeleton/apps/__app__/src/app/globals.css +1 -0
  64. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +10 -0
  65. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.ts +5 -0
  66. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +8 -0
  67. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +19 -0
  68. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +27 -0
  69. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/navigation.ts +5 -0
  70. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +13 -0
  71. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +10 -0
  72. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +9 -0
  73. package/templates/fe/skeleton/apps/__app__/src/proxy.ts +10 -0
  74. package/templates/fe/sonar-project.properties +11 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @starci/hfs
2
+
3
+ The HFS command line of a StarCi product repository. It is installed from the npm registry at the exact version in `knowledge/hfs/canon-pins.yaml` (see [`packages/README.md`](../README.md)),
4
+ and it is self-contained: `runtime/` carries the slot manifest, the canon pins, the Vietnamese why
5
+ catalog slice and the loader, so it runs where there is no runtime checkout.
6
+
7
+ ```sh
8
+ npx hfs check [--repo <dir>] [--json] # exit 1 on any error-level finding
9
+ npx hfs init [--repo <dir>] [--stdout] # write a starter hfs.json (never overwrites); --stdout only prints
10
+ npx hfs explain <path> [--repo <dir>] [--json]
11
+ npx hfs sync (--check | --write) [--root <dir>] # generated files: husky, CI, .gitignore block, sonar, codecov (sync/, templates/)
12
+ npx hfs work-hygiene # pre-commit guard for staged .starciwork / .starcistacks paths
13
+ ```
14
+
15
+ `hfs check` reads the repository's `hfs.json` and the tracked paths (`git ls-files`), and reports, each with a why code and its
16
+ Vietnamese text (`modules/kernel/failure-codes.yaml`):
17
+
18
+ | Code | Level | Meaning |
19
+ |---|---|---|
20
+ | `HFS_PATH_NO_SLOT` | error | a tracked path no slot owns (the nearest slot is named) |
21
+ | `HFS_SLOT_NOT_ENABLED` | error | a path in an opt-in slot `hfs.json` did not declare |
22
+ | `HFS_SLOT_AMBIGUOUS` | error | two slots own the path equally (a manifest gap) |
23
+ | `HFS_TRACKED_MUST_BE_IGNORED` | error | a tracked path in an `ignored` slot (build output, generated) |
24
+ | `HFS_FORBIDDEN_PRESENT` | error | a tracked path in a forbidden / `external` slot |
25
+ | `HFS_REQUIRED_MISSING` | error | a file or directory a required slot, app or instance must contain |
26
+ | `HFS_MIN_INSTANCES` | error | fewer instances of a slot than `minInstances` |
27
+ | `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml` |
28
+ | `HFS_SIZE_SOFT_BACKLOG` | info | a source file over `ruleParams.fileLines.soft`; report only, never fails |
29
+
30
+ Exit codes: 0 clean, 1 an error finding, 2 a refusal (not a Git work tree, bad flag). The command never writes to the repository
31
+ except `hfs init`, which writes `hfs.json` only when none exists.
32
+
33
+ ## Maintaining the bundle
34
+
35
+ `runtime/` is a byte copy of the runtime files listed in `scripts/sync-runtime.mjs` (plus the catalog slice of the codes the
36
+ check can emit). After changing `scripts/lib/hfs-check.mjs`, `scripts/lib/hfs-slots.mjs`, `knowledge/hfs/slots.yaml`,
37
+ `knowledge/hfs/canon-pins.yaml` or the catalog entries of those codes, run `node packages/hfs/scripts/sync-runtime.mjs`;
38
+ `tests/hfs-cli.spec.mjs` fails on a stale copy. Bump `version` here and in the pin when the behaviour changes.
package/bin/hfs.mjs ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // hfs - the HFS command line of a StarCi product repository.
3
+ // hfs check [--repo <dir>] [--json] every tracked path has a slot; required files exist; nothing forbidden or
4
+ // tracked-that-must-be-ignored; pins match; soft-size backlog (report only).
5
+ // Exit 1 on any error-level finding.
6
+ // hfs init [--repo <dir>] [--stdout] write a starter hfs.json by detecting the profile and the apps.
7
+ // hfs explain <path> [--repo <dir>] [--json] which slot owns the path, its tier, allowed imports, required tests.
8
+ // hfs sync (--check | --write) [--root <dir>] the generated files (husky, CI, .gitignore block, sonar, codecov); sync/cli.mjs
9
+ // hfs work-hygiene the pre-commit guard for staged .starciwork and .starcistacks paths; sync/cli.mjs
10
+ // Every finding names a why code and carries its Vietnamese text. The command reads the repository, never writes to it
11
+ // (init writes hfs.json only, and only when none exists). Exit codes: 0 clean, 1 error findings, 2 a refusal or bad usage.
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { checkRepo, explainPath, initRepo } from '../runtime/scripts/lib/hfs-check.mjs';
15
+ import { HfsSlotsError } from '../runtime/scripts/lib/hfs-slots.mjs';
16
+ import { main as syncMain } from '../sync/cli.mjs';
17
+
18
+ const USAGE = `hfs check [--repo <dir>] [--json]
19
+ hfs init [--repo <dir>] [--stdout]
20
+ hfs explain <path> [--repo <dir>] [--json]
21
+ hfs sync (--check | --write) [--root <dir>]
22
+ hfs work-hygiene
23
+ `;
24
+ const PER_CODE_LIMIT = 25;
25
+ const VALUE_FLAGS = new Set(['--repo']);
26
+ const BOOL_FLAGS = new Set(['--json', '--stdout']);
27
+
28
+ function parse(argv) {
29
+ const opts = { positional: [] };
30
+ for (let i = 0; i < argv.length; i += 1) {
31
+ const arg = argv[i];
32
+ if (VALUE_FLAGS.has(arg)) { opts[arg.slice(2)] = argv[i + 1]; i += 1; if (opts[arg.slice(2)] === undefined) throw new Error(`${arg} needs a value`); }
33
+ else if (BOOL_FLAGS.has(arg)) opts[arg.slice(2)] = true;
34
+ else if (arg.startsWith('--')) throw new Error(`unknown flag ${arg}`);
35
+ else opts.positional.push(arg);
36
+ }
37
+ return opts;
38
+ }
39
+
40
+ function printCheck(result, out) {
41
+ const { counts } = result;
42
+ out(`hfs check ${result.repoRoot} (profile ${result.profile ?? 'unknown'}, manifest ${result.manifest}, ${result.tracked} tracked paths)\n`);
43
+ const byCode = new Map();
44
+ for (const f of result.findings) byCode.set(f.code, [...(byCode.get(f.code) ?? []), f]);
45
+ for (const [code, list] of byCode) {
46
+ const first = list[0];
47
+ out(`\n[${first.level.toUpperCase()}] ${code} x${list.length}: ${first.titleVi}\n ${first.whyVi}\n -> ${first.nextStepVi}\n`);
48
+ for (const f of list.slice(0, PER_CODE_LIMIT)) out(` - ${f.message}\n`);
49
+ if (list.length > PER_CODE_LIMIT) out(` ... and ${list.length - PER_CODE_LIMIT} more (use --json for all)\n`);
50
+ }
51
+ out(`\n${counts.error} error finding${counts.error === 1 ? '' : 's'}, ${counts.info} report-only\n`);
52
+ }
53
+
54
+ function printExplain(e, out) {
55
+ out(`${e.path}\n`);
56
+ if (e.status === 'no-slot') {
57
+ out(` no slot owns this path (${e.code}): ${e.titleVi}\n ${e.whyVi}\n`);
58
+ if (e.nearest) out(` nearest slot ${e.nearest.slot}, pattern ${e.nearest.pattern}; matched ${e.nearest.matchedPrefix || '.'}, then expected ${e.nearest.expectedNext ?? 'nothing'}\n`);
59
+ return;
60
+ }
61
+ if (e.status === 'ambiguous') { out(` owned equally by ${e.candidates.join(', ')} (${e.code}): ${e.titleVi}\n`); return; }
62
+ out(` slot ${e.slot} (${e.pattern})\n presence ${e.presence}, ${e.tracking}\n tier ${e.tier}${e.owner ? ` owner ${e.owner.root} (${e.owner.slot})` : ''}\n`);
63
+ out(` imports ${e.allowedImports ? `may import ${e.allowedImports.join(', ')}; ${e.importRule}` : (e.importRule ?? 'not applicable')}\n`);
64
+ out(` tests ${e.tests}: ${e.testsMeaning}\n`);
65
+ if (e.requiredFiles.length) out(` requires ${e.requiredFiles.join(', ')}\n`);
66
+ if (e.goesTo) out(` belongs at ${e.goesTo}\n`);
67
+ if (e.rules) out(` rules ${e.rules.join(', ')}\n`);
68
+ if (e.code) out(` ${e.code}: ${e.titleVi}\n ${e.whyVi}\n`);
69
+ }
70
+
71
+ export async function main(argv, { stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s) } = {}) {
72
+ const [verb, ...rest] = argv;
73
+ if (!['check', 'init', 'explain', 'sync', 'work-hygiene'].includes(verb)) { stderr(USAGE); return 2; }
74
+ try {
75
+ if (verb === 'sync' || verb === 'work-hygiene') return await syncMain(argv);
76
+ const opts = parse(rest);
77
+ const repoRoot = path.resolve(opts.repo ?? process.cwd());
78
+ if (verb === 'check') {
79
+ if (opts.positional.length) throw new Error('hfs check takes no path');
80
+ const result = checkRepo({ repoRoot });
81
+ if (opts.json) stdout(`${JSON.stringify(result, null, 2)}\n`); else printCheck(result, stdout);
82
+ return result.ok ? 0 : 1;
83
+ }
84
+ if (verb === 'init') {
85
+ if (opts.positional.length) throw new Error('hfs init takes no path');
86
+ const result = initRepo({ repoRoot, write: !opts.stdout });
87
+ if (opts.stdout) stdout(result.text); else stdout(`hfs init: wrote ${result.file} (${result.declaration.profile}, ${result.declaration.apps.map((a) => `${a.name}:${a.kind}`).join(', ')})\n`);
88
+ return 0;
89
+ }
90
+ if (opts.positional.length !== 1) throw new Error('hfs explain takes exactly one path');
91
+ const explained = explainPath({ repoRoot, input: opts.positional[0] });
92
+ if (opts.json) stdout(`${JSON.stringify(explained, null, 2)}\n`); else printExplain(explained, stdout);
93
+ return explained.status === 'no-slot' || explained.status === 'ambiguous' ? 1 : 0;
94
+ } catch (error) {
95
+ stderr(error instanceof HfsSlotsError ? `${error.message}\n` : `hfs: ${error.message}\n${USAGE}`);
96
+ return 2;
97
+ }
98
+ }
99
+
100
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@starci/hfs",
3
+ "version": "1.0.0",
4
+ "description": "The HFS command line of a StarCi product repository: hfs check, init, explain, sync and work-hygiene. Self-contained: it carries the runtime files it reads.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "private": false,
8
+ "bin": {
9
+ "hfs": "./bin/hfs.mjs"
10
+ },
11
+ "files": [
12
+ "bin/hfs.mjs",
13
+ "runtime/**",
14
+ "sync/**",
15
+ "templates/**",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=22.13.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "sync": "node scripts/sync-runtime.mjs",
26
+ "sync:check": "node scripts/sync-runtime.mjs --check"
27
+ }
28
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+ import {parseYaml} from './yaml.mjs';
5
+
6
+ /**
7
+ * Runtime root helper. The runtime reads this source tree directly. `skillRoot` is the
8
+ * directory containing `package.json` (this module's parent) — the checkout, or the copy of it
9
+ * an install placed on the host.
10
+ *
11
+ * Every workflow, op, schema, knowledge and model contract read at runtime is an authored YAML
12
+ * file (or an authored JSON one where a document is stored that way). `readModuleJson` resolves a
13
+ * path under `skillRoot` and parses the file by its extension.
14
+ */
15
+ const moduleRoot = path.dirname(fileURLToPath(new URL('../package.json', import.meta.url)));
16
+
17
+ /** The runtime root: this source tree (or an immutable sealed payload of it). */
18
+ export const skillRoot = moduleRoot;
19
+
20
+ /**
21
+ * Read a runtime contract document. `parts` are path segments under `skillRoot`
22
+ * (e.g. readModuleJson('modules', 'models', 'kinds.yaml')).
23
+ */
24
+ export function readModuleJson(...parts) {
25
+ const rel = parts.join('/');
26
+ const file = path.join(skillRoot, rel);
27
+ if (!(file.startsWith(skillRoot) && fs.existsSync(file) && fs.statSync(file).isFile()))
28
+ throw new Error(`Required contract not found for ${rel} under ${skillRoot}`);
29
+ const text = fs.readFileSync(file, 'utf8');
30
+ if (/\.ya?ml$/i.test(file)) return parseYaml(text);
31
+ return JSON.parse(text);
32
+ }