Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ schema, so renaming a field fails the build instead of quietly importing a
blank column. **[The full walk, screen by screen, with what each step was
measured to do →](docs/import/walkthrough.md)**

### Or start from a catalog pack

A **catalog pack** is a ready-made catalog for one industry, shipped as seed
rows: `DULY_PACK=food-safety pnpm dev` opens an otherwise empty Duly with 38
recurring food-safety duties across 8 positions (食品安全周期性履职, written
from the PRC Food Safety Law, GB 14881-2013, SAMR Order No. 60 and HACCP),
ready to edit and apply to your people. It is a starting point, not legal
advice. **[What a pack is, how to choose, edit or remove one, and how to write
your own →](src/data/packs/README.md)**

## Try the demo

To see the finished picture before you load anything, start with the worked
Expand All @@ -151,8 +161,15 @@ have already seeded gives you **both** organisations in it;

Nothing about the fictional organisation is real: every address is on an
RFC 2606 reserved domain, and no real company, person, site or regulation is
named anywhere in it, in either language. Every reference the catalog cites is
an invented internal document, never a national or industry standard.
named anywhere in it, in either language. Every reference the organisation's
own catalog cites is an invented internal document, never a national or
industry standard.

Both demos also load the **food-safety catalog pack** beside that catalog, so
the role catalog shows what a template for a real industry looks like. Those
rows are the exception to the paragraph above: they cite real public
frameworks, because that is what a pack is for. `DULY_PACK=none pnpm demo`
leaves the pack out; see [catalog packs](src/data/packs/README.md).

## Customise

