@okfit/profiles 0.1.0 → 0.3.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.
- package/BodyProvenance.js +44 -0
- package/Derivation.js +200 -0
- package/GitHistory.js +107 -0
- package/Profile.js +38 -0
- package/Profiles.js +27 -0
- package/Provenance.js +94 -0
- package/README.md +163 -3
- package/SoftwareProject.js +170 -0
- package/index.d.ts +429 -6
- package/index.js +7 -14
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,12 +1,172 @@
|
|
|
1
1
|
# @okfit/profiles
|
|
2
2
|
|
|
3
|
-
Named configuration profiles for [okfit](https://github.com/spencerbeggs/okfit). A profile is a complete
|
|
3
|
+
Named configuration profiles for [okfit](https://github.com/spencerbeggs/okfit). A profile is a complete `OkfitConfig` value plus derivation rules for `generated.at` and `generated.by`. The first profile, `software-project`, describes a software repository's knowledge bundle.
|
|
4
4
|
|
|
5
5
|
> **Pre-release.** Part of the `@okfit/*` kit, in early development.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## The `software-project` profile
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`Profiles.softwareProject.config` is a hand-authored `OkfitConfig` (P-25) that sets only `concepts`, `types`, `tags`, and `extensions: {}`; `okf_version`, `bundle`, `lifecycle`, `actors`, and `lint` are inherited from `OkfitConfig.DEFAULTS` by whoever merges it (the CLI applies `DEFAULTS < profile < file` with `OkfitConfig.merge`). `Profiles.get(name)` resolves a profile by its exact name and answers `Option.none` for `"none"` and unknown names. The TOML below decodes to the exact same value and is checked against the literal by `__test__/SoftwareProject.test.ts`:
|
|
10
|
+
|
|
11
|
+
```toml
|
|
12
|
+
[concepts]
|
|
13
|
+
required = ["title", "description"]
|
|
14
|
+
tags.required = []
|
|
15
|
+
|
|
16
|
+
[types.Project]
|
|
17
|
+
description = "The repository's root concept: its purpose, boundaries, and non-goals."
|
|
18
|
+
guidance = "Exactly one Project exists and it lives at the bundle root as the project file. State the purpose in one paragraph and list what is deliberately out of scope so a reader never infers boundaries from silence."
|
|
19
|
+
|
|
20
|
+
[types.Module]
|
|
21
|
+
description = "A unit of code with an owner and a boundary."
|
|
22
|
+
guidance = "One per workspace package, plugin, website, or action. Link to the Decisions that shaped it and the Conventions it is bound by."
|
|
23
|
+
required = ["resource", "kind"]
|
|
24
|
+
|
|
25
|
+
[types.Module.fields.kind]
|
|
26
|
+
description = "What sort of unit this is; drives which conventions apply."
|
|
27
|
+
values.workspace = "The monorepo root: tooling, CI, release, shared config."
|
|
28
|
+
values.package = "A publishable npm package under packages/."
|
|
29
|
+
values.website = "A docs or marketing site, usually RSPress."
|
|
30
|
+
values.plugin = "A Claude Code or editor plugin distributed outside npm."
|
|
31
|
+
values.action = "A GitHub Action."
|
|
32
|
+
|
|
33
|
+
[types.Module.fields.resource]
|
|
34
|
+
description = "A path relative to this concept file, normally escaping the bundle, for example ../../packages/core."
|
|
35
|
+
kind = "path"
|
|
36
|
+
|
|
37
|
+
[types.Decision]
|
|
38
|
+
description = "A choice made, the alternatives rejected, and why."
|
|
39
|
+
guidance = "Never edit a stable Decision; deprecate it and write a new one that names it in supersedes. A Decision counts as settled only once a human has verified it."
|
|
40
|
+
require_verified = true
|
|
41
|
+
|
|
42
|
+
[types.Decision.fields.supersedes]
|
|
43
|
+
description = "The Decision this one replaces."
|
|
44
|
+
kind = "path"
|
|
45
|
+
|
|
46
|
+
[types.Convention]
|
|
47
|
+
description = "A rule contributors and agents must follow."
|
|
48
|
+
guidance = "State the rule as an instruction rather than a description of current behaviour. Give it a staleness window so it is re-examined on a cadence instead of rotting silently."
|
|
49
|
+
|
|
50
|
+
[types.Interface]
|
|
51
|
+
description = "A contract others depend on."
|
|
52
|
+
guidance = "Document the contract from the consumer's side: what stays stable, not how it is built. Point resource at the file, endpoint, or schema the promise lives in."
|
|
53
|
+
required = ["kind"]
|
|
54
|
+
|
|
55
|
+
[types.Interface.fields.kind]
|
|
56
|
+
description = "What kind of contract this is."
|
|
57
|
+
values.api = "A programmatic library surface: exported functions, classes, and types."
|
|
58
|
+
values.cli = "A command-line interface: commands, flags, and exit codes."
|
|
59
|
+
values.config = "A configuration file schema other tools read or write."
|
|
60
|
+
values.wire = "A network or IPC wire format."
|
|
61
|
+
values.mcp = "An MCP tool or resource surface."
|
|
62
|
+
|
|
63
|
+
[types.Interface.fields.resource]
|
|
64
|
+
description = "A path relative to this concept file, normally escaping the bundle, for example ../../packages/core/src/index.ts."
|
|
65
|
+
kind = "path"
|
|
66
|
+
|
|
67
|
+
[types.Reference]
|
|
68
|
+
description = "Mirrored external material kept under the references directory."
|
|
69
|
+
guidance = "Only for material this repository must cite reliably even if the original moves. Every Reference declares where it came from in sources."
|
|
70
|
+
required = ["sources"]
|
|
71
|
+
|
|
72
|
+
[tags.architecture]
|
|
73
|
+
description = "Concerns the shape of the system rather than one module."
|
|
74
|
+
|
|
75
|
+
[tags.testing]
|
|
76
|
+
description = "Concerns how the system is verified: strategy, fixtures, and coverage policy."
|
|
77
|
+
|
|
78
|
+
[tags.release]
|
|
79
|
+
description = "Concerns how changes ship: versioning, changelogs, publishing, and tagging."
|
|
80
|
+
|
|
81
|
+
[tags.security]
|
|
82
|
+
description = "Concerns trust boundaries, secrets, permissions, or attack surface."
|
|
83
|
+
|
|
84
|
+
[tags.performance]
|
|
85
|
+
description = "Concerns speed, memory, or resource cost and the trade-offs made for them."
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Only `concepts`, `types`, and `tags` carry real vocabulary; the literal's `extensions: {}` is `OkfitConfig`'s one required key and never appears on disk, since the codec folds unknown top-level keys into it (P-24: `fields.<k>.kind = "path"` and the `tags` table are vocabulary only — core enforces none of it yet). `Reference.required = ["sources"]` checks presence only: `sources = []` passes, and each entry's shape is core's `family-invalid` (P-23).
|
|
89
|
+
|
|
90
|
+
`Profiles.softwareProject.check(bundle)` additionally enforces, outside `OkfitConfig` entirely, that exactly one `Project` concept exists and lives at the bundle root (P-21): zero `Project`s is `project-missing`, more than one is `project-multiple` (one diagnostic per `Project` concept), and a `Project` whose path contains `/` is `project-not-at-root`. All three are severity `error` and are not configurable.
|
|
91
|
+
|
|
92
|
+
## Layout
|
|
93
|
+
|
|
94
|
+
`Profiles.softwareProject.layout` is scaffolding data for `okfit init`, never part of `OkfitConfig` (P-26):
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
root:
|
|
98
|
+
index.md -- bundle index
|
|
99
|
+
log.md -- bundle log
|
|
100
|
+
project.md -- the one Project concept, at the bundle root
|
|
101
|
+
|
|
102
|
+
directories:
|
|
103
|
+
modules/ -> Module
|
|
104
|
+
decisions/ -> Decision
|
|
105
|
+
conventions/ -> Convention
|
|
106
|
+
interfaces/ -> Interface
|
|
107
|
+
references/ -> Reference
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each directory carries its own `index.md`. What `okfit init` writes into these files is the CLI plan's concern, not this package's.
|
|
111
|
+
|
|
112
|
+
## Derivation
|
|
113
|
+
|
|
114
|
+
`Derivation` is package-global, not per profile:
|
|
115
|
+
|
|
116
|
+
- **`generated.at` is the author date, not the stamp date (P-4).** Walking a path's git history newest-first, the commit that changed the body is the one whose blob first differs from its predecessor's; that commit's *author* date (`%aI`) is `generated.at`. Its committer date, author name, and author email are carried on the result for callers that want a history-based policy later, but `generated.at` itself is never the date of a later frontmatter-only stamp.
|
|
117
|
+
- **The path log is `@effected/git`'s `Git.log(cwd, { paths: [path], follow: true, firstParentDiffMerges: true })` (P-5, amended P-46, decision 56).** `firstParentDiffMerges` surfaces a conflict-resolving merge whose blob differs from both parents (probed on git 2.54.0), which plain `--follow` can omit and which would otherwise attribute that body change to the wrong commit. `Git.log`'s unconditional `-z` makes `--name-only` emit paths raw — no C-style quoting — so no `core.quotePath` override is needed and a path containing a space, a quote, or a newline round-trips verbatim. The dirty check compares the worktree body against `Git.show(root, "HEAD", path)`, never against the newest log entry. `--follow`'s rename detection can itself stop at a commit that renames a file and rewrites its content enough that git no longer treats it as a continuation of the same history; when that happens, `generated.at` answers that rename commit rather than an older one.
|
|
118
|
+
- **Uncommitted is the caller's to interpret (P-10).** `Derivation.generatedAt` reports `{ _tag: "uncommitted", reason: "untracked" | "dirty" | "unborn" }` and never substitutes `now`. The recommended policy, followed by the snippet below: omit `generated.at` entirely until the body is committed.
|
|
119
|
+
- **The body digest answers "has the body changed", where dates cannot (issue #19).** `Derivation.bodyDigest(text)` is a sha256, hex-encoded, over the same normalised body `Derivation.body` produces (CRLF and lone CR to LF, trailing whitespace at end of text removed), so a `core.autocrlf` worktree and a trailing-newline change hash identically and a frontmatter-only edit does not move the digest at all. It is computed through effect's own `Crypto` service rather than `node:crypto`, so it carries `Crypto.Crypto` in its R channel; `NodeServices.layer` already supplies it wherever this package is used. `okfit sync` records it as `generated.body_sha256` — okfit's own extension key, not an OKF v0.2 field — and `Provenance.lint` compares it. Git cannot make this judgement on its own: the stamp lives in the same blob as the body, so a squash merge (one new commit carrying both) is indistinguishable by history from a body edited without a re-stamp.
|
|
120
|
+
- **Human actor resolution (P-13).** Given git identity `{ name?, email? }` and `config.actors.humans`, the first hit wins: (1) a `human:<id>` entry whose id matches the email's local part or the name's slug, case-insensitively (config spelling returned); (2) `human:<local part>`; (3) `human:<slug of name>`; (4) unresolved. The slug lowercases `name`, replaces runs of whitespace and characters outside `[A-Za-z0-9._-]` with `-`, and trims leading and trailing `-`.
|
|
121
|
+
- **Identity is read at merged config scope (P-15).** `Derivation.generatedBy` calls `Git.configGet(cwd, "user.name")` and `Git.configGet(cwd, "user.email")` with no `scope` option. Environment overrides (`GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`, `EMAIL`, `user.useConfigOnly`) are out of scope for phase 1.
|
|
122
|
+
- **`stale_after` is pure addition (P-19).** `Derivation.staleAfter(from, config)` is `from + (config.lifecycle?.default_stale_after ?? OkfitConfig.DEFAULTS.lifecycle.default_stale_after)`, falling back to 90 days. `from` should be `generated.at` when the provenance is `committed`, otherwise the caller's own `now`; re-stamp whenever `generated.at` changes.
|
|
123
|
+
|
|
124
|
+
Recommended stamping policy, followed end to end:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const by = yield* Derivation.generatedBy({ writer, cwd: path.dirname(file), config }); // P-16; agent needs actors.agent (P-17)
|
|
128
|
+
const provenance = yield* Derivation.generatedAt({ file, config }); // P-2, P-9, P-39
|
|
129
|
+
const at = provenance._tag === "committed" ? Option.some(provenance.at) : Option.none(); // omit `at` until committed (P-10)
|
|
130
|
+
const staleAfter = Derivation.staleAfter(Option.getOrElse(at, () => now), config); // P-19; `now` is the caller's
|
|
131
|
+
// serialisation is the CLI's: Schema.encodeSync(Timestamp) writes "...T08:00:00Z" after the P-18 core patch (P-41)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Frontmatter serialisation, the uncommitted policy's enforcement, `now`, layer composition (`Layer.mergeAll(Git.layer, GitHistory.layer).pipe(Layer.provideMerge(NodeServices.layer))`), exit codes, and everything `okfit init` writes belong to the CLI plan, not this package (P-30, P-41, P-44).
|
|
135
|
+
|
|
136
|
+
## Provenance
|
|
137
|
+
|
|
138
|
+
`Provenance.lint(bundle, config, options?)` is the `generated-at-drift`
|
|
139
|
+
lint (`lint.generated_at_drift`, config default `"warn"`), and it runs in
|
|
140
|
+
two tiers (issue #19).
|
|
141
|
+
|
|
142
|
+
Tier 1 applies to a concept that records `generated.body_sha256`: the
|
|
143
|
+
recorded digest is compared against
|
|
144
|
+
`Derivation.bodyDigest(concept.document.source)`, a pure comparison over
|
|
145
|
+
text already in memory with no git call and no filesystem read. Drift here
|
|
146
|
+
means the body has changed since it was last stamped -- never that a squash
|
|
147
|
+
or rebase merge minted a new author date for the commit that carried it.
|
|
148
|
+
Because the comparison is against the worktree, a body edited but not yet
|
|
149
|
+
committed reports immediately.
|
|
150
|
+
|
|
151
|
+
Tier 2 applies to a concept stamped before the digest field existed: the
|
|
152
|
+
original walk, one `Derivation.generatedAt` per concept, reporting when a
|
|
153
|
+
committed body's `generated.at` is missing or is not the derived instant
|
|
154
|
+
(compared as decoded `DateTime.Utc` values, never encoded strings). Tier 2
|
|
155
|
+
is silent for an uncommitted body and returns `[]` the moment it discovers
|
|
156
|
+
the bundle is outside a git repository at all.
|
|
157
|
+
|
|
158
|
+
A concept with no `generated` block is skipped by both tiers. The lint
|
|
159
|
+
emits no `range` -- tier 1's message carries both digests and tier 2's
|
|
160
|
+
carries both instants and a 7-character sha, which is what a reader acts
|
|
161
|
+
on. It is a plain facade over `Derivation`, not a `Profile` member:
|
|
162
|
+
`severityFor`, the `[lint]` table, the renderers, the PostToolUse hook, and
|
|
163
|
+
the MCP `validate_bundle` tool all treat its `Diagnostic`s like every other
|
|
164
|
+
lint. It is total over severity
|
|
165
|
+
(S-28): when the resolved `generated-at-drift` severity is `"off"` it
|
|
166
|
+
returns `[]` before any per-concept work and before any git call, so it is
|
|
167
|
+
safe to call at any severity. The CLI (`okfit validate`, `okfit sync`'s own
|
|
168
|
+
reporting) also gates on `"off"` before calling at all; the two agree by
|
|
169
|
+
construction.
|
|
10
170
|
|
|
11
171
|
## License
|
|
12
172
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//#region src/SoftwareProject.ts
|
|
2
|
+
/**
|
|
3
|
+
* Hand-authored, typed `OkfitConfig` (P-25). Sets ONLY `concepts`, `types`,
|
|
4
|
+
* `tags` and `extensions: {}`; `okf_version`, `bundle`, `lifecycle`, `actors`
|
|
5
|
+
* and `lint` are inherited from `OkfitConfig.DEFAULTS` by whoever merges.
|
|
6
|
+
* `actors.agent` is deliberately unset (P-17): which agent writes is a fact
|
|
7
|
+
* about the repository, not about the software-project shape. Every string
|
|
8
|
+
* obeys P-42 (one-sentence descriptions, at most two-sentence guidance, plain
|
|
9
|
+
* text). No explicit `required = []` anywhere (P-20). The literal type-checks
|
|
10
|
+
* as `OkfitConfig` without a decode step because no branded `Actor` is set
|
|
11
|
+
* (`CORE/OkfitConfig.ts:172`; `OkfitConfig.test.ts:102-107` precedent).
|
|
12
|
+
*/
|
|
13
|
+
const config = {
|
|
14
|
+
concepts: {
|
|
15
|
+
required: ["title", "description"],
|
|
16
|
+
tags: { required: [] }
|
|
17
|
+
},
|
|
18
|
+
types: {
|
|
19
|
+
Project: {
|
|
20
|
+
description: "The repository's root concept: its purpose, boundaries, and non-goals.",
|
|
21
|
+
guidance: "Exactly one Project exists and it lives at the bundle root as the project file. State the purpose in one paragraph and list what is deliberately out of scope so a reader never infers boundaries from silence."
|
|
22
|
+
},
|
|
23
|
+
Module: {
|
|
24
|
+
description: "A unit of code with an owner and a boundary.",
|
|
25
|
+
guidance: "One per workspace package, plugin, website, or action. Link to the Decisions that shaped it and the Conventions it is bound by.",
|
|
26
|
+
required: ["resource", "kind"],
|
|
27
|
+
fields: {
|
|
28
|
+
kind: {
|
|
29
|
+
description: "What sort of unit this is; drives which conventions apply.",
|
|
30
|
+
values: {
|
|
31
|
+
workspace: "The monorepo root: tooling, CI, release, shared config.",
|
|
32
|
+
package: "A publishable npm package under packages/.",
|
|
33
|
+
website: "A docs or marketing site, usually RSPress.",
|
|
34
|
+
plugin: "A Claude Code or editor plugin distributed outside npm.",
|
|
35
|
+
action: "A GitHub Action."
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
resource: {
|
|
39
|
+
description: "A path relative to this concept file, normally escaping the bundle, for example ../../packages/core.",
|
|
40
|
+
kind: "path"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
Decision: {
|
|
45
|
+
description: "A choice made, the alternatives rejected, and why.",
|
|
46
|
+
guidance: "Never edit a stable Decision; deprecate it and write a new one that names it in supersedes. A Decision counts as settled only once a human has verified it.",
|
|
47
|
+
require_verified: true,
|
|
48
|
+
fields: { supersedes: {
|
|
49
|
+
description: "The Decision this one replaces.",
|
|
50
|
+
kind: "path"
|
|
51
|
+
} }
|
|
52
|
+
},
|
|
53
|
+
Convention: {
|
|
54
|
+
description: "A rule contributors and agents must follow.",
|
|
55
|
+
guidance: "State the rule as an instruction rather than a description of current behaviour. Give it a staleness window so it is re-examined on a cadence instead of rotting silently."
|
|
56
|
+
},
|
|
57
|
+
Interface: {
|
|
58
|
+
description: "A contract others depend on.",
|
|
59
|
+
guidance: "Document the contract from the consumer's side: what stays stable, not how it is built. Point resource at the file, endpoint, or schema the promise lives in.",
|
|
60
|
+
required: ["kind"],
|
|
61
|
+
fields: {
|
|
62
|
+
kind: {
|
|
63
|
+
description: "What kind of contract this is.",
|
|
64
|
+
values: {
|
|
65
|
+
api: "A programmatic library surface: exported functions, classes, and types.",
|
|
66
|
+
cli: "A command-line interface: commands, flags, and exit codes.",
|
|
67
|
+
config: "A configuration file schema other tools read or write.",
|
|
68
|
+
wire: "A network or IPC wire format.",
|
|
69
|
+
mcp: "An MCP tool or resource surface."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
resource: {
|
|
73
|
+
description: "A path relative to this concept file, normally escaping the bundle, for example ../../packages/core/src/index.ts.",
|
|
74
|
+
kind: "path"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
Reference: {
|
|
79
|
+
description: "Mirrored external material kept under the references directory.",
|
|
80
|
+
guidance: "Only for material this repository must cite reliably even if the original moves. Every Reference declares where it came from in sources.",
|
|
81
|
+
required: ["sources"]
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
tags: {
|
|
85
|
+
architecture: { description: "Concerns the shape of the system rather than one module." },
|
|
86
|
+
testing: { description: "Concerns how the system is verified: strategy, fixtures, and coverage policy." },
|
|
87
|
+
release: { description: "Concerns how changes ship: versioning, changelogs, publishing, and tagging." },
|
|
88
|
+
security: { description: "Concerns trust boundaries, secrets, permissions, or attack surface." },
|
|
89
|
+
performance: { description: "Concerns speed, memory, or resource cost and the trade-offs made for them." }
|
|
90
|
+
},
|
|
91
|
+
extensions: {}
|
|
92
|
+
};
|
|
93
|
+
/** Spec 5.2 as data (P-26); consumed by `okfit init`, never by validation. */
|
|
94
|
+
const layout = {
|
|
95
|
+
root: {
|
|
96
|
+
index: "index.md",
|
|
97
|
+
log: "log.md",
|
|
98
|
+
project: "project.md"
|
|
99
|
+
},
|
|
100
|
+
directories: [
|
|
101
|
+
{
|
|
102
|
+
directory: "modules",
|
|
103
|
+
type: "Module"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
directory: "decisions",
|
|
107
|
+
type: "Decision"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
directory: "conventions",
|
|
111
|
+
type: "Convention"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
directory: "interfaces",
|
|
115
|
+
type: "Interface"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
directory: "references",
|
|
119
|
+
type: "Reference"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
};
|
|
123
|
+
const PROJECT_TYPE = "Project";
|
|
124
|
+
const diagnostic = (file, code, message) => ({
|
|
125
|
+
file,
|
|
126
|
+
code,
|
|
127
|
+
severity: "error",
|
|
128
|
+
message
|
|
129
|
+
});
|
|
130
|
+
/** Plain code-unit order on `file` then `code` (decision 6); never locale-dependent. */
|
|
131
|
+
const compare = (a, b) => {
|
|
132
|
+
if (a.file < b.file) return -1;
|
|
133
|
+
if (a.file > b.file) return 1;
|
|
134
|
+
if (a.code < b.code) return -1;
|
|
135
|
+
if (a.code > b.code) return 1;
|
|
136
|
+
return 0;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Exactly one `Project`, at the bundle root (P-21). Pure over `bundle.concepts`
|
|
140
|
+
* (`CORE/Bundle.ts:98`; `Concept.type` `CORE/Concept.ts:27`). The rules are
|
|
141
|
+
* independent: two Projects with one nested yield three diagnostics. The file
|
|
142
|
+
* name `project.md` is layout guidance, not checked. Severity is always
|
|
143
|
+
* `error`; no `range`. Output is sorted by `file` then `code`.
|
|
144
|
+
*/
|
|
145
|
+
const check = (bundle) => {
|
|
146
|
+
const projects = [...bundle.concepts.values()].filter((concept) => concept.frontmatter.type === PROJECT_TYPE);
|
|
147
|
+
const out = [];
|
|
148
|
+
if (projects.length === 0) out.push(diagnostic("", "project-missing", `No Project concept in the bundle; software-project expects exactly one at the bundle root (${layout.root.project})`));
|
|
149
|
+
if (projects.length > 1) for (const project of projects) {
|
|
150
|
+
const others = projects.filter((other) => other !== project).map((other) => other.path).sort();
|
|
151
|
+
out.push(diagnostic(project.path, "project-multiple", `Project concept "${project.path}" is one of ${projects.length}; software-project expects exactly one (others: ${others.join(", ")})`));
|
|
152
|
+
}
|
|
153
|
+
for (const project of projects) if (project.path.includes("/")) out.push(diagnostic(project.path, "project-not-at-root", `Project concept "${project.path}" is below the bundle root; software-project expects it beside ${layout.root.index}`));
|
|
154
|
+
return out.sort(compare);
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* The `software-project` profile (spec 5; P-20 to P-26, P-42). Reachable
|
|
158
|
+
* publicly only as `Profiles.softwareProject` (P-38).
|
|
159
|
+
*
|
|
160
|
+
* @public
|
|
161
|
+
*/
|
|
162
|
+
const softwareProject = {
|
|
163
|
+
name: "software-project",
|
|
164
|
+
config,
|
|
165
|
+
layout,
|
|
166
|
+
check
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
export { softwareProject };
|