Expand Down Expand Up @@ -227,7 +244,7 @@ src/hooks/ object lifecycle hooks (collected here, not from object
src/functions/ pure callables a `script` flow node resolves by name
src/datasets/ src/dashboards/ the semantic layer and what reads it
src/security/ positions, permission sets, sharing rules
src/mappings/ src/data/ catalog import and the demo fixture (src/data/demo-*)
src/mappings/ src/data/ catalog import, the demo fixture (src/data/demo-*) and catalog packs (src/data/packs/)
src/translations/ en (source) · zh-CN
scripts/ pnpm demo — start with the example loaded, in one boot
samples/ CSVs for the platform's standard Import (see above)
Expand Down
13 changes: 13 additions & 0 deletions scripts/demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ const SCHEDULED_WORK_ENV_VAR = 'OS_AUTOMATION_SCHEDULED_WORK_ENABLED';

if (process.env[SCHEDULED_WORK_ENV_VAR] === undefined) process.env[SCHEDULED_WORK_ENV_VAR] = 'true';

// ── Why the demo loads the food-safety catalog pack ──────────────────────
//
// `DULY_PACK` chooses a starting catalog (src/data/packs/README.md). The demo
// loads `food-safety` beside its own organisation's catalog, so the first
// screen shows what a template catalog for a real industry looks like. Set
// here, and ONLY when the caller has not set it, for the same reason as the
// variable above: `DULY_PACK=none pnpm demo` is the demo without a pack, and
// an explicit value is the caller's decision. Not in package.json, because a
// shell default there does not run on Windows.
const PACK_ENV_VAR = 'DULY_PACK';
const DEFAULT_DEMO_PACK = 'food-safety';
if (process.env[PACK_ENV_VAR] === undefined) process.env[PACK_ENV_VAR] = DEFAULT_DEMO_PACK;

/**
* `sys_user.name` the account you sign in as carries, per locale.
*
Expand Down
22 changes: 18 additions & 4 deletions src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { catalogSeed } from './catalog.seed.js';
import { dutySeed } from './duty.seed.js';
import { logEntrySeed } from './log-entry.seed.js';
import { DEMO_STAFF } from './demo-staff.js';
import { packSeeds } from './packs/index.js';
import { adminPermissionSetSeed, businessUnitMemberSeed, businessUnitSeed, userSeed } from './org.seed.js';
import type { DemoStaff } from '@objectstack/spec/system';
import {
Expand Down Expand Up @@ -198,15 +199,28 @@ const OPT_IN = ['1', 'true', 'on', 'yes'];
export const demoSeedRequested = (): boolean =>
OPT_IN.includes((process.env[DEMO_SEED_ENV_VAR] ?? '').trim().toLowerCase());

// ─── …and a third chooses a catalog PACK (#134) ─────────────────────────────
//
// `DULY_PACK` (unset by default; `food-safety` for the first pack) adds a
// starting catalog of one industry's recurring duties: `duly_catalog_item`
// rows and nothing else. Read at compile time like the two above, refused
// loudly when it names no pack, and independent of the demo: a pack with no
// demo is an empty app with a catalog in it, which is the template path; the
// demo with no pack is the demo as it was. `pnpm demo` turns it on by default
// (`scripts/demo.mjs`). Everything about packs lives in `./packs/`.
export { CATALOG_PACKS, PACK_ENV_VAR, packSeeds } from './packs/index.js';

/**
* What `defineStack({ data })` installs: nothing at all, unless the demo was
* asked for.
* What `defineStack({ data })` installs: nothing at all, unless the demo or a
* pack was asked for.
*
* `test/demo-seed-opt-in.test.ts` fails if this stops being empty by default —
* which is what keeps the demo from drifting back into the default path one
* convenient row at a time.
* convenient row at a time. With no pack it is the demo array itself, not a
* copy of it.
*/
export const dulySeeds: Seed[] = demoSeedRequested() ? demoSeeds : [];
const demoOrNothing: Seed[] = demoSeedRequested() ? demoSeeds : [];
export const dulySeeds: Seed[] = packSeeds.length === 0 ? demoOrNothing : [...demoOrNothing, ...packSeeds];

/**
* What `defineStack({ demoStaff })` declares: the twelve demo people as
Expand Down
139 changes: 139 additions & 0 deletions src/data/packs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Catalog packs

A **pack** is a ready-made catalog of recurring duties for one industry: who
does what, how often, and what record shows it was done. Turn one on, and a
new Duly opens with a real catalog you can edit, instead of an empty table.

A pack is **data only**. It adds rows to the role catalog (`duly_catalog_item`),
the same rows you would otherwise import from a spreadsheet. It adds no
screens, fields or automation, and it creates no people, duties or tasks.

> **A pack is a starting point, not legal advice.** The food-safety pack is
> written from public sources. It is not a compliance opinion and not any
> company's actual list. Check it against the rules that apply to your plant,
> then change it until it describes your plant.

## The packs in this repo

| Id | What it covers | Positions | Duties |
|:---|:---|:---|:---|
| `food-safety` | Recurring food-safety duties of a food manufacturer (食品生产企业食品安全周期性履职): the PRC Food Safety Law, GB 14881-2013, SAMR Order No. 60 (日管控 / 周排查 / 月调度) and HACCP | 8 | 38 |

## Turn a pack on

Packs are chosen with one environment variable, `DULY_PACK`. It is read when
the app is compiled, and every `pnpm dev` / `pnpm demo` compiles, so a pack takes
effect on the next start.

| You want | Run |
|:---|:---|
| An empty app (the default) | `pnpm dev` |
| An empty app with the food-safety catalog in it | `DULY_PACK=food-safety pnpm dev` |
| The same, in Chinese | `DULY_PACK=food-safety DULY_DEMO_LOCALE=zh-CN pnpm dev` |
| The demo (it loads `food-safety` by itself) | `pnpm demo` / `pnpm demo:zh` |
| The demo without the pack | `DULY_PACK=none pnpm demo` |

On Windows, set the variable first (`set DULY_PACK=food-safety`, or
`$env:DULY_PACK="food-safety"` in PowerShell), then run the command.

- **Language.** A pack writes each row in one language: Chinese when
`DULY_DEMO_LOCALE=zh-CN`, English otherwise. The variable is the same one the
demo uses. Starting a second time in the other language adds the other
language's rows next to the first ones, so start clean
(`rm -rf .objectstack/data`) when you switch.
- **Several packs.** Separate their ids with commas: `DULY_PACK=food-safety,another-pack`.
- **A typo stops the start.** An id that names no pack is refused with the list
of ids there are. It never loads nothing quietly.

## What happens to your edits

The pack writes a row **only when no row of that name exists yet.** Once it is
in, the row is yours: change the wording, the frequency or the position, and a
later start with the pack still on leaves your version alone. The row's
**name** is how the pack recognises it. Rename a row, and the pack sees its
own row as missing and adds it again.

A row you **delete** comes back on the next start with the pack on, because
it is missing again. So once the catalog is yours, **turn the pack off**:
start without `DULY_PACK` (or with `DULY_PACK=none`). Everything the pack
wrote stays; it is ordinary data.

To remove a pack's rows entirely, turn the pack off, then delete the rows on
**Duly → Setup → Role catalog** (select them, then delete). On a development
machine, `rm -rf .objectstack/data` starts from nothing.

## From the catalog to people's work

A catalog row is a duty **template** for a position. Nobody owes it yet. To
hand it out, use **Apply role catalog** on the role catalog: pick the position
and the people who hold it, and each of them gets their own duty for every
active row of that position. Those duties then go through confirmation and
approval and are dispatched on their cadence. See [Getting started](../../../docs/getting-started.md).

## The row shape

Each pack is one file, `src/data/packs/<pack-id>/pack.ts`. Every duty is one
entry in its `duties` list, one column per line, in this order:

```ts
{
position: 'qc',
name: { zh: '检验与监控仪器校准核查', en: 'Instrument calibration check' },
done: { zh: '核对温度计、压力表……', en: 'Confirm that thermometers, pressure gauges…' },
evidence: { zh: '仪器校准台账和校准证书。', en: 'The calibration register and the certificates.' },
form: 'recurring', frequency: 'monthly', dueAnchor: 'period_start', dueOffsetDays: 5, leadDays: 7, graceDays: 3,
reference: gb14881('5.2.2', '监控设备', 'monitoring equipment'),
},
```

| Column | Becomes | What to write |
|:---|:---|:---|
| `position` | Position | A key from the pack's `positions` list, such as `qc`. The list gives each key its name in both languages, and that name is what the catalog shows. Positions here are plain names; you do not need to set them up anywhere first. |
| `name` | Duty | The duty in a few words, in Chinese (`zh`) and English (`en`). **It must be unique**, across this pack, every other pack and the demo catalog. It is the key the pack recognises the row by. |
| `done` | What "done" means | One or two sentences: what someone has actually done when this is done. |
| `evidence` | added to What "done" means, as **Evidence to keep: …** / **留存证据:…** | The record a plant keeps to show it was done. This is guidance. Duly never makes someone attach evidence to complete a task. |
| `form` | Form | `recurring` (has a cadence), `standing` (an ongoing responsibility that is never "done", only confirmed) or `one_off`. |
| `frequency` | Frequency | `daily`, `weekly`, `fortnightly`, `monthly`, `quarterly`, `semiannual` or `annual`. Recurring rows only. |
| `dueAnchor` | Due date anchored to | `period_start` or `period_end`. Recurring rows only. |
| `dueOffsetDays` | Offset | Whole days from the anchor day. From the start: 0 is the first day and 4 the fifth. From the end: 0 is the last day and -3 three days before it. Recurring rows only. |
| `leadDays` | Lead time | How many days before the due date the task appears. Recurring rows only. |
| `graceDays` | Grace | Days after the due date before the task counts as late, from 0 to 30. Not on standing rows. |
| `reference` | Reference | Optional. The law, standard or clause the duty comes from, in both languages. Give an article or clause number only where you checked it against the published text, and leave the column out where there is no single source. |

A **standing** row leaves out all five cadence columns. A **one-off** row keeps
only `graceDays`. The app refuses a row that breaks either rule, so the start
tells you, rather than a duty quietly going wrong.

### Add one duty

1. Open `src/data/packs/food-safety/pack.ts`.
2. Copy an entry from the same position and change it.
3. Restart with the pack on. The new row is in the role catalog, and the rows
you already had are left alone.

To take a duty out of the pack, delete its entry. That stops the pack adding
it to a new database. A database that already has the row keeps it until you
delete it there (see above).

### From a spreadsheet

A spreadsheet with one row per duty maps onto this file column for column.
Lay it out with the headers `position`, `name_zh`, `name_en`, `done_zh`,
`done_en`, `evidence_zh`, `evidence_en`, `form`, `frequency`, `dueAnchor`,
`dueOffsetDays`, `leadDays`, `graceDays`, `reference_zh`, `reference_en`, and
each row becomes one entry above. If you only want your own catalog and not a
pack, you do not need this file at all: import the spreadsheet on the role
catalog's **Import** button ([samples/catalog-items.csv](../../../samples/catalog-items.csv)
is the template).

### A new pack

1. Copy `food-safety/` to `src/data/packs/<your-pack-id>/`. Ids are lowercase
and hyphenated.
2. Give it its own `id`, `title`, `positions` and `duties`.
3. Add it to `CATALOG_PACKS` in `src/data/packs/index.ts`.
4. Run `pnpm verify`. `test/catalog-pack.test.ts` checks every pack in that
list: both languages on every string, unique names, no name the demo
already uses, every position used, every column within its limit. It also
starts the app with the pack on, in both languages, and checks that every
row landed.
Loading
Loading